tmux-ide 1.3.1 → 2.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.
Files changed (236) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +27 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/attach.ts +20 -0
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/detect.ts +248 -0
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/inspect.ts +212 -0
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/output.ts +76 -0
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/session-options.ts +100 -0
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/sizes.ts +36 -0
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/yaml-io.ts +27 -0
  103. package/src/ls.test.ts +76 -0
  104. package/src/ls.ts +40 -0
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/dist/attach.d.ts +0 -3
  194. package/dist/attach.js +0 -14
  195. package/dist/config.d.ts +0 -5
  196. package/dist/config.js +0 -269
  197. package/dist/detect.d.ts +0 -15
  198. package/dist/detect.js +0 -228
  199. package/dist/doctor.d.ts +0 -3
  200. package/dist/doctor.js +0 -66
  201. package/dist/init.d.ts +0 -4
  202. package/dist/init.js +0 -67
  203. package/dist/inspect.d.ts +0 -54
  204. package/dist/inspect.js +0 -118
  205. package/dist/launch.d.ts +0 -22
  206. package/dist/launch.js +0 -174
  207. package/dist/lib/dot-path.d.ts +0 -2
  208. package/dist/lib/dot-path.js +0 -17
  209. package/dist/lib/errors.d.ts +0 -29
  210. package/dist/lib/errors.js +0 -37
  211. package/dist/lib/launch-plan.d.ts +0 -6
  212. package/dist/lib/launch-plan.js +0 -40
  213. package/dist/lib/output.d.ts +0 -9
  214. package/dist/lib/output.js +0 -65
  215. package/dist/lib/session-monitor.d.ts +0 -12
  216. package/dist/lib/session-monitor.js +0 -160
  217. package/dist/lib/session-options.d.ts +0 -13
  218. package/dist/lib/session-options.js +0 -85
  219. package/dist/lib/sizes.d.ts +0 -16
  220. package/dist/lib/sizes.js +0 -36
  221. package/dist/lib/tmux.d.ts +0 -42
  222. package/dist/lib/tmux.js +0 -225
  223. package/dist/lib/yaml-io.d.ts +0 -10
  224. package/dist/lib/yaml-io.js +0 -24
  225. package/dist/ls.d.ts +0 -3
  226. package/dist/ls.js +0 -35
  227. package/dist/restart.d.ts +0 -4
  228. package/dist/restart.js +0 -13
  229. package/dist/status.d.ts +0 -3
  230. package/dist/status.js +0 -29
  231. package/dist/stop.d.ts +0 -3
  232. package/dist/stop.js +0 -21
  233. package/dist/types.d.ts +0 -41
  234. package/dist/types.js +0 -1
  235. package/dist/validate.d.ts +0 -4
  236. package/dist/validate.js +0 -172
