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/bin/cli.js
CHANGED
|
@@ -3,32 +3,59 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const command = process.argv[2];
|
|
6
|
+
const { banner, colors, header } = require('../src/ui');
|
|
7
|
+
const versionCheck = require('../src/version-check');
|
|
8
|
+
|
|
9
|
+
// Start non-blocking version check immediately
|
|
10
|
+
const showUpdateNotice = versionCheck.start();
|
|
6
11
|
|
|
7
12
|
const COMMANDS = {
|
|
8
|
-
|
|
13
|
+
// Primary commands
|
|
14
|
+
summon: () => {
|
|
15
|
+
if (process.argv.includes('--dry-run')) {
|
|
16
|
+
console.log('\n' + banner());
|
|
17
|
+
console.log(require('../src/init').dryRun(process.cwd()));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
return require('../src/init').run();
|
|
21
|
+
},
|
|
9
22
|
update: () => require('../src/update').run(),
|
|
23
|
+
dismantle: () => require('../src/remove').run(),
|
|
24
|
+
heal: () => require('../src/doctor').run(),
|
|
25
|
+
// Aliases (backward compat)
|
|
26
|
+
init: () => {
|
|
27
|
+
if (process.argv.includes('--dry-run')) {
|
|
28
|
+
console.log('\n' + banner());
|
|
29
|
+
console.log(require('../src/init').dryRun(process.cwd()));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return require('../src/init').run();
|
|
33
|
+
},
|
|
10
34
|
remove: () => require('../src/remove').run(),
|
|
11
35
|
doctor: () => require('../src/doctor').run(),
|
|
12
36
|
};
|
|
13
37
|
|
|
14
38
|
if (!command || !COMMANDS[command]) {
|
|
15
|
-
console.log(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
console.log('\n' + banner());
|
|
40
|
+
console.log(header('Commands') + '\n');
|
|
41
|
+
const cmds = [
|
|
42
|
+
['summon', 'Summon the party into this realm'],
|
|
43
|
+
['update', 'Reforge the party\'s arsenal'],
|
|
44
|
+
['dismantle', 'Dismantle the camp and retreat'],
|
|
45
|
+
['heal', 'Diagnose wounds and prepare for battle'],
|
|
46
|
+
];
|
|
47
|
+
for (const [name, desc] of cmds) {
|
|
48
|
+
console.log(' ' + colors.bold(name.padEnd(12)) + desc);
|
|
49
|
+
}
|
|
50
|
+
console.log(header('Begin the Raid') + '\n');
|
|
51
|
+
console.log(' claude --agent wizard\n');
|
|
52
|
+
console.log(colors.dim(' github.com/pedropicardi/claude-raid') + '\n');
|
|
26
53
|
process.exit(command ? 1 : 0);
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
56
|
+
Promise.resolve(COMMANDS[command]())
|
|
57
|
+
.then(() => showUpdateNotice(colors))
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
console.error(`\nclaude-raid: ${err.message}\n`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Single source of truth for file descriptions used by dry-run and install output.
|
|
4
|
+
// Keep descriptions under 80 chars — they appear in terminal columns.
|
|
5
|
+
|
|
6
|
+
const AGENTS = {
|
|
7
|
+
'wizard.md': 'Dungeon master — opens phases, dispatches team, closes with rulings',
|
|
8
|
+
'warrior.md': 'Stress-tester — breaks things under load, edge cases, pressure',
|
|
9
|
+
'archer.md': 'Pattern-seeker — traces ripple effects, naming drift, contract violations',
|
|
10
|
+
'rogue.md': 'Assumption-destroyer — thinks like a failing system, malicious input',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const HOOKS = {
|
|
14
|
+
lifecycle: [
|
|
15
|
+
{ name: 'raid-lib.sh', desc: 'Shared config — reads raid.json, exports session state' },
|
|
16
|
+
{ name: 'raid-session-start.sh', desc: 'Activates Raid workflow when session begins' },
|
|
17
|
+
{ name: 'raid-session-end.sh', desc: 'Archives Dungeon, cleans up when session ends' },
|
|
18
|
+
{ name: 'raid-stop.sh', desc: 'Backs up Dungeon on phase transitions' },
|
|
19
|
+
{ name: 'raid-pre-compact.sh', desc: 'Backs up Dungeon before message compaction' },
|
|
20
|
+
{ name: 'raid-task-created.sh', desc: 'Validates task subjects are meaningful' },
|
|
21
|
+
{ name: 'raid-task-completed.sh', desc: 'Blocks task completion without test evidence' },
|
|
22
|
+
{ name: 'raid-teammate-idle.sh', desc: 'Nudges idle agents to participate' },
|
|
23
|
+
],
|
|
24
|
+
gates: [
|
|
25
|
+
{ name: 'validate-commit.sh', desc: 'Enforces conventional commits + test gate' },
|
|
26
|
+
{ name: 'validate-write-gate.sh', desc: 'Blocks implementation before design doc exists' },
|
|
27
|
+
{ name: 'validate-file-naming.sh', desc: 'Enforces naming convention (kebab-case, etc.)' },
|
|
28
|
+
{ name: 'validate-no-placeholders.sh', desc: 'Blocks TBD/TODO in specs and plans' },
|
|
29
|
+
{ name: 'validate-dungeon.sh', desc: 'Requires multi-agent verification on pins' },
|
|
30
|
+
{ name: 'validate-browser-tests-exist.sh', desc: 'Checks Playwright tests exist before commits' },
|
|
31
|
+
{ name: 'validate-browser-cleanup.sh', desc: 'Verifies browser processes cleaned up properly' },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SKILLS = {
|
|
36
|
+
'raid-protocol': 'Session lifecycle and team rules',
|
|
37
|
+
'raid-design': 'Phase 1: adversarial exploration',
|
|
38
|
+
'raid-implementation-plan': 'Phase 2: task decomposition',
|
|
39
|
+
'raid-implementation': 'Phase 3: TDD with direct challenge',
|
|
40
|
+
'raid-review': 'Phase 4: independent review + fighting',
|
|
41
|
+
'raid-finishing': 'Completeness debate + merge options',
|
|
42
|
+
'raid-tdd': 'RED-GREEN-REFACTOR enforcement',
|
|
43
|
+
'raid-debugging': 'Root-cause investigation',
|
|
44
|
+
'raid-verification': 'Evidence-before-claims gate',
|
|
45
|
+
'raid-git-worktrees': 'Isolated workspace creation',
|
|
46
|
+
'raid-browser': 'Browser startup discovery',
|
|
47
|
+
'raid-browser-playwright': 'Playwright test authoring',
|
|
48
|
+
'raid-browser-chrome': 'Live browser inspection',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const CONFIG = {
|
|
52
|
+
'raid.json': 'Project settings (editable)',
|
|
53
|
+
'raid-rules.md': '17 team rules (editable)',
|
|
54
|
+
'settings.json': 'Hooks merged into existing (backup created)',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = { AGENTS, HOOKS, SKILLS, CONFIG };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Single-file detectors: first matching variant wins
|
|
7
|
+
const SINGLE_FILE_DETECTORS = [
|
|
8
|
+
{
|
|
9
|
+
variants: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
10
|
+
framework: 'next',
|
|
11
|
+
devCommand: (run) => `${run} dev`,
|
|
12
|
+
defaultPort: 3000,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
variants: ['nuxt.config.ts', 'nuxt.config.js'],
|
|
16
|
+
framework: 'nuxt',
|
|
17
|
+
devCommand: (run) => `${run} dev`,
|
|
18
|
+
defaultPort: 3000,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
variants: ['remix.config.js', 'remix.config.ts'],
|
|
22
|
+
framework: 'remix',
|
|
23
|
+
devCommand: (run) => `${run} dev`,
|
|
24
|
+
defaultPort: 3000,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
variants: ['svelte.config.js', 'svelte.config.ts'],
|
|
28
|
+
framework: 'svelte',
|
|
29
|
+
devCommand: (run) => `${run} dev`,
|
|
30
|
+
defaultPort: 5173,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
variants: ['vite.config.ts', 'vite.config.js', 'vite.config.mjs'],
|
|
34
|
+
framework: 'vite',
|
|
35
|
+
devCommand: (run) => `${run} dev`,
|
|
36
|
+
defaultPort: 5173,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
variants: ['angular.json'],
|
|
40
|
+
framework: 'angular',
|
|
41
|
+
devCommand: () => 'ng serve',
|
|
42
|
+
defaultPort: 4200,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
variants: ['astro.config.mjs', 'astro.config.js', 'astro.config.ts'],
|
|
46
|
+
framework: 'astro',
|
|
47
|
+
devCommand: (run) => `${run} dev`,
|
|
48
|
+
defaultPort: 4321,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
variants: ['gatsby-config.js', 'gatsby-config.ts'],
|
|
52
|
+
framework: 'gatsby',
|
|
53
|
+
devCommand: (run) => `${run} develop`,
|
|
54
|
+
defaultPort: 8000,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
variants: ['manage.py'],
|
|
58
|
+
framework: 'django',
|
|
59
|
+
devCommand: () => 'python manage.py runserver',
|
|
60
|
+
defaultPort: 8000,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
variants: ['trunk.toml'],
|
|
64
|
+
framework: 'trunk',
|
|
65
|
+
devCommand: () => 'trunk serve',
|
|
66
|
+
defaultPort: 8080,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// Multi-file detectors: ALL listed files must exist
|
|
71
|
+
const MULTI_FILE_DETECTORS = [
|
|
72
|
+
{
|
|
73
|
+
files: ['webpack.config.js', 'index.html'],
|
|
74
|
+
framework: 'webpack',
|
|
75
|
+
devCommand: (run) => `${run} dev`,
|
|
76
|
+
defaultPort: 8080,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Content-checked detectors: file must exist AND contain a marker string
|
|
81
|
+
const CONTENT_DETECTORS = [
|
|
82
|
+
{
|
|
83
|
+
file: 'app.py',
|
|
84
|
+
markers: ['flask', 'Flask'],
|
|
85
|
+
framework: 'flask',
|
|
86
|
+
devCommand: () => 'flask run',
|
|
87
|
+
defaultPort: 5000,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Directory+file detectors: the nested path must exist
|
|
92
|
+
const DIR_FILE_DETECTORS = [
|
|
93
|
+
{
|
|
94
|
+
filePath: path.join('app', 'root.tsx'),
|
|
95
|
+
framework: 'remix',
|
|
96
|
+
devCommand: (run) => `${run} dev`,
|
|
97
|
+
defaultPort: 3000,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
function detectBrowser(cwd, runCommand) {
|
|
102
|
+
// Check single-file detectors (first match wins)
|
|
103
|
+
for (const detector of SINGLE_FILE_DETECTORS) {
|
|
104
|
+
for (const variant of detector.variants) {
|
|
105
|
+
if (fs.existsSync(path.join(cwd, variant))) {
|
|
106
|
+
return {
|
|
107
|
+
detected: true,
|
|
108
|
+
framework: detector.framework,
|
|
109
|
+
devCommand: detector.devCommand(runCommand),
|
|
110
|
+
defaultPort: detector.defaultPort,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check content-based detectors (file must exist and contain marker)
|
|
117
|
+
for (const detector of CONTENT_DETECTORS) {
|
|
118
|
+
const filePath = path.join(cwd, detector.file);
|
|
119
|
+
if (fs.existsSync(filePath)) {
|
|
120
|
+
try {
|
|
121
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
122
|
+
if (detector.markers.some(m => content.includes(m))) {
|
|
123
|
+
return {
|
|
124
|
+
detected: true,
|
|
125
|
+
framework: detector.framework,
|
|
126
|
+
devCommand: detector.devCommand(runCommand),
|
|
127
|
+
defaultPort: detector.defaultPort,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Unreadable file, skip
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check multi-file detectors (all files must exist)
|
|
137
|
+
for (const detector of MULTI_FILE_DETECTORS) {
|
|
138
|
+
const allExist = detector.files.every((f) => fs.existsSync(path.join(cwd, f)));
|
|
139
|
+
if (allExist) {
|
|
140
|
+
return {
|
|
141
|
+
detected: true,
|
|
142
|
+
framework: detector.framework,
|
|
143
|
+
devCommand: detector.devCommand(runCommand),
|
|
144
|
+
defaultPort: detector.defaultPort,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check directory+file detectors
|
|
150
|
+
for (const detector of DIR_FILE_DETECTORS) {
|
|
151
|
+
if (fs.existsSync(path.join(cwd, detector.filePath))) {
|
|
152
|
+
return {
|
|
153
|
+
detected: true,
|
|
154
|
+
framework: detector.framework,
|
|
155
|
+
devCommand: detector.devCommand(runCommand),
|
|
156
|
+
defaultPort: detector.defaultPort,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { detectBrowser };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const JS_LOCKFILES = [
|
|
7
|
+
{
|
|
8
|
+
file: 'pnpm-lock.yaml',
|
|
9
|
+
packageManager: 'pnpm',
|
|
10
|
+
runCommand: 'pnpm',
|
|
11
|
+
execCommand: 'pnpm dlx',
|
|
12
|
+
installCommand: 'pnpm add',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
file: 'yarn.lock',
|
|
16
|
+
packageManager: 'yarn',
|
|
17
|
+
runCommand: 'yarn',
|
|
18
|
+
execCommand: 'yarn dlx',
|
|
19
|
+
installCommand: 'yarn add',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
file: 'bun.lockb',
|
|
23
|
+
packageManager: 'bun',
|
|
24
|
+
runCommand: 'bun',
|
|
25
|
+
execCommand: 'bunx',
|
|
26
|
+
installCommand: 'bun add',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
file: 'bun.lock',
|
|
30
|
+
packageManager: 'bun',
|
|
31
|
+
runCommand: 'bun',
|
|
32
|
+
execCommand: 'bunx',
|
|
33
|
+
installCommand: 'bun add',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
file: 'package-lock.json',
|
|
37
|
+
packageManager: 'npm',
|
|
38
|
+
runCommand: 'npm run',
|
|
39
|
+
execCommand: 'npx',
|
|
40
|
+
installCommand: 'npm install',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const PYTHON_LOCKFILES = [
|
|
45
|
+
{
|
|
46
|
+
file: 'uv.lock',
|
|
47
|
+
packageManager: 'uv',
|
|
48
|
+
runCommand: 'uv run',
|
|
49
|
+
execCommand: 'uvx',
|
|
50
|
+
installCommand: 'uv add',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
file: 'poetry.lock',
|
|
54
|
+
packageManager: 'poetry',
|
|
55
|
+
runCommand: 'poetry run',
|
|
56
|
+
execCommand: 'poetry run',
|
|
57
|
+
installCommand: 'poetry add',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const JS_FALLBACK = {
|
|
62
|
+
packageManager: 'npm',
|
|
63
|
+
runCommand: 'npm run',
|
|
64
|
+
execCommand: 'npx',
|
|
65
|
+
installCommand: 'npm install',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const PYTHON_FALLBACK = {
|
|
69
|
+
packageManager: 'pip',
|
|
70
|
+
runCommand: 'python -m',
|
|
71
|
+
execCommand: 'python -m',
|
|
72
|
+
installCommand: 'pip install',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function detectPackageManager(cwd, language) {
|
|
76
|
+
if (language === 'javascript') {
|
|
77
|
+
for (const entry of JS_LOCKFILES) {
|
|
78
|
+
if (fs.existsSync(path.join(cwd, entry.file))) {
|
|
79
|
+
return {
|
|
80
|
+
packageManager: entry.packageManager,
|
|
81
|
+
runCommand: entry.runCommand,
|
|
82
|
+
execCommand: entry.execCommand,
|
|
83
|
+
installCommand: entry.installCommand,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { ...JS_FALLBACK };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (language === 'python') {
|
|
91
|
+
for (const entry of PYTHON_LOCKFILES) {
|
|
92
|
+
if (fs.existsSync(path.join(cwd, entry.file))) {
|
|
93
|
+
return {
|
|
94
|
+
packageManager: entry.packageManager,
|
|
95
|
+
runCommand: entry.runCommand,
|
|
96
|
+
execCommand: entry.execCommand,
|
|
97
|
+
installCommand: entry.installCommand,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ...PYTHON_FALLBACK };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { detectPackageManager };
|
package/src/detect-project.js
CHANGED
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { detectPackageManager } = require('./detect-package-manager');
|
|
6
|
+
const { detectBrowser } = require('./detect-browser');
|
|
5
7
|
|
|
6
8
|
const DETECTORS = [
|
|
7
9
|
{
|
|
8
10
|
file: 'package.json',
|
|
9
11
|
language: 'javascript',
|
|
10
|
-
detect(cwd) {
|
|
12
|
+
detect(cwd, pm) {
|
|
11
13
|
const pkgPath = path.join(cwd, 'package.json');
|
|
14
|
+
const run = pm ? pm.runCommand : 'npm run';
|
|
15
|
+
// For pnpm/yarn/bun the runCommand is just the pm name (e.g. 'pnpm'),
|
|
16
|
+
// so 'pnpm test' is correct. For npm the runCommand is 'npm run',
|
|
17
|
+
// so we special-case 'npm run test' -> 'npm test'.
|
|
18
|
+
const testCmd = (run === 'npm run') ? 'npm test' : `${run} test`;
|
|
12
19
|
try {
|
|
13
20
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
14
21
|
const scripts = pkg.scripts || {};
|
|
15
22
|
return {
|
|
16
23
|
language: 'javascript',
|
|
17
|
-
testCommand: scripts.test ?
|
|
18
|
-
lintCommand: scripts.lint ?
|
|
19
|
-
buildCommand: scripts.build ?
|
|
24
|
+
testCommand: scripts.test ? testCmd : '',
|
|
25
|
+
lintCommand: scripts.lint ? `${run} lint` : '',
|
|
26
|
+
buildCommand: scripts.build ? `${run} build` : '',
|
|
20
27
|
name: pkg.name || path.basename(cwd),
|
|
21
28
|
};
|
|
22
29
|
} catch {
|
|
@@ -40,10 +47,29 @@ const DETECTORS = [
|
|
|
40
47
|
{
|
|
41
48
|
file: 'pyproject.toml',
|
|
42
49
|
language: 'python',
|
|
43
|
-
detect(cwd) {
|
|
50
|
+
detect(cwd, pm) {
|
|
44
51
|
try {
|
|
45
52
|
const content = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf8');
|
|
46
53
|
const usesPoetry = content.includes('[tool.poetry]');
|
|
54
|
+
// If a pm is provided (uv or poetry), use its runCommand
|
|
55
|
+
if (pm && pm.packageManager === 'uv') {
|
|
56
|
+
return {
|
|
57
|
+
language: 'python',
|
|
58
|
+
testCommand: 'uv run pytest',
|
|
59
|
+
lintCommand: 'uv run ruff check .',
|
|
60
|
+
buildCommand: 'uv run python -m build',
|
|
61
|
+
name: path.basename(cwd),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (pm && pm.packageManager === 'poetry') {
|
|
65
|
+
return {
|
|
66
|
+
language: 'python',
|
|
67
|
+
testCommand: 'poetry run pytest',
|
|
68
|
+
lintCommand: 'poetry run ruff check .',
|
|
69
|
+
buildCommand: 'poetry build',
|
|
70
|
+
name: path.basename(cwd),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
47
73
|
return {
|
|
48
74
|
language: 'python',
|
|
49
75
|
testCommand: usesPoetry ? 'poetry run pytest' : 'pytest',
|
|
@@ -89,7 +115,15 @@ function detectProject(cwd) {
|
|
|
89
115
|
|
|
90
116
|
for (const detector of DETECTORS) {
|
|
91
117
|
if (fs.existsSync(path.join(cwd, detector.file))) {
|
|
92
|
-
|
|
118
|
+
const pm = detectPackageManager(cwd, detector.language);
|
|
119
|
+
const entry = detector.detect(cwd, pm);
|
|
120
|
+
if (pm) {
|
|
121
|
+
entry.packageManager = pm.packageManager;
|
|
122
|
+
entry.runCommand = pm.runCommand;
|
|
123
|
+
entry.execCommand = pm.execCommand;
|
|
124
|
+
entry.installCommand = pm.installCommand;
|
|
125
|
+
}
|
|
126
|
+
detected.push(entry);
|
|
93
127
|
}
|
|
94
128
|
}
|
|
95
129
|
|
|
@@ -100,11 +134,15 @@ function detectProject(cwd) {
|
|
|
100
134
|
lintCommand: '',
|
|
101
135
|
buildCommand: '',
|
|
102
136
|
name: path.basename(cwd),
|
|
137
|
+
browser: null,
|
|
103
138
|
detected: [],
|
|
104
139
|
};
|
|
105
140
|
}
|
|
106
141
|
|
|
107
142
|
const primary = detected[0];
|
|
143
|
+
// Use the primary entry's runCommand for browser detection, fall back to 'npm run'
|
|
144
|
+
const primaryRunCommand = primary.runCommand || 'npm run';
|
|
145
|
+
primary.browser = detectBrowser(cwd, primaryRunCommand);
|
|
108
146
|
primary.detected = detected;
|
|
109
147
|
return primary;
|
|
110
148
|
}
|