tmux-ide 1.0.0 → 1.2.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 -35
- package/bin/cli.js +133 -85
- package/package.json +26 -5
- package/scripts/postinstall.js +46 -7
- package/skill/SKILL.md +63 -12
- package/src/attach.js +7 -12
- package/src/config.js +100 -18
- package/src/detect.js +77 -23
- package/src/doctor.js +23 -14
- package/src/init.js +10 -18
- package/src/inspect.js +127 -0
- package/src/launch.js +186 -179
- package/src/lib/errors.js +35 -0
- package/src/lib/launch-plan.js +49 -0
- package/src/lib/output.js +18 -22
- package/src/lib/session-monitor.js +179 -0
- package/src/lib/session-options.js +95 -0
- package/src/lib/tmux.js +234 -0
- package/src/lib/yaml-io.js +2 -2
- package/src/ls.js +2 -4
- package/src/restart.js +6 -13
- package/src/status.js +5 -23
- package/src/stop.js +12 -10
- package/src/validate.js +1 -17
- package/src/detect.test.js +0 -115
- package/src/integration.test.js +0 -94
- package/src/lib/dot-path.test.js +0 -67
- package/src/lib/sizes.test.js +0 -74
- package/src/validate.test.js +0 -141
package/skill/SKILL.md
CHANGED
|
@@ -17,6 +17,7 @@ tmux-ide turns any project into a tmux-powered terminal IDE using a simple `ide.
|
|
|
17
17
|
3. **Present 2-3 layout options using ASCII diagrams** before writing config. Example:
|
|
18
18
|
|
|
19
19
|
**Option A — Dual Claude + Dev (recommended)**
|
|
20
|
+
|
|
20
21
|
```
|
|
21
22
|
┌─────────────────┬─────────────────┐
|
|
22
23
|
│ │ │
|
|
@@ -28,6 +29,7 @@ tmux-ide turns any project into a tmux-powered terminal IDE using a simple `ide.
|
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
**Option B — Triple Claude**
|
|
32
|
+
|
|
31
33
|
```
|
|
32
34
|
┌───────────┬───────────┬───────────┐
|
|
33
35
|
│ │ │ │
|
|
@@ -39,6 +41,7 @@ tmux-ide turns any project into a tmux-powered terminal IDE using a simple `ide.
|
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
**Option C — Single Claude + wide dev**
|
|
44
|
+
|
|
42
45
|
```
|
|
43
46
|
┌─────────────────────────────────────┐
|
|
44
47
|
│ Claude │ 60%
|
|
@@ -55,7 +58,7 @@ tmux-ide turns any project into a tmux-powered terminal IDE using a simple `ide.
|
|
|
55
58
|
|
|
56
59
|
## Agent Teams workflow
|
|
57
60
|
|
|
58
|
-
Agent teams coordinate multiple Claude Code instances where a lead delegates tasks to teammates. Each gets its own tmux pane.
|
|
61
|
+
Agent teams coordinate multiple Claude Code instances where a lead delegates tasks to teammates. Each gets its own tmux pane, and tmux-ide prepares that layout before Claude starts the actual team workflow.
|
|
59
62
|
|
|
60
63
|
### When to suggest agent teams
|
|
61
64
|
|
|
@@ -66,38 +69,50 @@ Agent teams coordinate multiple Claude Code instances where a lead delegates tas
|
|
|
66
69
|
|
|
67
70
|
### Prerequisites
|
|
68
71
|
|
|
69
|
-
Agent teams require `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. tmux-ide
|
|
72
|
+
Agent teams require `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. The tmux-ide install hook enables this in Claude Code settings, and tmux-ide also sets it automatically inside tmux sessions when `team` is configured in `ide.yml`.
|
|
70
73
|
|
|
71
74
|
### Setup from scratch
|
|
72
75
|
|
|
73
76
|
1. **Scaffold with agent team template:**
|
|
77
|
+
|
|
74
78
|
```bash
|
|
75
79
|
tmux-ide init --template agent-team
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
2. **Or enable teams on an existing config:**
|
|
83
|
+
|
|
79
84
|
```bash
|
|
80
85
|
tmux-ide config enable-team --name "my-team"
|
|
81
86
|
```
|
|
87
|
+
|
|
82
88
|
This finds all `command: claude` panes and assigns the first as `lead`, the rest as `teammate`.
|
|
83
89
|
|
|
84
|
-
3. **Assign initial
|
|
90
|
+
3. **Assign initial task hints to teammate panes:**
|
|
91
|
+
|
|
85
92
|
```bash
|
|
86
93
|
tmux-ide config set rows.0.panes.1.task "Work on frontend components"
|
|
87
94
|
tmux-ide config set rows.0.panes.2.task "Work on API routes"
|
|
88
95
|
```
|
|
89
96
|
|
|
90
|
-
4. **Validate and launch:**
|
|
97
|
+
4. **Validate and launch the layout:**
|
|
98
|
+
|
|
91
99
|
```bash
|
|
92
100
|
tmux-ide validate --json
|
|
93
101
|
tmux-ide
|
|
94
102
|
```
|
|
95
103
|
|
|
104
|
+
5. **Inside the lead pane, ask Claude to form the team:**
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Start an agent team named my-team. Use the Frontend pane for components and the Backend pane for API routes.
|
|
108
|
+
```
|
|
109
|
+
|
|
96
110
|
### Present team layout options
|
|
97
111
|
|
|
98
|
-
When suggesting agent team layouts, show the roles:
|
|
112
|
+
When suggesting agent team layouts, show the roles and note that Claude will create the team after launch:
|
|
99
113
|
|
|
100
114
|
**Option A — Lead + 2 Teammates**
|
|
115
|
+
|
|
101
116
|
```
|
|
102
117
|
┌───────────┬───────────┬───────────┐
|
|
103
118
|
│ │ │ │
|
|
@@ -109,6 +124,7 @@ When suggesting agent team layouts, show the roles:
|
|
|
109
124
|
```
|
|
110
125
|
|
|
111
126
|
**Option B — Lead + 3 Specialized Teammates**
|
|
127
|
+
|
|
112
128
|
```
|
|
113
129
|
┌────────┬────────┬────────┬────────┐
|
|
114
130
|
│ │Frontend│Backend │ Review │
|
|
@@ -121,7 +137,7 @@ When suggesting agent team layouts, show the roles:
|
|
|
121
137
|
|
|
122
138
|
### Team lead self-configuration
|
|
123
139
|
|
|
124
|
-
When running as the team lead inside a tmux-ide session, you can reconfigure the team:
|
|
140
|
+
When running as the team lead inside a tmux-ide session, you can reconfigure the layout for the team:
|
|
125
141
|
|
|
126
142
|
```bash
|
|
127
143
|
# Read current config
|
|
@@ -148,6 +164,38 @@ tmux-ide config disable-team
|
|
|
148
164
|
|
|
149
165
|
Removes the `team` config and all `role`/`task` fields from panes.
|
|
150
166
|
|
|
167
|
+
## Session features (v1.2.0)
|
|
168
|
+
|
|
169
|
+
tmux-ide sessions include these built-in features:
|
|
170
|
+
|
|
171
|
+
### Mouse support
|
|
172
|
+
|
|
173
|
+
Mouse is enabled by default. Users can click to focus panes, scroll with trackpad, and drag pane borders to resize.
|
|
174
|
+
|
|
175
|
+
### Two-line status bar
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Line 0: MY-PROJECT IDE ● 14:30 │ Mar 17
|
|
179
|
+
Line 1: ⏺ Claude 1 │ ● Claude 2 │ ⏺ Dev Server │ Shell
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- Line 0: session name, window indicators, time/date
|
|
183
|
+
- Line 1: clickable pane tabs (click to switch panes)
|
|
184
|
+
- Green `⏺` next to panes with a running dev server (listening TCP port)
|
|
185
|
+
- Pulsing `⏺` next to panes where Claude/Codex is actively working
|
|
186
|
+
- Dim `●` next to panes where Claude/Codex is idle
|
|
187
|
+
|
|
188
|
+
### Config drift detection
|
|
189
|
+
|
|
190
|
+
If `ide.yml` is edited while a session is running, `tmux-ide` warns the user and suggests `tmux-ide restart` to apply changes.
|
|
191
|
+
|
|
192
|
+
### Debugging
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
tmux-ide --verbose # Log all tmux commands to stderr
|
|
196
|
+
TMUX_IDE_DEBUG=1 tmux-ide # Same via env var
|
|
197
|
+
```
|
|
198
|
+
|
|
151
199
|
## Programmatic CLI
|
|
152
200
|
|
|
153
201
|
All commands support `--json` for structured output.
|
|
@@ -184,6 +232,7 @@ tmux-ide stop # Kill session
|
|
|
184
232
|
tmux-ide restart # Stop and relaunch
|
|
185
233
|
tmux-ide attach # Reattach
|
|
186
234
|
tmux-ide init # Scaffold config
|
|
235
|
+
tmux-ide --verbose # Launch with tmux command tracing
|
|
187
236
|
```
|
|
188
237
|
|
|
189
238
|
## Modification workflow
|
|
@@ -191,6 +240,7 @@ tmux-ide init # Scaffold config
|
|
|
191
240
|
1. Read: `tmux-ide config --json`
|
|
192
241
|
2. Modify: `tmux-ide config set <path> <value>` or `add-pane`/`remove-pane`
|
|
193
242
|
3. Validate: `tmux-ide validate --json`
|
|
243
|
+
4. Apply: `tmux-ide restart` (needed if session is already running)
|
|
194
244
|
|
|
195
245
|
## Best practices
|
|
196
246
|
|
|
@@ -200,27 +250,28 @@ tmux-ide init # Scaffold config
|
|
|
200
250
|
- 2-3 Claude panes in the top row (or lead + 2 teammates for teams)
|
|
201
251
|
- Dev servers + shell in the bottom row
|
|
202
252
|
- Use `detect --json` first to understand the project stack
|
|
203
|
-
- For agent teams: assign specific tasks to
|
|
253
|
+
- For agent teams: assign specific tasks to teammate panes so your prompts stay focused
|
|
204
254
|
- The team lead should have `focus: true` for easy access
|
|
255
|
+
- Use `tmux-ide --verbose` or `TMUX_IDE_DEBUG=1` when debugging layout issues
|
|
205
256
|
|
|
206
257
|
## ide.yml format
|
|
207
258
|
|
|
208
259
|
```yaml
|
|
209
260
|
name: project-name
|
|
210
|
-
before: pnpm install
|
|
211
|
-
team:
|
|
261
|
+
before: pnpm install # optional pre-launch hook
|
|
262
|
+
team: # optional agent team config
|
|
212
263
|
name: my-team
|
|
213
264
|
rows:
|
|
214
265
|
- size: 70%
|
|
215
266
|
panes:
|
|
216
267
|
- title: Lead
|
|
217
268
|
command: claude
|
|
218
|
-
role: lead
|
|
269
|
+
role: lead # optional layout metadata: "lead" or "teammate"
|
|
219
270
|
focus: true
|
|
220
271
|
- title: Teammate 1
|
|
221
272
|
command: claude
|
|
222
273
|
role: teammate
|
|
223
|
-
task: "Work on frontend"
|
|
274
|
+
task: "Work on frontend" # suggested task text for your prompts
|
|
224
275
|
- title: Teammate 2
|
|
225
276
|
command: claude
|
|
226
277
|
role: teammate
|
|
@@ -228,7 +279,7 @@ rows:
|
|
|
228
279
|
- panes:
|
|
229
280
|
- title: Dev Server
|
|
230
281
|
command: pnpm dev
|
|
231
|
-
dir: apps/web
|
|
282
|
+
dir: apps/web # per-pane working directory
|
|
232
283
|
env:
|
|
233
284
|
PORT: 3000
|
|
234
285
|
- title: Shell
|
package/src/attach.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { getSessionName } from "./lib/yaml-io.js";
|
|
4
3
|
import { outputError } from "./lib/output.js";
|
|
4
|
+
import { attachSession, getSessionState } from "./lib/tmux.js";
|
|
5
5
|
|
|
6
|
-
export async function attach(targetDir, { json } = {}) {
|
|
6
|
+
export async function attach(targetDir, { json: _json } = {}) {
|
|
7
7
|
const dir = resolve(targetDir ?? ".");
|
|
8
|
-
const session = getSessionName(dir);
|
|
8
|
+
const { name: session } = getSessionName(dir);
|
|
9
|
+
const state = getSessionState(session);
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} catch {
|
|
13
|
-
outputError(
|
|
14
|
-
`Session "${session}" is not running. Start it with: tmux-ide`,
|
|
15
|
-
"NOT_RUNNING",
|
|
16
|
-
{ json }
|
|
17
|
-
);
|
|
11
|
+
if (!state.running) {
|
|
12
|
+
outputError(`Session "${session}" is not running. Start it with: tmux-ide`, "NOT_RUNNING");
|
|
18
13
|
return;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
attachSession(session);
|
|
22
17
|
}
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { readConfig, writeConfig } from "./lib/yaml-io.js";
|
|
3
|
-
import {
|
|
3
|
+
import { setByPath } from "./lib/dot-path.js";
|
|
4
4
|
import { outputError } from "./lib/output.js";
|
|
5
5
|
|
|
6
6
|
export async function config(targetDir, { json, action, args } = {}) {
|
|
@@ -31,7 +31,7 @@ function dumpConfig(dir, { json }) {
|
|
|
31
31
|
try {
|
|
32
32
|
({ config: cfg } = readConfig(dir));
|
|
33
33
|
} catch (e) {
|
|
34
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
34
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -46,7 +46,7 @@ function dumpConfig(dir, { json }) {
|
|
|
46
46
|
function setConfig(dir, args, { json }) {
|
|
47
47
|
const [dotpath, ...rest] = args;
|
|
48
48
|
if (!dotpath || rest.length === 0) {
|
|
49
|
-
outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE"
|
|
49
|
+
outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE");
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -54,7 +54,12 @@ function setConfig(dir, args, { json }) {
|
|
|
54
54
|
try {
|
|
55
55
|
({ config: cfg } = readConfig(dir));
|
|
56
56
|
} catch (e) {
|
|
57
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
57
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!isConfigObject(cfg)) {
|
|
62
|
+
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -77,7 +82,10 @@ function setConfig(dir, args, { json }) {
|
|
|
77
82
|
function addPane(dir, args, { json }) {
|
|
78
83
|
const { row, title, command, size } = parseNamedArgs(args);
|
|
79
84
|
if (row === undefined) {
|
|
80
|
-
outputError(
|
|
85
|
+
outputError(
|
|
86
|
+
"Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]",
|
|
87
|
+
"USAGE",
|
|
88
|
+
);
|
|
81
89
|
return;
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -85,13 +93,28 @@ function addPane(dir, args, { json }) {
|
|
|
85
93
|
try {
|
|
86
94
|
({ config: cfg } = readConfig(dir));
|
|
87
95
|
} catch (e) {
|
|
88
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
96
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(cfg?.rows)) {
|
|
101
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rowIdx = parseIndex(row);
|
|
106
|
+
if (rowIdx == null) {
|
|
107
|
+
outputError(`Invalid row index "${row}"`, "USAGE");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!cfg.rows[rowIdx]) {
|
|
112
|
+
outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW");
|
|
89
113
|
return;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW", { json });
|
|
116
|
+
if (!Array.isArray(cfg.rows[rowIdx].panes)) {
|
|
117
|
+
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
95
118
|
return;
|
|
96
119
|
}
|
|
97
120
|
|
|
@@ -113,7 +136,7 @@ function addPane(dir, args, { json }) {
|
|
|
113
136
|
function removePane(dir, args, { json }) {
|
|
114
137
|
const { row, pane } = parseNamedArgs(args);
|
|
115
138
|
if (row === undefined || pane === undefined) {
|
|
116
|
-
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE"
|
|
139
|
+
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
117
140
|
return;
|
|
118
141
|
}
|
|
119
142
|
|
|
@@ -121,15 +144,29 @@ function removePane(dir, args, { json }) {
|
|
|
121
144
|
try {
|
|
122
145
|
({ config: cfg } = readConfig(dir));
|
|
123
146
|
} catch (e) {
|
|
124
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
147
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!Array.isArray(cfg?.rows)) {
|
|
152
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
125
153
|
return;
|
|
126
154
|
}
|
|
127
155
|
|
|
128
|
-
const rowIdx =
|
|
129
|
-
const paneIdx =
|
|
156
|
+
const rowIdx = parseIndex(row);
|
|
157
|
+
const paneIdx = parseIndex(pane);
|
|
158
|
+
if (rowIdx == null || paneIdx == null) {
|
|
159
|
+
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
130
162
|
|
|
131
|
-
if (!cfg.rows
|
|
132
|
-
outputError(`
|
|
163
|
+
if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
|
|
164
|
+
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!cfg.rows[rowIdx].panes[paneIdx]) {
|
|
169
|
+
outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE");
|
|
133
170
|
return;
|
|
134
171
|
}
|
|
135
172
|
|
|
@@ -150,7 +187,17 @@ function addRow(dir, args, { json }) {
|
|
|
150
187
|
try {
|
|
151
188
|
({ config: cfg } = readConfig(dir));
|
|
152
189
|
} catch (e) {
|
|
153
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
190
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isConfigObject(cfg)) {
|
|
195
|
+
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
200
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
154
201
|
return;
|
|
155
202
|
}
|
|
156
203
|
|
|
@@ -176,7 +223,17 @@ function enableTeam(dir, args, { json }) {
|
|
|
176
223
|
try {
|
|
177
224
|
({ config: cfg } = readConfig(dir));
|
|
178
225
|
} catch (e) {
|
|
179
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
226
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!isConfigObject(cfg)) {
|
|
231
|
+
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
236
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
180
237
|
return;
|
|
181
238
|
}
|
|
182
239
|
|
|
@@ -197,6 +254,11 @@ function enableTeam(dir, args, { json }) {
|
|
|
197
254
|
}
|
|
198
255
|
}
|
|
199
256
|
}
|
|
257
|
+
if (!leadAssigned) {
|
|
258
|
+
delete cfg.team;
|
|
259
|
+
outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
200
262
|
|
|
201
263
|
writeConfig(dir, cfg);
|
|
202
264
|
|
|
@@ -212,12 +274,23 @@ function disableTeam(dir, { json }) {
|
|
|
212
274
|
try {
|
|
213
275
|
({ config: cfg } = readConfig(dir));
|
|
214
276
|
} catch (e) {
|
|
215
|
-
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR"
|
|
277
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!isConfigObject(cfg)) {
|
|
282
|
+
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
287
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
216
288
|
return;
|
|
217
289
|
}
|
|
218
290
|
|
|
219
291
|
delete cfg.team;
|
|
220
292
|
for (const row of cfg.rows ?? []) {
|
|
293
|
+
if (!Array.isArray(row?.panes)) continue;
|
|
221
294
|
for (const pane of row.panes ?? []) {
|
|
222
295
|
delete pane.role;
|
|
223
296
|
delete pane.task;
|
|
@@ -257,3 +330,12 @@ function parseNamedArgs(args) {
|
|
|
257
330
|
}
|
|
258
331
|
return result;
|
|
259
332
|
}
|
|
333
|
+
|
|
334
|
+
function isConfigObject(value) {
|
|
335
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseIndex(value) {
|
|
339
|
+
if (!/^\d+$/.test(String(value))) return null;
|
|
340
|
+
return Number.parseInt(value, 10);
|
|
341
|
+
}
|
package/src/detect.js
CHANGED
|
@@ -20,60 +20,98 @@ export function detectStack(dir) {
|
|
|
20
20
|
frameworks: [],
|
|
21
21
|
devCommand: null,
|
|
22
22
|
language: null,
|
|
23
|
+
reasons: [],
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
// Detect package manager from lockfile
|
|
26
|
-
if (fileExists(dir, "pnpm-lock.yaml"))
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else if (fileExists(dir, "
|
|
27
|
+
if (fileExists(dir, "pnpm-lock.yaml")) {
|
|
28
|
+
detected.packageManager = "pnpm";
|
|
29
|
+
detected.reasons.push('Detected pnpm from "pnpm-lock.yaml".');
|
|
30
|
+
} else if (fileExists(dir, "bun.lockb") || fileExists(dir, "bun.lock")) {
|
|
31
|
+
detected.packageManager = "bun";
|
|
32
|
+
detected.reasons.push('Detected bun from "bun.lockb" or "bun.lock".');
|
|
33
|
+
} else if (fileExists(dir, "yarn.lock")) {
|
|
34
|
+
detected.packageManager = "yarn";
|
|
35
|
+
detected.reasons.push('Detected yarn from "yarn.lock".');
|
|
36
|
+
} else if (fileExists(dir, "package-lock.json")) {
|
|
37
|
+
detected.packageManager = "npm";
|
|
38
|
+
detected.reasons.push('Detected npm from "package-lock.json".');
|
|
39
|
+
}
|
|
30
40
|
|
|
31
41
|
const pkg = readJson(dir, "package.json");
|
|
32
42
|
if (pkg) {
|
|
33
43
|
detected.language = "javascript";
|
|
44
|
+
detected.reasons.push('Detected JavaScript from "package.json".');
|
|
34
45
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35
46
|
|
|
36
|
-
if (deps["next"]) detected
|
|
37
|
-
if (deps["convex"]) detected
|
|
38
|
-
if (deps["vite"]) detected
|
|
39
|
-
if (deps["remix"] || deps["@remix-run/node"])
|
|
40
|
-
|
|
41
|
-
if (deps["
|
|
42
|
-
if (deps["
|
|
47
|
+
if (deps["next"]) pushFramework(detected, "next", 'Found dependency "next".');
|
|
48
|
+
if (deps["convex"]) pushFramework(detected, "convex", 'Found dependency "convex".');
|
|
49
|
+
if (deps["vite"]) pushFramework(detected, "vite", 'Found dependency "vite".');
|
|
50
|
+
if (deps["remix"] || deps["@remix-run/node"])
|
|
51
|
+
pushFramework(detected, "remix", "Found Remix dependency.");
|
|
52
|
+
if (deps["nuxt"]) pushFramework(detected, "nuxt", 'Found dependency "nuxt".');
|
|
53
|
+
if (deps["astro"]) pushFramework(detected, "astro", 'Found dependency "astro".');
|
|
54
|
+
if (deps["svelte"] || deps["@sveltejs/kit"])
|
|
55
|
+
pushFramework(detected, "svelte", "Found Svelte dependency.");
|
|
43
56
|
|
|
44
57
|
// Detect dev command
|
|
45
58
|
const pm = detected.packageManager ?? "npm";
|
|
46
59
|
const run = pm === "npm" ? "npm run" : pm;
|
|
47
|
-
if (pkg.scripts?.dev)
|
|
48
|
-
|
|
60
|
+
if (pkg.scripts?.dev) {
|
|
61
|
+
detected.devCommand = `${run} dev`;
|
|
62
|
+
detected.reasons.push(
|
|
63
|
+
`Using dev command "${detected.devCommand}" from package.json scripts.`,
|
|
64
|
+
);
|
|
65
|
+
} else if (pkg.scripts?.start) {
|
|
66
|
+
detected.devCommand = `${run} start`;
|
|
67
|
+
detected.reasons.push(
|
|
68
|
+
`Using start command "${detected.devCommand}" from package.json scripts.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
// Python
|
|
52
74
|
if (fileExists(dir, "pyproject.toml") || fileExists(dir, "requirements.txt")) {
|
|
53
75
|
detected.language = detected.language ?? "python";
|
|
76
|
+
detected.reasons.push('Detected Python from "pyproject.toml" or "requirements.txt".');
|
|
54
77
|
try {
|
|
55
78
|
const pyproject = readFileSync(resolve(dir, "pyproject.toml"), "utf-8");
|
|
56
|
-
if (pyproject.includes("fastapi"))
|
|
57
|
-
|
|
58
|
-
else if (pyproject.includes("
|
|
59
|
-
|
|
79
|
+
if (pyproject.includes("fastapi"))
|
|
80
|
+
pushFramework(detected, "fastapi", 'Found "fastapi" in pyproject.toml.');
|
|
81
|
+
else if (pyproject.includes("django"))
|
|
82
|
+
pushFramework(detected, "django", 'Found "django" in pyproject.toml.');
|
|
83
|
+
else if (pyproject.includes("flask"))
|
|
84
|
+
pushFramework(detected, "flask", 'Found "flask" in pyproject.toml.');
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore missing or unreadable pyproject metadata.
|
|
87
|
+
}
|
|
60
88
|
}
|
|
61
89
|
|
|
62
90
|
// Rust
|
|
63
91
|
if (fileExists(dir, "Cargo.toml")) {
|
|
64
92
|
detected.language = detected.language ?? "rust";
|
|
65
|
-
detected.
|
|
93
|
+
detected.reasons.push('Detected Rust from "Cargo.toml".');
|
|
94
|
+
pushFramework(detected, "cargo", 'Using Cargo workflow from "Cargo.toml".');
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
// Go
|
|
69
98
|
if (fileExists(dir, "go.mod")) {
|
|
70
99
|
detected.language = detected.language ?? "go";
|
|
71
|
-
detected.
|
|
100
|
+
detected.reasons.push('Detected Go from "go.mod".');
|
|
101
|
+
pushFramework(detected, "go", 'Using Go workflow from "go.mod".');
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
// Docker
|
|
75
105
|
if (fileExists(dir, "docker-compose.yml") || fileExists(dir, "docker-compose.yaml")) {
|
|
76
|
-
|
|
106
|
+
pushFramework(
|
|
107
|
+
detected,
|
|
108
|
+
"docker",
|
|
109
|
+
'Detected Docker from "docker-compose.yml" or "docker-compose.yaml".',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (detected.reasons.length === 0) {
|
|
114
|
+
detected.reasons.push("No framework-specific signals found; using the generic layout.");
|
|
77
115
|
}
|
|
78
116
|
|
|
79
117
|
return detected;
|
|
@@ -156,10 +194,15 @@ export async function detect(targetDir, { json, write } = {}) {
|
|
|
156
194
|
if (json) {
|
|
157
195
|
console.log(JSON.stringify({ detected, suggestedConfig: suggested, written: true }, null, 2));
|
|
158
196
|
} else {
|
|
159
|
-
const desc =
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
const desc =
|
|
198
|
+
detected.frameworks.length > 0
|
|
199
|
+
? detected.frameworks.join(" + ")
|
|
200
|
+
: (detected.language ?? "generic project");
|
|
162
201
|
console.log(`Detected ${desc}. Created ide.yml.`);
|
|
202
|
+
console.log("\nWhy this layout:");
|
|
203
|
+
for (const reason of detected.reasons) {
|
|
204
|
+
console.log(` - ${reason}`);
|
|
205
|
+
}
|
|
163
206
|
}
|
|
164
207
|
return;
|
|
165
208
|
}
|
|
@@ -174,5 +217,16 @@ export async function detect(targetDir, { json, write } = {}) {
|
|
|
174
217
|
if (detected.language) console.log(` Language: ${detected.language}`);
|
|
175
218
|
if (detected.frameworks.length) console.log(` Frameworks: ${detected.frameworks.join(", ")}`);
|
|
176
219
|
if (detected.devCommand) console.log(` Dev command: ${detected.devCommand}`);
|
|
220
|
+
console.log("\nReasoning:");
|
|
221
|
+
for (const reason of detected.reasons) {
|
|
222
|
+
console.log(` - ${reason}`);
|
|
223
|
+
}
|
|
177
224
|
console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
|
|
178
225
|
}
|
|
226
|
+
|
|
227
|
+
function pushFramework(detected, framework, reason) {
|
|
228
|
+
if (!detected.frameworks.includes(framework)) {
|
|
229
|
+
detected.frameworks.push(framework);
|
|
230
|
+
}
|
|
231
|
+
detected.reasons.push(reason);
|
|
232
|
+
}
|
package/src/doctor.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function doctor({ json } = {}) {
|
|
|
18
18
|
check("tmux installed", () => {
|
|
19
19
|
execSync("which tmux", { stdio: "ignore" });
|
|
20
20
|
return "found";
|
|
21
|
-
})
|
|
21
|
+
}),
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
checks.push(
|
|
@@ -27,7 +27,7 @@ export async function doctor({ json } = {}) {
|
|
|
27
27
|
const num = parseFloat(version.replace(/[^0-9.]/g, ""));
|
|
28
28
|
if (num < 3.0) throw new Error(`${version} (need ≥ 3.0)`);
|
|
29
29
|
return version;
|
|
30
|
-
})
|
|
30
|
+
}),
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
checks.push(
|
|
@@ -35,17 +35,22 @@ export async function doctor({ json } = {}) {
|
|
|
35
35
|
const major = parseInt(process.versions.node.split(".")[0]);
|
|
36
36
|
if (major < 18) throw new Error(`Node ${process.versions.node} (need ≥ 18)`);
|
|
37
37
|
return `v${process.versions.node}`;
|
|
38
|
-
})
|
|
38
|
+
}),
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
checks.push(
|
|
42
42
|
check("256-color terminal", () => {
|
|
43
43
|
const term = process.env.TERM ?? "";
|
|
44
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
!term.includes("256color") &&
|
|
46
|
+
!term.includes("ghostty") &&
|
|
47
|
+
!term.includes("kitty") &&
|
|
48
|
+
term !== "tmux-256color"
|
|
49
|
+
) {
|
|
45
50
|
throw new Error(`$TERM is "${term}"`);
|
|
46
51
|
}
|
|
47
52
|
return term;
|
|
48
|
-
})
|
|
53
|
+
}),
|
|
49
54
|
);
|
|
50
55
|
|
|
51
56
|
checks.push(
|
|
@@ -53,20 +58,25 @@ export async function doctor({ json } = {}) {
|
|
|
53
58
|
const path = resolve(".", "ide.yml");
|
|
54
59
|
if (!existsSync(path)) throw new Error("not found in current directory");
|
|
55
60
|
return "found";
|
|
56
|
-
})
|
|
61
|
+
}),
|
|
57
62
|
);
|
|
58
63
|
|
|
59
64
|
checks.push(
|
|
60
|
-
check(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
check(
|
|
66
|
+
"Claude Code agent teams",
|
|
67
|
+
() => {
|
|
68
|
+
if (process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
69
|
+
throw new Error("not set (enable with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)");
|
|
70
|
+
}
|
|
71
|
+
return "enabled";
|
|
72
|
+
},
|
|
73
|
+
{ optional: true },
|
|
74
|
+
),
|
|
66
75
|
);
|
|
67
76
|
|
|
77
|
+
const allPass = checks.every((c) => c.pass || c.optional);
|
|
78
|
+
|
|
68
79
|
if (json) {
|
|
69
|
-
const allPass = checks.every((c) => c.pass || c.optional);
|
|
70
80
|
console.log(JSON.stringify({ ok: allPass, checks }, null, 2));
|
|
71
81
|
return;
|
|
72
82
|
}
|
|
@@ -77,6 +87,5 @@ export async function doctor({ json } = {}) {
|
|
|
77
87
|
console.log(`${color}${icon}\x1b[0m ${c.label} — ${c.detail}`);
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
const allPass = checks.every((c) => c.pass);
|
|
81
90
|
if (!allPass) process.exitCode = 1;
|
|
82
91
|
}
|