tmux-ide 2.6.0 → 2.7.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 (271) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1024 -519
  3. package/bin/cli.ts +63 -6
  4. package/bunfig.toml +4 -0
  5. package/package.json +18 -7
  6. package/packages/contracts/package.json +22 -0
  7. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  8. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  9. package/packages/contracts/src/actions-contract.ts +310 -0
  10. package/packages/contracts/src/actions-errors.ts +41 -0
  11. package/packages/contracts/src/domain.ts +36 -0
  12. package/packages/contracts/src/ide-config.ts +170 -0
  13. package/packages/contracts/src/index.ts +24 -0
  14. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  15. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  16. package/packages/contracts/src/terminals.ts +116 -0
  17. package/packages/contracts/src/tmux.ts +60 -0
  18. package/packages/contracts/src/workspace.ts +67 -0
  19. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  20. package/packages/daemon/dist/agent-explain.js +19 -3
  21. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  22. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  23. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  24. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  25. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  26. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  27. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  28. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  29. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  30. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  31. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  32. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  33. package/packages/daemon/src/agent-explain.ts +298 -0
  34. package/packages/daemon/src/attach.ts +20 -0
  35. package/packages/daemon/src/bin.ts +4 -0
  36. package/packages/daemon/src/canonical.ts +7 -0
  37. package/packages/daemon/src/cli.ts +499 -0
  38. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  39. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  40. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  41. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  42. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  43. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  44. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  45. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  46. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  47. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  48. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  49. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  50. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  51. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  52. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  53. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  54. package/packages/daemon/src/command-center/discovery.ts +96 -0
  55. package/packages/daemon/src/command-center/index.ts +31 -0
  56. package/packages/daemon/src/command-center/schemas.ts +85 -0
  57. package/packages/daemon/src/command-center/server.ts +1260 -0
  58. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  59. package/packages/daemon/src/config.ts +549 -0
  60. package/packages/daemon/src/detect.ts +248 -0
  61. package/packages/daemon/src/doctor.ts +242 -0
  62. package/packages/daemon/src/embed.ts +5 -0
  63. package/packages/daemon/src/index.ts +12 -0
  64. package/packages/daemon/src/init.ts +211 -0
  65. package/packages/daemon/src/inspect.ts +178 -0
  66. package/packages/daemon/src/js-yaml.d.ts +10 -0
  67. package/packages/daemon/src/launch.ts +349 -0
  68. package/packages/daemon/src/lib/active-projects.ts +49 -0
  69. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  70. package/packages/daemon/src/lib/app-config.ts +427 -0
  71. package/packages/daemon/src/lib/app-settings.ts +53 -0
  72. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  73. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  74. package/packages/daemon/src/lib/auth/types.ts +2 -0
  75. package/packages/daemon/src/lib/auth-token.ts +5 -0
  76. package/packages/daemon/src/lib/authorship.ts +280 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  78. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  79. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  80. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  81. package/packages/daemon/src/lib/daemon.ts +79 -0
  82. package/packages/daemon/src/lib/dot-path.ts +17 -0
  83. package/packages/daemon/src/lib/errors.ts +67 -0
  84. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  85. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  86. package/packages/daemon/src/lib/log.ts +134 -0
  87. package/packages/daemon/src/lib/output.ts +76 -0
  88. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  89. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  90. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  91. package/packages/daemon/src/lib/project-probe.ts +92 -0
  92. package/packages/daemon/src/lib/project-registry.ts +296 -0
  93. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  94. package/packages/daemon/src/lib/session-options.ts +100 -0
  95. package/packages/daemon/src/lib/shell.ts +8 -0
  96. package/packages/daemon/src/lib/sizes.ts +36 -0
  97. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  98. package/packages/daemon/src/lib/slugify.ts +10 -0
  99. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  100. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  101. package/packages/daemon/src/lib/update-check.ts +298 -0
  102. package/packages/daemon/src/lib/update.ts +158 -0
  103. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  104. package/packages/daemon/src/lib/worktree.ts +289 -0
  105. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  106. package/packages/daemon/src/ls.ts +40 -0
  107. package/packages/daemon/src/restart.ts +24 -0
  108. package/packages/daemon/src/restore.ts +514 -0
  109. package/packages/daemon/src/schemas/domain.ts +2 -0
  110. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  111. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  112. package/packages/daemon/src/schemas/index.ts +59 -0
  113. package/packages/daemon/src/schemas/inspect.ts +67 -0
  114. package/packages/daemon/src/schemas/registry.ts +55 -0
  115. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  116. package/packages/daemon/src/send.ts +171 -0
  117. package/packages/daemon/src/server/README.md +15 -0
  118. package/packages/daemon/src/server/index.ts +74 -0
  119. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  120. package/packages/daemon/src/server/standalone.ts +18 -0
  121. package/packages/daemon/src/server/ws-route.ts +483 -0
  122. package/packages/daemon/src/status.ts +59 -0
  123. package/packages/daemon/src/stop.ts +28 -0
  124. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  125. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  126. package/packages/daemon/src/terminal/README.md +92 -0
  127. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  128. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  129. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  130. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  131. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  132. package/packages/daemon/src/tui/chrome/notify.ts +382 -0
  133. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  134. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  135. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  136. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  137. package/packages/daemon/src/tui/chrome/updater.ts +510 -0
  138. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  139. package/packages/daemon/src/tui/compiled.ts +121 -0
  140. package/packages/daemon/src/tui/detect/classify.ts +208 -0
  141. package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
  142. package/packages/daemon/src/tui/detect/manifest.ts +199 -0
  143. package/packages/daemon/src/tui/detect/manifests.ts +354 -0
  144. package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
  145. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  146. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  147. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  148. package/packages/daemon/src/tui/main.ts +82 -0
  149. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  150. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  151. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  152. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  153. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  154. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  155. package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
  156. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  157. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  158. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  159. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  160. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  161. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  162. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  163. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  164. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  165. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  166. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  167. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  168. package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
  169. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  170. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  171. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  172. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  173. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  174. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  175. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  176. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  177. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  178. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  179. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  180. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  181. package/packages/daemon/src/tui/team/entry.ts +38 -0
  182. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  183. package/packages/daemon/src/tui/team/home.ts +170 -0
  184. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  185. package/packages/daemon/src/tui/team/input.ts +34 -0
  186. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  187. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  188. package/packages/daemon/src/tui/team/nav.ts +31 -0
  189. package/packages/daemon/src/tui/team/preview.ts +34 -0
  190. package/packages/daemon/src/tui/team/projects.ts +191 -0
  191. package/packages/daemon/src/tui/team/report.ts +83 -0
  192. package/packages/daemon/src/tui/team/sessions.ts +433 -0
  193. package/packages/daemon/src/tui/team/tree.ts +62 -0
  194. package/packages/daemon/src/types.ts +13 -0
  195. package/packages/daemon/src/ui/index.ts +32 -0
  196. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  197. package/packages/daemon/src/ui/types.ts +91 -0
  198. package/packages/daemon/src/ui/web/base.css +80 -0
  199. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  200. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  201. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  202. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  203. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  204. package/packages/daemon/src/ui/web/index.ts +27 -0
  205. package/packages/daemon/src/ui/web/render.ts +77 -0
  206. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  207. package/packages/daemon/src/validate.ts +217 -0
  208. package/packages/daemon/src/widgets/changes/README.md +3 -0
  209. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  210. package/packages/daemon/src/widgets/config/README.md +3 -0
  211. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  212. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  213. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  214. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  215. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  216. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  217. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  218. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  219. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  220. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  221. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  222. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  223. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  224. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  225. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  226. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  227. package/packages/daemon/src/widgets/preview/README.md +3 -0
  228. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  229. package/packages/daemon/src/widgets/resolve.ts +121 -0
  230. package/packages/daemon/src/widgets/setup/README.md +3 -0
  231. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  232. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  233. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  234. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  235. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  236. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  237. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  238. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  239. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  240. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  241. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  242. package/packages/tmux-bridge/package.json +22 -0
  243. package/packages/tmux-bridge/src/errors.ts +28 -0
  244. package/packages/tmux-bridge/src/index.ts +31 -0
  245. package/packages/tmux-bridge/src/monitor.ts +77 -0
  246. package/packages/tmux-bridge/src/panes.ts +136 -0
  247. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  248. package/packages/tmux-bridge/src/runner.ts +91 -0
  249. package/packages/tmux-bridge/src/sessions.ts +126 -0
  250. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  251. package/packages/tmux-bridge/src/targeting.ts +90 -0
  252. package/scripts/build-tui.mjs +11 -4
  253. package/scripts/perf-mirror.mjs +313 -0
  254. package/scripts/postinstall.js +26 -2
  255. package/skill/SKILL.md +22 -0
  256. package/templates/AGENTS.md +14 -7
  257. package/templates/agent-team-monorepo.yml +8 -0
  258. package/templates/agent-team-nextjs.yml +8 -0
  259. package/templates/agent-team.yml +10 -0
  260. package/templates/convex.yml +2 -0
  261. package/templates/default.yml +11 -5
  262. package/templates/go.yml +4 -0
  263. package/templates/missions.yml +6 -0
  264. package/templates/nextjs.yml +4 -0
  265. package/templates/python.yml +4 -0
  266. package/templates/skills/backend.md +5 -12
  267. package/templates/skills/frontend.md +5 -12
  268. package/templates/skills/general-worker.md +5 -12
  269. package/templates/skills/researcher.md +7 -12
  270. package/templates/skills/reviewer.md +7 -16
  271. package/templates/vite.yml +4 -0
