tmux-ide 1.2.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 (205) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +33 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/{attach.js → attach.ts} +7 -4
  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.js → detect.ts} +31 -15
  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.js → inspect.ts} +95 -10
  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.js → output.ts} +18 -10
  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.js → session-options.ts} +11 -6
  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.js → sizes.ts} +4 -4
  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.js → yaml-io.ts} +5 -4
  103. package/src/ls.test.ts +76 -0
  104. package/src/{ls.js → ls.ts} +4 -4
  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/src/config.js +0 -341
  194. package/src/doctor.js +0 -91
  195. package/src/init.js +0 -73
  196. package/src/launch.js +0 -245
  197. package/src/lib/dot-path.js +0 -16
  198. package/src/lib/errors.js +0 -35
  199. package/src/lib/launch-plan.js +0 -49
  200. package/src/lib/session-monitor.js +0 -179
  201. package/src/lib/tmux.js +0 -237
  202. package/src/restart.js +0 -16
  203. package/src/status.js +0 -35
  204. package/src/stop.js +0 -25
  205. package/src/validate.js +0 -154
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { detectStack, suggestConfig } from "./detect.ts";
3
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ describe("suggestConfig", () => {
8
+ it("creates Next.js config with pnpm", () => {
9
+ const config = suggestConfig("/project", {
10
+ packageManager: "pnpm",
11
+ frameworks: ["next"],
12
+ devCommand: "pnpm dev",
13
+ language: "javascript",
14
+ });
15
+ expect(config.rows[0].size).toBe("70%");
16
+ expect(config.rows[0].panes.length).toBe(2);
17
+ const bottom = config.rows[1].panes;
18
+ expect(bottom[0].title).toBe("Next.js");
19
+ expect(bottom[0].command).toBe("pnpm dev");
20
+ expect(bottom[bottom.length - 1].title).toBe("Shell");
21
+ });
22
+
23
+ it("creates Next.js + Convex config with 3 Claude panes", () => {
24
+ const config = suggestConfig("/project", {
25
+ packageManager: "pnpm",
26
+ frameworks: ["next", "convex"],
27
+ devCommand: "pnpm dev",
28
+ language: "javascript",
29
+ });
30
+ expect(config.rows[0].panes.length).toBe(3);
31
+ const bottom = config.rows[1].panes;
32
+ const titles = bottom.map((p) => p.title);
33
+ expect(titles.includes("Next.js")).toBeTruthy();
34
+ expect(titles.includes("Convex")).toBeTruthy();
35
+ expect(titles.includes("Shell")).toBeTruthy();
36
+ });
37
+
38
+ it("creates Go config", () => {
39
+ const config = suggestConfig("/project", {
40
+ packageManager: null,
41
+ frameworks: ["go"],
42
+ devCommand: null,
43
+ language: "go",
44
+ });
45
+ const bottom = config.rows[1].panes;
46
+ expect(bottom[0].title).toBe("Go");
47
+ expect(bottom[0].command).toBe("go run .");
48
+ });
49
+
50
+ it("creates Cargo config", () => {
51
+ const config = suggestConfig("/project", {
52
+ packageManager: null,
53
+ frameworks: ["cargo"],
54
+ devCommand: null,
55
+ language: "rust",
56
+ });
57
+ const bottom = config.rows[1].panes;
58
+ expect(bottom[0].title).toBe("Cargo");
59
+ expect(bottom[0].command).toBe("cargo watch -x run");
60
+ });
61
+
62
+ it("falls back to dev command when no framework detected", () => {
63
+ const config = suggestConfig("/project", {
64
+ packageManager: "npm",
65
+ frameworks: [],
66
+ devCommand: "npm run dev",
67
+ language: "javascript",
68
+ });
69
+ const bottom = config.rows[1].panes;
70
+ expect(bottom[0].title).toBe("Dev Server");
71
+ expect(bottom[0].command).toBe("npm run dev");
72
+ });
73
+
74
+ it("creates minimal config with just shell when nothing detected", () => {
75
+ const config = suggestConfig("/project", {
76
+ packageManager: null,
77
+ frameworks: [],
78
+ devCommand: null,
79
+ language: null,
80
+ });
81
+ const bottom = config.rows[1].panes;
82
+ expect(bottom.length).toBe(1);
83
+ expect(bottom[0].title).toBe("Shell");
84
+ });
85
+
86
+ it("uses npm run for npm package manager", () => {
87
+ const config = suggestConfig("/project", {
88
+ packageManager: "npm",
89
+ frameworks: ["next"],
90
+ devCommand: "npm run dev",
91
+ language: "javascript",
92
+ });
93
+ const bottom = config.rows[1].panes;
94
+ expect(bottom[0].command).toBe("npm run dev");
95
+ });
96
+
97
+ it("uses bun for bun package manager", () => {
98
+ const config = suggestConfig("/project", {
99
+ packageManager: "bun",
100
+ frameworks: ["vite"],
101
+ devCommand: "bun dev",
102
+ language: "javascript",
103
+ });
104
+ const bottom = config.rows[1].panes;
105
+ expect(bottom[0].command).toBe("bun dev");
106
+ });
107
+
108
+ it("uses project directory basename as session name", () => {
109
+ const config = suggestConfig("/home/user/my-app", {
110
+ packageManager: null,
111
+ frameworks: [],
112
+ devCommand: null,
113
+ language: null,
114
+ });
115
+ expect(config.name).toBe("my-app");
116
+ });
117
+ });
118
+
119
+ describe("detectStack reasoning", () => {
120
+ it("includes reasons for detected frameworks and command choices", () => {
121
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-detect-test-"));
122
+
123
+ try {
124
+ writeFileSync(
125
+ join(dir, "package.json"),
126
+ JSON.stringify({
127
+ dependencies: { next: "latest", convex: "latest" },
128
+ scripts: { dev: "next dev" },
129
+ }),
130
+ );
131
+ writeFileSync(join(dir, "pnpm-lock.yaml"), "lockfileVersion: 9");
132
+
133
+ const detected = detectStack(dir);
134
+ expect(detected.packageManager).toBe("pnpm");
135
+ expect(detected.reasons.some((reason) => reason.includes("pnpm-lock.yaml"))).toBeTruthy();
136
+ expect(detected.reasons.some((reason) => reason.includes('dependency "next"'))).toBeTruthy();
137
+ expect(detected.reasons.some((reason) => reason.includes("dev command"))).toBeTruthy();
138
+ } finally {
139
+ rmSync(dir, { recursive: true, force: true });
140
+ }
141
+ });
142
+ });
@@ -1,21 +1,30 @@
1
1
  import { resolve, basename } from "node:path";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
