tmux-ide 1.0.0 → 1.1.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
@@ -200,27 +216,27 @@ tmux-ide init # Scaffold config
200
216
  - 2-3 Claude panes in the top row (or lead + 2 teammates for teams)
201
217
  - Dev servers + shell in the bottom row
202
218
  - Use `detect --json` first to understand the project stack
203
- - For agent teams: assign specific tasks to teammates for focused parallel work
219
+ - For agent teams: assign specific tasks to teammate panes so your prompts stay focused
204
220
  - The team lead should have `focus: true` for easy access
205
221
 
206
222
  ## ide.yml format
207
223
 
208
224
  ```yaml
209
225
  name: project-name
210
- before: pnpm install # optional pre-launch hook
211
- team: # optional agent team config
226
+ before: pnpm install # optional pre-launch hook
227
+ team: # optional agent team config
212
228
  name: my-team
213
229
  rows:
214
230
  - size: 70%
215
231
  panes:
216
232
  - title: Lead
217
233
  command: claude
218
- role: lead # "lead" or "teammate"
234
+ role: lead # optional layout metadata: "lead" or "teammate"
219
235
  focus: true
220
236
  - title: Teammate 1
221
237
  command: claude
222
238
  role: teammate
223
- task: "Work on frontend" # initial task for teammate
239
+ task: "Work on frontend" # suggested task text for your prompts
224
240
  - title: Teammate 2
225
241
  command: claude
226
242
  role: teammate
@@ -228,7 +244,7 @@ rows:
228
244
  - panes:
229
245
  - title: Dev Server
230
246
  command: pnpm dev
231
- dir: apps/web # per-pane working directory
247
+ dir: apps/web # per-pane working directory
232
248
  env:
233
249
  PORT: 3000
234
250
  - title: Shell
package/src/attach.js CHANGED
@@ -1,22 +1,19 @@
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
6
  export async function attach(targetDir, { json } = {}) {
7
7
  const dir = resolve(targetDir ?? ".");
8
8
  const 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", {
13
+ json,
14
+ });
18
15
  return;
19
16
  }
20
17
 
21
- execSync(`tmux attach -t "${session}"`, { stdio: "inherit" });
18
+ attachSession(session);
22
19
  }
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 } = {}) {
@@ -58,6 +58,11 @@ function setConfig(dir, args, { json }) {
58
58
  return;
59
59
  }
60
60
 
61
+ if (!isConfigObject(cfg)) {
62
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
63
+ return;
64
+ }
65
+
61
66
  let value = rest.join(" ");
62
67
  // Try to parse as number or boolean
63
68
  if (value === "true") value = true;
@@ -77,7 +82,11 @@ 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
+ { json },
89
+ );
81
90
  return;
82
91
  }
83
92
 
@@ -89,12 +98,29 @@ function addPane(dir, args, { json }) {
89
98
  return;
90
99
  }
91
100
 
92
- const rowIdx = parseInt(row);
93
- if (!cfg.rows?.[rowIdx]) {
101
+ if (!Array.isArray(cfg?.rows)) {
102
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
103
+ return;
104
+ }
105
+
106
+ const rowIdx = parseIndex(row);
107
+ if (rowIdx == null) {
108
+ outputError(`Invalid row index "${row}"`, "USAGE", { json });
109
+ return;
110
+ }
111
+
112
+ if (!cfg.rows[rowIdx]) {
94
113
  outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW", { json });
95
114
  return;
96
115
  }
97
116
 
117
+ if (!Array.isArray(cfg.rows[rowIdx].panes)) {
118
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG", {
119
+ json,
120
+ });
121
+ return;
122
+ }
123
+
98
124
  const pane = {};
99
125
  if (title) pane.title = title;
100
126
  if (command) pane.command = command;
@@ -125,10 +151,26 @@ function removePane(dir, args, { json }) {
125
151
  return;
126
152
  }
127
153
 
128
- const rowIdx = parseInt(row);
129
- const paneIdx = parseInt(pane);
154
+ if (!Array.isArray(cfg?.rows)) {
155
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
156
+ return;
157
+ }
158
+
159
+ const rowIdx = parseIndex(row);
160
+ const paneIdx = parseIndex(pane);
161
+ if (rowIdx == null || paneIdx == null) {
162
+ outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE", { json });
163
+ return;
164
+ }
130
165
 
131
- if (!cfg.rows?.[rowIdx]?.panes?.[paneIdx]) {
166
+ if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
167
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG", {
168
+ json,
169
+ });
170
+ return;
171
+ }
172
+
173
+ if (!cfg.rows[rowIdx].panes[paneIdx]) {
132
174
  outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE", { json });
133
175
  return;
134
176
  }
@@ -154,6 +196,16 @@ function addRow(dir, args, { json }) {
154
196
  return;
155
197
  }
156
198
 
199
+ if (!isConfigObject(cfg)) {
200
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
201
+ return;
202
+ }
203
+
204
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
205
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
206
+ return;
207
+ }
208
+
157
209
  const row = { panes: [{ title: "Shell" }] };
158
210
  if (size) row.size = size;
159
211
 
@@ -180,6 +232,16 @@ function enableTeam(dir, args, { json }) {
180
232
  return;
181
233
  }
182
234
 
235
+ if (!isConfigObject(cfg)) {
236
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
237
+ return;
238
+ }
239
+
240
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
241
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
242
+ return;
243
+ }
244
+
183
245
  const teamName = name ?? cfg.name ?? "my-team";
184
246
  cfg.team = { name: teamName };
185
247
 
@@ -197,6 +259,11 @@ function enableTeam(dir, args, { json }) {
197
259
  }
198
260
  }
199
261
  }
262
+ if (!leadAssigned) {
263
+ delete cfg.team;
264
+ outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG", { json });
265
+ return;
266
+ }
200
267
 
201
268
  writeConfig(dir, cfg);
202
269
 
@@ -216,8 +283,19 @@ function disableTeam(dir, { json }) {
216
283
  return;
217
284
  }
218
285
 
286
+ if (!isConfigObject(cfg)) {
287
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
288
+ return;
289
+ }
290
+
291
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
292
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
293
+ return;
294
+ }
295
+
219
296
  delete cfg.team;
220
297
  for (const row of cfg.rows ?? []) {
298
+ if (!Array.isArray(row?.panes)) continue;
221
299
  for (const pane of row.panes ?? []) {
222
300
  delete pane.role;
223
301
  delete pane.task;
@@ -257,3 +335,12 @@ function parseNamedArgs(args) {
257
335
  }
258
336
  return result;
259
337
  }
338
+
339
+ function isConfigObject(value) {
340
+ return value != null && typeof value === "object" && !Array.isArray(value);
341
+ }
342
+
343
+ function parseIndex(value) {
344
+ if (!/^\d+$/.test(String(value))) return null;
345
+ return Number.parseInt(value, 10);
346
+ }
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
  }
package/src/init.js CHANGED
@@ -1,32 +1,30 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve, basename } from "node:path";
3
- import { writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve, basename, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
4
6
  import { detectStack, suggestConfig } from "./detect.js";
5
- import { printLayout } from "./lib/output.js";
7
+ import { outputError, printLayout } from "./lib/output.js";
6
8
 
7
9
  export async function init({ template, json } = {}) {
8
10
  const dir = process.cwd();
9
11
  const configPath = resolve(dir, "ide.yml");
10
12
 
11
13
  if (existsSync(configPath)) {
12
- if (json) {
13
- console.error(JSON.stringify({ error: "ide.yml already exists", code: "EXISTS" }));
14
- } else {
15
- console.error("ide.yml already exists in this directory");
16
- }
17
- process.exit(1);
14
+ outputError("ide.yml already exists in this directory", "EXISTS", { json });
18
15
  }
19
16
 
20
17
  // If a specific template is requested, use it
21
18
  if (template) {
22
- const templatePath = resolve(import.meta.dirname, "..", "templates", `${template}.yml`);
19
+ const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
23
20
  if (!existsSync(templatePath)) {
24
- if (json) {
25
- console.error(JSON.stringify({ error: `Template "${template}" not found`, code: "NOT_FOUND" }));
26
- } else {
27
- console.error(`Template "${template}" not found. Available: default, nextjs, convex, vite, python, go, agent-team, agent-team-nextjs, agent-team-monorepo`);
28
- }
29
- process.exit(1);
21
+ outputError(
22
+ json
23
+ ? `Template "${template}" not found`
24
+ : `Template "${template}" not found. Available: default, nextjs, convex, vite, python, go, agent-team, agent-team-nextjs, agent-team-monorepo`,
25
+ "NOT_FOUND",
26
+ { json },
27
+ );
30
28
  }
31
29
 
32
30
  let content = readFileSync(templatePath, "utf-8");
@@ -64,7 +62,7 @@ export async function init({ template, json } = {}) {
64
62
  }
65
63
  } else {
66
64
  // Fallback to default template
67
- const templatePath = resolve(import.meta.dirname, "..", "templates", "default.yml");
65
+ const templatePath = resolve(__dirname, "..", "templates", "default.yml");
68
66
  let content = readFileSync(templatePath, "utf-8");
69
67
  content = content.replace(/^name: .+/m, `name: ${name}`);
70
68
  writeFileSync(configPath, content);