package/src/doctor.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { readConfig } from "./lib/yaml-io.ts";
5
+ import { loadSkills } from "./lib/skill-registry.ts";
6
+
7
+ interface CheckResult {
8
+ label: string;
9
+ pass: boolean;
10
+ detail: string;
11
+ optional: boolean;
12
+ }
13
+
14
+ function check(
15
+ label: string,
16
+ fn: () => string,
17
+ { optional = false }: { optional?: boolean } = {},
18
+ ): CheckResult {
19
+ try {
20
+ const result = fn();
21
+ return { label, pass: true, detail: result, optional };
22
+ } catch (e) {
23
+ return { label, pass: false, detail: (e as Error).message, optional };
24
+ }
25
+ }
26
+
27
+ export async function doctor({ json }: { json?: boolean } = {}): Promise<void> {
28
+ const checks: CheckResult[] = [];
29
+
30
+ checks.push(
31
+ check("tmux installed", () => {
32
+ execSync("which tmux", { stdio: "ignore" });
33
+ return "found";
34
+ }),
35
+ );
36
+
37
+ checks.push(
38
+ check("tmux version ≥ 3.0", () => {
39
+ const version = execSync("tmux -V", { encoding: "utf-8" }).trim();
40
+ const num = parseFloat(version.replace(/[^0-9.]/g, ""));
41
+ if (num < 3.0) throw new Error(`${version} (need ≥ 3.0)`);
42
+ return version;
43
+ }),
44
+ );
45
+
46
+ checks.push(
47
+ check("Node.js ≥ 18", () => {
48
+ const major = parseInt(process.versions.node.split(".")[0]!);
49
+ if (major < 18) throw new Error(`Node ${process.versions.node} (need ≥ 18)`);
50
+ return `v${process.versions.node}`;
51
+ }),
52
+ );
53
+
54
+ checks.push(
55
+ check(
56
+ "256-color terminal",
57
+ () => {
58
+ const term = process.env.TERM ?? "";
59
+ if (
60
+ !term.includes("256color") &&
61
+ !term.includes("ghostty") &&
62
+ !term.includes("kitty") &&
63
+ term !== "tmux-256color"
64
+ ) {
65
+ throw new Error(`$TERM is "${term}"`);
66
+ }
67
+ return term;
68
+ },
69
+ { optional: true },
70
+ ),
71
+ );
72
+
73
+ checks.push(
74
+ check("ide.yml exists", () => {
75
+ const path = resolve(".", "ide.yml");
76
+ if (!existsSync(path)) throw new Error("not found in current directory");
77
+ return "found";
78
+ }),
79
+ );
80
+
81
+ checks.push(
82
+ check(
83
+ "Claude Code agent teams",
84
+ () => {
85
+ if (process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS !== "1") {
86
+ throw new Error("not set (enable with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)");
87
+ }
88
+ return "enabled";
89
+ },
90
+ { optional: true },
91
+ ),
92
+ );
93
+
94
+ checks.push(
95
+ check(
96
+ "pane skill references",
97
+ () => {
98
+ const dir = resolve(".");
99
+ const { config } = readConfig(dir);
100
+ const skills = loadSkills(dir);
101
+ const skillNames = new Set(skills.map((s) => s.name));
102
+ const unresolved: string[] = [];
103
+ for (const row of config.rows) {
104
+ for (const pane of row.panes) {
105
+ if (pane.skill && !skillNames.has(pane.skill)) {
106
+ unresolved.push(`${pane.title ?? "untitled"} → ${pane.skill}`);
107
+ }
108
+ }
109
+ }
110
+ if (unresolved.length > 0) {
111
+ throw new Error(`${unresolved.length} unresolved: ${unresolved.join(", ")}`);
112
+ }
113
+ const count = config.rows.flatMap((r) => r.panes).filter((p) => p.skill).length;
114
+ return count > 0 ? `${count} reference(s), all resolved` : "no skill references";
115
+ },
116
+ { optional: true },
117
+ ),
118
+ );
119
+
120
+ checks.push(
121
+ check(
122
+ "tailscale CLI",
123
+ () => {
124
+ const version = execSync("tailscale version", { encoding: "utf-8" }).trim().split("\n")[0]!;
125
+ return version;
126
+ },
127
+ { optional: true },
128
+ ),
129
+ );
130
+
131
+ checks.push(
132
+ check(
133
+ "ngrok CLI",
134
+ () => {
135
+ const version = execSync("ngrok version", { encoding: "utf-8" }).trim();
136
+ return version;
137
+ },
138
+ { optional: true },
139
+ ),
140
+ );
141
+
142
+ checks.push(
143
+ check(
144
+ "cloudflared CLI",
145
+ () => {
146
+ const version = execSync("cloudflared --version", { encoding: "utf-8" }).trim();
147
+ return version;
148
+ },
149
+ { optional: true },
150
+ ),
151
+ );
152
+
153
+ const allPass = checks.every((c) => c.pass || c.optional);
154
+
155
+ if (json) {
156
+ console.log(JSON.stringify({ ok: allPass, checks }, null, 2));
157
+ return;
158
+ }
159
+
160
+ for (const c of checks) {
161
+ const icon = c.pass ? "✓" : c.optional ? "○" : "✗";
162
+ const color = c.pass ? "\x1b[32m" : c.optional ? "\x1b[33m" : "\x1b[31m";
163
+ console.log(`${color}${icon}\x1b[0m ${c.label} — ${c.detail}`);
164
+ }
165
+
166
+ if (!allPass) process.exitCode = 1;
167
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { init } from "./init.ts";
6
+
7
+ let tmpDir;
8
+ let origCwd;
9
+ let origLog;
10
+ let logged;
11
+
12
+ beforeEach(() => {
13
+ tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-init-test-"));
14
+ origCwd = process.cwd();
15
+ process.chdir(tmpDir);
16
+ logged = [];
17
+ origLog = console.log;
18
+ console.log = (...a) => logged.push(a.join(" "));
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.chdir(origCwd);
23
+ console.log = origLog;
24
+ rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("init", () => {
28
+ it("creates ide.yml from default template when no stack detected", async () => {
29
+ await init({ json: true });
30
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
31
+ const output = JSON.parse(logged[0]);
32
+ expect(output.created).toBe(true);
33
+ expect(output.template).toBe("default");
34
+ });
35
+
36
+ it("creates ide.yml from named template", async () => {
37
+ await init({ template: "nextjs", json: true });
38
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
39
+ const content = readFileSync(join(tmpDir, "ide.yml"), "utf-8");
40
+ expect(content.includes("rows")).toBeTruthy();
41
+ const output = JSON.parse(logged[0]);
42
+ expect(output.created).toBe(true);
43
+ expect(output.template).toBe("nextjs");
44
+ });
45
+
46
+ it("creates ide.yml from detected stack", async () => {
47
+ // Create a package.json with a Next.js dependency to trigger detection
48
+ writeFileSync(
49
+ join(tmpDir, "package.json"),
50
+ JSON.stringify({
51
+ name: "test-app",
52
+ dependencies: { next: "14.0.0" },
53
+ scripts: { dev: "next dev" },
54
+ }),
55
+ );
56
+ await init({ json: true });
57
+ expect(existsSync(join(tmpDir, "ide.yml"))).toBeTruthy();
58
+ const output = JSON.parse(logged[0]);
59
+ expect(output.created).toBe(true);
60
+ expect(output.detected.length > 0).toBeTruthy();
61
+ });
62
+
63
+ it("rejects when ide.yml already exists", async () => {
64
+ writeFileSync(join(tmpDir, "ide.yml"), "name: existing\n");
65
+ await expect(init({ json: true })).rejects.toThrow();
66
+ });
67
+
68
+ it("rejects unknown template name", async () => {
69
+ await expect(init({ template: "nonexistent-template-xyz", json: true })).rejects.toThrow();
70
+ });
71
+
72
+ it("replaces the name field with the directory basename", async () => {
73
+ await init({ template: "default", json: true });
74
+ const content = readFileSync(join(tmpDir, "ide.yml"), "utf-8");
75
+ const dirName = tmpDir.split("/").pop();
76
+ expect(content.includes(`name: ${dirName}`)).toBeTruthy();
77
+ });
78
+
79
+ it("prints human-readable output without --json", async () => {
80
+ await init();
81
+ expect(logged.some((l) => l.includes("Created ide.yml"))).toBeTruthy();
82
+ });
83
+
84
+ it("scaffolds missions skills, library, and AGENTS.md", async () => {
85
+ await init({ template: "missions", json: true });
86
+
87
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "frontend.md"))).toBeTruthy();
88
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "backend.md"))).toBeTruthy();
89
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "reviewer.md"))).toBeTruthy();
90
+ expect(existsSync(join(tmpDir, ".tmux-ide", "skills", "researcher.md"))).toBeTruthy();
91
+ expect(existsSync(join(tmpDir, ".tmux-ide", "library"))).toBeTruthy();
92
+ expect(existsSync(join(tmpDir, "AGENTS.md"))).toBeTruthy();
93
+ expect(readFileSync(join(tmpDir, "AGENTS.md"), "utf-8")).toContain(
94
+ `## Project: ${join(tmpDir).split("/").pop()}`,
95
+ );
96
+
97
+ const output = JSON.parse(logged[0]);
98
+ expect(output.template).toBe("missions");
99
+ expect(output.paths.some((path: string) => path.endsWith("frontend.md"))).toBeTruthy();
100
+ });
101
+ });
package/src/init.ts ADDED
@@ -0,0 +1,144 @@
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ writeFileSync,
5
+ renameSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ copyFileSync,
9
+ } from "node:fs";
10
+ import { resolve, join, basename, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ import { detectStack, suggestConfig } from "./detect.ts";
15
+ import { outputError, printLayout } from "./lib/output.ts";
16
+ import type { IdeConfig } from "./types.ts";
17
+
18
+ function copyTemplateSkills(targetDir: string): string[] {
19
+ const created: string[] = [];
20
+ const templateSkillsDir = resolve(__dirname, "..", "templates", "skills");
21
+ if (!existsSync(templateSkillsDir)) return created;
22
+
23
+ mkdirSync(targetDir, { recursive: true });
24
+ for (const file of readdirSync(templateSkillsDir)) {
25
+ if (!file.endsWith(".md")) continue;
26
+ const destination = join(targetDir, file);
27
+ copyFileSync(join(templateSkillsDir, file), destination);
28
+ created.push(destination);
29
+ }
30
+ return created;
31
+ }
32
+
33
+ function scaffoldMissionsWorkspace(dir: string, name: string): string[] {
34
+ const created: string[] = [];
35
+ const skillsDir = join(dir, ".tmux-ide", "skills");
36
+ created.push(...copyTemplateSkills(skillsDir));
37
+
38
+ const libraryDir = join(dir, ".tmux-ide", "library");
39
+ if (!existsSync(libraryDir)) {
40
+ mkdirSync(libraryDir, { recursive: true });
41
+ created.push(libraryDir);
42
+ }
43
+
44
+ const agentsTemplatePath = resolve(__dirname, "..", "templates", "AGENTS.md");
45
+ if (existsSync(agentsTemplatePath)) {
46
+ const agentsPath = join(dir, "AGENTS.md");
47
+ const content = readFileSync(agentsTemplatePath, "utf-8").replace(/{{name}}/g, name);
48
+ writeFileSync(agentsPath, content);
49
+ created.push(agentsPath);
50
+ }
51
+
52
+ return created;
53
+ }
54
+
55
+ export async function init({
56
+ template,
57
+ json,
58
+ }: { template?: string; json?: boolean } = {}): Promise<void> {
59
+ const dir = process.cwd();
60
+ const configPath = resolve(dir, "ide.yml");
61
+
62
+ if (existsSync(configPath)) {
63
+ outputError("ide.yml already exists in this directory", "EXISTS");
64
+ }
65
+
66
+ // If a specific template is requested, use it
67
+ if (template) {
68
+ const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
69
+ if (!existsSync(templatePath)) {
70
+ outputError(`Template "${template}" not found`, "NOT_FOUND");
71
+ }
72
+
73
+ let content = readFileSync(templatePath, "utf-8");
74
+ const name = basename(dir);
75
+ content = content.replace(/^name: .+/m, `name: ${name}`);
76
+ const tmpPath = configPath + ".tmp";
77
+ writeFileSync(tmpPath, content);
78
+ renameSync(tmpPath, configPath);
79
+ const created =
80
+ template === "missions"
81
+ ? scaffoldMissionsWorkspace(dir, name)
82
+ : copyTemplateSkills(join(dir, ".tmux-ide", "skills"));
83
+
84
+ if (json) {
85
+ console.log(JSON.stringify({ created: true, template, name, paths: created }));
86
+ } else {
87
+ console.log(`Created ide.yml from "${template}" template for "${name}"`);
88
+ const yaml = (await import("js-yaml")).default;
89
+ printLayout(yaml.load(content) as IdeConfig);
90
+ for (const createdPath of created) {
91
+ console.log(`Created ${createdPath.replace(dir + "/", "")}`);
92
+ }
93
+ }
94
+ return;
95
+ }
96
+
97
+ // Smart detection
98
+ const detected = detectStack(dir);
99
+ const name = basename(dir);
100
+
101
+ if (detected.frameworks.length > 0) {
102
+ // Use detected stack to generate config
103
+ const config = suggestConfig(dir, detected);
104
+ const yaml = (await import("js-yaml")).default;
105
+ const tmpPath2 = configPath + ".tmp";
106
+ writeFileSync(tmpPath2, yaml.dump(config, { lineWidth: -1, noRefs: true, quotingType: '"' }));
107
+ renameSync(tmpPath2, configPath);
108
+
109
+ const desc = detected.frameworks.join(" + ");
110
+ if (json) {
111
+ console.log(JSON.stringify({ created: true, detected: detected.frameworks, name }));
112
+ } else {
113
+ console.log(`Detected ${desc}. Created ide.yml for "${name}".`);
114
+ printLayout(config);
115
+ console.log("Edit it to customize, then run: tmux-ide");
116
+ }
117
+ } else {
118
+ // Fallback to default template
119
+ const templatePath = resolve(__dirname, "..", "templates", "default.yml");
120
+ let content = readFileSync(templatePath, "utf-8");
121
+ content = content.replace(/^name: .+/m, `name: ${name}`);
122
+ const tmpPath3 = configPath + ".tmp";
123
+ writeFileSync(tmpPath3, content);
124
+ renameSync(tmpPath3, configPath);
125
+
126
+ if (json) {
127
+ console.log(JSON.stringify({ created: true, template: "default", name }));
128
+ } else {
129
+ console.log(`Created ide.yml for "${name}"`);
130
+ const yaml = (await import("js-yaml")).default;
131
+ printLayout(yaml.load(content) as IdeConfig);
132
+ console.log("Edit it to configure your workspace, then run: tmux-ide");
133
+ }
134
+ }
135
+
136
+ // Copy built-in skills if .tmux-ide/skills/ doesn't exist
137
+ const skillsDir = join(dir, ".tmux-ide", "skills");
138
+ if (!existsSync(skillsDir)) {
139
+ const created = copyTemplateSkills(skillsDir);
140
+ if (created.length > 0 && !json) {
141
+ console.log("Copied built-in skill templates to .tmux-ide/skills/");
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const cli = join(__dirname, "..", "bin", "cli.js");
10
+
11
+ function makeProject(config = "name: inspect-session\nrows:\n - panes:\n - title: Shell\n") {
12
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-inspect-test-"));
13
+ writeFileSync(join(dir, "ide.yml"), config);
14
+ return dir;
15
+ }
16
+
17
+ function runCli(args, cwd) {
18
+ return spawnSync("node", [cli, ...args], {
19
+ cwd,
20
+ encoding: "utf-8",
21
+ });
22
+ }
23
+
24
+ describe("inspect command", () => {
25
+ it("prints resolved config state as json", () => {
26
+ const dir = makeProject(
27
+ "name: inspect-session\nrows:\n - size: 70%\n panes:\n - title: Claude\n command: claude\n focus: true\n - title: Shell\n",
28
+ );
29
+
30
+ try {
31
+ const result = runCli(["inspect", "--json"], dir);
32
+
33
+ expect(result.status).toBe(0);
34
+ const payload = JSON.parse(result.stdout);
35
+ expect(payload.session).toBe("inspect-session");
36
+ expect(payload.valid).toBe(true);
37
+ expect(payload.summary.rows).toBe(1);
38
+ expect(payload.summary.panes).toBe(2);
39
+ expect(payload.summary.focus).toBe("rows.0.panes.0");
40
+ expect(payload.tmux.running).toBe(false);
41
+ } finally {
42
+ rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ it("reports validation errors for invalid pane arrays instead of crashing", () => {
47
+ const dir = makeProject("name: inspect-session\nrows:\n - panes: nope\n");
48
+
49
+ try {
50
+ const result = runCli(["inspect", "--json"], dir);
51
+
52
+ expect(result.status).toBe(0);
53
+ const payload = JSON.parse(result.stdout);
54
+ expect(payload.valid).toBe(false);
55
+ expect(JSON.stringify(payload.errors)).toMatch(/rows\[0\]\.panes must be an array/);
56
+ expect(payload.rows).toEqual([
57
+ {
58
+ index: 0,
59
+ size: null,
60
+ panes: [],
61
+ },
62
+ ]);
63
+ } finally {
64
+ rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ });
67
+ });
package/src/inspect.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { resolve, basename } from "node:path";
2
+ import { readConfig } from "./lib/yaml-io.ts";
3
+ import { validateConfig } from "./validate.ts";
4
+ import { outputError } from "./lib/output.ts";
5
+ import { getSessionState, listPanes } from "./lib/tmux.ts";
6
+ import { loadSkills } from "./lib/skill-registry.ts";
7
+ import type { IdeConfig } from "./types.ts";
8
+
9
+ interface ResolvedPane {
10
+ index: number;
11
+ title: string | null;
12
+ command: string | null;
13
+ dir: string;
14
+ size: string | null;
15
+ focus: boolean;
16
+ role: string | null;
17
+ task: string | null;
18
+ env: Record<string, unknown>;
19
+ }
20
+
21
+ interface ResolvedRow {
22
+ index: number;
23
+ size: string | null;
24
+ panes: ResolvedPane[];
25
+ }
26
+
27
+ interface SkillInfo {
28
+ name: string;
29
+ specialties: string[];
30
+ }
31
+
32
+ interface PaneSkillMapping {
33
+ pane: string;
34
+ skill: string;
35
+ resolved: boolean;
36
+ }
37
+
38
+ interface Inspection {
39
+ dir: string;
40
+ configPath: string;
41
+ valid: boolean;
42
+ errors: string[];
43
+ session: string;
44
+ before: string | null;
45
+ summary: { rows: number; panes: number; focus: string | null };
46
+ team: IdeConfig["team"] | null;
47
+ theme: IdeConfig["theme"] | null;
48
+ focus: { row: number; pane: number; title: string | null } | null;
49
+ rows: ResolvedRow[];
50
+ rawConfig: IdeConfig;
51
+ tmux: { running: boolean; panes: ReturnType<typeof listPanes> };
52
+ skills: SkillInfo[];
53
+ paneSkillMap: PaneSkillMapping[];
54
+ }
55
+
56
+ export function buildInspection(
57
+ dir: string,
58
+ {
59
+ config,
60
+ configPath,
61
+ running,
62
+ panes,
63
+ }: {
64
+ config: IdeConfig;
65
+ configPath: string;
66
+ running: boolean;
67
+ panes: ReturnType<typeof listPanes>;
68
+ },
69
+ ): Inspection {
70
+ const errors = validateConfig(config);
71
+ const rows = Array.isArray(config?.rows) ? config.rows : [];
72
+ const resolvedRows: ResolvedRow[] = rows.map((row, rowIndex) => ({
73
+ index: rowIndex,
74
+ size: row.size ?? null,
75
+ panes: (Array.isArray(row?.panes) ? row.panes : []).map((pane, paneIndex) => ({
76
+ index: paneIndex,
77
+ title: pane.title ?? null,
78
+ command: pane.command ?? null,
79
+ dir: pane.dir ?? ".",
80
+ size: pane.size ?? null,
81
+ focus: pane.focus === true,
82
+ role: pane.role ?? null,
83
+ task: pane.task ?? null,
84
+ env: pane.env ?? {},
85
+ })),
86
+ }));
87
+
88
+ const focusPane =
89
+ resolvedRows
90
+ .flatMap((row) => row.panes.map((pane) => ({ row: row.index, pane })))
91
+ .find(({ pane }) => pane.focus) ?? null;
92
+ const session = config?.name ?? basename(dir);
93
+
94
+ // Skills
95
+ const allSkills = loadSkills(dir);
96
+ const skillNames = new Set(allSkills.map((s) => s.name));
97
+ const skills: SkillInfo[] = allSkills.map((s) => ({ name: s.name, specialties: s.specialties }));
98
+
99
+ const paneSkillMap: PaneSkillMapping[] = [];
100
+ for (const row of rows) {
101
+ for (const pane of row.panes ?? []) {
102
+ if (pane.skill) {
103
+ paneSkillMap.push({
104
+ pane: pane.title ?? "untitled",
105
+ skill: pane.skill,
106
+ resolved: skillNames.has(pane.skill),
107
+ });
108
+ }
109
+ }
110
+ }
111
+
112
+ return {
113
+ dir,
114
+ configPath,
115
+ valid: errors.length === 0,
116
+ errors,
117
+ session,
118
+ before: config?.before ?? null,
119
+ summary: {
120
+ rows: resolvedRows.length,
121
+ panes: resolvedRows.reduce((sum, row) => sum + row.panes.length, 0),
122
+ focus: focusPane ? `rows.${focusPane.row}.panes.${focusPane.pane.index}` : null,
123
+ },
124
+ team: config?.team ?? null,
125
+ theme: config?.theme ?? null,
126
+ focus: focusPane
127
+ ? {
128
+ row: focusPane.row,
129
+ pane: focusPane.pane.index,
130
+ title: focusPane.pane.title,
131
+ }
132
+ : null,
133
+ rows: resolvedRows,
134
+ rawConfig: config,
135
+ tmux: {
136
+ running,
137
+ panes,
138
+ },
139
+ skills,
140
+ paneSkillMap,
141
+ };
142
+ }
143
+
144
+ export async function inspect(
145
+ targetDir: string | undefined,
146
+ { json }: { json?: boolean } = {},
147
+ ): Promise<void> {
148
+ const dir = resolve(targetDir ?? ".");
149
+
150
+ let config;
151
+ let configPath: string;
152
+ try {
153
+ ({ config, configPath } = readConfig(dir));
154
+ } catch (error) {
155
+ outputError(`Cannot read ide.yml: ${(error as Error).message}`, "READ_ERROR");
156
+ return;
157
+ }
158
+
159
+ const session = config?.name ?? basename(dir);
160
+ const state = getSessionState(session);
161
+ const panes = state.running ? listPanes(session) : [];
162
+ const data = buildInspection(dir, { config, configPath, running: state.running, panes });
163
+
164
+ if (json) {
165
+ console.log(JSON.stringify(data, null, 2));
166
+ return;
167
+ }
168
+
169
+ console.log(`Directory: ${data.dir}`);
170
+ console.log(`Config: ${data.configPath}`);
171
+ console.log(`Valid: ${data.valid ? "yes" : "no"}`);
172
+ console.log(`Session: ${data.session}`);
173
+ console.log(`Running: ${data.tmux.running ? "yes" : "no"}`);
174
+ console.log(`Rows: ${data.summary.rows}`);
175
+ console.log(`Panes: ${data.summary.panes}`);
176
+ console.log(`Team: ${data.team ? data.team.name : "disabled"}`);
177
+
178
+ if (data.focus) {
179
+ console.log(
180
+ `Focus: row ${data.focus.row}, pane ${data.focus.pane}${data.focus.title ? ` (${data.focus.title})` : ""}`,
181
+ );
182
+ }
183
+
184
+ if (!data.valid) {
185
+ console.log("\nValidation Errors:");
186
+ for (const error of data.errors) {
187
+ console.log(` - ${error}`);
188
+ }
189
+ }
190
+
191
+ console.log("\nResolved Layout:");
192
+ for (const row of data.rows) {
193
+ console.log(` Row ${row.index}${row.size ? ` (${row.size})` : ""}`);
194
+ for (const pane of row.panes) {
195
+ const parts: string[] = [];
196
+ if (pane.title) parts.push(pane.title);
197
+ if (pane.command) parts.push(`cmd=${pane.command}`);
198
+ if (pane.dir && pane.dir !== ".") parts.push(`dir=${pane.dir}`);
199
+ if (pane.role) parts.push(`role=${pane.role}`);
200
+ if (pane.focus) parts.push("focus");
201
+ console.log(` - pane ${pane.index}: ${parts.join(" | ") || "shell"}`);
202
+ }
203
+ }
204
+
205
+ if (data.tmux.running && data.tmux.panes.length > 0) {
206
+ console.log("\nLive Panes:");
207
+ for (const pane of data.tmux.panes) {
208
+ const active = pane.active ? " (active)" : "";
209
+ console.log(` ${pane.index}: ${pane.title} [${pane.width}x${pane.height}]${active}`);
210
+ }
211
+ }
212
+ }