- import { writeConfig } from "./lib/yaml-io.js";
3
+ import { writeConfig } from "./lib/yaml-io.ts";
4
+ import type { IdeConfig } from "./types.ts";
5
+
6
+ interface DetectedStack {
7
+ packageManager: string | null;
8
+ frameworks: string[];
9
+ devCommand: string | null;
10
+ language: string | null;
11
+ reasons: string[];
12
+ }
4
13
 
5
- function fileExists(dir, name) {
14
+ function fileExists(dir: string, name: string): boolean {
6
15
  return existsSync(resolve(dir, name));
7
16
  }
8
17
 
9
- function readJson(dir, name) {
18
+ function readJson(dir: string, name: string): Record<string, unknown> | null {
10
19
  try {
11
- return JSON.parse(readFileSync(resolve(dir, name), "utf-8"));
20
+ return JSON.parse(readFileSync(resolve(dir, name), "utf-8")) as Record<string, unknown>;
12
21
  } catch {
13
22
  return null;
14
23
  }
15
24
  }
16
25
 
17
- export function detectStack(dir) {
18
- const detected = {
26
+ export function detectStack(dir: string): DetectedStack {
27
+ const detected: DetectedStack = {
19
28
  packageManager: null,
20
29
  frameworks: [],
21
30
  devCommand: null,
@@ -42,7 +51,10 @@ export function detectStack(dir) {
42
51
  if (pkg) {
43
52
  detected.language = "javascript";
44
53
  detected.reasons.push('Detected JavaScript from "package.json".');
45
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
54
+ const deps = {
55
+ ...(pkg.dependencies as Record<string, unknown> | undefined),
56
+ ...(pkg.devDependencies as Record<string, unknown> | undefined),
57
+ };
46
58
 
47
59
  if (deps["next"]) pushFramework(detected, "next", 'Found dependency "next".');
48
60
  if (deps["convex"]) pushFramework(detected, "convex", 'Found dependency "convex".');
@@ -57,12 +69,13 @@ export function detectStack(dir) {
57
69
  // Detect dev command
58
70
  const pm = detected.packageManager ?? "npm";
59
71
  const run = pm === "npm" ? "npm run" : pm;
60
- if (pkg.scripts?.dev) {
72
+ const scripts = pkg.scripts as Record<string, unknown> | undefined;
73
+ if (scripts?.dev) {
61
74
  detected.devCommand = `${run} dev`;
62
75
  detected.reasons.push(
63
76
  `Using dev command "${detected.devCommand}" from package.json scripts.`,
64
77
  );
65
- } else if (pkg.scripts?.start) {
78
+ } else if (scripts?.start) {
66
79
  detected.devCommand = `${run} start`;
67
80
  detected.reasons.push(
68
81
  `Using start command "${detected.devCommand}" from package.json scripts.`,
@@ -117,13 +130,13 @@ export function detectStack(dir) {
117
130
  return detected;
118
131
  }
119
132
 
120
- export function suggestConfig(dir, detected) {
133
+ export function suggestConfig(dir: string, detected: DetectedStack): IdeConfig {
121
134
  const name = basename(dir);
122
135
  const pm = detected.packageManager ?? "npm";
123
136
  const run = pm === "npm" ? "npm run" : pm;
124
137
 
125
138
  // Default: 2 claude panes + shell
126
- const config = {
139
+ const config: IdeConfig = {
127
140
  name,
128
141
  rows: [
129
142
  {
@@ -139,12 +152,12 @@ export function suggestConfig(dir, detected) {
139
152
  ],
140
153
  };
141
154
 
142
- const bottom = config.rows[1].panes;
155
+ const bottom = config.rows[1]!.panes;
143
156
  const frameworks = detected.frameworks;
144
157
 
145
158
  // Add 3rd claude pane for complex stacks
146
159
  if (frameworks.length >= 2) {
147
- config.rows[0].panes.push({ title: "Claude 3", command: "claude" });
160
+ config.rows[0]!.panes.push({ title: "Claude 3", command: "claude" });
148
161
  }
149
162
 
150
163
  // Add dev servers
@@ -184,7 +197,10 @@ export function suggestConfig(dir, detected) {
184
197
  return config;
185
198
  }
186
199
 
187
- export async function detect(targetDir, { json, write } = {}) {
200
+ export async function detect(
201
+ targetDir: string | undefined,
202
+ { json, write }: { json?: boolean; write?: boolean } = {},
203
+ ): Promise<void> {
188
204
  const dir = resolve(targetDir ?? ".");
189
205
  const detected = detectStack(dir);
190
206
  const suggested = suggestConfig(dir, detected);
@@ -224,7 +240,7 @@ export async function detect(targetDir, { json, write } = {}) {
224
240
  console.log("\nRun with --write to create ide.yml, or --json to see the suggested config.");
225
241
  }
226
242
 
227
- function pushFramework(detected, framework, reason) {
243
+ function pushFramework(detected: DetectedStack, framework: string, reason: string): void {
228
244
  if (!detected.frameworks.includes(framework)) {
229
245
  detected.frameworks.push(framework);
230
246
  }
@@ -0,0 +1,102 @@
1
+ import { resolve, join } from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { loadMission, loadGoal, loadTask } from "./lib/task-store.ts";
4
+ import { IdeError } from "./lib/errors.ts";
5
+
6
+ interface DispatchOptions {
7
+ taskId: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Print task dispatch context to stdout so agents can read it directly.
13
+ *
14
+ * 1. If a dispatch file exists (.tasks/dispatch/{id}.md), print it.
15
+ * 2. Otherwise, build the context from the task store (mission + goal + task).
16
+ * 3. In JSON mode, output a structured object.
17
+ * 4. Always end with the completion command hint.
18
+ */
19
+ export async function dispatch(
20
+ targetDir: string | null | undefined,
21
+ opts: DispatchOptions,
22
+ ): Promise<void> {
23
+ const dir = resolve(targetDir ?? ".");
24
+ const { taskId, json } = opts;
25
+
26
+ if (!taskId) {
27
+ throw new IdeError("Missing task ID. Usage: tmux-ide dispatch <id>", {
28
+ code: "USAGE",
29
+ });
30
+ }
31
+
32
+ // Try dispatch file first
33
+ const dispatchFile = join(dir, ".tasks", "dispatch", `${taskId}.md`);
34
+ const hasDispatchFile = existsSync(dispatchFile);
35
+
36
+ // Always load the task for structured data
37
+ const task = loadTask(dir, taskId);
38
+ if (!task && !hasDispatchFile) {
39
+ throw new IdeError(`Task "${taskId}" not found and no dispatch file exists`, {
40
+ code: "TASK_NOT_FOUND",
41
+ });
42
+ }
43
+
44
+ // Load mission and goal context
45
+ const mission = loadMission(dir);
46
+ const goal = task?.goal ? loadGoal(dir, task.goal) : null;
47
+
48
+ if (json) {
49
+ const prompt = hasDispatchFile ? readFileSync(dispatchFile, "utf-8") : buildPrompt(dir, task!);
50
+ const result: Record<string, unknown> = {
51
+ task: task ?? null,
52
+ mission: mission ?? null,
53
+ goal: goal ?? null,
54
+ prompt,
55
+ };
56
+ console.log(JSON.stringify(result, null, 2));
57
+ return;
58
+ }
59
+
60
+ // Text mode: print the readable prompt
61
+ if (hasDispatchFile) {
62
+ const content = readFileSync(dispatchFile, "utf-8");
63
+ console.log(content);
64
+ } else {
65
+ console.log(buildPrompt(dir, task!));
66
+ }
67
+
68
+ // Always print the completion hint
69
+ console.log("");
70
+ console.log(`When done: tmux-ide task done ${taskId} --proof "what you did"`);
71
+ }
72
+
73
+ /**
74
+ * Build a human-readable prompt from the task store.
75
+ * Mirrors the logic of buildTaskPrompt() in orchestrator.ts but without
76
+ * requiring an orchestrator config.
77
+ */
78
+ function buildPrompt(dir: string, task: NonNullable<ReturnType<typeof loadTask>>): string {
79
+ let prompt = "";
80
+
81
+ const mission = loadMission(dir);
82
+ if (mission) {
83
+ prompt += `Mission: ${mission.title}\n`;
84
+ if (mission.description) prompt += `${mission.description}\n`;
85
+ prompt += "\n";
86
+ }
87
+
88
+ if (task.goal) {
89
+ const goal = loadGoal(dir, task.goal);
90
+ if (goal) {
91
+ prompt += `Goal: ${goal.title}\n`;
92
+ if (goal.acceptance) prompt += `Acceptance: ${goal.acceptance}\n`;
93
+ prompt += "\n";
94
+ }
95
+ }
96
+
97
+ prompt += `Your Task: ${task.title}\n`;
98
+ if (task.description) prompt += `${task.description}\n`;
99
+ prompt += `\nPriority: P${task.priority}\n`;
100
+ if (task.tags?.length) prompt += `Tags: ${task.tags.join(", ")}\n`;
101
+ return prompt.trim();
102
+ }
@@ -0,0 +1,119 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { doctor } from "./doctor.ts";
6
+
7
+ let tmpDir: string;
8
+ let origCwd: string;
9
+ let origLog: typeof console.log;
10
+ let logged: string[];
11
+ let origExitCode: number | undefined;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-doctor-test-"));
15
+ origCwd = process.cwd();
16
+ logged = [];
17
+ origLog = console.log;
18
+ origExitCode = process.exitCode;
19
+ console.log = (...a: string[]) => logged.push(a.join(" "));
20
+ });
21
+
22
+ afterEach(() => {
23
+ process.chdir(origCwd);
24
+ console.log = origLog;
25
+ process.exitCode = origExitCode;
26
+ rmSync(tmpDir, { recursive: true, force: true });
27
+ });
28
+
29
+ describe("doctor", () => {
30
+ it("reports tmux installed check", async () => {
31
+ process.chdir(tmpDir);
32
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
33
+ await doctor({ json: true });
34
+ const output = JSON.parse(logged[0]);
35
+ const tmuxCheck = output.checks.find((c) => c.label === "tmux installed");
36
+ expect(tmuxCheck).toBeTruthy();
37
+ // tmux is installed in this CI/test environment, so it should pass
38
+ expect(tmuxCheck.pass).toBe(true);
39
+ });
40
+
41
+ it("reports tmux version check", async () => {
42
+ process.chdir(tmpDir);
43
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
44
+ await doctor({ json: true });
45
+ const output = JSON.parse(logged[0]);
46
+ const versionCheck = output.checks.find((c) => c.label.includes("tmux version"));
47
+ expect(versionCheck).toBeTruthy();
48
+ });
49
+
50
+ it("reports Node.js version check as passing", async () => {
51
+ process.chdir(tmpDir);
52
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
53
+ await doctor({ json: true });
54
+ const output = JSON.parse(logged[0]);
55
+ const nodeCheck = output.checks.find((c) => c.label.includes("Node.js"));
56
+ expect(nodeCheck).toBeTruthy();
57
+ expect(nodeCheck.pass).toBe(true);
58
+ });
59
+
60
+ it("reports ide.yml exists check as passing when present", async () => {
61
+ process.chdir(tmpDir);
62
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
63
+ await doctor({ json: true });
64
+ const output = JSON.parse(logged[0]);
65
+ const ideCheck = output.checks.find((c) => c.label.includes("ide.yml"));
66
+ expect(ideCheck).toBeTruthy();
67
+ expect(ideCheck.pass).toBe(true);
68
+ });
69
+
70
+ it("reports ide.yml exists check as failing when absent", async () => {
71
+ process.chdir(tmpDir);
72
+ // No ide.yml written
73
+ await doctor({ json: true });
74
+ const output = JSON.parse(logged[0]);
75
+ const ideCheck = output.checks.find((c) => c.label.includes("ide.yml"));
76
+ expect(ideCheck).toBeTruthy();
77
+ expect(ideCheck.pass).toBe(false);
78
+ });
79
+
80
+ it("marks agent teams check as optional", async () => {
81
+ process.chdir(tmpDir);
82
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
83
+ await doctor({ json: true });
84
+ const output = JSON.parse(logged[0]);
85
+ const teamsCheck = output.checks.find((c) => c.label.includes("agent teams"));
86
+ expect(teamsCheck).toBeTruthy();
87
+ expect(teamsCheck.optional).toBe(true);
88
+ });
89
+
90
+ it("optional checks don't fail the overall result", async () => {
91
+ process.chdir(tmpDir);
92
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
93
+ // Even if agent teams env var is not set, ok should still be true
94
+ const origEnv = process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
95
+ delete process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
96
+ try {
97
+ await doctor({ json: true });
98
+ const output = JSON.parse(logged[0]);
99
+ // ok depends on required checks, not optional ones
100
+ const requiredFailing = output.checks.filter((c) => !c.pass && !c.optional);
101
+ if (requiredFailing.length === 0) {
102
+ expect(output.ok).toBe(true);
103
+ }
104
+ } finally {
105
+ if (origEnv !== undefined) {
106
+ process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = origEnv;
107
+ }
108
+ }
109
+ });
110
+
111
+ it("prints human-readable output without --json", async () => {
112
+ process.chdir(tmpDir);
113
+ writeFileSync(join(tmpDir, "ide.yml"), "name: test\nrows:\n - panes:\n - title: Shell\n");
114
+ await doctor();
115
+ // Should have printed colored output with ✓ or ✗
116
+ expect(logged.length > 0).toBeTruthy();
117
+ expect(logged.some((l) => l.includes("tmux installed"))).toBeTruthy();
118
+ });
119
+ });
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
+ }