tmux-ide 1.2.1 → 1.3.1
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/bin/cli.js +14 -14
- package/dist/attach.d.ts +3 -0
- package/dist/attach.js +14 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +269 -0
- package/dist/detect.d.ts +15 -0
- package/dist/detect.js +228 -0
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +66 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +67 -0
- package/dist/inspect.d.ts +54 -0
- package/dist/inspect.js +118 -0
- package/dist/launch.d.ts +22 -0
- package/dist/launch.js +174 -0
- package/dist/lib/dot-path.d.ts +2 -0
- package/dist/lib/dot-path.js +17 -0
- package/dist/lib/errors.d.ts +29 -0
- package/dist/lib/errors.js +37 -0
- package/dist/lib/launch-plan.d.ts +6 -0
- package/dist/lib/launch-plan.js +40 -0
- package/dist/lib/output.d.ts +9 -0
- package/dist/lib/output.js +65 -0
- package/dist/lib/session-monitor.d.ts +12 -0
- package/dist/lib/session-monitor.js +160 -0
- package/dist/lib/session-options.d.ts +13 -0
- package/dist/lib/session-options.js +85 -0
- package/dist/lib/sizes.d.ts +16 -0
- package/dist/lib/sizes.js +36 -0
- package/dist/lib/tmux.d.ts +42 -0
- package/dist/lib/tmux.js +225 -0
- package/dist/lib/yaml-io.d.ts +10 -0
- package/dist/lib/yaml-io.js +24 -0
- package/dist/ls.d.ts +3 -0
- package/dist/ls.js +35 -0
- package/dist/restart.d.ts +4 -0
- package/dist/restart.js +13 -0
- package/dist/status.d.ts +3 -0
- package/dist/status.js +29 -0
- package/dist/stop.d.ts +3 -0
- package/dist/stop.js +21 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +172 -0
- package/package.json +16 -10
- package/src/attach.js +0 -17
- package/src/config.js +0 -341
- package/src/detect.js +0 -232
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/inspect.js +0 -127
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/output.js +0 -68
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/session-options.js +0 -95
- package/src/lib/sizes.js +0 -36
- package/src/lib/tmux.js +0 -237
- package/src/lib/yaml-io.js +0 -26
- package/src/ls.js +0 -40
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
package/bin/cli.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { launch } from "../
|
|
5
|
-
import { init } from "../
|
|
6
|
-
import { stop } from "../
|
|
7
|
-
import { attach } from "../
|
|
8
|
-
import { ls } from "../
|
|
9
|
-
import { doctor } from "../
|
|
10
|
-
import { status } from "../
|
|
11
|
-
import { inspect } from "../
|
|
12
|
-
import { validate } from "../
|
|
13
|
-
import { detect } from "../
|
|
14
|
-
import { config } from "../
|
|
15
|
-
import { restart } from "../
|
|
16
|
-
import { IdeError } from "../
|
|
17
|
-
import { printCommandError } from "../
|
|
4
|
+
import { launch } from "../dist/launch.js";
|
|
5
|
+
import { init } from "../dist/init.js";
|
|
6
|
+
import { stop } from "../dist/stop.js";
|
|
7
|
+
import { attach } from "../dist/attach.js";
|
|
8
|
+
import { ls } from "../dist/ls.js";
|
|
9
|
+
import { doctor } from "../dist/doctor.js";
|
|
10
|
+
import { status } from "../dist/status.js";
|
|
11
|
+
import { inspect } from "../dist/inspect.js";
|
|
12
|
+
import { validate } from "../dist/validate.js";
|
|
13
|
+
import { detect } from "../dist/detect.js";
|
|
14
|
+
import { config } from "../dist/config.js";
|
|
15
|
+
import { restart } from "../dist/restart.js";
|
|
16
|
+
import { IdeError } from "../dist/lib/errors.js";
|
|
17
|
+
import { printCommandError } from "../dist/lib/output.js";
|
|
18
18
|
|
|
19
19
|
const { positionals, values } = parseArgs({
|
|
20
20
|
allowPositionals: true,
|
package/dist/attach.d.ts
ADDED
package/dist/attach.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { getSessionName } from "./lib/yaml-io.js";
|
|
3
|
+
import { outputError } from "./lib/output.js";
|
|
4
|
+
import { attachSession, getSessionState } from "./lib/tmux.js";
|
|
5
|
+
export async function attach(targetDir, { json: _json } = {}) {
|
|
6
|
+
const dir = resolve(targetDir ?? ".");
|
|
7
|
+
const { name: session } = getSessionName(dir);
|
|
8
|
+
const state = getSessionState(session);
|
|
9
|
+
if (!state.running) {
|
|
10
|
+
outputError(`Session "${session}" is not running. Start it with: tmux-ide`, "NOT_RUNNING");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
attachSession(session);
|
|
14
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { readConfig, writeConfig } from "./lib/yaml-io.js";
|
|
3
|
+
import { setByPath } from "./lib/dot-path.js";
|
|
4
|
+
import { outputError } from "./lib/output.js";
|
|
5
|
+
/**
|
|
6
|
+
* Read config safely (read-only, no write). Returns config or undefined on error.
|
|
7
|
+
*/
|
|
8
|
+
function readConfigSafe(dir) {
|
|
9
|
+
let cfg;
|
|
10
|
+
try {
|
|
11
|
+
({ config: cfg } = readConfig(dir));
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return cfg;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read config, validate it's an object, run mutator, then write back.
|
|
21
|
+
* Returns the mutator's return value, or undefined on error.
|
|
22
|
+
*/
|
|
23
|
+
function withConfig(dir, mutator) {
|
|
24
|
+
const cfg = readConfigSafe(dir);
|
|
25
|
+
if (cfg === undefined)
|
|
26
|
+
return;
|
|
27
|
+
if (!isConfigObject(cfg)) {
|
|
28
|
+
outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const result = mutator(cfg);
|
|
32
|
+
writeConfig(dir, cfg);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export async function config(targetDir, { json, action, args } = {}) {
|
|
36
|
+
const dir = resolve(targetDir ?? ".");
|
|
37
|
+
switch (action) {
|
|
38
|
+
case "dump":
|
|
39
|
+
return dumpConfig(dir, { json });
|
|
40
|
+
case "set":
|
|
41
|
+
return setConfig(dir, args ?? [], { json });
|
|
42
|
+
case "add-pane":
|
|
43
|
+
return addPane(dir, args ?? [], { json });
|
|
44
|
+
case "remove-pane":
|
|
45
|
+
return removePane(dir, args ?? [], { json });
|
|
46
|
+
case "add-row":
|
|
47
|
+
return addRow(dir, args ?? [], { json });
|
|
48
|
+
case "enable-team":
|
|
49
|
+
return enableTeam(dir, args ?? [], { json });
|
|
50
|
+
case "disable-team":
|
|
51
|
+
return disableTeam(dir, { json });
|
|
52
|
+
default:
|
|
53
|
+
return dumpConfig(dir, { json });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function dumpConfig(dir, { json }) {
|
|
57
|
+
const cfg = readConfigSafe(dir);
|
|
58
|
+
if (cfg === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function setConfig(dir, args, { json }) {
|
|
68
|
+
const [dotpath, ...rest] = args;
|
|
69
|
+
if (!dotpath || rest.length === 0) {
|
|
70
|
+
outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let value = rest.join(" ");
|
|
74
|
+
if (value === "true")
|
|
75
|
+
value = true;
|
|
76
|
+
else if (value === "false")
|
|
77
|
+
value = false;
|
|
78
|
+
else if (/^\d+$/.test(value))
|
|
79
|
+
value = parseInt(value);
|
|
80
|
+
withConfig(dir, (cfg) => {
|
|
81
|
+
setByPath(cfg, dotpath, value);
|
|
82
|
+
});
|
|
83
|
+
if (json) {
|
|
84
|
+
console.log(JSON.stringify({ ok: true, path: dotpath, value }, null, 2));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(`Set ${dotpath} = ${JSON.stringify(value)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function addPane(dir, args, { json }) {
|
|
91
|
+
const { row, title, command, size } = parseNamedArgs(args);
|
|
92
|
+
if (row === undefined) {
|
|
93
|
+
outputError("Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]", "USAGE");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const rowIdx = parseIndex(row);
|
|
97
|
+
if (rowIdx == null) {
|
|
98
|
+
outputError(`Invalid row index "${row}"`, "USAGE");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const pane = {};
|
|
102
|
+
if (title)
|
|
103
|
+
pane.title = title;
|
|
104
|
+
if (command)
|
|
105
|
+
pane.command = command;
|
|
106
|
+
if (size)
|
|
107
|
+
pane.size = size;
|
|
108
|
+
withConfig(dir, (cfg) => {
|
|
109
|
+
if (!Array.isArray(cfg.rows)) {
|
|
110
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
111
|
+
}
|
|
112
|
+
if (!cfg.rows[rowIdx]) {
|
|
113
|
+
outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW");
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(cfg.rows[rowIdx].panes)) {
|
|
116
|
+
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
117
|
+
}
|
|
118
|
+
cfg.rows[rowIdx].panes.push(pane);
|
|
119
|
+
});
|
|
120
|
+
if (json) {
|
|
121
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, pane }, null, 2));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(`Added pane "${title ?? "untitled"}" to row ${rowIdx}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function removePane(dir, args, { json }) {
|
|
128
|
+
const { row, pane } = parseNamedArgs(args);
|
|
129
|
+
if (row === undefined || pane === undefined) {
|
|
130
|
+
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const rowIdx = parseIndex(row);
|
|
134
|
+
const paneIdx = parseIndex(pane);
|
|
135
|
+
if (rowIdx == null || paneIdx == null) {
|
|
136
|
+
outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let removed;
|
|
140
|
+
withConfig(dir, (cfg) => {
|
|
141
|
+
if (!Array.isArray(cfg.rows)) {
|
|
142
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
143
|
+
}
|
|
144
|
+
if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
|
|
145
|
+
outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
|
|
146
|
+
}
|
|
147
|
+
if (!cfg.rows[rowIdx].panes[paneIdx]) {
|
|
148
|
+
outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE");
|
|
149
|
+
}
|
|
150
|
+
removed = cfg.rows[rowIdx].panes.splice(paneIdx, 1)[0];
|
|
151
|
+
});
|
|
152
|
+
if (json) {
|
|
153
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, pane: paneIdx, removed }, null, 2));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(`Removed pane ${paneIdx} ("${removed?.title ?? "untitled"}") from row ${rowIdx}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function addRow(dir, args, { json }) {
|
|
160
|
+
const { size } = parseNamedArgs(args);
|
|
161
|
+
let rowIdx;
|
|
162
|
+
withConfig(dir, (cfg) => {
|
|
163
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
164
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
165
|
+
}
|
|
166
|
+
const row = { panes: [{ title: "Shell" }] };
|
|
167
|
+
if (size)
|
|
168
|
+
row.size = size;
|
|
169
|
+
cfg.rows = cfg.rows ?? [];
|
|
170
|
+
cfg.rows.push(row);
|
|
171
|
+
rowIdx = cfg.rows.length - 1;
|
|
172
|
+
});
|
|
173
|
+
if (json) {
|
|
174
|
+
console.log(JSON.stringify({ ok: true, row: rowIdx, size: size ?? null }, null, 2));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(`Added row ${rowIdx}${size ? ` (${size})` : ""}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function enableTeam(dir, args, { json }) {
|
|
181
|
+
const { name } = parseNamedArgs(args);
|
|
182
|
+
let teamName;
|
|
183
|
+
let result;
|
|
184
|
+
withConfig(dir, (cfg) => {
|
|
185
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
186
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
187
|
+
}
|
|
188
|
+
teamName = name ?? cfg.name ?? "my-team";
|
|
189
|
+
cfg.team = { name: teamName };
|
|
190
|
+
let leadAssigned = false;
|
|
191
|
+
for (const row of cfg.rows ?? []) {
|
|
192
|
+
for (const pane of row.panes ?? []) {
|
|
193
|
+
if (pane.command === "claude" || pane.role === "lead" || pane.role === "teammate") {
|
|
194
|
+
if (!leadAssigned) {
|
|
195
|
+
pane.role = "lead";
|
|
196
|
+
leadAssigned = true;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
pane.role = "teammate";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!leadAssigned) {
|
|
205
|
+
delete cfg.team;
|
|
206
|
+
outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG");
|
|
207
|
+
}
|
|
208
|
+
result = { team: cfg.team, roles: summarizeRoles(cfg) };
|
|
209
|
+
});
|
|
210
|
+
if (json) {
|
|
211
|
+
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.log(`Enabled agent team "${teamName}"`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function disableTeam(dir, { json }) {
|
|
218
|
+
withConfig(dir, (cfg) => {
|
|
219
|
+
if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
|
|
220
|
+
outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
|
|
221
|
+
}
|
|
222
|
+
delete cfg.team;
|
|
223
|
+
for (const row of cfg.rows ?? []) {
|
|
224
|
+
if (!Array.isArray(row?.panes))
|
|
225
|
+
continue;
|
|
226
|
+
for (const pane of row.panes) {
|
|
227
|
+
delete pane.role;
|
|
228
|
+
delete pane.task;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
if (json) {
|
|
233
|
+
console.log(JSON.stringify({ ok: true, disabled: true }, null, 2));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log("Disabled agent team");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function summarizeRoles(cfg) {
|
|
240
|
+
const roles = [];
|
|
241
|
+
for (let i = 0; i < (cfg.rows ?? []).length; i++) {
|
|
242
|
+
for (let j = 0; j < (cfg.rows[i].panes ?? []).length; j++) {
|
|
243
|
+
const p = cfg.rows[i].panes[j];
|
|
244
|
+
if (p.role) {
|
|
245
|
+
roles.push({ row: i, pane: j, title: p.title ?? null, role: p.role });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return roles;
|
|
250
|
+
}
|
|
251
|
+
function parseNamedArgs(args) {
|
|
252
|
+
const result = {};
|
|
253
|
+
for (let i = 0; i < args.length; i++) {
|
|
254
|
+
if (args[i].startsWith("--") && i + 1 < args.length) {
|
|
255
|
+
const key = args[i].slice(2);
|
|
256
|
+
result[key] = args[i + 1];
|
|
257
|
+
i++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function isConfigObject(value) {
|
|
263
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
264
|
+
}
|
|
265
|
+
function parseIndex(value) {
|
|
266
|
+
if (!/^\d+$/.test(String(value)))
|
|
267
|
+
return null;
|
|
268
|
+
return Number.parseInt(value, 10);
|
|
269
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IdeConfig } from "./types.ts";
|
|
2
|
+
interface DetectedStack {
|
|
3
|
+
packageManager: string | null;
|
|
4
|
+
frameworks: string[];
|
|
5
|
+
devCommand: string | null;
|
|
6
|
+
language: string | null;
|
|
7
|
+
reasons: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function detectStack(dir: string): DetectedStack;
|
|
10
|
+
export declare function suggestConfig(dir: string, detected: DetectedStack): IdeConfig;
|
|
11
|
+
export declare function detect(targetDir: string | undefined, { json, write }?: {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
write?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export {};
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { writeConfig } from "./lib/yaml-io.js";
|
|
4
|
+
function fileExists(dir, name) {
|
|
5
|
+
return existsSync(resolve(dir, name));
|
|
6
|
+
}
|
|
7
|
+
function readJson(dir, name) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(resolve(dir, name), "utf-8"));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function detectStack(dir) {
|
|
16
|
+
const detected = {
|
|
17
|
+
packageManager: null,
|
|
18
|
+
frameworks: [],
|
|
19
|
+
devCommand: null,
|
|
20
|
+
language: null,
|
|
21
|
+
reasons: [],
|
|
22
|
+
};
|
|
23
|
+
// Detect package manager from lockfile
|
|
24
|
+
if (fileExists(dir, "pnpm-lock.yaml")) {
|
|
25
|
+
detected.packageManager = "pnpm";
|
|
26
|
+
detected.reasons.push('Detected pnpm from "pnpm-lock.yaml".');
|
|
27
|
+
}
|
|
28
|
+
else if (fileExists(dir, "bun.lockb") || fileExists(dir, "bun.lock")) {
|
|
29
|
+
detected.packageManager = "bun";
|
|
30
|
+
detected.reasons.push('Detected bun from "bun.lockb" or "bun.lock".');
|
|
31
|
+
}
|
|
32
|
+
else if (fileExists(dir, "yarn.lock")) {
|
|
33
|
+
detected.packageManager = "yarn";
|
|
34
|
+
detected.reasons.push('Detected yarn from "yarn.lock".');
|
|
35
|
+
}
|
|
36
|
+
else if (fileExists(dir, "package-lock.json")) {
|
|
37
|
+
detected.packageManager = "npm";
|
|
38
|
+
detected.reasons.push('Detected npm from "package-lock.json".');
|
|
39
|
+
}
|
|
40
|
+
const pkg = readJson(dir, "package.json");
|
|
41
|
+
if (pkg) {
|
|
42
|
+
detected.language = "javascript";
|
|
43
|
+
detected.reasons.push('Detected JavaScript from "package.json".');
|
|
44
|
+
const deps = {
|
|
45
|
+
...pkg.dependencies,
|
|
46
|
+
...pkg.devDependencies,
|
|
47
|
+
};
|
|
48
|
+
if (deps["next"])
|
|
49
|
+
pushFramework(detected, "next", 'Found dependency "next".');
|
|
50
|
+
if (deps["convex"])
|
|
51
|
+
pushFramework(detected, "convex", 'Found dependency "convex".');
|
|
52
|
+
if (deps["vite"])
|
|
53
|
+
pushFramework(detected, "vite", 'Found dependency "vite".');
|
|
54
|
+
if (deps["remix"] || deps["@remix-run/node"])
|
|
55
|
+
pushFramework(detected, "remix", "Found Remix dependency.");
|
|
56
|
+
if (deps["nuxt"])
|
|
57
|
+
pushFramework(detected, "nuxt", 'Found dependency "nuxt".');
|
|
58
|
+
if (deps["astro"])
|
|
59
|
+
pushFramework(detected, "astro", 'Found dependency "astro".');
|
|
60
|
+
if (deps["svelte"] || deps["@sveltejs/kit"])
|
|
61
|
+
pushFramework(detected, "svelte", "Found Svelte dependency.");
|
|
62
|
+
// Detect dev command
|
|
63
|
+
const pm = detected.packageManager ?? "npm";
|
|
64
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
65
|
+
const scripts = pkg.scripts;
|
|
66
|
+
if (scripts?.dev) {
|
|
67
|
+
detected.devCommand = `${run} dev`;
|
|
68
|
+
detected.reasons.push(`Using dev command "${detected.devCommand}" from package.json scripts.`);
|
|
69
|
+
}
|
|
70
|
+
else if (scripts?.start) {
|
|
71
|
+
detected.devCommand = `${run} start`;
|
|
72
|
+
detected.reasons.push(`Using start command "${detected.devCommand}" from package.json scripts.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Python
|
|
76
|
+
if (fileExists(dir, "pyproject.toml") || fileExists(dir, "requirements.txt")) {
|
|
77
|
+
detected.language = detected.language ?? "python";
|
|
78
|
+
detected.reasons.push('Detected Python from "pyproject.toml" or "requirements.txt".');
|
|
79
|
+
try {
|
|
80
|
+
const pyproject = readFileSync(resolve(dir, "pyproject.toml"), "utf-8");
|
|
81
|
+
if (pyproject.includes("fastapi"))
|
|
82
|
+
pushFramework(detected, "fastapi", 'Found "fastapi" in pyproject.toml.');
|
|
83
|
+
else if (pyproject.includes("django"))
|
|
84
|
+
pushFramework(detected, "django", 'Found "django" in pyproject.toml.');
|
|
85
|
+
else if (pyproject.includes("flask"))
|
|
86
|
+
pushFramework(detected, "flask", 'Found "flask" in pyproject.toml.');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Ignore missing or unreadable pyproject metadata.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Rust
|
|
93
|
+
if (fileExists(dir, "Cargo.toml")) {
|
|
94
|
+
detected.language = detected.language ?? "rust";
|
|
95
|
+
detected.reasons.push('Detected Rust from "Cargo.toml".');
|
|
96
|
+
pushFramework(detected, "cargo", 'Using Cargo workflow from "Cargo.toml".');
|
|
97
|
+
}
|
|
98
|
+
// Go
|
|
99
|
+
if (fileExists(dir, "go.mod")) {
|
|
100
|
+
detected.language = detected.language ?? "go";
|
|
101
|
+
detected.reasons.push('Detected Go from "go.mod".');
|
|
102
|
+
pushFramework(detected, "go", 'Using Go workflow from "go.mod".');
|
|
103
|
+
}
|
|
104
|
+
// Docker
|
|
105
|
+
if (fileExists(dir, "docker-compose.yml") || fileExists(dir, "docker-compose.yaml")) {
|
|
106
|
+
pushFramework(detected, "docker", 'Detected Docker from "docker-compose.yml" or "docker-compose.yaml".');
|
|
107
|
+
}
|
|
108
|
+
if (detected.reasons.length === 0) {
|
|
109
|
+
detected.reasons.push("No framework-specific signals found; using the generic layout.");
|
|
110
|
+
}
|
|
111
|
+
return detected;
|
|
112
|
+
}
|
|
113
|
+
export function suggestConfig(dir, detected) {
|
|
114
|
+
const name = basename(dir);
|
|
115
|
+
const pm = detected.packageManager ?? "npm";
|
|
116
|
+
const run = pm === "npm" ? "npm run" : pm;
|
|
117
|
+
// Default: 2 claude panes + shell
|
|
118
|
+
const config = {
|
|
119
|
+
name,
|
|
120
|
+
rows: [
|
|
121
|
+
{
|
|
122
|
+
size: "70%",
|
|
123
|
+
panes: [
|
|
124
|
+
{ title: "Claude 1", command: "claude" },
|
|
125
|
+
{ title: "Claude 2", command: "claude" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
panes: [],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
const bottom = config.rows[1].panes;
|
|
134
|
+
const frameworks = detected.frameworks;
|
|
135
|
+
// Add 3rd claude pane for complex stacks
|
|
136
|
+
if (frameworks.length >= 2) {
|
|
137
|
+
config.rows[0].panes.push({ title: "Claude 3", command: "claude" });
|
|
138
|
+
}
|
|
139
|
+
// Add dev servers
|
|
140
|
+
if (frameworks.includes("next")) {
|
|
141
|
+
bottom.push({ title: "Next.js", command: `${run} dev` });
|
|
142
|
+
}
|
|
143
|
+
else if (frameworks.includes("vite")) {
|
|
144
|
+
bottom.push({ title: "Vite", command: `${run} dev` });
|
|
145
|
+
}
|
|
146
|
+
else if (frameworks.includes("nuxt")) {
|
|
147
|
+
bottom.push({ title: "Nuxt", command: `${run} dev` });
|
|
148
|
+
}
|
|
149
|
+
else if (frameworks.includes("remix")) {
|
|
150
|
+
bottom.push({ title: "Remix", command: `${run} dev` });
|
|
151
|
+
}
|
|
152
|
+
else if (frameworks.includes("astro")) {
|
|
153
|
+
bottom.push({ title: "Astro", command: `${run} dev` });
|
|
154
|
+
}
|
|
155
|
+
else if (frameworks.includes("svelte")) {
|
|
156
|
+
bottom.push({ title: "SvelteKit", command: `${run} dev` });
|
|
157
|
+
}
|
|
158
|
+
else if (frameworks.includes("fastapi")) {
|
|
159
|
+
bottom.push({ title: "FastAPI", command: "uvicorn main:app --reload" });
|
|
160
|
+
}
|
|
161
|
+
else if (frameworks.includes("django")) {
|
|
162
|
+
bottom.push({ title: "Django", command: "python manage.py runserver" });
|
|
163
|
+
}
|
|
164
|
+
else if (frameworks.includes("flask")) {
|
|
165
|
+
bottom.push({ title: "Flask", command: "flask run --reload" });
|
|
166
|
+
}
|
|
167
|
+
else if (frameworks.includes("cargo")) {
|
|
168
|
+
bottom.push({ title: "Cargo", command: "cargo watch -x run" });
|
|
169
|
+
}
|
|
170
|
+
else if (frameworks.includes("go")) {
|
|
171
|
+
bottom.push({ title: "Go", command: "go run ." });
|
|
172
|
+
}
|
|
173
|
+
else if (detected.devCommand) {
|
|
174
|
+
bottom.push({ title: "Dev Server", command: detected.devCommand });
|
|
175
|
+
}
|
|
176
|
+
if (frameworks.includes("convex")) {
|
|
177
|
+
bottom.push({ title: "Convex", command: "npx convex dev" });
|
|
178
|
+
}
|
|
179
|
+
// Always add shell
|
|
180
|
+
bottom.push({ title: "Shell" });
|
|
181
|
+
return config;
|
|
182
|
+
}
|
|
183
|
+
export async function detect(targetDir, { json, write } = {}) {
|
|
184
|
+
const dir = resolve(targetDir ?? ".");
|
|
185
|
+
const detected = detectStack(dir);
|
|
186
|
+
const suggested = suggestConfig(dir, detected);
|
|
187
|
+
if (write) {
|
|
188
|
+
writeConfig(dir, suggested);
|
|
189
|
+
if (json) {
|
|
190
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested, written: true }, null, 2));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const desc = detected.frameworks.length > 0
|
|
194
|
+
? detected.frameworks.join(" + ")
|
|
195
|
+
: (detected.language ?? "generic project");
|
|
196
|
+
console.log(`Detected ${desc}. Created ide.yml.`);
|
|
197
|
+
console.log("\nWhy this layout:");
|
|
198
|
+
for (const reason of detected.reasons) {
|
|
199
|
+
console.log(` - ${reason}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (json) {
|
|
205
|
+
console.log(JSON.stringify({ detected, suggestedConfig: suggested }, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log("Detected stack:");
|
|
209
|
+
if (detected.packageManager)
|
|
210
|
+
console.log(` Package manager: ${detected.packageManager}`);
|
|
211
|
+
if (detected.language)
|
|
212
|
+
console.log(` Language: ${detected.language}`);
|
|
213
|
+
if (detected.frameworks.length)
|
|
214
|
+
console.log(` Frameworks: ${detected.frameworks.join(", ")}`);
|
|
215
|
+
if (detected.devCommand)
|
|
216
|
+
console.log(` Dev command: ${detected.devCommand}`);
|
|
217
|
+
console.log("\nReasoning:");
|
|
218
|
+
for (const reason of detected.reasons) {
|
|
219
|
+
console.log(` - ${reason}`);
|
|
220
|
+
}
|
|
221
|
+
console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
|
|
222
|
+
}
|
|
223
|
+
function pushFramework(detected, framework, reason) {
|
|
224
|
+
if (!detected.frameworks.includes(framework)) {
|
|
225
|
+
detected.frameworks.push(framework);
|
|
226
|
+
}
|
|
227
|
+
detected.reasons.push(reason);
|
|
228
|
+
}
|
package/dist/doctor.d.ts
ADDED
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
function check(label, fn, { optional = false } = {}) {
|
|
5
|
+
try {
|
|
6
|
+
const result = fn();
|
|
7
|
+
return { label, pass: true, detail: result, optional };
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
return { label, pass: false, detail: e.message, optional };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function doctor({ json } = {}) {
|
|
14
|
+
const checks = [];
|
|
15
|
+
checks.push(check("tmux installed", () => {
|
|
16
|
+
execSync("which tmux", { stdio: "ignore" });
|
|
17
|
+
return "found";
|
|
18
|
+
}));
|
|
19
|
+
checks.push(check("tmux version ≥ 3.0", () => {
|
|
20
|
+
const version = execSync("tmux -V", { encoding: "utf-8" }).trim();
|
|
21
|
+
const num = parseFloat(version.replace(/[^0-9.]/g, ""));
|
|
22
|
+
if (num < 3.0)
|
|
23
|
+
throw new Error(`${version} (need ≥ 3.0)`);
|
|
24
|
+
return version;
|
|
25
|
+
}));
|
|
26
|
+
checks.push(check("Node.js ≥ 18", () => {
|
|
27
|
+
const major = parseInt(process.versions.node.split(".")[0]);
|
|
28
|
+
if (major < 18)
|
|
29
|
+
throw new Error(`Node ${process.versions.node} (need ≥ 18)`);
|
|
30
|
+
return `v${process.versions.node}`;
|
|
31
|
+
}));
|
|
32
|
+
checks.push(check("256-color terminal", () => {
|
|
33
|
+
const term = process.env.TERM ?? "";
|
|
34
|
+
if (!term.includes("256color") &&
|
|
35
|
+
!term.includes("ghostty") &&
|
|
36
|
+
!term.includes("kitty") &&
|
|
37
|
+
term !== "tmux-256color") {
|
|
38
|
+
throw new Error(`$TERM is "${term}"`);
|
|
39
|
+
}
|
|
40
|
+
return term;
|
|
41
|
+
}));
|
|
42
|
+
checks.push(check("ide.yml exists", () => {
|
|
43
|
+
const path = resolve(".", "ide.yml");
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
throw new Error("not found in current directory");
|
|
46
|
+
return "found";
|
|
47
|
+
}));
|
|
48
|
+
checks.push(check("Claude Code agent teams", () => {
|
|
49
|
+
if (process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
|
|
50
|
+
throw new Error("not set (enable with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)");
|
|
51
|
+
}
|
|
52
|
+
return "enabled";
|
|
53
|
+
}, { optional: true }));
|
|
54
|
+
const allPass = checks.every((c) => c.pass || c.optional);
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({ ok: allPass, checks }, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const c of checks) {
|
|
60
|
+
const icon = c.pass ? "✓" : c.optional ? "○" : "✗";
|
|
61
|
+
const color = c.pass ? "\x1b[32m" : c.optional ? "\x1b[33m" : "\x1b[31m";
|
|
62
|
+
console.log(`${color}${icon}\x1b[0m ${c.label} — ${c.detail}`);
|
|
63
|
+
}
|
|
64
|
+
if (!allPass)
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
package/dist/init.d.ts
ADDED