tmux-ide 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/cli.js +168 -0
- package/package.json +44 -0
- package/scripts/postinstall.js +13 -0
- package/skill/SKILL.md +238 -0
- package/src/attach.js +22 -0
- package/src/config.js +259 -0
- package/src/detect.js +178 -0
- package/src/detect.test.js +115 -0
- package/src/doctor.js +82 -0
- package/src/init.js +81 -0
- package/src/integration.test.js +94 -0
- package/src/launch.js +238 -0
- package/src/lib/dot-path.js +16 -0
- package/src/lib/dot-path.test.js +67 -0
- package/src/lib/output.js +72 -0
- package/src/lib/sizes.js +36 -0
- package/src/lib/sizes.test.js +74 -0
- package/src/lib/yaml-io.js +26 -0
- package/src/ls.js +42 -0
- package/src/restart.js +23 -0
- package/src/status.js +53 -0
- package/src/stop.js +23 -0
- package/src/validate.js +170 -0
- package/src/validate.test.js +141 -0
- package/templates/agent-team-monorepo.yml +31 -0
- package/templates/agent-team-nextjs.yml +27 -0
- package/templates/agent-team.yml +23 -0
- package/templates/convex.yml +19 -0
- package/templates/default.yml +20 -0
- package/templates/go.yml +15 -0
- package/templates/nextjs.yml +17 -0
- package/templates/python.yml +15 -0
- package/templates/vite.yml +15 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readConfig, writeConfig } from "./lib/yaml-io.js";
|
|
3
|
+
import { getByPath, setByPath } from "./lib/dot-path.js";
|
|
4
|
+
import { outputError } from "./lib/output.js";
|
|
5
|
+
|
|
6
|
+
export async function config(targetDir, { json, action, args } = {}) {
|
|
7
|
+
const dir = resolve(targetDir ?? ".");
|
|
8
|
+
|
|
9
|
+
switch (action) {
|
|
10
|
+
case "dump":
|
|
11
|
+
return dumpConfig(dir, { json });
|
|
12
|
+
case "set":
|
|
13
|
+
return setConfig(dir, args, { json });
|
|
14
|
+
case "add-pane":
|
|
15
|
+
return addPane(dir, args, { json });
|
|
16
|
+
case "remove-pane":
|
|
17
|
+
return removePane(dir, args, { json });
|
|
18
|
+
case "add-row":
|
|
19
|
+
return addRow(dir, args, { json });
|
|
20
|
+
case "enable-team":
|
|
21
|
+
return enableTeam(dir, args, { json });
|
|
22
|
+
case "disable-team":
|
|
23
|
+
return disableTeam(dir, { json });
|
|
24
|
+
default:
|
|
25
|
+
return dumpConfig(dir, { json });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dumpConfig(dir, { json }) {
|
|
30
|
+
let cfg;
|
|
31
|
+
try {
|
|
32
|
+
({ config: cfg } = readConfig(dir));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (json) {
|
|
39
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
40
|
+
} else {
|
|
41
|
+
// Pretty print for humans
|
|
42
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setConfig(dir, args, { json }) {
|
|
47
|
+
const [dotpath, ...rest] = args;
|
|
48
|
+
if (!dotpath || rest.length === 0) {
|
|
49
|
+
outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE", { json });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let cfg;
|
|
54
|
+
try {
|
|
55
|
+
({ config: cfg } = readConfig(dir));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let value = rest.join(" ");
|
|
62
|
+
// Try to parse as number or boolean
|
|
63
|
+
if (value === "true") value = true;
|
|
64
|
+
else if (value === "false") value = false;
|
|
65
|
+
else if (/^\d+$/.test(value)) value = parseInt(value);
|
|
66
|
+
|
|
67
|
+
setByPath(cfg, dotpath, value);
|
|
68
|
+
writeConfig(dir, cfg);
|
|
69
|
+
|
|
70
|
+
if (json) {
|
|
71
|
+
console.log(JSON.stringify({ ok: true, path: dotpath, value }, null, 2));
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`Set ${dotpath} = ${JSON.stringify(value)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function addPane(dir, args, { json }) {
|
|
78
|
+
const { row, title, command, size } = parseNamedArgs(args);
|
|
79
|
+
if (row === undefined) {
|
|
80
|
+
outputError("Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]", "USAGE", { json });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let cfg;
|
|
85
|
+
try {
|
|
86
|
+
({ config: cfg } = readConfig(dir));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rowIdx = parseInt(row);
|
|
93
|
+
if (!cfg.rows?.[rowIdx]) {
|
|
94
|
+
outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW", { json });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pane = {};
|
|
99
|
+
if (title) pane.title = title;
|
|
100
|
+
if (command) pane.command = command;
|
|
101
|
+
if (size) pane.size = size;
|
|
102
|
+
|
|
103
|
+
cfg.rows[rowIdx].panes.push(pane);
|
|
104
|
+
writeConfig(dir, cfg);
|
|
105
|
+
|
|
106
|
+
if (json) {
|
|
107
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, pane }, null, 2));
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`Added pane "${title ?? "untitled"}" to row ${rowIdx}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removePane(dir, args, { json }) {
|
|
114
|
+
const { row, pane } = parseNamedArgs(args);
|
|
115
|
+
if (row === undefined || pane === undefined) {
|
|
116
|
+
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE", { json });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let cfg;
|
|
121
|
+
try {
|
|
122
|
+
({ config: cfg } = readConfig(dir));
|
|
123
|
+
} catch (e) {
|
|
124
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rowIdx = parseInt(row);
|
|
129
|
+
const paneIdx = parseInt(pane);
|
|
130
|
+
|
|
131
|
+
if (!cfg.rows?.[rowIdx]?.panes?.[paneIdx]) {
|
|
132
|
+
outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE", { json });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const removed = cfg.rows[rowIdx].panes.splice(paneIdx, 1)[0];
|
|
137
|
+
writeConfig(dir, cfg);
|
|
138
|
+
|
|
139
|
+
if (json) {
|
|
140
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, pane: paneIdx, removed }, null, 2));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(`Removed pane ${paneIdx} ("${removed.title ?? "untitled"}") from row ${rowIdx}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function addRow(dir, args, { json }) {
|
|
147
|
+
const { size } = parseNamedArgs(args);
|
|
148
|
+
|
|
149
|
+
let cfg;
|
|
150
|
+
try {
|
|
151
|
+
({ config: cfg } = readConfig(dir));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const row = { panes: [{ title: "Shell" }] };
|
|
158
|
+
if (size) row.size = size;
|
|
159
|
+
|
|
160
|
+
cfg.rows = cfg.rows ?? [];
|
|
161
|
+
cfg.rows.push(row);
|
|
162
|
+
writeConfig(dir, cfg);
|
|
163
|
+
|
|
164
|
+
const rowIdx = cfg.rows.length - 1;
|
|
165
|
+
if (json) {
|
|
166
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, size: size ?? null }, null, 2));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(`Added row ${rowIdx}${size ? ` (${size})` : ""}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function enableTeam(dir, args, { json }) {
|
|
173
|
+
const { name } = parseNamedArgs(args);
|
|
174
|
+
|
|
175
|
+
let cfg;
|
|
176
|
+
try {
|
|
177
|
+
({ config: cfg } = readConfig(dir));
|
|
178
|
+
} catch (e) {
|
|
179
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const teamName = name ?? cfg.name ?? "my-team";
|
|
184
|
+
cfg.team = { name: teamName };
|
|
185
|
+
|
|
186
|
+
// Find claude panes and assign roles: first as lead, rest as teammates
|
|
187
|
+
let leadAssigned = false;
|
|
188
|
+
for (const row of cfg.rows ?? []) {
|
|
189
|
+
for (const pane of row.panes ?? []) {
|
|
190
|
+
if (pane.command === "claude" || pane.role === "lead" || pane.role === "teammate") {
|
|
191
|
+
if (!leadAssigned) {
|
|
192
|
+
pane.role = "lead";
|
|
193
|
+
leadAssigned = true;
|
|
194
|
+
} else {
|
|
195
|
+
pane.role = "teammate";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
writeConfig(dir, cfg);
|
|
202
|
+
|
|
203
|
+
if (json) {
|
|
204
|
+
console.log(JSON.stringify({ ok: true, team: cfg.team, roles: summarizeRoles(cfg) }, null, 2));
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`Enabled agent team "${teamName}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function disableTeam(dir, { json }) {
|
|
211
|
+
let cfg;
|
|
212
|
+
try {
|
|
213
|
+
({ config: cfg } = readConfig(dir));
|
|
214
|
+
} catch (e) {
|
|
215
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
delete cfg.team;
|
|
220
|
+
for (const row of cfg.rows ?? []) {
|
|
221
|
+
for (const pane of row.panes ?? []) {
|
|
222
|
+
delete pane.role;
|
|
223
|
+
delete pane.task;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
writeConfig(dir, cfg);
|
|
228
|
+
|
|
229
|
+
if (json) {
|
|
230
|
+
console.log(JSON.stringify({ ok: true, disabled: true }, null, 2));
|
|
231
|
+
} else {
|
|
232
|
+
console.log("Disabled agent team");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function summarizeRoles(cfg) {
|
|
237
|
+
const roles = [];
|
|
238
|
+
for (let i = 0; i < (cfg.rows ?? []).length; i++) {
|
|
239
|
+
for (let j = 0; j < (cfg.rows[i].panes ?? []).length; j++) {
|
|
240
|
+
const p = cfg.rows[i].panes[j];
|
|
241
|
+
if (p.role) {
|
|
242
|
+
roles.push({ row: i, pane: j, title: p.title ?? null, role: p.role });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return roles;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseNamedArgs(args) {
|
|
250
|
+
const result = {};
|
|
251
|
+
for (let i = 0; i < args.length; i++) {
|
|
252
|
+
if (args[i].startsWith("--") && i + 1 < args.length) {
|
|
253
|
+
const key = args[i].slice(2);
|
|
254
|
+
result[key] = args[i + 1];
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { writeConfig } from "./lib/yaml-io.js";
|
|
4
|
+
|
|
5
|
+
function fileExists(dir, name) {
|
|
6
|
+
return existsSync(resolve(dir, name));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readJson(dir, name) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(resolve(dir, name), "utf-8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function detectStack(dir) {
|
|
18
|
+
const detected = {
|
|
19
|
+
packageManager: null,
|
|
20
|
+
frameworks: [],
|
|
21
|
+
devCommand: null,
|
|
22
|
+
language: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 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";
|
|
30
|
+
|
|
31
|
+
const pkg = readJson(dir, "package.json");
|
|
32
|
+
if (pkg) {
|
|
33
|
+
detected.language = "javascript";
|
|
34
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35
|
+
|
|
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");
|
|
43
|
+
|
|
44
|
+
// Detect dev command
|
|
45
|
+
const pm = detected.packageManager ?? "npm";
|
|
46
|
+
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`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Python
|
|
52
|
+
if (fileExists(dir, "pyproject.toml") || fileExists(dir, "requirements.txt")) {
|
|
53
|
+
detected.language = detected.language ?? "python";
|
|
54
|
+
try {
|
|
55
|
+
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 {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Rust
|
|
63
|
+
if (fileExists(dir, "Cargo.toml")) {
|
|
64
|
+
detected.language = detected.language ?? "rust";
|
|
65
|
+
detected.frameworks.push("cargo");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Go
|
|
69
|
+
if (fileExists(dir, "go.mod")) {
|
|
70
|
+
detected.language = detected.language ?? "go";
|
|
71
|
+
detected.frameworks.push("go");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Docker
|
|
75
|
+
if (fileExists(dir, "docker-compose.yml") || fileExists(dir, "docker-compose.yaml")) {
|
|
76
|
+
detected.frameworks.push("docker");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return detected;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function suggestConfig(dir, detected) {
|
|
83
|
+
const name = basename(dir);
|
|
84
|
+
const pm = detected.packageManager ?? "npm";
|
|
85
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
86
|
+
|
|
87
|
+
// Default: 2 claude panes + shell
|
|
88
|
+
const config = {
|
|
89
|
+
name,
|
|
90
|
+
rows: [
|
|
91
|
+
{
|
|
92
|
+
size: "70%",
|
|
93
|
+
panes: [
|
|
94
|
+
{ title: "Claude 1", command: "claude" },
|
|
95
|
+
{ title: "Claude 2", command: "claude" },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
panes: [],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const bottom = config.rows[1].panes;
|
|
105
|
+
const frameworks = detected.frameworks;
|
|
106
|
+
|
|
107
|
+
// Add 3rd claude pane for complex stacks
|
|
108
|
+
if (frameworks.length >= 2) {
|
|
109
|
+
config.rows[0].panes.push({ title: "Claude 3", command: "claude" });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add dev servers
|
|
113
|
+
if (frameworks.includes("next")) {
|
|
114
|
+
bottom.push({ title: "Next.js", command: `${run} dev` });
|
|
115
|
+
} else if (frameworks.includes("vite")) {
|
|
116
|
+
bottom.push({ title: "Vite", command: `${run} dev` });
|
|
117
|
+
} else if (frameworks.includes("nuxt")) {
|
|
118
|
+
bottom.push({ title: "Nuxt", command: `${run} dev` });
|
|
119
|
+
} else if (frameworks.includes("remix")) {
|
|
120
|
+
bottom.push({ title: "Remix", command: `${run} dev` });
|
|
121
|
+
} else if (frameworks.includes("astro")) {
|
|
122
|
+
bottom.push({ title: "Astro", command: `${run} dev` });
|
|
123
|
+
} else if (frameworks.includes("svelte")) {
|
|
124
|
+
bottom.push({ title: "SvelteKit", command: `${run} dev` });
|
|
125
|
+
} else if (frameworks.includes("fastapi")) {
|
|
126
|
+
bottom.push({ title: "FastAPI", command: "uvicorn main:app --reload" });
|
|
127
|
+
} else if (frameworks.includes("django")) {
|
|
128
|
+
bottom.push({ title: "Django", command: "python manage.py runserver" });
|
|
129
|
+
} else if (frameworks.includes("flask")) {
|
|
130
|
+
bottom.push({ title: "Flask", command: "flask run --reload" });
|
|
131
|
+
} else if (frameworks.includes("cargo")) {
|
|
132
|
+
bottom.push({ title: "Cargo", command: "cargo watch -x run" });
|
|
133
|
+
} else if (frameworks.includes("go")) {
|
|
134
|
+
bottom.push({ title: "Go", command: "go run ." });
|
|
135
|
+
} else if (detected.devCommand) {
|
|
136
|
+
bottom.push({ title: "Dev Server", command: detected.devCommand });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (frameworks.includes("convex")) {
|
|
140
|
+
bottom.push({ title: "Convex", command: "npx convex dev" });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Always add shell
|
|
144
|
+
bottom.push({ title: "Shell" });
|
|
145
|
+
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function detect(targetDir, { json, write } = {}) {
|
|
150
|
+
const dir = resolve(targetDir ?? ".");
|
|
151
|
+
const detected = detectStack(dir);
|
|
152
|
+
const suggested = suggestConfig(dir, detected);
|
|
153
|
+
|
|
154
|
+
if (write) {
|
|
155
|
+
writeConfig(dir, suggested);
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested, written: true }, null, 2));
|
|
158
|
+
} else {
|
|
159
|
+
const desc = detected.frameworks.length > 0
|
|
160
|
+
? detected.frameworks.join(" + ")
|
|
161
|
+
: detected.language ?? "generic project";
|
|
162
|
+
console.log(`Detected ${desc}. Created ide.yml.`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (json) {
|
|
168
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested }, null, 2));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log("Detected stack:");
|
|
173
|
+
if (detected.packageManager) console.log(` Package manager: ${detected.packageManager}`);
|
|
174
|
+
if (detected.language) console.log(` Language: ${detected.language}`);
|
|
175
|
+
if (detected.frameworks.length) console.log(` Frameworks: ${detected.frameworks.join(", ")}`);
|
|
176
|
+
if (detected.devCommand) console.log(` Dev command: ${detected.devCommand}`);
|
|
177
|
+
console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
|
|
178
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { suggestConfig } from "./detect.js";
|
|
4
|
+
|
|
5
|
+
describe("suggestConfig", () => {
|
|
6
|
+
it("creates Next.js config with pnpm", () => {
|
|
7
|
+
const config = suggestConfig("/project", {
|
|
8
|
+
packageManager: "pnpm",
|
|
9
|
+
frameworks: ["next"],
|
|
10
|
+
devCommand: "pnpm dev",
|
|
11
|
+
language: "javascript",
|
|
12
|
+
});
|
|
13
|
+
assert.strictEqual(config.rows[0].size, "70%");
|
|
14
|
+
assert.strictEqual(config.rows[0].panes.length, 2);
|
|
15
|
+
const bottom = config.rows[1].panes;
|
|
16
|
+
assert.strictEqual(bottom[0].title, "Next.js");
|
|
17
|
+
assert.strictEqual(bottom[0].command, "pnpm dev");
|
|
18
|
+
assert.strictEqual(bottom[bottom.length - 1].title, "Shell");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("creates Next.js + Convex config with 3 Claude panes", () => {
|
|
22
|
+
const config = suggestConfig("/project", {
|
|
23
|
+
packageManager: "pnpm",
|
|
24
|
+
frameworks: ["next", "convex"],
|
|
25
|
+
devCommand: "pnpm dev",
|
|
26
|
+
language: "javascript",
|
|
27
|
+
});
|
|
28
|
+
assert.strictEqual(config.rows[0].panes.length, 3);
|
|
29
|
+
const bottom = config.rows[1].panes;
|
|
30
|
+
const titles = bottom.map((p) => p.title);
|
|
31
|
+
assert.ok(titles.includes("Next.js"));
|
|
32
|
+
assert.ok(titles.includes("Convex"));
|
|
33
|
+
assert.ok(titles.includes("Shell"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("creates Go config", () => {
|
|
37
|
+
const config = suggestConfig("/project", {
|
|
38
|
+
packageManager: null,
|
|
39
|
+
frameworks: ["go"],
|
|
40
|
+
devCommand: null,
|
|
41
|
+
language: "go",
|
|
42
|
+
});
|
|
43
|
+
const bottom = config.rows[1].panes;
|
|
44
|
+
assert.strictEqual(bottom[0].title, "Go");
|
|
45
|
+
assert.strictEqual(bottom[0].command, "go run .");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("creates Cargo config", () => {
|
|
49
|
+
const config = suggestConfig("/project", {
|
|
50
|
+
packageManager: null,
|
|
51
|
+
frameworks: ["cargo"],
|
|
52
|
+
devCommand: null,
|
|
53
|
+
language: "rust",
|
|
54
|
+
});
|
|
55
|
+
const bottom = config.rows[1].panes;
|
|
56
|
+
assert.strictEqual(bottom[0].title, "Cargo");
|
|
57
|
+
assert.strictEqual(bottom[0].command, "cargo watch -x run");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("falls back to dev command when no framework detected", () => {
|
|
61
|
+
const config = suggestConfig("/project", {
|
|
62
|
+
packageManager: "npm",
|
|
63
|
+
frameworks: [],
|
|
64
|
+
devCommand: "npm run dev",
|
|
65
|
+
language: "javascript",
|
|
66
|
+
});
|
|
67
|
+
const bottom = config.rows[1].panes;
|
|
68
|
+
assert.strictEqual(bottom[0].title, "Dev Server");
|
|
69
|
+
assert.strictEqual(bottom[0].command, "npm run dev");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("creates minimal config with just shell when nothing detected", () => {
|
|
73
|
+
const config = suggestConfig("/project", {
|
|
74
|
+
packageManager: null,
|
|
75
|
+
frameworks: [],
|
|
76
|
+
devCommand: null,
|
|
77
|
+
language: null,
|
|
78
|
+
});
|
|
79
|
+
const bottom = config.rows[1].panes;
|
|
80
|
+
assert.strictEqual(bottom.length, 1);
|
|
81
|
+
assert.strictEqual(bottom[0].title, "Shell");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses npm run for npm package manager", () => {
|
|
85
|
+
const config = suggestConfig("/project", {
|
|
86
|
+
packageManager: "npm",
|
|
87
|
+
frameworks: ["next"],
|
|
88
|
+
devCommand: "npm run dev",
|
|
89
|
+
language: "javascript",
|
|
90
|
+
});
|
|
91
|
+
const bottom = config.rows[1].panes;
|
|
92
|
+
assert.strictEqual(bottom[0].command, "npm run dev");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("uses bun for bun package manager", () => {
|
|
96
|
+
const config = suggestConfig("/project", {
|
|
97
|
+
packageManager: "bun",
|
|
98
|
+
frameworks: ["vite"],
|
|
99
|
+
devCommand: "bun dev",
|
|
100
|
+
language: "javascript",
|
|
101
|
+
});
|
|
102
|
+
const bottom = config.rows[1].panes;
|
|
103
|
+
assert.strictEqual(bottom[0].command, "bun dev");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("uses project directory basename as session name", () => {
|
|
107
|
+
const config = suggestConfig("/home/user/my-app", {
|
|
108
|
+
packageManager: null,
|
|
109
|
+
frameworks: [],
|
|
110
|
+
devCommand: null,
|
|
111
|
+
language: null,
|
|
112
|
+
});
|
|
113
|
+
assert.strictEqual(config.name, "my-app");
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
function check(label, fn, { optional = false } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
const result = fn();
|
|
8
|
+
return { label, pass: true, detail: result, optional };
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return { label, pass: false, detail: e.message, optional };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function doctor({ json } = {}) {
|
|
15
|
+
const checks = [];
|
|
16
|
+
|
|
17
|
+
checks.push(
|
|
18
|
+
check("tmux installed", () => {
|
|
19
|
+
execSync("which tmux", { stdio: "ignore" });
|
|
20
|
+
return "found";
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
checks.push(
|
|
25
|
+
check("tmux version ≥ 3.0", () => {
|
|
26
|
+
const version = execSync("tmux -V", { encoding: "utf-8" }).trim();
|
|
27
|
+
const num = parseFloat(version.replace(/[^0-9.]/g, ""));
|
|
28
|
+
if (num < 3.0) throw new Error(`${version} (need ≥ 3.0)`);
|
|
29
|
+
return version;
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
checks.push(
|
|
34
|
+
check("Node.js ≥ 18", () => {
|
|
35
|
+
const major = parseInt(process.versions.node.split(".")[0]);
|
|
36
|
+
if (major < 18) throw new Error(`Node ${process.versions.node} (need ≥ 18)`);
|
|
37
|
+
return `v${process.versions.node}`;
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
checks.push(
|
|
42
|
+
check("256-color terminal", () => {
|
|
43
|
+
const term = process.env.TERM ?? "";
|
|
44
|
+
if (!term.includes("256color") && !term.includes("ghostty") && !term.includes("kitty") && term !== "tmux-256color") {
|
|
45
|
+
throw new Error(`$TERM is "${term}"`);
|
|
46
|
+
}
|
|
47
|
+
return term;
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
checks.push(
|
|
52
|
+
check("ide.yml exists", () => {
|
|
53
|
+
const path = resolve(".", "ide.yml");
|
|
54
|
+
if (!existsSync(path)) throw new Error("not found in current directory");
|
|
55
|
+
return "found";
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
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 })
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (json) {
|
|
69
|
+
const allPass = checks.every((c) => c.pass || c.optional);
|
|
70
|
+
console.log(JSON.stringify({ ok: allPass, checks }, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const c of checks) {
|
|
75
|
+
const icon = c.pass ? "✓" : c.optional ? "○" : "✗";
|
|
76
|
+
const color = c.pass ? "\x1b[32m" : c.optional ? "\x1b[33m" : "\x1b[31m";
|
|
77
|
+
console.log(`${color}${icon}\x1b[0m ${c.label} — ${c.detail}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const allPass = checks.every((c) => c.pass);
|
|
81
|
+
if (!allPass) process.exitCode = 1;
|
|
82
|
+
}
|