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/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 sets this automatically when `team` is configured in `ide.yml`.
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 tasks to teammates:**
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 teammates for focused parallel work
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 # optional pre-launch hook
211
- team: # optional agent team config
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 # "lead" or "teammate"
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" # initial task for teammate
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 # per-pane working directory
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
- try {
11
- execSync(`tmux has-session -t "${session}"`, { stdio: "ignore" });
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
- execSync(`tmux attach -t "${session}"`, { stdio: "inherit" });
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 { getByPath, setByPath } from "./lib/dot-path.js";
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", { json });
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", { json });
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", { json });
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("Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]", "USAGE", { json });
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", { json });
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
- const rowIdx = parseInt(row);
93
- if (!cfg.rows?.[rowIdx]) {
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", { json });
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", { json });
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 = parseInt(row);
129
- const paneIdx = parseInt(pane);
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?.[rowIdx]?.panes?.[paneIdx]) {
132
- outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE", { json });
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", { json });
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", { json });
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", { json });
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")) detected.packageManager = "pnpm";
27
- else if (fileExists(dir, "bun.lockb") || fileExists(dir, "bun.lock")) detected.packageManager = "bun";
28
- else if (fileExists(dir, "yarn.lock")) detected.packageManager = "yarn";
29
- else if (fileExists(dir, "package-lock.json")) detected.packageManager = "npm";
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.frameworks.push("next");
37
- if (deps["convex"]) detected.frameworks.push("convex");
38
- if (deps["vite"]) detected.frameworks.push("vite");
39
- if (deps["remix"] || deps["@remix-run/node"]) detected.frameworks.push("remix");
40
- if (deps["nuxt"]) detected.frameworks.push("nuxt");
41
- if (deps["astro"]) detected.frameworks.push("astro");
42
- if (deps["svelte"] || deps["@sveltejs/kit"]) detected.frameworks.push("svelte");
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) detected.devCommand = `${run} dev`;
48
- else if (pkg.scripts?.start) detected.devCommand = `${run} start`;
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")) detected.frameworks.push("fastapi");
57
- else if (pyproject.includes("django")) detected.frameworks.push("django");
58
- else if (pyproject.includes("flask")) detected.frameworks.push("flask");
59
- } catch {}
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.frameworks.push("cargo");
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.frameworks.push("go");
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
- detected.frameworks.push("docker");
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 = detected.frameworks.length > 0
160
- ? detected.frameworks.join(" + ")
161
- : detected.language ?? "generic project";
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 (!term.includes("256color") && !term.includes("ghostty") && !term.includes("kitty") && term !== "tmux-256color") {
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("Claude Code agent teams", () => {
61
- if (process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
62
- throw new Error("not set (enable with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)");
63
- }
64
- return "enabled";
65
- }, { optional: true })
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
  }