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/README.md +84 -35
- package/bin/cli.js +127 -85
- package/package.json +26 -5
- package/scripts/postinstall.js +46 -7
- package/skill/SKILL.md +28 -12
- package/src/attach.js +7 -10
- package/src/config.js +94 -7
- package/src/detect.js +77 -23
- package/src/doctor.js +23 -14
- package/src/init.js +16 -18
- package/src/inspect.js +127 -0
- package/src/launch.js +171 -177
- package/src/lib/launch-plan.js +83 -0
- package/src/lib/output.js +21 -4
- package/src/lib/tmux.js +200 -0
- package/src/ls.js +2 -4
- package/src/restart.js +5 -12
- package/src/status.js +4 -22
- package/src/stop.js +10 -9
- package/src/validate.js +0 -16
- 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
|
|
@@ -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
|
|
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
|
|
211
|
-
team:
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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"))
|
|
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
|
}
|
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 {
|
|
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
|
-
|
|
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(
|
|
19
|
+
const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
|
|
23
20
|
if (!existsSync(templatePath)) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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(
|
|
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);
|