claude-raid 0.1.0 → 0.1.2

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.
Files changed (50) hide show
  1. package/README.md +82 -28
  2. package/bin/cli.js +31 -18
  3. package/package.json +3 -3
  4. package/src/detect-browser.js +164 -0
  5. package/src/detect-package-manager.js +107 -0
  6. package/src/detect-project.js +44 -6
  7. package/src/doctor.js +45 -196
  8. package/src/init.js +57 -17
  9. package/src/merge-settings.js +62 -6
  10. package/src/remove.js +28 -4
  11. package/src/setup.js +363 -0
  12. package/src/ui.js +103 -0
  13. package/src/update.js +62 -5
  14. package/src/version-check.js +130 -0
  15. package/template/.claude/agents/archer.md +46 -51
  16. package/template/.claude/agents/rogue.md +43 -49
  17. package/template/.claude/agents/warrior.md +48 -53
  18. package/template/.claude/agents/wizard.md +60 -64
  19. package/template/.claude/hooks/raid-lib.sh +168 -0
  20. package/template/.claude/hooks/raid-pre-compact.sh +41 -0
  21. package/template/.claude/hooks/raid-session-end.sh +116 -0
  22. package/template/.claude/hooks/raid-session-start.sh +55 -0
  23. package/template/.claude/hooks/raid-stop.sh +73 -0
  24. package/template/.claude/hooks/raid-task-completed.sh +33 -0
  25. package/template/.claude/hooks/raid-task-created.sh +40 -0
  26. package/template/.claude/hooks/raid-teammate-idle.sh +21 -0
  27. package/template/.claude/hooks/validate-browser-cleanup.sh +36 -0
  28. package/template/.claude/hooks/validate-browser-tests-exist.sh +52 -0
  29. package/template/.claude/hooks/validate-commit.sh +126 -0
  30. package/template/.claude/hooks/validate-dungeon.sh +115 -0
  31. package/template/.claude/hooks/validate-file-naming.sh +13 -27
  32. package/template/.claude/hooks/validate-no-placeholders.sh +11 -21
  33. package/template/.claude/hooks/validate-write-gate.sh +60 -0
  34. package/template/.claude/raid-rules.md +27 -18
  35. package/template/.claude/skills/raid-browser/SKILL.md +186 -0
  36. package/template/.claude/skills/raid-browser-chrome/SKILL.md +189 -0
  37. package/template/.claude/skills/raid-browser-playwright/SKILL.md +163 -0
  38. package/template/.claude/skills/raid-debugging/SKILL.md +6 -6
  39. package/template/.claude/skills/raid-design/SKILL.md +10 -10
  40. package/template/.claude/skills/raid-finishing/SKILL.md +11 -3
  41. package/template/.claude/skills/raid-implementation/SKILL.md +25 -10
  42. package/template/.claude/skills/raid-implementation-plan/SKILL.md +15 -4
  43. package/template/.claude/skills/raid-protocol/SKILL.md +57 -32
  44. package/template/.claude/skills/raid-review/SKILL.md +42 -13
  45. package/template/.claude/skills/raid-tdd/SKILL.md +45 -3
  46. package/template/.claude/skills/raid-verification/SKILL.md +12 -1
  47. package/template/.claude/hooks/validate-commit-message.sh +0 -78
  48. package/template/.claude/hooks/validate-phase-gate.sh +0 -60
  49. package/template/.claude/hooks/validate-tests-pass.sh +0 -43
  50. package/template/.claude/hooks/validate-verification.sh +0 -70
package/README.md CHANGED
@@ -1,6 +1,25 @@
1
1
  # claude-raid
2
2
 
