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.
- package/README.md +82 -28
- package/bin/cli.js +31 -18
- package/package.json +3 -3
- 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 +45 -196
- package/src/init.js +57 -17
- package/src/merge-settings.js +62 -6
- package/src/remove.js +28 -4
- package/src/setup.js +363 -0
- package/src/ui.js +103 -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 +60 -64
- package/template/.claude/hooks/raid-lib.sh +168 -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 +55 -0
- package/template/.claude/hooks/raid-stop.sh +73 -0
- package/template/.claude/hooks/raid-task-completed.sh +33 -0
- package/template/.claude/hooks/raid-task-created.sh +40 -0
- package/template/.claude/hooks/raid-teammate-idle.sh +21 -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 +126 -0
- package/template/.claude/hooks/validate-dungeon.sh +115 -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 +25 -10
- 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/README.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# claude-raid
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
61
|
+
Diagnose your environment anytime:
|
|
21
62
|
|
|
22
63
|
```bash
|
|
23
|
-
claude
|
|
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
|
|
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
|
-
`
|
|
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
|
-
##
|
|
286
|
+
## Commands
|
|
246
287
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
267
|
-
- Does **not** touch `raid.json` (your
|
|
268
|
-
- Re-runs settings merge to
|
|
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
|
-
### `
|
|
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 #
|
|
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 #
|
|
285
|
-
│ ├── warrior.md # Aggressive
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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": "
|
|
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 };
|
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
|
}
|