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.
Files changed (67) hide show
  1. package/bin/cli.js +14 -14
  2. package/dist/attach.d.ts +3 -0
  3. package/dist/attach.js +14 -0
  4. package/dist/config.d.ts +5 -0
  5. package/dist/config.js +269 -0
  6. package/dist/detect.d.ts +15 -0
  7. package/dist/detect.js +228 -0
  8. package/dist/doctor.d.ts +3 -0
  9. package/dist/doctor.js +66 -0
  10. package/dist/init.d.ts +4 -0
  11. package/dist/init.js +67 -0
  12. package/dist/inspect.d.ts +54 -0
  13. package/dist/inspect.js +118 -0
  14. package/dist/launch.d.ts +22 -0
  15. package/dist/launch.js +174 -0
  16. package/dist/lib/dot-path.d.ts +2 -0
  17. package/dist/lib/dot-path.js +17 -0
  18. package/dist/lib/errors.d.ts +29 -0
  19. package/dist/lib/errors.js +37 -0
  20. package/dist/lib/launch-plan.d.ts +6 -0
  21. package/dist/lib/launch-plan.js +40 -0
  22. package/dist/lib/output.d.ts +9 -0
  23. package/dist/lib/output.js +65 -0
  24. package/dist/lib/session-monitor.d.ts +12 -0
  25. package/dist/lib/session-monitor.js +160 -0
  26. package/dist/lib/session-options.d.ts +13 -0
  27. package/dist/lib/session-options.js +85 -0
  28. package/dist/lib/sizes.d.ts +16 -0
  29. package/dist/lib/sizes.js +36 -0
  30. package/dist/lib/tmux.d.ts +42 -0
  31. package/dist/lib/tmux.js +225 -0
  32. package/dist/lib/yaml-io.d.ts +10 -0
  33. package/dist/lib/yaml-io.js +24 -0
  34. package/dist/ls.d.ts +3 -0
  35. package/dist/ls.js +35 -0
  36. package/dist/restart.d.ts +4 -0
  37. package/dist/restart.js +13 -0
  38. package/dist/status.d.ts +3 -0
  39. package/dist/status.js +29 -0
  40. package/dist/stop.d.ts +3 -0
  41. package/dist/stop.js +21 -0
  42. package/dist/types.d.ts +41 -0
  43. package/dist/types.js +1 -0
  44. package/dist/validate.d.ts +4 -0
  45. package/dist/validate.js +172 -0
  46. package/package.json +16 -10
  47. package/src/attach.js +0 -17
  48. package/src/config.js +0 -341
  49. package/src/detect.js +0 -232
  50. package/src/doctor.js +0 -91
  51. package/src/init.js +0 -73
  52. package/src/inspect.js +0 -127
  53. package/src/launch.js +0 -245
  54. package/src/lib/dot-path.js +0 -16
  55. package/src/lib/errors.js +0 -35
  56. package/src/lib/launch-plan.js +0 -49
  57. package/src/lib/output.js +0 -68
  58. package/src/lib/session-monitor.js +0 -179
  59. package/src/lib/session-options.js +0 -95
  60. package/src/lib/sizes.js +0 -36
  61. package/src/lib/tmux.js +0 -237
  62. package/src/lib/yaml-io.js +0 -26
  63. package/src/ls.js +0 -40
  64. package/src/restart.js +0 -16
  65. package/src/status.js +0 -35
  66. package/src/stop.js +0 -25
  67. 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 "../src/launch.js";
5
- import { init } from "../src/init.js";
6
- import { stop } from "../src/stop.js";
7
- import { attach } from "../src/attach.js";
8
- import { ls } from "../src/ls.js";
9
- import { doctor } from "../src/doctor.js";
10
- import { status } from "../src/status.js";
11
- import { inspect } from "../src/inspect.js";
12
- import { validate } from "../src/validate.js";
13
- import { detect } from "../src/detect.js";
14
- import { config } from "../src/config.js";
15
- import { restart } from "../src/restart.js";
16
- import { IdeError } from "../src/lib/errors.js";
17
- import { printCommandError } from "../src/lib/output.js";
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,
@@ -0,0 +1,3 @@
1
+ export declare function attach(targetDir: string | undefined, { json: _json }?: {
2
+ json?: boolean;
3
+ }): Promise<void>;
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
+ }
@@ -0,0 +1,5 @@
1
+ export declare function config(targetDir: string | null, { json, action, args }?: {
2
+ json?: boolean;
3
+ action?: string;
4
+ args?: string[];
5
+ }): Promise<void>;
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
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare function doctor({ json }?: {
2
+ json?: boolean;
3
+ }): Promise<void>;
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
@@ -0,0 +1,4 @@
1
+ export declare function init({ template, json, }?: {
2
+ template?: string;
3
+ json?: boolean;
4
+ }): Promise<void>;