@@ -0,0 +1,549 @@
1
+ import { resolve } from "node:path";
2
+ import { readConfig, writeConfig } from "./lib/yaml-io.ts";
3
+ import { setByPath } from "./lib/dot-path.ts";
4
+ import { outputError } from "./lib/output.ts";
5
+ import { IdeConfigSchema } from "./schemas/ide-config.ts";
6
+ import type { IdeConfig, Pane, Row } from "./types.ts";
7
+ import { CliActionInvocationError, tryDispatchAction } from "./lib/cli-action-bridge.ts";
8
+
9
+ /**
10
+ * Read config safely (read-only, no write). Returns config or undefined on error.
11
+ */
12
+ function readConfigSafe(dir: string): IdeConfig | undefined {
13
+ let cfg: IdeConfig;
14
+ try {
15
+ ({ config: cfg } = readConfig(dir));
16
+ } catch (e) {
17
+ outputError(`Cannot read ide.yml: ${(e as Error).message}`, "READ_ERROR");
18
+ return;
19
+ }
20
+ return cfg;
21
+ }
22
+
23
+ /**
24
+ * Read config, validate it's an object, run mutator, then write back.
25
+ * Returns the mutator's return value, or undefined on error.
26
+ */
27
+ function withConfig<T>(dir: string, mutator: (cfg: IdeConfig) => T): T | undefined {
28
+ const cfg = readConfigSafe(dir);
29
+ if (cfg === undefined) return;
30
+
31
+ if (!isConfigObject(cfg)) {
32
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
33
+ return;
34
+ }
35
+
36
+ const result = mutator(cfg);
37
+
38
+ const validation = IdeConfigSchema.safeParse(cfg);
39
+ if (!validation.success) {
40
+ const issues = validation.error.issues
41
+ .map((i) => ` ${i.path.join(".")}: ${i.message}`)
42
+ .join("\n");
43
+ outputError(`Invalid config after mutation:\n${issues}`, "INVALID_CONFIG");
44
+ return;
45
+ }
46
+
47
+ writeConfig(dir, cfg);
48
+ return result;
49
+ }
50
+
51
+ export function mutateConfig<T>(
52
+ dir: string,
53
+ mutator: (cfg: IdeConfig) => T,
54
+ ): {
55
+ config: IdeConfig;
56
+ result: T;
57
+ } {
58
+ const { config: cfg } = readConfig(dir);
59
+ if (!isConfigObject(cfg)) {
60
+ throw new Error("Invalid ide.yml: config root must be an object");
61
+ }
62
+
63
+ const result = mutator(cfg);
64
+ const validation = IdeConfigSchema.safeParse(cfg);
65
+ if (!validation.success) {
66
+ const issues = validation.error.issues
67
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
68
+ .join("; ");
69
+ throw new Error(`Invalid config after mutation: ${issues}`);
70
+ }
71
+
72
+ writeConfig(dir, cfg);
73
+ return { config: cfg, result };
74
+ }
75
+
76
+ function assertDotPath(path: string): void {
77
+ const parts = path.split(".");
78
+ if (
79
+ !path.trim() ||
80
+ parts.some((part) => !part) ||
81
+ parts.some((part) => part === "__proto__" || part === "prototype" || part === "constructor")
82
+ ) {
83
+ throw new Error(`Invalid config path "${path}"`);
84
+ }
85
+ }
86
+
87
+ export function configSetValue(dir: string, path: string, value: unknown): IdeConfig {
88
+ assertDotPath(path);
89
+ return mutateConfig(dir, (cfg) => {
90
+ setByPath(cfg as unknown as Record<string, unknown>, path, value);
91
+ }).config;
92
+ }
93
+
94
+ export function configAddPane(dir: string, rowIndex: number, pane: Partial<Pane>): IdeConfig {
95
+ return mutateConfig(dir, (cfg) => {
96
+ if (!Array.isArray(cfg.rows)) {
97
+ throw new Error("Invalid ide.yml: 'rows' must be an array");
98
+ }
99
+ if (!cfg.rows[rowIndex]) {
100
+ throw new Error(`Row ${rowIndex} does not exist`);
101
+ }
102
+ if (!Array.isArray(cfg.rows[rowIndex]!.panes)) {
103
+ throw new Error(`Invalid ide.yml: row ${rowIndex} panes must be an array`);
104
+ }
105
+ cfg.rows[rowIndex]!.panes.push(pane as Pane);
106
+ }).config;
107
+ }
108
+
109
+ export function configRemovePane(
110
+ dir: string,
111
+ rowIndex: number,
112
+ paneIndex: number,
113
+ ): {
114
+ config: IdeConfig;
115
+ removed: Pane;
116
+ } {
117
+ const updated = mutateConfig(dir, (cfg) => {
118
+ if (!Array.isArray(cfg.rows)) {
119
+ throw new Error("Invalid ide.yml: 'rows' must be an array");
120
+ }
121
+ if (!Array.isArray(cfg.rows[rowIndex]?.panes)) {
122
+ throw new Error(`Invalid ide.yml: row ${rowIndex} panes must be an array`);
123
+ }
124
+ const removed = cfg.rows[rowIndex]!.panes[paneIndex];
125
+ if (!removed) {
126
+ throw new Error(`Pane ${paneIndex} in row ${rowIndex} does not exist`);
127
+ }
128
+ cfg.rows[rowIndex]!.panes.splice(paneIndex, 1);
129
+ return removed;
130
+ });
131
+ return { config: updated.config, removed: updated.result };
132
+ }
133
+
134
+ export function configAddRow(dir: string, size?: string): IdeConfig {
135
+ return mutateConfig(dir, (cfg) => {
136
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
137
+ throw new Error("Invalid ide.yml: 'rows' must be an array");
138
+ }
139
+ const row: Row = { panes: [{ title: "Shell" }] };
140
+ if (size) row.size = size;
141
+ cfg.rows = cfg.rows ?? [];
142
+ cfg.rows.push(row);
143
+ }).config;
144
+ }
145
+
146
+ export function configEnableTeam(dir: string, name?: string): IdeConfig {
147
+ return mutateConfig(dir, (cfg) => {
148
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
149
+ throw new Error("Invalid ide.yml: 'rows' must be an array");
150
+ }
151
+ cfg.team = { name: name ?? cfg.name ?? "my-team" };
152
+ let leadAssigned = false;
153
+ for (const row of cfg.rows ?? []) {
154
+ for (const pane of row.panes ?? []) {
155
+ if (pane.command === "claude" || pane.role === "lead" || pane.role === "teammate") {
156
+ pane.role = leadAssigned ? "teammate" : "lead";
157
+ leadAssigned = true;
158
+ }
159
+ }
160
+ }
161
+ if (!leadAssigned) {
162
+ delete cfg.team;
163
+ throw new Error("Cannot enable agent team: no Claude panes found");
164
+ }
165
+ }).config;
166
+ }
167
+
168
+ export function configDisableTeam(dir: string): IdeConfig {
169
+ return mutateConfig(dir, (cfg) => {
170
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
171
+ throw new Error("Invalid ide.yml: 'rows' must be an array");
172
+ }
173
+ delete cfg.team;
174
+ for (const row of cfg.rows ?? []) {
175
+ if (!Array.isArray(row?.panes)) continue;
176
+ for (const pane of row.panes) {
177
+ delete pane.role;
178
+ delete pane.task;
179
+ }
180
+ }
181
+ }).config;
182
+ }
183
+
184
+ export async function config(
185
+ targetDir: string | null,
186
+ { json, action, args }: { json?: boolean; action?: string; args?: string[] } = {},
187
+ ): Promise<void> {
188
+ const dir = resolve(targetDir ?? ".");
189
+
190
+ if (await tryDispatchConfigAction(dir, { json, action, args: args ?? [] })) return;
191
+
192
+ switch (action) {
193
+ case "dump":
194
+ return dumpConfig(dir, { json });
195
+ case "set":
196
+ return setConfig(dir, args ?? [], { json });
197
+ case "add-pane":
198
+ return addPane(dir, args ?? [], { json });
199
+ case "remove-pane":
200
+ return removePane(dir, args ?? [], { json });
201
+ case "add-row":
202
+ return addRow(dir, args ?? [], { json });
203
+ case "enable-team":
204
+ return enableTeam(dir, args ?? [], { json });
205
+ case "disable-team":
206
+ return disableTeam(dir, { json });
207
+ default:
208
+ return dumpConfig(dir, { json });
209
+ }
210
+ }
211
+
212
+ function dumpConfig(dir: string, { json }: { json?: boolean }): void {
213
+ const cfg = readConfigSafe(dir);
214
+ if (cfg === undefined) return;
215
+
216
+ if (json) {
217
+ console.log(JSON.stringify(cfg, null, 2));
218
+ } else {
219
+ console.log(JSON.stringify(cfg, null, 2));
220
+ }
221
+ }
222
+
223
+ function printConfigActionError(err: unknown): void {
224
+ if (err instanceof CliActionInvocationError) {
225
+ outputError(err.message, err.code.toUpperCase());
226
+ }
227
+ throw err;
228
+ }
229
+
230
+ function coerceConfigValue(raw: string): string | boolean | number {
231
+ if (raw === "true") return true;
232
+ if (raw === "false") return false;
233
+ if (/^\d+$/.test(raw)) return parseInt(raw);
234
+ return raw;
235
+ }
236
+
237
+ async function tryDispatchConfigAction(
238
+ dir: string,
239
+ { json, action, args }: { json?: boolean; action?: string; args: string[] },
240
+ ): Promise<boolean> {
241
+ try {
242
+ if (action === "set") {
243
+ const [dotpath, ...rest] = args;
244
+ if (!dotpath || rest.length === 0) return false;
245
+ const value = coerceConfigValue(rest.join(" "));
246
+ const result = await tryDispatchAction("config.set", { path: dotpath, value }, { cwd: dir });
247
+ if (!result) return false;
248
+ if (json) console.log(JSON.stringify({ ok: true, path: dotpath, value }, null, 2));
249
+ else console.log(`Set ${dotpath} = ${JSON.stringify(value)}`);
250
+ return true;
251
+ }
252
+
253
+ if (action === "add-pane") {
254
+ const { row, title, command, size } = parseNamedArgs(args);
255
+ if (row === undefined) return false;
256
+ const rowIndex = parseIndex(row);
257
+ if (rowIndex == null) return false;
258
+ const result = await tryDispatchAction(
259
+ "config.addPane",
260
+ { rowIndex, title, command, size },
261
+ { cwd: dir },
262
+ );
263
+ if (!result) return false;
264
+ const pane = { title, command, size };
265
+ if (json) console.log(JSON.stringify({ ok: true, row: rowIndex, pane }, null, 2));
266
+ else console.log(`Added pane "${title ?? "untitled"}" to row ${rowIndex}`);
267
+ return true;
268
+ }
269
+
270
+ if (action === "remove-pane") {
271
+ const { row, pane } = parseNamedArgs(args);
272
+ if (row === undefined || pane === undefined) return false;
273
+ const rowIndex = parseIndex(row);
274
+ const paneIndex = parseIndex(pane);
275
+ if (rowIndex == null || paneIndex == null) return false;
276
+ const before = readConfigSafe(dir);
277
+ const removed = before?.rows[rowIndex]?.panes[paneIndex] ?? null;
278
+ const result = await tryDispatchAction(
279
+ "config.removePane",
280
+ { rowIndex, paneIndex },
281
+ { cwd: dir },
282
+ );
283
+ if (!result) return false;
284
+ if (json) {
285
+ console.log(JSON.stringify({ ok: true, row: rowIndex, pane: paneIndex, removed }, null, 2));
286
+ } else {
287
+ console.log(`Removed pane ${paneIndex} from row ${rowIndex}`);
288
+ }
289
+ return true;
290
+ }
291
+
292
+ if (action === "add-row") {
293
+ const { size } = parseNamedArgs(args);
294
+ const result = await tryDispatchAction("config.addRow", { size }, { cwd: dir });
295
+ if (!result) return false;
296
+ const row = result.config.rows.length - 1;
297
+ if (json) console.log(JSON.stringify({ ok: true, row, size: size ?? null }, null, 2));
298
+ else console.log(`Added row ${row}${size ? ` (${size})` : ""}`);
299
+ return true;
300
+ }
301
+
302
+ if (action === "enable-team") {
303
+ const { name } = parseNamedArgs(args);
304
+ const result = await tryDispatchAction("config.enableTeam", { name }, { cwd: dir });
305
+ if (!result) return false;
306
+ const teamName = result.config.team?.name ?? name ?? result.config.name ?? "my-team";
307
+ if (json) console.log(JSON.stringify({ ok: true, team: result.config.team }, null, 2));
308
+ else console.log(`Enabled agent team "${teamName}"`);
309
+ return true;
310
+ }
311
+
312
+ if (action === "disable-team") {
313
+ const result = await tryDispatchAction("config.disableTeam", {}, { cwd: dir });
314
+ if (!result) return false;
315
+ if (json) console.log(JSON.stringify({ ok: true, disabled: true }, null, 2));
316
+ else console.log("Disabled agent team");
317
+ return true;
318
+ }
319
+ } catch (err) {
320
+ printConfigActionError(err);
321
+ }
322
+
323
+ return false;
324
+ }
325
+
326
+ function setConfig(dir: string, args: string[], { json }: { json?: boolean }): void {
327
+ const [dotpath, ...rest] = args;
328
+ if (!dotpath || rest.length === 0) {
329
+ outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE");
330
+ return;
331
+ }
332
+
333
+ const value = coerceConfigValue(rest.join(" "));
334
+
335
+ withConfig(dir, (cfg) => {
336
+ setByPath(cfg as unknown as Record<string, unknown>, dotpath, value);
337
+ });
338
+
339
+ if (json) {
340
+ console.log(JSON.stringify({ ok: true, path: dotpath, value }, null, 2));
341
+ } else {
342
+ console.log(`Set ${dotpath} = ${JSON.stringify(value)}`);
343
+ }
344
+ }
345
+
346
+ function addPane(dir: string, args: string[], { json }: { json?: boolean }): void {
347
+ const { row, title, command, size } = parseNamedArgs(args);
348
+ if (row === undefined) {
349
+ outputError(
350
+ "Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]",
351
+ "USAGE",
352
+ );
353
+ return;
354
+ }
355
+
356
+ const rowIdx = parseIndex(row);
357
+ if (rowIdx == null) {
358
+ outputError(`Invalid row index "${row}"`, "USAGE");
359
+ return;
360
+ }
361
+
362
+ const pane: Partial<Pane> = {};
363
+ if (title) pane.title = title;
364
+ if (command) pane.command = command;
365
+ if (size) pane.size = size;
366
+
367
+ withConfig(dir, (cfg) => {
368
+ if (!Array.isArray(cfg.rows)) {
369
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
370
+ }
371
+
372
+ if (!cfg.rows[rowIdx]) {
373
+ outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW");
374
+ }
375
+
376
+ if (!Array.isArray(cfg.rows[rowIdx]!.panes)) {
377
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
378
+ }
379
+
380
+ cfg.rows[rowIdx]!.panes.push(pane as Pane);
381
+ });
382
+
383
+ if (json) {
384
+ console.log(JSON.stringify({ ok: true, row: rowIdx, pane }, null, 2));
385
+ } else {
386
+ console.log(`Added pane "${title ?? "untitled"}" to row ${rowIdx}`);
387
+ }
388
+ }
389
+
390
+ function removePane(dir: string, args: string[], { json }: { json?: boolean }): void {
391
+ const { row, pane } = parseNamedArgs(args);
392
+ if (row === undefined || pane === undefined) {
393
+ outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
394
+ return;
395
+ }
396
+
397
+ const rowIdx = parseIndex(row);
398
+ const paneIdx = parseIndex(pane);
399
+ if (rowIdx == null || paneIdx == null) {
400
+ outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
401
+ return;
402
+ }
403
+
404
+ let removed: Pane | undefined;
405
+ withConfig(dir, (cfg) => {
406
+ if (!Array.isArray(cfg.rows)) {
407
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
408
+ }
409
+
410
+ if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
411
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
412
+ }
413
+
414
+ if (!cfg.rows[rowIdx]!.panes[paneIdx]) {
415
+ outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE");
416
+ }
417
+
418
+ removed = cfg.rows[rowIdx]!.panes.splice(paneIdx, 1)[0];
419
+ });
420
+
421
+ if (json) {
422
+ console.log(JSON.stringify({ ok: true, row: rowIdx, pane: paneIdx, removed }, null, 2));
423
+ } else {
424
+ console.log(`Removed pane ${paneIdx} ("${removed?.title ?? "untitled"}") from row ${rowIdx}`);
425
+ }
426
+ }
427
+
428
+ function addRow(dir: string, args: string[], { json }: { json?: boolean }): void {
429
+ const { size } = parseNamedArgs(args);
430
+
431
+ let rowIdx: number | undefined;
432
+ withConfig(dir, (cfg) => {
433
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
434
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
435
+ }
436
+
437
+ const row: Row = { panes: [{ title: "Shell" }] };
438
+ if (size) row.size = size;
439
+ cfg.rows = cfg.rows ?? [];
440
+ cfg.rows.push(row);
441
+ rowIdx = cfg.rows.length - 1;
442
+ });
443
+
444
+ if (json) {
445
+ console.log(JSON.stringify({ ok: true, row: rowIdx, size: size ?? null }, null, 2));
446
+ } else {
447
+ console.log(`Added row ${rowIdx}${size ? ` (${size})` : ""}`);
448
+ }
449
+ }
450
+
451
+ function enableTeam(dir: string, args: string[], { json }: { json?: boolean }): void {
452
+ const { name } = parseNamedArgs(args);
453
+
454
+ let teamName: string | undefined;
455
+ let result: { team: IdeConfig["team"]; roles: ReturnType<typeof summarizeRoles> } | undefined;
456
+ withConfig(dir, (cfg) => {
457
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
458
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
459
+ }
460
+
461
+ teamName = name ?? cfg.name ?? "my-team";
462
+ cfg.team = { name: teamName };
463
+
464
+ let leadAssigned = false;
465
+ for (const row of cfg.rows ?? []) {
466
+ for (const pane of row.panes ?? []) {
467
+ if (pane.command === "claude" || pane.role === "lead" || pane.role === "teammate") {
468
+ if (!leadAssigned) {
469
+ pane.role = "lead";
470
+ leadAssigned = true;
471
+ } else {
472
+ pane.role = "teammate";
473
+ }
474
+ }
475
+ }
476
+ }
477
+ if (!leadAssigned) {
478
+ delete cfg.team;
479
+ outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG");
480
+ }
481
+
482
+ result = { team: cfg.team, roles: summarizeRoles(cfg) };
483
+ });
484
+
485
+ if (json) {
486
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
487
+ } else {
488
+ console.log(`Enabled agent team "${teamName}"`);
489
+ }
490
+ }
491
+
492
+ function disableTeam(dir: string, { json }: { json?: boolean }): void {
493
+ withConfig(dir, (cfg) => {
494
+ if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
495
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
496
+ }
497
+
498
+ delete cfg.team;
499
+ for (const row of cfg.rows ?? []) {
500
+ if (!Array.isArray(row?.panes)) continue;
501
+ for (const pane of row.panes) {
502
+ delete pane.role;
503
+ delete pane.task;
504
+ }
505
+ }
506
+ });
507
+
508
+ if (json) {
509
+ console.log(JSON.stringify({ ok: true, disabled: true }, null, 2));
510
+ } else {
511
+ console.log("Disabled agent team");
512
+ }
513
+ }
514
+
515
+ function summarizeRoles(
516
+ cfg: IdeConfig,
517
+ ): { row: number; pane: number; title: string | null; role: string }[] {
518
+ const roles: { row: number; pane: number; title: string | null; role: string }[] = [];
519
+ for (let i = 0; i < (cfg.rows ?? []).length; i++) {
520
+ for (let j = 0; j < (cfg.rows[i]!.panes ?? []).length; j++) {
521
+ const p = cfg.rows[i]!.panes[j]!;
522
+ if (p.role) {
523
+ roles.push({ row: i, pane: j, title: p.title ?? null, role: p.role });
524
+ }
525
+ }
526
+ }
527
+ return roles;
528
+ }
529
+
530
+ function parseNamedArgs(args: string[]): Record<string, string> {
531
+ const result: Record<string, string> = {};
532
+ for (let i = 0; i < args.length; i++) {
533
+ if (args[i]!.startsWith("--") && i + 1 < args.length) {
534
+ const key = args[i]!.slice(2);
535
+ result[key] = args[i + 1]!;
536
+ i++;
537
+ }
538
+ }
539
+ return result;
540
+ }
541
+
542
+ function isConfigObject(value: unknown): value is Record<string, unknown> {
543
+ return value != null && typeof value === "object" && !Array.isArray(value);
544
+ }
545
+
546
+ function parseIndex(value: string): number | null {
547
+ if (!/^\d+$/.test(String(value))) return null;
548
+ return Number.parseInt(value, 10);
549
+ }