3
- **Adversarial multi-agent development system for [Claude Code](https://claude.ai/code).**
3
+ ```ansi
4
+ [33m ⚔ ═══════════════════════════════════════════════════════ ⚔[0m
5
+
6
+ [1;33m ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗[0m
7
+ [1;33m ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝[0m
8
+ [1;33m ██║ ██║ ███████║██║ ██║██║ ██║█████╗ [0m
9
+ [1;33m ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ [0m
10
+ [1;33m ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗[0m
11
+ [1;33m ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝[0m
12
+ [1;31m ██████╗ █████╗ ██╗██████╗ [0m
13
+ [1;31m ██╔══██╗██╔══██╗██║██╔══██╗[0m
14
+ [1;31m ██████╔╝███████║██║██║ ██║[0m
15
+ [1;31m ██╔══██╗██╔══██║██║██║ ██║[0m
16
+ [1;31m ██║ ██║██║ ██║██║██████╔╝[0m
17
+ [1;31m ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═════╝ [0m
18
+
19
+ [90m Adversarial multi-agent warfare for Claude Code[0m
20
+
21
+ [33m ⚔ ═══════════════════════════════════════════════════════ ⚔[0m
22
+ ```
4
23
 
5
24
  Four agents -- Wizard, Warrior, Archer, Rogue -- work through a strict 4-phase workflow where every decision, implementation, and review is stress-tested by competing agents who learn from each other's mistakes and push every finding to its edges.
6
25
 
@@ -8,19 +27,41 @@ Adapted from [obra/superpowers](https://github.com/obra/superpowers) by Jesse Vi
8
27
 
9
28
  ---
10
29
 
11
- ## Quick Start
30
+ ## Summon the Party
31
+
32
+ ```bash
33
+ npx claude-raid summon
34
+ ```
35
+
36
+ The installer auto-detects your realm (project type), copies agents, skills, and hooks into `.claude/`, and walks you through environment setup -- all in one command.
37
+
38
+ Then start a Raid:
12
39
 
13
40
  ```bash
14
- npx claude-raid init
15
41
  claude --agent wizard
16
42
  ```
17
43
 
18
- That's it. The installer auto-detects your project type, generates configuration, and merges with your existing Claude Code setup. Nothing is overwritten.
44
+ That's it. Describe your task and the Wizard takes over.
45
+
46
+ ### Prerequisites
47
+
48
+ The setup wizard checks these automatically during `summon`:
49
+
50
+ | Requirement | Why | Auto-configured? |
51
+ |---|---|---|
52
+ | **Claude Code** v2.1.32+ | Agent teams support | No -- install/update manually |
53
+ | **Node.js** 18+ | Runs the installer | No -- install manually |
54
+ | **teammateMode** in `~/.claude.json` | Display mode for agent sessions | Yes -- wizard prompts you |
55
+ | **tmux** or **iTerm2** | Split-pane mode (optional) | No -- install manually |
56
+
57
+ `jq` is required for hooks (pre-installed on macOS, `apt install jq` on Linux).
58
+
59
+ The experimental agent teams flag (`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`) is set automatically in your project's `.claude/settings.json` during install.
19
60
 
20
- For split-pane mode (recommended for watching agents interact):
61
+ Diagnose your environment anytime:
21
62
 
22
63
  ```bash
23
- claude --agent wizard --teammate-mode tmux
64
+ npx claude-raid heal
24
65
  ```
25
66
 
26
67
  ## How It Works
@@ -40,7 +81,7 @@ Phase 2: PLAN Agents decompose the design into tasks.
40
81
  Agreed tasks pinned to the Dungeon.
41
82
 
42
83
  Phase 3: IMPLEMENTATION One agent implements each task. The others attack
43
- directly and attack each other's reviews too.
84
+ directly -- and attack each other's reviews too.
44
85
  TDD enforced. Every task earns approval.
45
86
 
46
87
  Phase 4: REVIEW Independent reviews, then agents fight over findings
@@ -188,7 +229,7 @@ Edit this file to add project-specific rules. Updates via `claude-raid update` w
188
229
 
189
230
  ## Configuration
190
231
 
191
- `npx claude-raid init` auto-detects your project and generates `.claude/raid.json`:
232
+ `claude-raid summon` auto-detects your realm and generates `.claude/raid.json`:
192
233
 
193
234
  ```json
194
235
  {
@@ -242,15 +283,20 @@ Commands are auto-detected from your project files (e.g., `scripts.test` in `pac
242
283
  | `conventions.maxDepth` | `8` | Maximum file nesting depth |
243
284
  | `raid.defaultMode` | `full` | Default mode: `full`, `skirmish`, `scout` |
244
285
 
245
- ## CLI Commands
286
+ ## Commands
246
287
 
247
- ```bash
248
- npx claude-raid init # Install into current project
249
- npx claude-raid update # Update agents, skills, hooks (preserves raid.json)
250
- npx claude-raid remove # Uninstall and restore original settings
251
- ```
288
+ | Command | What it does |
289
+ |---|---|
290
+ | `claude-raid summon` | Summon the party into this realm |
291
+ | `claude-raid update` | Reforge the party's arsenal |
292
+ | `claude-raid dismantle` | Dismantle the camp and retreat |
293
+ | `claude-raid heal` | Diagnose wounds and prepare for battle |
252
294
 
253
- ### `init`
295
+ > Old names (`init`, `remove`, `doctor`) still work as aliases.
296
+
297
+ ### `summon`
298
+
299
+ Summons the full Raid party into your project.
254
300
 
255
301
  - Creates `.claude/` if absent
256
302
  - Auto-detects project type and generates `raid.json`
@@ -258,31 +304,45 @@ npx claude-raid remove # Uninstall and restore original settings
258
304
  - Merges settings into existing `settings.json` (with backup)
259
305
  - Makes hooks executable
260
306
  - Adds session files to `.gitignore`
307
+ - Runs the setup wizard to check and configure prerequisites
261
308
  - **Never overwrites** existing files with the same name
262
309
 
263
310
  ### `update`
264
311
 
312
+ Reforges the party's weapons and armor with the latest version.
313
+
265
314
  - Overwrites hooks, skills, and `raid-rules.md` with latest versions
266
- - **Skips customized agents** (warns you which ones were preserved)
267
- - Does **not** touch `raid.json` (your project config)
268
- - Re-runs settings merge to add any new hooks
315
+ - **Skips customized agents** (warns you which warriors were preserved)
316
+ - Does **not** touch `raid.json` (your realm config stays intact)
317
+ - Re-runs settings merge to pick up new hooks
269
318
 
270
- ### `remove`
319
+ ### `dismantle`
320
+
321
+ Dismantles the camp and restores your realm to its former state.
271
322
 
272
323
  - Removes all Raid agents, hooks, skills, and config files
273
324
  - Restores `settings.json` from backup (if backup exists)
274
325
  - Preserves non-Raid files in `.claude/`
275
326
 
327
+ ### `heal`
328
+
329
+ Diagnoses wounds and prepares the party for battle.
330
+
331
+ - Checks Node.js, Claude Code, teammateMode, and split-pane support
332
+ - Offers to fix missing configuration interactively
333
+ - Shows Quick Start, Controls, and Raid Modes reference
334
+ - Exits with code 1 in CI when required checks fail
335
+
276
336
  ## What Gets Installed
277
337
 
278
338
  ```
279
339
  .claude/
280
- ├── raid.json # Project config (auto-generated, editable)
340
+ ├── raid.json # Realm config (auto-generated, editable)
281
341
  ├── raid-rules.md # Team rules (editable)
282
342
  ├── settings.json # Merged with existing (backup at .pre-raid-backup)
283
343
  ├── agents/
284
- │ ├── wizard.md # Lead coordinator
285
- │ ├── warrior.md # Aggressive explorer
344
+ │ ├── wizard.md # Dungeon master
345
+ │ ├── warrior.md # Aggressive stress-tester
286
346
  │ ├── archer.md # Precision pattern-seeker
287
347
  │ └── rogue.md # Adversarial assumption-destroyer
288
348
  ├── hooks/
@@ -318,12 +378,6 @@ The Raid is a tool in your toolkit, not your project's operating system.
318
378
  - **Clean removal** restores your original `settings.json` from backup
319
379
  - **Zero npm dependencies** -- pure Node.js stdlib, fast `npx` cold-start
320
380
 
321
- ## Requirements
322
-
323
- - [Claude Code](https://claude.ai/code) v2.1.32+
324
- - Node.js 18+ (for installation only -- the installed files are language-agnostic)
325
- - `jq` (for hooks -- pre-installed on macOS, available via `apt install jq` on Linux)
326
-
327
381
  ## Inherited from Superpowers
328
382
 
329
383
  The Raid inherits and adapts the [Superpowers](https://github.com/obra/superpowers) behavioral harness:
package/bin/cli.js CHANGED
@@ -3,32 +3,45 @@
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
- init: () => require('../src/init').run(),
13
+ // Primary commands
14
+ summon: () => require('../src/init').run(),
9
15
  update: () => require('../src/update').run(),
16
+ dismantle: () => require('../src/remove').run(),
17
+ heal: () => require('../src/doctor').run(),
18
+ // Aliases (backward compat)
19
+ init: () => require('../src/init').run(),
10
20
  remove: () => require('../src/remove').run(),
11
21
  doctor: () => require('../src/doctor').run(),
12
22
  };
13
23
 
14
24
  if (!command || !COMMANDS[command]) {
15
- console.log(`
16
- claude-raid Adversarial multi-agent development for Claude Code
17
-
18
- Usage:
19
- claude-raid init Install The Raid into the current project
20
- claude-raid update Update to the latest version
21
- claude-raid remove Uninstall The Raid
22
- claude-raid doctor Check prerequisites and show quick start guide
23
-
24
- Learn more: https://github.com/pedropicardi/claude-raid
25
- `);
25
+ console.log('\n' + banner());
26
+ console.log(header('Commands') + '\n');
27
+ const cmds = [
28
+ ['summon', 'Summon the party into this realm'],
29
+ ['update', 'Reforge the party\'s arsenal'],
30
+ ['dismantle', 'Dismantle the camp and retreat'],
31
+ ['heal', 'Diagnose wounds and prepare for battle'],
32
+ ];
33
+ for (const [name, desc] of cmds) {
34
+ console.log(' ' + colors.bold(name.padEnd(12)) + desc);
35
+ }
36
+ console.log(header('Begin the Raid') + '\n');
37
+ console.log(' claude --agent wizard\n');
38
+ console.log(colors.dim(' github.com/pedropicardi/claude-raid') + '\n');
26
39
  process.exit(command ? 1 : 0);
27
40
  }
28
41
 
29
- try {
30
- COMMANDS[command]();
31
- } catch (err) {
32
- console.error(`\nclaude-raid: ${err.message}\n`);
33
- process.exit(1);
34
- }
42
+ Promise.resolve(COMMANDS[command]())
43
+ .then(() => showUpdateNotice(colors))
44
+ .catch((err) => {
45
+ console.error(`\nclaude-raid: ${err.message}\n`);
46
+ process.exit(1);
47
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-raid",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "commonjs",
5
5
  "description": "Adversarial multi-agent development system for Claude Code",
6
6
  "author": "Pedro Picardi",
@@ -9,10 +9,10 @@
9
9
  "bugs": "https://github.com/pedropicardi/claude-raid/issues",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/pedropicardi/claude-raid.git"
12
+ "url": "git+https://github.com/pedropicardi/claude-raid.git"
13
13
  },
14
14
  "bin": {
15
- "claude-raid": "./bin/cli.js"
15
+ "claude-raid": "bin/cli.js"
16
16
  },
17
17
  "files": [
18
18
  "bin/",
@@ -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 };
@@ -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 ? 'npm test' : '',
18
- lintCommand: scripts.lint ? 'npm run lint' : '',
19
- buildCommand: scripts.build ? 'npm run 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
- detected.push(detector.detect(cwd));
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
  }