claude-raid 0.1.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/LICENSE +21 -0
- package/README.md +345 -0
- package/bin/cli.js +34 -0
- package/package.json +37 -0
- package/src/detect-project.js +112 -0
- package/src/doctor.js +201 -0
- package/src/init.js +138 -0
- package/src/merge-settings.js +119 -0
- package/src/remove.js +92 -0
- package/src/update.js +110 -0
- package/template/.claude/agents/archer.md +115 -0
- package/template/.claude/agents/rogue.md +116 -0
- package/template/.claude/agents/warrior.md +114 -0
- package/template/.claude/agents/wizard.md +206 -0
- package/template/.claude/hooks/validate-commit-message.sh +78 -0
- package/template/.claude/hooks/validate-file-naming.sh +73 -0
- package/template/.claude/hooks/validate-no-placeholders.sh +67 -0
- package/template/.claude/hooks/validate-phase-gate.sh +60 -0
- package/template/.claude/hooks/validate-tests-pass.sh +43 -0
- package/template/.claude/hooks/validate-verification.sh +70 -0
- package/template/.claude/raid-rules.md +21 -0
- package/template/.claude/skills/raid-debugging/SKILL.md +159 -0
- package/template/.claude/skills/raid-design/SKILL.md +208 -0
- package/template/.claude/skills/raid-finishing/SKILL.md +123 -0
- package/template/.claude/skills/raid-git-worktrees/SKILL.md +96 -0
- package/template/.claude/skills/raid-implementation/SKILL.md +155 -0
- package/template/.claude/skills/raid-implementation-plan/SKILL.md +173 -0
- package/template/.claude/skills/raid-protocol/SKILL.md +288 -0
- package/template/.claude/skills/raid-review/SKILL.md +133 -0
- package/template/.claude/skills/raid-tdd/SKILL.md +147 -0
- package/template/.claude/skills/raid-verification/SKILL.md +113 -0
package/src/doctor.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function tryExec(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseVersion(str) {
|
|
17
|
+
const match = str && str.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
return { major: +match[1], minor: +match[2], patch: +match[3] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function versionGte(v, min) {
|
|
23
|
+
if (v.major !== min.major) return v.major > min.major;
|
|
24
|
+
if (v.minor !== min.minor) return v.minor > min.minor;
|
|
25
|
+
return v.patch >= min.patch;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MIN_CLAUDE = { major: 2, minor: 1, patch: 32 };
|
|
29
|
+
|
|
30
|
+
function checkNode() {
|
|
31
|
+
const v = process.version;
|
|
32
|
+
return { id: 'node', ok: true, label: 'Node.js', detail: v };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkClaude(exec) {
|
|
36
|
+
const raw = exec('claude --version');
|
|
37
|
+
if (!raw) {
|
|
38
|
+
return {
|
|
39
|
+
id: 'claude',
|
|
40
|
+
ok: false,
|
|
41
|
+
label: 'Claude Code',
|
|
42
|
+
detail: 'not found',
|
|
43
|
+
hint: 'Install: npm install -g @anthropic-ai/claude-code',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const ver = parseVersion(raw);
|
|
47
|
+
if (!ver) {
|
|
48
|
+
return {
|
|
49
|
+
id: 'claude',
|
|
50
|
+
ok: false,
|
|
51
|
+
label: 'Claude Code',
|
|
52
|
+
detail: `unknown version: ${raw}`,
|
|
53
|
+
hint: 'Expected semver from "claude --version"',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const ok = versionGte(ver, MIN_CLAUDE);
|
|
57
|
+
const tag = `v${ver.major}.${ver.minor}.${ver.patch}`;
|
|
58
|
+
return {
|
|
59
|
+
id: 'claude',
|
|
60
|
+
ok,
|
|
61
|
+
label: 'Claude Code',
|
|
62
|
+
detail: ok
|
|
63
|
+
? `${tag} (≥ ${MIN_CLAUDE.major}.${MIN_CLAUDE.minor}.${MIN_CLAUDE.patch} required)`
|
|
64
|
+
: `${tag} — update required (≥ ${MIN_CLAUDE.major}.${MIN_CLAUDE.minor}.${MIN_CLAUDE.patch})`,
|
|
65
|
+
hint: ok ? undefined : 'Update: npm update -g @anthropic-ai/claude-code',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkTmux(exec) {
|
|
70
|
+
const raw = exec('command -v tmux');
|
|
71
|
+
if (!raw) {
|
|
72
|
+
return {
|
|
73
|
+
id: 'tmux',
|
|
74
|
+
ok: false,
|
|
75
|
+
label: 'tmux',
|
|
76
|
+
detail: 'not installed',
|
|
77
|
+
hint: process.platform === 'darwin'
|
|
78
|
+
? 'Install: brew install tmux'
|
|
79
|
+
: 'Install tmux via your package manager (apt, dnf, brew, etc.)',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { id: 'tmux', ok: true, label: 'tmux', detail: 'installed' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function checkTeammateMode(homedir) {
|
|
86
|
+
const configPath = path.join(homedir, '.claude.json');
|
|
87
|
+
if (!fs.existsSync(configPath)) {
|
|
88
|
+
return {
|
|
89
|
+
id: 'teammate-mode',
|
|
90
|
+
ok: false,
|
|
91
|
+
label: 'teammateMode',
|
|
92
|
+
detail: 'not set — ~/.claude.json not found',
|
|
93
|
+
hint: 'Create ~/.claude.json with: { "teammateMode": "tmux" }',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
let config;
|
|
97
|
+
try {
|
|
98
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
99
|
+
} catch {
|
|
100
|
+
return {
|
|
101
|
+
id: 'teammate-mode',
|
|
102
|
+
ok: false,
|
|
103
|
+
label: 'teammateMode',
|
|
104
|
+
detail: '~/.claude.json is not valid JSON',
|
|
105
|
+
hint: 'Fix the JSON syntax, then add: "teammateMode": "tmux"',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (config.teammateMode === 'tmux') {
|
|
109
|
+
return {
|
|
110
|
+
id: 'teammate-mode',
|
|
111
|
+
ok: true,
|
|
112
|
+
label: 'teammateMode',
|
|
113
|
+
detail: 'tmux',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: 'teammate-mode',
|
|
118
|
+
ok: false,
|
|
119
|
+
label: 'teammateMode',
|
|
120
|
+
detail: config.teammateMode
|
|
121
|
+
? `set to "${config.teammateMode}" (expected "tmux")`
|
|
122
|
+
: 'not set in ~/.claude.json',
|
|
123
|
+
hint: 'Add "teammateMode": "tmux" to ~/.claude.json',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function diagnose(opts = {}) {
|
|
128
|
+
const homedir = opts.homedir || os.homedir();
|
|
129
|
+
const exec = opts.exec || tryExec;
|
|
130
|
+
|
|
131
|
+
const checks = [
|
|
132
|
+
checkNode(),
|
|
133
|
+
checkClaude(exec),
|
|
134
|
+
checkTmux(exec),
|
|
135
|
+
checkTeammateMode(homedir),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
checks,
|
|
140
|
+
allOk: checks.every(c => c.ok),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatChecks(checks) {
|
|
145
|
+
const lines = [];
|
|
146
|
+
const maxLabel = Math.max(...checks.map(c => c.label.length));
|
|
147
|
+
|
|
148
|
+
for (const check of checks) {
|
|
149
|
+
const icon = check.ok ? '✔' : '✖';
|
|
150
|
+
const pad = ' '.repeat(maxLabel - check.label.length + 2);
|
|
151
|
+
lines.push(` ${icon} ${check.label}${pad}${check.detail}`);
|
|
152
|
+
if (check.hint) {
|
|
153
|
+
lines.push(` → ${check.hint}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function run() {
|
|
160
|
+
const result = diagnose();
|
|
161
|
+
|
|
162
|
+
const output = `
|
|
163
|
+
claude-raid doctor — Environment & Quick Start
|
|
164
|
+
|
|
165
|
+
─── Prerequisites ───────────────────────────────
|
|
166
|
+
|
|
167
|
+
${formatChecks(result.checks)}
|
|
168
|
+
|
|
169
|
+
─── Quick Start ─────────────────────────────────
|
|
170
|
+
|
|
171
|
+
In-process mode (any terminal):
|
|
172
|
+
|
|
173
|
+
claude --agent wizard
|
|
174
|
+
|
|
175
|
+
Split-pane mode (tmux):
|
|
176
|
+
|
|
177
|
+
tmux new-session -s raid
|
|
178
|
+
claude --agent wizard --teammate-mode tmux
|
|
179
|
+
|
|
180
|
+
─── Navigating Teammates ────────────────────────
|
|
181
|
+
|
|
182
|
+
Shift+Down Cycle through teammates
|
|
183
|
+
Enter View a teammate's session
|
|
184
|
+
Escape Interrupt a teammate's turn
|
|
185
|
+
Ctrl+T Toggle the shared task list
|
|
186
|
+
Click pane Interact directly (split-pane mode)
|
|
187
|
+
|
|
188
|
+
─── Raid Modes ──────────────────────────────────
|
|
189
|
+
|
|
190
|
+
Full Raid Warrior + Archer + Rogue (3 agents)
|
|
191
|
+
Skirmish 2 agents, lightweight
|
|
192
|
+
Scout Wizard solo review
|
|
193
|
+
|
|
194
|
+
The Wizard recommends a mode based on task
|
|
195
|
+
complexity. You confirm before agents spawn.
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
console.log(output);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { diagnose, run };
|
package/src/init.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { detectProject } = require('./detect-project');
|
|
6
|
+
const { mergeSettings } = require('./merge-settings');
|
|
7
|
+
|
|
8
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', 'template', '.claude');
|
|
9
|
+
|
|
10
|
+
function copyRecursive(src, dest, skipped) {
|
|
11
|
+
if (!fs.existsSync(src)) return;
|
|
12
|
+
|
|
13
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
14
|
+
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const srcPath = path.join(src, entry.name);
|
|
17
|
+
const destPath = path.join(dest, entry.name);
|
|
18
|
+
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
21
|
+
copyRecursive(srcPath, destPath, skipped);
|
|
22
|
+
} else {
|
|
23
|
+
if (fs.existsSync(destPath)) {
|
|
24
|
+
skipped.push(destPath);
|
|
25
|
+
} else {
|
|
26
|
+
fs.copyFileSync(srcPath, destPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function install(cwd) {
|
|
33
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
34
|
+
const result = { skipped: [], alreadyInstalled: false, detected: null };
|
|
35
|
+
|
|
36
|
+
// Check if already installed
|
|
37
|
+
if (fs.existsSync(path.join(claudeDir, 'raid-rules.md'))) {
|
|
38
|
+
result.alreadyInstalled = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create .claude directory
|
|
42
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
// Detect project
|
|
45
|
+
const detected = detectProject(cwd);
|
|
46
|
+
result.detected = detected;
|
|
47
|
+
|
|
48
|
+
// Copy template files (agents, hooks, skills, raid-rules.md)
|
|
49
|
+
copyRecursive(TEMPLATE_DIR, claudeDir, result.skipped);
|
|
50
|
+
|
|
51
|
+
// Make hooks executable
|
|
52
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
53
|
+
if (fs.existsSync(hooksDir)) {
|
|
54
|
+
const hooks = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
55
|
+
for (const hook of hooks) {
|
|
56
|
+
const hookPath = path.join(hooksDir, hook);
|
|
57
|
+
fs.chmodSync(hookPath, 0o755);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Generate raid.json (skip if it already exists to preserve user config)
|
|
62
|
+
const raidConfigPath = path.join(claudeDir, 'raid.json');
|
|
63
|
+
if (!fs.existsSync(raidConfigPath)) {
|
|
64
|
+
const raidConfig = {
|
|
65
|
+
project: {
|
|
66
|
+
name: detected.name || path.basename(cwd),
|
|
67
|
+
language: detected.language,
|
|
68
|
+
testCommand: detected.testCommand || '',
|
|
69
|
+
lintCommand: detected.lintCommand || '',
|
|
70
|
+
buildCommand: detected.buildCommand || '',
|
|
71
|
+
},
|
|
72
|
+
paths: {
|
|
73
|
+
specs: 'docs/raid/specs',
|
|
74
|
+
plans: 'docs/raid/plans',
|
|
75
|
+
worktrees: '.worktrees',
|
|
76
|
+
},
|
|
77
|
+
conventions: {
|
|
78
|
+
fileNaming: 'none',
|
|
79
|
+
commits: 'conventional',
|
|
80
|
+
},
|
|
81
|
+
raid: {
|
|
82
|
+
defaultMode: 'full',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
fs.writeFileSync(raidConfigPath, JSON.stringify(raidConfig, null, 2) + '\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Merge settings
|
|
89
|
+
mergeSettings(cwd);
|
|
90
|
+
|
|
91
|
+
// Add raid-last-test-run to .gitignore
|
|
92
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
93
|
+
const ignoreEntries = ['.claude/raid-last-test-run', '.claude/raid-session', '.claude/raid-dungeon.md', '.claude/raid-dungeon-phase-*'];
|
|
94
|
+
if (fs.existsSync(gitignorePath)) {
|
|
95
|
+
let content = fs.readFileSync(gitignorePath, 'utf8');
|
|
96
|
+
const toAdd = ignoreEntries.filter(e => !content.includes(e));
|
|
97
|
+
if (toAdd.length > 0) {
|
|
98
|
+
const sep = content.endsWith('\n') ? '' : '\n';
|
|
99
|
+
fs.appendFileSync(gitignorePath, sep + toAdd.join('\n') + '\n');
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
fs.writeFileSync(gitignorePath, ignoreEntries.join('\n') + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function run() {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
console.log('\nclaude-raid — Installing The Raid\n');
|
|
111
|
+
|
|
112
|
+
const result = install(cwd);
|
|
113
|
+
|
|
114
|
+
if (result.alreadyInstalled) {
|
|
115
|
+
console.log('The Raid is already installed. Use `claude-raid update` to update.');
|
|
116
|
+
console.log('Proceeding with re-install...\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`Detected: ${result.detected.language}`);
|
|
120
|
+
if (result.detected.testCommand) {
|
|
121
|
+
console.log(`Test command: ${result.detected.testCommand}`);
|
|
122
|
+
}
|
|
123
|
+
if (result.skipped.length > 0) {
|
|
124
|
+
console.log(`\nSkipped (existing files):`);
|
|
125
|
+
result.skipped.forEach(f => console.log(` - ${path.relative(cwd, f)}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`
|
|
129
|
+
The Raid is installed.
|
|
130
|
+
|
|
131
|
+
Configuration: .claude/raid.json (edit to customize)
|
|
132
|
+
Team rules: .claude/raid-rules.md (editable)
|
|
133
|
+
|
|
134
|
+
Run 'claude-raid doctor' for setup guide and environment check.
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { install, run };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const RAID_ENV = {
|
|
7
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const RAID_PERMISSIONS = ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'];
|
|
11
|
+
|
|
12
|
+
const RAID_HOOK_MARKER = '#claude-raid';
|
|
13
|
+
|
|
14
|
+
const RAID_HOOKS = {
|
|
15
|
+
PostToolUse: [
|
|
16
|
+
{
|
|
17
|
+
matcher: 'Write|Edit',
|
|
18
|
+
hooks: [
|
|
19
|
+
{ type: 'command', command: `bash .claude/hooks/validate-file-naming.sh ${RAID_HOOK_MARKER}` },
|
|
20
|
+
{ type: 'command', command: `bash .claude/hooks/validate-no-placeholders.sh ${RAID_HOOK_MARKER}` },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
PreToolUse: [
|
|
25
|
+
{
|
|
26
|
+
matcher: 'Bash',
|
|
27
|
+
hooks: [
|
|
28
|
+
{ type: 'command', command: `bash .claude/hooks/validate-commit-message.sh ${RAID_HOOK_MARKER}` },
|
|
29
|
+
{ type: 'command', command: `bash .claude/hooks/validate-tests-pass.sh ${RAID_HOOK_MARKER}` },
|
|
30
|
+
{ type: 'command', command: `bash .claude/hooks/validate-verification.sh ${RAID_HOOK_MARKER}` },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
matcher: 'Write',
|
|
35
|
+
hooks: [
|
|
36
|
+
{ type: 'command', command: `bash .claude/hooks/validate-phase-gate.sh ${RAID_HOOK_MARKER}` },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function isRaidHookEntry(entry) {
|
|
43
|
+
return entry.hooks && entry.hooks.some(h => h.command && h.command.includes(RAID_HOOK_MARKER));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mergeSettings(cwd) {
|
|
47
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
48
|
+
let existing = {};
|
|
49
|
+
|
|
50
|
+
if (fs.existsSync(settingsPath)) {
|
|
51
|
+
const backupPath = settingsPath + '.pre-raid-backup';
|
|
52
|
+
if (!fs.existsSync(backupPath)) {
|
|
53
|
+
fs.copyFileSync(settingsPath, backupPath);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'Your existing .claude/settings.json contains invalid JSON. Please fix it before running claude-raid.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
existing.env = { ...(existing.env || {}), ...RAID_ENV };
|
|
65
|
+
|
|
66
|
+
const existingPerms = (existing.permissions && existing.permissions.allow) || [];
|
|
67
|
+
const merged = [...new Set([...existingPerms, ...RAID_PERMISSIONS])];
|
|
68
|
+
existing.permissions = { ...(existing.permissions || {}), allow: merged };
|
|
69
|
+
|
|
70
|
+
if (!existing.hooks) existing.hooks = {};
|
|
71
|
+
|
|
72
|
+
for (const [event, raidEntries] of Object.entries(RAID_HOOKS)) {
|
|
73
|
+
if (!existing.hooks[event]) {
|
|
74
|
+
existing.hooks[event] = [];
|
|
75
|
+
}
|
|
76
|
+
existing.hooks[event] = existing.hooks[event].filter(entry => !isRaidHookEntry(entry));
|
|
77
|
+
existing.hooks[event].push(...raidEntries);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeRaidSettings(cwd) {
|
|
84
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
85
|
+
const backupPath = settingsPath + '.pre-raid-backup';
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(backupPath)) {
|
|
88
|
+
fs.copyFileSync(backupPath, settingsPath);
|
|
89
|
+
fs.unlinkSync(backupPath);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(settingsPath)) return;
|
|
94
|
+
|
|
95
|
+
let settings;
|
|
96
|
+
try {
|
|
97
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'Your .claude/settings.json contains invalid JSON. Please fix it before running claude-raid remove.'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (settings.env) {
|
|
105
|
+
delete settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (settings.hooks) {
|
|
109
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
110
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => !isRaidHookEntry(entry));
|
|
111
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { mergeSettings, removeRaidSettings };
|
package/src/remove.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { removeRaidSettings } = require('./merge-settings');
|
|
6
|
+
|
|
7
|
+
const RAID_AGENTS = ['wizard.md', 'warrior.md', 'archer.md', 'rogue.md'];
|
|
8
|
+
const RAID_SKILLS = [
|
|
9
|
+
'raid-protocol', 'raid-design', 'raid-implementation-plan', 'raid-implementation',
|
|
10
|
+
'raid-review', 'raid-finishing', 'raid-tdd', 'raid-debugging',
|
|
11
|
+
'raid-verification', 'raid-git-worktrees',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function rmSafe(filePath) {
|
|
15
|
+
try { fs.unlinkSync(filePath); } catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function rmDirSafe(dirPath) {
|
|
19
|
+
try { fs.rmSync(dirPath, { recursive: true }); } catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rmDirIfEmpty(dirPath) {
|
|
23
|
+
try {
|
|
24
|
+
const entries = fs.readdirSync(dirPath);
|
|
25
|
+
if (entries.length === 0) fs.rmdirSync(dirPath);
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function performRemove(cwd) {
|
|
30
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
31
|
+
|
|
32
|
+
for (const agent of RAID_AGENTS) {
|
|
33
|
+
rmSafe(path.join(claudeDir, 'agents', agent));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
37
|
+
if (fs.existsSync(hooksDir)) {
|
|
38
|
+
const hooks = fs.readdirSync(hooksDir).filter(f => f.startsWith('validate-') && f.endsWith('.sh'));
|
|
39
|
+
for (const hook of hooks) {
|
|
40
|
+
rmSafe(path.join(hooksDir, hook));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const skill of RAID_SKILLS) {
|
|
45
|
+
rmDirSafe(path.join(claudeDir, 'skills', skill));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Clean up empty directories
|
|
49
|
+
rmDirIfEmpty(path.join(claudeDir, 'agents'));
|
|
50
|
+
rmDirIfEmpty(path.join(claudeDir, 'hooks'));
|
|
51
|
+
rmDirIfEmpty(path.join(claudeDir, 'skills'));
|
|
52
|
+
|
|
53
|
+
rmSafe(path.join(claudeDir, 'raid-rules.md'));
|
|
54
|
+
rmSafe(path.join(claudeDir, 'raid.json'));
|
|
55
|
+
rmSafe(path.join(claudeDir, 'raid-last-test-run'));
|
|
56
|
+
rmSafe(path.join(claudeDir, 'raid-session'));
|
|
57
|
+
|
|
58
|
+
// Clean up Dungeon files
|
|
59
|
+
rmSafe(path.join(claudeDir, 'raid-dungeon.md'));
|
|
60
|
+
if (fs.existsSync(claudeDir)) {
|
|
61
|
+
const dungeonFiles = fs.readdirSync(claudeDir).filter(f => f.startsWith('raid-dungeon-phase-'));
|
|
62
|
+
for (const file of dungeonFiles) {
|
|
63
|
+
rmSafe(path.join(claudeDir, file));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
removeRaidSettings(cwd);
|
|
68
|
+
|
|
69
|
+
// Clean .gitignore entries
|
|
70
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
71
|
+
const raidIgnoreEntries = ['.claude/raid-last-test-run', '.claude/raid-session', '.claude/raid-dungeon.md', '.claude/raid-dungeon-phase-*'];
|
|
72
|
+
if (fs.existsSync(gitignorePath)) {
|
|
73
|
+
const lines = fs.readFileSync(gitignorePath, 'utf8').split('\n');
|
|
74
|
+
const filtered = lines.filter(line => !raidIgnoreEntries.includes(line.trim()));
|
|
75
|
+
// Remove trailing blank lines caused by removal
|
|
76
|
+
while (filtered.length > 0 && filtered[filtered.length - 1] === '') {
|
|
77
|
+
filtered.pop();
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(gitignorePath, filtered.join('\n') + '\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { success: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function run() {
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
console.log('\nclaude-raid — Removing The Raid\n');
|
|
88
|
+
performRemove(cwd);
|
|
89
|
+
console.log('The Raid has been removed. Your project settings have been restored.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { performRemove, run };
|
package/src/update.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { mergeSettings } = require('./merge-settings');
|
|
6
|
+
|
|
7
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', 'template', '.claude');
|
|
8
|
+
|
|
9
|
+
function filesAreEqual(pathA, pathB) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.readFileSync(pathA, 'utf8') === fs.readFileSync(pathB, 'utf8');
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function copyForceRecursive(src, dest) {
|
|
18
|
+
if (!fs.existsSync(src)) return;
|
|
19
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const srcPath = path.join(src, entry.name);
|
|
22
|
+
const destPath = path.join(dest, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
25
|
+
copyForceRecursive(srcPath, destPath);
|
|
26
|
+
} else {
|
|
27
|
+
fs.copyFileSync(srcPath, destPath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function performUpdate(cwd) {
|
|
33
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
34
|
+
const skippedAgents = [];
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(path.join(claudeDir, 'raid-rules.md'))) {
|
|
37
|
+
return { success: false, message: 'The Raid is not installed. Run `claude-raid init` first.', skippedAgents };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Update agents — skip if user has customized them
|
|
41
|
+
const agentsSrc = path.join(TEMPLATE_DIR, 'agents');
|
|
42
|
+
const agentsDest = path.join(claudeDir, 'agents');
|
|
43
|
+
if (fs.existsSync(agentsSrc)) {
|
|
44
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
45
|
+
const agents = fs.readdirSync(agentsSrc).filter(f => f.endsWith('.md'));
|
|
46
|
+
for (const agent of agents) {
|
|
47
|
+
const srcPath = path.join(agentsSrc, agent);
|
|
48
|
+
const destPath = path.join(agentsDest, agent);
|
|
49
|
+
if (fs.existsSync(destPath) && !filesAreEqual(srcPath, destPath)) {
|
|
50
|
+
skippedAgents.push(agent);
|
|
51
|
+
} else {
|
|
52
|
+
fs.copyFileSync(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update hooks and skills — always overwrite (these are framework code, not user content)
|
|
58
|
+
for (const subdir of ['hooks', 'skills']) {
|
|
59
|
+
const src = path.join(TEMPLATE_DIR, subdir);
|
|
60
|
+
const dest = path.join(claudeDir, subdir);
|
|
61
|
+
if (fs.existsSync(src)) {
|
|
62
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
63
|
+
copyForceRecursive(src, dest);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update raid-rules.md — skip if user has customized it
|
|
68
|
+
const rulesSrc = path.join(TEMPLATE_DIR, 'raid-rules.md');
|
|
69
|
+
const rulesDest = path.join(claudeDir, 'raid-rules.md');
|
|
70
|
+
let skippedRules = false;
|
|
71
|
+
if (fs.existsSync(rulesSrc)) {
|
|
72
|
+
if (fs.existsSync(rulesDest) && !filesAreEqual(rulesSrc, rulesDest)) {
|
|
73
|
+
skippedRules = true;
|
|
74
|
+
} else {
|
|
75
|
+
fs.copyFileSync(rulesSrc, rulesDest);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
80
|
+
if (fs.existsSync(hooksDir)) {
|
|
81
|
+
const hooks = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
|
|
82
|
+
for (const hook of hooks) {
|
|
83
|
+
fs.chmodSync(path.join(hooksDir, hook), 0o755);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mergeSettings(cwd);
|
|
88
|
+
|
|
89
|
+
let message = 'The Raid has been updated to the latest version.';
|
|
90
|
+
if (skippedAgents.length > 0) {
|
|
91
|
+
message += `\nSkipped customized agents: ${skippedAgents.join(', ')}`;
|
|
92
|
+
}
|
|
93
|
+
if (skippedRules) {
|
|
94
|
+
message += '\nSkipped customized raid-rules.md';
|
|
95
|
+
}
|
|
96
|
+
if (skippedAgents.length > 0 || skippedRules) {
|
|
97
|
+
message += '\nUse `claude-raid remove` then `claude-raid init` to reset.';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { success: true, message, skippedAgents };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function run() {
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
console.log('\nclaude-raid — Updating The Raid\n');
|
|
106
|
+
const result = performUpdate(cwd);
|
|
107
|
+
console.log(result.message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { performUpdate, run };
|