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,186 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { Task, Goal, Mission } from "../lib/task-store.ts";
5
+ import type { PaneInfo } from "../widgets/lib/pane-comms.ts";
6
+ import type { OrchestratorConfig, OrchestratorState } from "../lib/orchestrator.ts";
7
+ import type { Mark, MarkKind, MarkRange } from "../lib/authorship.ts";
8
+ import { ensureTasksDir, saveTask, saveGoal } from "../lib/task-store.ts";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Factory: Task
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function makeTask(overrides: Partial<Task> = {}): Task {
15
+ return {
16
+ id: "001",
17
+ title: "Test task",
18
+ description: "",
19
+ goal: null,
20
+ status: "todo",
21
+ assignee: null,
22
+ priority: 1,
23
+ created: "2026-01-01T00:00:00Z",
24
+ updated: "2026-01-01T00:00:00Z",
25
+ tags: [],
26
+ proof: null,
27
+ retryCount: 0,
28
+ maxRetries: 5,
29
+ lastError: null,
30
+ nextRetryAt: null,
31
+ depends_on: [],
32
+ milestone: null,
33
+ specialty: null,
34
+ fulfills: [],
35
+ discoveredIssues: [],
36
+ salientSummary: null,
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Factory: Goal
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export function makeGoal(overrides: Partial<Goal> = {}): Goal {
46
+ return {
47
+ id: "01",
48
+ title: "Test goal",
49
+ description: "",
50
+ status: "todo",
51
+ acceptance: "",
52
+ priority: 1,
53
+ created: "2026-01-01T00:00:00Z",
54
+ updated: "2026-01-01T00:00:00Z",
55
+ assignee: null,
56
+ specialty: null,
57
+ milestone: null,
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Factory: Mission
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export function makeMission(overrides: Partial<Mission> = {}): Mission {
67
+ return {
68
+ title: "Test mission",
69
+ description: "",
70
+ status: "active",
71
+ branch: null,
72
+ milestones: [],
73
+ created: "2026-01-01T00:00:00Z",
74
+ updated: "2026-01-01T00:00:00Z",
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Factory: PaneInfo
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export function makePane(overrides: Partial<PaneInfo> = {}): PaneInfo {
84
+ return {
85
+ id: "%1",
86
+ index: 0,
87
+ title: "Agent 1",
88
+ currentCommand: "zsh",
89
+ width: 80,
90
+ height: 24,
91
+ active: false,
92
+ role: null,
93
+ name: null,
94
+ type: null,
95
+ ...overrides,
96
+ };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Factory: OrchestratorConfig
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export function makeOrchestratorConfig(
104
+ dir: string,
105
+ overrides: Partial<OrchestratorConfig> = {},
106
+ ): OrchestratorConfig {
107
+ return {
108
+ session: "test",
109
+ dir,
110
+ autoDispatch: true,
111
+ stallTimeout: 300000,
112
+ pollInterval: 5000,
113
+ masterPane: "Master",
114
+ beforeRun: null,
115
+ afterRun: null,
116
+ maxConcurrentAgents: 10,
117
+ dispatchMode: "tasks",
118
+ paneSpecialties: new Map(),
119
+ services: {},
120
+ ...overrides,
121
+ };
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Factory: OrchestratorState
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export function makeOrchestratorState(
129
+ overrides: Partial<OrchestratorState> = {},
130
+ ): OrchestratorState {
131
+ return {
132
+ lastActivity: new Map(),
133
+ previousTasks: new Map(),
134
+ claimedTasks: new Set(),
135
+ taskClaimTimes: new Map(),
136
+ ...overrides,
137
+ };
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Factory: Mark
142
+ // ---------------------------------------------------------------------------
143
+
144
+ export function makeMark(overrides: Partial<Mark> = {}): Mark {
145
+ return {
146
+ id: "m1",
147
+ kind: "authored" as MarkKind,
148
+ by: "ai:test",
149
+ at: "2026-01-01T00:00:00Z",
150
+ range: { from: 0, to: 10 } as MarkRange,
151
+ quote: "test quote",
152
+ ...overrides,
153
+ };
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // TestProject: temp directory with .tasks scaffolding
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export class TestProject {
161
+ readonly dir: string;
162
+
163
+ constructor() {
164
+ this.dir = mkdtempSync(join(tmpdir(), "tmux-ide-test-"));
165
+ }
166
+
167
+ cleanup(): void {
168
+ rmSync(this.dir, { recursive: true, force: true });
169
+ }
170
+
171
+ initTasks(): void {
172
+ ensureTasksDir(this.dir);
173
+ }
174
+
175
+ addTask(overrides: Partial<Task> = {}): Task {
176
+ const task = makeTask(overrides);
177
+ saveTask(this.dir, task);
178
+ return task;
179
+ }
180
+
181
+ addGoal(overrides: Partial<Goal> = {}): Goal {
182
+ const goal = makeGoal(overrides);
183
+ saveGoal(this.dir, goal);
184
+ return goal;
185
+ }
186
+ }
@@ -1,9 +1,12 @@
1
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";
2
+ import { getSessionName } from "./lib/yaml-io.ts";
3
+ import { outputError } from "./lib/output.ts";
4
+ import { attachSession, getSessionState } from "./lib/tmux.ts";
5
5
 
6
- export async function attach(targetDir, { json: _json } = {}) {
6
+ export async function attach(
7
+ targetDir: string | undefined,
8
+ { json: _json }: { json?: boolean } = {},
9
+ ): Promise<void> {
7
10
  const dir = resolve(targetDir ?? ".");
8
11
  const { name: session } = getSessionName(dir);
9
12
  const state = getSessionState(session);
@@ -0,0 +1,314 @@
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: test-session\nrows:\n - panes:\n - title: Shell\n") {
12
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-cli-test-"));
13
+ writeFileSync(join(dir, "ide.yml"), config);
14
+ return dir;
15
+ }
16
+
17
+ function runCli(args, { cwd, env } = {}) {
18
+ const mergedEnv = { ...process.env };
19
+ for (const [key, value] of Object.entries(env ?? {})) {
20
+ if (value === undefined) {
21
+ delete mergedEnv[key];
22
+ } else {
23
+ mergedEnv[key] = value;
24
+ }
25
+ }
26
+
27
+ return spawnSync("node", [cli, ...args], {
28
+ cwd,
29
+ env: mergedEnv,
30
+ encoding: "utf-8",
31
+ });
32
+ }
33
+
34
+ describe("cli contract regressions", () => {
35
+ it("treats an unknown first positional as a start target path", () => {
36
+ const missingDir = join(tmpdir(), "tmux-ide-missing-project");
37
+ const result = runCli([missingDir]);
38
+
39
+ expect(result.status).not.toBe(0);
40
+ expect(result.stderr).toMatch(/No ide\.yml found in/);
41
+ expect(result.stderr).not.toMatch(/Unknown command:/);
42
+ });
43
+
44
+ it("returns a structured start json error when ide.yml is missing", () => {
45
+ const missingDir = join(tmpdir(), "tmux-ide-missing-project-json");
46
+ const result = runCli([missingDir, "--json"]);
47
+
48
+ expect(result.status).not.toBe(0);
49
+ const payload = JSON.parse(result.stderr);
50
+ expect(payload.code).toBe("CONFIG_NOT_FOUND");
51
+ expect(payload.error).toMatch(/No ide\.yml found in/);
52
+ expect(payload.error).toMatch(/tmux-ide init/);
53
+ expect(payload.error).toMatch(/tmux-ide detect --write/);
54
+ });
55
+
56
+ it("prints help for --help without trying to launch tmux", () => {
57
+ const result = runCli(["--help"]);
58
+
59
+ expect(result.status).toBe(0);
60
+ expect(result.stdout).toMatch(/Usage:/);
61
+ expect(result.stdout).toMatch(/tmux-ide <path>/);
62
+ expect(result.stderr).not.toMatch(/tmux new-session/);
63
+ });
64
+
65
+ it("includes inspect in the CLI help output", () => {
66
+ const result = runCli(["--help"]);
67
+
68
+ expect(result.status).toBe(0);
69
+ expect(result.stdout).toMatch(/tmux-ide inspect/);
70
+ });
71
+
72
+ it("returns the full non-running status json shape", () => {
73
+ const dir = makeProject();
74
+
75
+ try {
76
+ const result = runCli(["status", "--json"], { cwd: dir });
77
+
78
+ expect(result.status).toBe(0);
79
+ const output = JSON.parse(result.stdout);
80
+ expect(output.session).toBe("test-session");
81
+ expect(output.running).toBe(false);
82
+ expect(output.configExists).toBe(true);
83
+ expect(output.panes).toEqual([]);
84
+ } finally {
85
+ rmSync(dir, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ it("returns the detect json envelope with reasoning", () => {
90
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-detect-json-test-"));
91
+ writeFileSync(
92
+ join(dir, "package.json"),
93
+ JSON.stringify({
94
+ name: "demo",
95
+ scripts: { dev: "vite" },
96
+ dependencies: { vite: "^5.0.0" },
97
+ }),
98
+ );
99
+
100
+ try {
101
+ const result = runCli(["detect", "--json"], { cwd: dir });
102
+
103
+ expect(result.status).toBe(0);
104
+ const payload = JSON.parse(result.stdout);
105
+ expect(Object.keys(payload).sort()).toEqual(["detected", "suggestedConfig"]);
106
+ expect(Array.isArray(payload.detected.reasons)).toBeTruthy();
107
+ expect(Array.isArray(payload.suggestedConfig.rows)).toBeTruthy();
108
+ } finally {
109
+ rmSync(dir, { recursive: true, force: true });
110
+ }
111
+ });
112
+
113
+ it("returns a clean attach error without throwing a TypeError", () => {
114
+ const dir = makeProject();
115
+
116
+ try {
117
+ const result = runCli(["attach"], { cwd: dir });
118
+
119
+ expect(result.status).not.toBe(0);
120
+ expect(result.stderr).toMatch(/Session "test-session" is not running/);
121
+ expect(result.stderr).not.toMatch(/ERR_INVALID_ARG_TYPE/);
122
+ } finally {
123
+ rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ it("returns a clean init error when ide.yml already exists", () => {
128
+ const dir = makeProject();
129
+
130
+ try {
131
+ const result = runCli(["init"], { cwd: dir });
132
+
133
+ expect(result.status).not.toBe(0);
134
+ expect(result.stderr).toMatch(/ide\.yml already exists/i);
135
+ } finally {
136
+ rmSync(dir, { recursive: true, force: true });
137
+ }
138
+ });
139
+
140
+ it("returns a structured init json error when ide.yml already exists", () => {
141
+ const dir = makeProject();
142
+
143
+ try {
144
+ const result = runCli(["init", "--json"], { cwd: dir });
145
+
146
+ expect(result.status).not.toBe(0);
147
+ expect(JSON.parse(result.stderr)).toEqual({
148
+ error: "ide.yml already exists in this directory",
149
+ code: "EXISTS",
150
+ });
151
+ } finally {
152
+ rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ it("returns a structured init json error for an unknown template", () => {
157
+ const dir = mkdtempSync(join(tmpdir(), "tmux-ide-cli-init-test-"));
158
+
159
+ try {
160
+ const result = runCli(["init", "--template", "missing-template", "--json"], { cwd: dir });
161
+
162
+ expect(result.status).not.toBe(0);
163
+ expect(JSON.parse(result.stderr)).toEqual({
164
+ error: 'Template "missing-template" not found',
165
+ code: "NOT_FOUND",
166
+ });
167
+ } finally {
168
+ rmSync(dir, { recursive: true, force: true });
169
+ }
170
+ });
171
+
172
+ it("returns a clean start error for invalid config", () => {
173
+ const dir = makeProject("name: broken\nrows: []\n");
174
+
175
+ try {
176
+ const result = runCli([], { cwd: dir });
177
+
178
+ expect(result.status).not.toBe(0);
179
+ expect(result.stderr).toMatch(/Invalid ide\.yml/);
180
+ expect(result.stderr).toMatch(/tmux-ide validate/);
181
+ } finally {
182
+ rmSync(dir, { recursive: true, force: true });
183
+ }
184
+ });
185
+
186
+ it("returns a structured start json error for invalid config", () => {
187
+ const dir = makeProject("name: broken\nrows: []\n");
188
+
189
+ try {
190
+ const result = runCli(["--json"], { cwd: dir });
191
+
192
+ expect(result.status).not.toBe(0);
193
+ const payload = JSON.parse(result.stderr);
194
+ expect(payload.code).toBe("INVALID_CONFIG");
195
+ expect(payload.error).toMatch(/Invalid ide\.yml/);
196
+ expect(payload.error).toMatch(/tmux-ide validate/);
197
+ } finally {
198
+ rmSync(dir, { recursive: true, force: true });
199
+ }
200
+ });
201
+
202
+ it("returns a structured restart json error for invalid config", () => {
203
+ const dir = makeProject("name: broken\nrows: []\n");
204
+
205
+ try {
206
+ const result = runCli(["restart", "--json"], { cwd: dir });
207
+
208
+ expect(result.status).not.toBe(0);
209
+ const payload = JSON.parse(result.stderr);
210
+ expect(payload.code).toBe("INVALID_CONFIG");
211
+ expect(payload.error).toMatch(/Invalid ide\.yml/);
212
+ } finally {
213
+ rmSync(dir, { recursive: true, force: true });
214
+ }
215
+ });
216
+
217
+ it("returns a clean launch error when the before hook fails", () => {
218
+ const dir = makeProject(
219
+ 'name: before-fails\nbefore: node -e "process.exit(3)"\nrows:\n - panes:\n - title: Shell\n',
220
+ );
221
+
222
+ try {
223
+ const result = runCli([], { cwd: dir });
224
+
225
+ expect(result.status).not.toBe(0);
226
+ expect(result.stderr).toMatch(/before.*hook failed/i);
227
+ expect(result.stderr).toMatch(/node -e/);
228
+ } finally {
229
+ rmSync(dir, { recursive: true, force: true });
230
+ }
231
+ });
232
+
233
+ it("returns a structured start json error when the before hook fails", () => {
234
+ const dir = makeProject(
235
+ 'name: before-fails\nbefore: node -e "process.exit(3)"\nrows:\n - panes:\n - title: Shell\n',
236
+ );
237
+
238
+ try {
239
+ const result = runCli(["--json"], { cwd: dir });
240
+
241
+ expect(result.status).not.toBe(0);
242
+ const payload = JSON.parse(result.stderr);
243
+ expect(payload.code).toBe("BEFORE_HOOK_FAILED");
244
+ expect(payload.error).toMatch(/before.*hook/i);
245
+ expect(payload.error).toMatch(/node -e/);
246
+ } finally {
247
+ rmSync(dir, { recursive: true, force: true });
248
+ }
249
+ });
250
+
251
+ it("returns a structured attach json error with a non-zero exit code", () => {
252
+ const dir = makeProject();
253
+
254
+ try {
255
+ const result = runCli(["attach", "--json"], { cwd: dir });
256
+
257
+ expect(result.status).not.toBe(0);
258
+ expect(JSON.parse(result.stderr)).toEqual({
259
+ error: 'Session "test-session" is not running. Start it with: tmux-ide',
260
+ code: "NOT_RUNNING",
261
+ });
262
+ } finally {
263
+ rmSync(dir, { recursive: true, force: true });
264
+ }
265
+ });
266
+
267
+ it("returns a non-zero exit code when stop cannot find the session", () => {
268
+ const dir = makeProject();
269
+
270
+ try {
271
+ const result = runCli(["stop"], { cwd: dir });
272
+
273
+ expect(result.status).not.toBe(0);
274
+ expect(result.stderr).toMatch(/No active session "test-session" found/);
275
+ } finally {
276
+ rmSync(dir, { recursive: true, force: true });
277
+ }
278
+ });
279
+
280
+ it("returns a structured stop json error with a non-zero exit code", () => {
281
+ const dir = makeProject();
282
+
283
+ try {
284
+ const result = runCli(["stop", "--json"], { cwd: dir });
285
+
286
+ expect(result.status).not.toBe(0);
287
+ expect(JSON.parse(result.stderr)).toEqual({
288
+ error: 'No active session "test-session" found',
289
+ code: "NOT_RUNNING",
290
+ });
291
+ } finally {
292
+ rmSync(dir, { recursive: true, force: true });
293
+ }
294
+ });
295
+
296
+ it("keeps doctor human and json success semantics aligned for optional checks", () => {
297
+ const dir = makeProject();
298
+
299
+ try {
300
+ const human = runCli(["doctor"], {
301
+ cwd: dir,
302
+ env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: undefined },
303
+ });
304
+ const json = runCli(["doctor", "--json"], {
305
+ cwd: dir,
306
+ env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: undefined },
307
+ });
308
+
309
+ expect(human.status === 0).toBe(JSON.parse(json.stdout).ok);
310
+ } finally {
311
+ rmSync(dir, { recursive: true, force: true });
312
+ }
313
+ });
314
+ });