cru-teams 1.0.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/README.md +84 -0
- package/package.json +33 -0
- package/skills/cru/SKILL.md +54 -0
- package/src/cli.ts +49 -0
- package/src/commands/clean.ts +78 -0
- package/src/commands/config.ts +11 -0
- package/src/commands/doctor.ts +82 -0
- package/src/commands/init.ts +35 -0
- package/src/commands/logs.ts +302 -0
- package/src/commands/panes.ts +376 -0
- package/src/commands/tasks.ts +110 -0
- package/src/commands/teams.ts +102 -0
- package/src/lib/config.ts +61 -0
- package/src/lib/env.ts +45 -0
- package/src/lib/ghostty.ts +153 -0
- package/src/lib/layout.ts +162 -0
- package/src/lib/mirror.ts +149 -0
- package/src/lib/panes.ts +62 -0
- package/src/lib/paths.ts +6 -0
- package/src/lib/preflight.ts +94 -0
- package/src/lib/teams.ts +57 -0
- package/src/lib/tmux.ts +167 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<p align="center"><img src="assets/logo.svg" width="128"/></p>
|
|
2
|
+
|
|
3
|
+
# ◫ cru
|
|
4
|
+
|
|
5
|
+
Layout for [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams), fixed.
|
|
6
|
+
|
|
7
|
+
Agent teams let multiple Claude Code instances work together in parallel — cru handles the terminal layout so you don't have to.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
╭──────────────────┬────────────────┬────────────────╮
|
|
11
|
+
│ │ │ │
|
|
12
|
+
│ │ ⚡ worker-1 │ ⚡ worker-2 │
|
|
13
|
+
│ │ │ │
|
|
14
|
+
│ lead ├────────────────┼────────────────┤
|
|
15
|
+
│ │ │ │
|
|
16
|
+
│ │ ⚡ worker-3 │ ⚡ worker-4 │
|
|
17
|
+
│ │ │ │
|
|
18
|
+
╰──────────────────┴────────────────┴────────────────╯
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## Terminal setup
|
|
24
|
+
|
|
25
|
+
- **Already using tmux?** You're good.
|
|
26
|
+
- **iTerm2?** Use [`tmux -CC`](https://iterm2.com/documentation-tmux-integration.html) for native pane integration.
|
|
27
|
+
- **Ghostty?** Workers still run in tmux (that's how Claude Code spawns agents), but cru mirrors them into native Ghostty splits via [AppleScript](https://ghostty.org/docs/features/applescript) — so you get Ghostty's UI instead of working inside tmux yourself. Requires Ghostty v1.3.0+.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g cru-teams
|
|
33
|
+
```
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add -g cru-teams
|
|
36
|
+
```
|
|
37
|
+
```bash
|
|
38
|
+
bun add -g cru-teams
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
1. Set up cru in your project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cru init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. In Claude Code, spawn a team:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/cru split the auth module into subtasks and parallelize across workers
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it — cru creates the panes, applies the grid layout, and your workers are ready to go.
|
|
56
|
+
|
|
57
|
+
## Use cases
|
|
58
|
+
|
|
59
|
+
**[Parallel feature work](https://code.claude.com/docs/en/agent-teams#when-to-use-agent-teams)** — split subtasks across workers, merge when done
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/cru break down the checkout flow into vertical slices, one worker per slice
|
|
63
|
+
```
|
|
64
|
+
**[Review crew](https://code.claude.com/docs/en/agent-teams#run-a-parallel-code-review)** — workers build, one reviews
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
/cru 3 review PR #142 — one on security, one on performance, one on test coverage
|
|
68
|
+
```
|
|
69
|
+
**[Competing hypotheses](https://code.claude.com/docs/en/agent-teams#investigate-with-competing-hypotheses)** — debug faster with multiple theories at once
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/cru users report the app crashes on login — each worker investigates a different theory
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**[Research spike](https://code.claude.com/docs/en/agent-teams#start-with-research-and-review)** — explore different approaches simultaneously
|
|
76
|
+
```
|
|
77
|
+
/cru 3 evaluate auth libraries — one on passport, one on lucia, one on arctic
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [CLI reference](CLI.md) — all commands, flags, and examples
|
|
83
|
+
- [Configuration](CONFIG.md) — layout options, config files, resolution order
|
|
84
|
+
- [Testing](TESTING.md) — test structure and how to run tests
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cru-teams",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "◫ Manage terminal layouts for Claude Code agent teams",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cru": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"skills/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun src/cli.ts",
|
|
15
|
+
"test": "bun test tests/unit/",
|
|
16
|
+
"test:e2e": "bun test tests/e2e/ --timeout 120000",
|
|
17
|
+
"publish": "npm publish --access public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"cru",
|
|
22
|
+
"tmux",
|
|
23
|
+
"layout"
|
|
24
|
+
],
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"incur": "^0.2.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "^1.3.10",
|
|
31
|
+
"@types/node": "^25.3.5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cru
|
|
3
|
+
description: Spawn an agent team with workers arranged in a grid layout. Use when the user wants to create a team of agents.
|
|
4
|
+
argument-hint: "<task description>"
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Spawn Agent Team
|
|
9
|
+
|
|
10
|
+
## Arguments
|
|
11
|
+
|
|
12
|
+
Parse `$ARGUMENTS` as a single string:
|
|
13
|
+
|
|
14
|
+
- If the **first word is a number**, use it as the worker count and the rest as the task description.
|
|
15
|
+
- If the **first word is NOT a number**, use the entire string as the task description and decide the worker count yourself based on the task (typically 2–5).
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
1. **Determine worker count and task** from `$ARGUMENTS` using the rules above.
|
|
20
|
+
|
|
21
|
+
2. **Create the team** using TeamCreate.
|
|
22
|
+
|
|
23
|
+
3. **Spawn workers** using the Agent tool. For each worker (worker-1 through worker-N):
|
|
24
|
+
- Set `team_name` to the team name from step 2
|
|
25
|
+
- Set `name` to "worker-1", "worker-2", etc.
|
|
26
|
+
- Set `subagent_type` to "general-purpose"
|
|
27
|
+
- Set `run_in_background` to true
|
|
28
|
+
- Give each worker its specific task slice in the `prompt`, plus:
|
|
29
|
+
- Context about what other workers are doing
|
|
30
|
+
- An instruction to message teammates to share findings and discuss
|
|
31
|
+
|
|
32
|
+
**IMPORTANT:** Spawn all workers AND apply the grid layout in a **single message** — include all Agent calls AND the Bash call for `cru panes grid` together. This ensures the grid command starts polling immediately while workers are still launching.
|
|
33
|
+
|
|
34
|
+
4. **Apply grid layout** (in the same message as step 3):
|
|
35
|
+
```bash
|
|
36
|
+
cru panes grid <team-name> --expect <worker-count>
|
|
37
|
+
```
|
|
38
|
+
This polls for worker panes (up to 30s) and arranges them in a grid. In Ghostty, it automatically mirrors tmux panes into native splits.
|
|
39
|
+
|
|
40
|
+
If the grid command fails (e.g., not in a tmux session), that's OK — workers still run as background agents with the team bar visible. Tell the user they can start a tmux session for the grid layout.
|
|
41
|
+
|
|
42
|
+
5. **Report** the team is ready. Tell the user:
|
|
43
|
+
- What each worker is focused on
|
|
44
|
+
- `cru panes close <team-name>` to shut down when done
|
|
45
|
+
|
|
46
|
+
## Shutdown
|
|
47
|
+
|
|
48
|
+
1. **Send shutdown requests first** — use `SendMessage` with `type: "shutdown_request"` to each worker. Wait for approvals (or idle notifications).
|
|
49
|
+
2. **Then close panes** — `cru panes close <team-name>`
|
|
50
|
+
3. **Then delete team** — `TeamDelete`
|
|
51
|
+
|
|
52
|
+
Closing panes before agents approve kills them mid-process, which causes `TeamDelete` to see "active" members and refuse cleanup.
|
|
53
|
+
|
|
54
|
+
Team data (logs, messages) is preserved after close — reviewable via `cru logs <team>`. Use `cru clean` to remove old teams.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Cli } from 'incur'
|
|
3
|
+
import { preflight } from '@/lib/preflight'
|
|
4
|
+
|
|
5
|
+
// Entity commands
|
|
6
|
+
import { teams } from '@/commands/teams'
|
|
7
|
+
import { panes } from '@/commands/panes'
|
|
8
|
+
import { tasks } from '@/commands/tasks'
|
|
9
|
+
|
|
10
|
+
// Layout
|
|
11
|
+
import { config } from '@/commands/config'
|
|
12
|
+
|
|
13
|
+
// Meta
|
|
14
|
+
import { init } from '@/commands/init'
|
|
15
|
+
import { doctor } from '@/commands/doctor'
|
|
16
|
+
import { logs } from '@/commands/logs'
|
|
17
|
+
import { clean } from '@/commands/clean'
|
|
18
|
+
|
|
19
|
+
// Commands that require specific preflight checks
|
|
20
|
+
const PREFLIGHT: Record<string, string[]> = {
|
|
21
|
+
panes: ['pane-session'],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Cli.create('cru', {
|
|
25
|
+
description: '◫ Manage pane layouts for Claude Code agent teams',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
})
|
|
28
|
+
.use(async (c, next) => {
|
|
29
|
+
const checks = PREFLIGHT[c.command]
|
|
30
|
+
if (checks) {
|
|
31
|
+
const result = preflight(...checks)
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
const e = result.errors[0]
|
|
34
|
+
return c.error({ code: e.check.toUpperCase(), message: e.message })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await next()
|
|
38
|
+
})
|
|
39
|
+
// Entity commands
|
|
40
|
+
.command('teams', teams)
|
|
41
|
+
.command('panes', panes)
|
|
42
|
+
.command('tasks', tasks)
|
|
43
|
+
// Config & meta
|
|
44
|
+
.command('config', config)
|
|
45
|
+
.command('init', init)
|
|
46
|
+
.command('doctor', doctor)
|
|
47
|
+
.command('logs', logs)
|
|
48
|
+
.command('clean', clean)
|
|
49
|
+
.serve()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readdirSync, rmSync, statSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { z } from 'incur'
|
|
5
|
+
import { isTeamAlive } from '@/lib/panes'
|
|
6
|
+
import { teamsDir } from '@/lib/paths'
|
|
7
|
+
|
|
8
|
+
export const clean = {
|
|
9
|
+
description: 'Remove old team data from ~/.claude/teams',
|
|
10
|
+
options: z.object({
|
|
11
|
+
days: z.coerce.number().default(7).describe('Remove teams older than N days'),
|
|
12
|
+
all: z.boolean().default(false).describe('Remove all teams'),
|
|
13
|
+
'dry-run': z.boolean().default(false).describe('Show what would be removed without deleting'),
|
|
14
|
+
}),
|
|
15
|
+
run(c) {
|
|
16
|
+
const dir = teamsDir()
|
|
17
|
+
let teams: string[]
|
|
18
|
+
try {
|
|
19
|
+
teams = readdirSync(dir).filter((d) => {
|
|
20
|
+
try {
|
|
21
|
+
statSync(join(dir, d, 'config.json'))
|
|
22
|
+
return true
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
} catch {
|
|
28
|
+
return { removed: 0, teams: [] }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
const maxAge = c.options.days * 24 * 60 * 60 * 1000
|
|
33
|
+
const toRemove: Array<{ name: string; age: string }> = []
|
|
34
|
+
|
|
35
|
+
for (const name of teams) {
|
|
36
|
+
const configPath = join(dir, name, 'config.json')
|
|
37
|
+
try {
|
|
38
|
+
const stat = statSync(configPath)
|
|
39
|
+
const age = now - stat.mtimeMs
|
|
40
|
+
const dead = !isTeamAlive(name)
|
|
41
|
+
|
|
42
|
+
if (c.options.all || age > maxAge || dead) {
|
|
43
|
+
const days = Math.floor(age / (24 * 60 * 60 * 1000))
|
|
44
|
+
const reason = dead ? 'dead' : days === 0 ? 'today' : `${days}d ago`
|
|
45
|
+
toRemove.push({ name, age: reason })
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (toRemove.length === 0) {
|
|
51
|
+
if (!c.agent) console.log('Nothing to clean')
|
|
52
|
+
return { removed: 0, teams: [] }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (c.options['dry-run']) {
|
|
56
|
+
if (!c.agent) {
|
|
57
|
+
console.log(`Would remove ${toRemove.length} teams:\n`)
|
|
58
|
+
for (const t of toRemove) console.log(` ${t.name} ${t.age}`)
|
|
59
|
+
}
|
|
60
|
+
return { 'dry-run': true, would_remove: toRemove.length, teams: toRemove }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tasksBaseDir = join(homedir(), '.claude', 'tasks')
|
|
64
|
+
for (const t of toRemove) {
|
|
65
|
+
rmSync(join(dir, t.name), { recursive: true, force: true })
|
|
66
|
+
// Also clean up tasks for this team
|
|
67
|
+
try {
|
|
68
|
+
rmSync(join(tasksBaseDir, t.name), { recursive: true, force: true })
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!c.agent) {
|
|
73
|
+
console.log(`Removed ${toRemove.length} teams`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { removed: toRemove.length, teams: toRemove }
|
|
77
|
+
},
|
|
78
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { configPaths, loadConfig } from '@/lib/config'
|
|
3
|
+
|
|
4
|
+
export const config = {
|
|
5
|
+
description: 'Show resolved config (merged defaults + overrides)',
|
|
6
|
+
run() {
|
|
7
|
+
const conf = loadConfig()
|
|
8
|
+
const loaded = configPaths().find((p) => existsSync(p)) || 'defaults'
|
|
9
|
+
return { source: loaded, config: conf }
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'incur'
|
|
2
|
+
import { hasBinary, getVersion, inTmux, inGhostty, detectTerminal } from '@/lib/env'
|
|
3
|
+
|
|
4
|
+
export const doctor = {
|
|
5
|
+
description: 'Check environment requirements for cru',
|
|
6
|
+
args: z.object({}),
|
|
7
|
+
options: z.object({
|
|
8
|
+
json: z.boolean().default(false).describe('Output as JSON'),
|
|
9
|
+
}),
|
|
10
|
+
run(c) {
|
|
11
|
+
const terminal = detectTerminal()
|
|
12
|
+
const tmuxCmd = terminal === 'iterm2' ? 'tmux -CC' : 'tmux'
|
|
13
|
+
|
|
14
|
+
const checks: Array<{
|
|
15
|
+
name: string
|
|
16
|
+
status: 'ok' | 'fail'
|
|
17
|
+
detail: string
|
|
18
|
+
fix?: string
|
|
19
|
+
}> = []
|
|
20
|
+
|
|
21
|
+
const tmuxPath = hasBinary('tmux')
|
|
22
|
+
if (!tmuxPath) {
|
|
23
|
+
checks.push({
|
|
24
|
+
name: 'tmux',
|
|
25
|
+
status: 'fail',
|
|
26
|
+
detail: 'not installed',
|
|
27
|
+
fix: process.platform === 'darwin' ? 'brew install tmux' : 'sudo apt install tmux',
|
|
28
|
+
})
|
|
29
|
+
} else {
|
|
30
|
+
checks.push({ name: 'tmux', status: 'ok', detail: getVersion('tmux -V') || 'installed' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (tmuxPath && !inTmux() && !inGhostty()) {
|
|
34
|
+
checks.push({ name: 'tmux-session', status: 'fail', detail: 'not inside a tmux session', fix: tmuxCmd })
|
|
35
|
+
} else if (tmuxPath && inTmux()) {
|
|
36
|
+
checks.push({ name: 'tmux-session', status: 'ok', detail: 'active' })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const claudePath = hasBinary('claude')
|
|
40
|
+
if (!claudePath) {
|
|
41
|
+
checks.push({ name: 'claude', status: 'fail', detail: 'not installed', fix: 'npm install -g @anthropic-ai/claude-code' })
|
|
42
|
+
} else {
|
|
43
|
+
checks.push({ name: 'claude', status: 'ok', detail: getVersion('claude --version') || 'installed' })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ghostty native pane support (AppleScript)
|
|
47
|
+
if (terminal === 'ghostty') {
|
|
48
|
+
if (process.platform !== 'darwin') {
|
|
49
|
+
checks.push({ name: 'ghostty', status: 'fail', detail: 'AppleScript only available on macOS' })
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
const { ghosttyVersion, isGhosttyScriptable } = require('@/lib/ghostty')
|
|
53
|
+
if (isGhosttyScriptable()) {
|
|
54
|
+
const ver = ghosttyVersion() || 'unknown'
|
|
55
|
+
checks.push({ name: 'ghostty', status: 'ok', detail: `v${ver} (AppleScript enabled)` })
|
|
56
|
+
} else {
|
|
57
|
+
checks.push({ name: 'ghostty', status: 'fail', detail: 'AppleScript not responding', fix: 'Set macos-applescript = true in Ghostty config' })
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
checks.push({ name: 'ghostty', status: 'fail', detail: 'cannot connect via AppleScript' })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (inGhostty()) {
|
|
65
|
+
checks.push({ name: 'pane-backend', status: 'ok', detail: 'ghostty (native AppleScript)' })
|
|
66
|
+
} else if (inTmux()) {
|
|
67
|
+
checks.push({ name: 'pane-backend', status: 'ok', detail: 'tmux (inside Ghostty)' })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bunPath = hasBinary('bun')
|
|
72
|
+
if (!bunPath) {
|
|
73
|
+
checks.push({ name: 'bun', status: 'fail', detail: 'not installed', fix: 'curl -fsSL https://bun.sh/install | bash' })
|
|
74
|
+
} else {
|
|
75
|
+
checks.push({ name: 'bun', status: 'ok', detail: `v${getVersion('bun --version')}` })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ok = checks.every((ch) => ch.status === 'ok')
|
|
79
|
+
|
|
80
|
+
return { ok, terminal, tmuxCmd, checks }
|
|
81
|
+
},
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'incur'
|
|
2
|
+
import { existsSync, mkdirSync, cpSync } from 'node:fs'
|
|
3
|
+
import { join, dirname } from 'node:path'
|
|
4
|
+
import { CONFIG_NAME, DEFAULTS, writeConfig } from '@/lib/config'
|
|
5
|
+
|
|
6
|
+
export const init = {
|
|
7
|
+
description: 'Set up cru in the current project',
|
|
8
|
+
options: z.object({
|
|
9
|
+
force: z.boolean().default(false).describe('Overwrite existing files'),
|
|
10
|
+
}),
|
|
11
|
+
run(c) {
|
|
12
|
+
const cwd = process.cwd()
|
|
13
|
+
const files: Record<string, string> = {}
|
|
14
|
+
|
|
15
|
+
// 1. Config file
|
|
16
|
+
const configPath = join(cwd, CONFIG_NAME)
|
|
17
|
+
if (!existsSync(configPath) || c.options.force) {
|
|
18
|
+
writeConfig(configPath, DEFAULTS)
|
|
19
|
+
files[CONFIG_NAME] = 'created'
|
|
20
|
+
} else {
|
|
21
|
+
files[CONFIG_NAME] = 'exists'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Install skill to .claude/skills/
|
|
25
|
+
const pkgRoot = dirname(dirname(import.meta.dirname))
|
|
26
|
+
const skillSrc = join(pkgRoot, 'skills', 'cru')
|
|
27
|
+
const skillDest = join(cwd, '.claude', 'skills', 'cru')
|
|
28
|
+
|
|
29
|
+
mkdirSync(join(cwd, '.claude', 'skills'), { recursive: true })
|
|
30
|
+
cpSync(skillSrc, skillDest, { recursive: true })
|
|
31
|
+
files['.claude/skills/cru/'] = existsSync(join(skillDest, 'SKILL.md')) ? 'updated' : 'installed'
|
|
32
|
+
|
|
33
|
+
return { files, tip: 'Use /cru in Claude Code to spawn a team.' }
|
|
34
|
+
},
|
|
35
|
+
}
|