tmux-ide 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +27 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/attach.ts +20 -0
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/detect.ts +248 -0
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/inspect.ts +212 -0
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/output.ts +76 -0
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/session-options.ts +100 -0
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/sizes.ts +36 -0
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/yaml-io.ts +27 -0
  103. package/src/ls.test.ts +76 -0
  104. package/src/ls.ts +40 -0
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/dist/attach.d.ts +0 -3
  194. package/dist/attach.js +0 -14
  195. package/dist/config.d.ts +0 -5
  196. package/dist/config.js +0 -269
  197. package/dist/detect.d.ts +0 -15
  198. package/dist/detect.js +0 -228
  199. package/dist/doctor.d.ts +0 -3
  200. package/dist/doctor.js +0 -66
  201. package/dist/init.d.ts +0 -4
  202. package/dist/init.js +0 -67
  203. package/dist/inspect.d.ts +0 -54
  204. package/dist/inspect.js +0 -118
  205. package/dist/launch.d.ts +0 -22
  206. package/dist/launch.js +0 -174
  207. package/dist/lib/dot-path.d.ts +0 -2
  208. package/dist/lib/dot-path.js +0 -17
  209. package/dist/lib/errors.d.ts +0 -29
  210. package/dist/lib/errors.js +0 -37
  211. package/dist/lib/launch-plan.d.ts +0 -6
  212. package/dist/lib/launch-plan.js +0 -40
  213. package/dist/lib/output.d.ts +0 -9
  214. package/dist/lib/output.js +0 -65
  215. package/dist/lib/session-monitor.d.ts +0 -12
  216. package/dist/lib/session-monitor.js +0 -160
  217. package/dist/lib/session-options.d.ts +0 -13
  218. package/dist/lib/session-options.js +0 -85
  219. package/dist/lib/sizes.d.ts +0 -16
  220. package/dist/lib/sizes.js +0 -36
  221. package/dist/lib/tmux.d.ts +0 -42
  222. package/dist/lib/tmux.js +0 -225
  223. package/dist/lib/yaml-io.d.ts +0 -10
  224. package/dist/lib/yaml-io.js +0 -24
  225. package/dist/ls.d.ts +0 -3
  226. package/dist/ls.js +0 -35
  227. package/dist/restart.d.ts +0 -4
  228. package/dist/restart.js +0 -13
  229. package/dist/status.d.ts +0 -3
  230. package/dist/status.js +0 -29
  231. package/dist/stop.d.ts +0 -3
  232. package/dist/stop.js +0 -21
  233. package/dist/types.d.ts +0 -41
  234. package/dist/types.js +0 -1
  235. package/dist/validate.d.ts +0 -4
  236. package/dist/validate.js +0 -172
package/dist/launch.js DELETED
@@ -1,174 +0,0 @@
1
- import { resolve, dirname } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { execSync } from "node:child_process";
4
- import { createHash } from "node:crypto";
5
- import { readConfig, getSessionName } from "./lib/yaml-io.js";
6
- import { computeSizes, toSplitPercents } from "./lib/sizes.js";
7
- import { outputError } from "./lib/output.js";
8
- import { collectPaneStartupPlan } from "./lib/launch-plan.js";
9
- import { buildSessionOptions } from "./lib/session-options.js";
10
- import { attachSession, createDetachedSession, getPaneCurrentCommand, getSessionVariable, hasSession, runSessionCommand, selectPane, sendLiteral, setPaneTitle, setSessionEnvironment, setSessionVariable, splitPane, startSessionMonitor, } from "./lib/tmux.js";
11
- import { validateConfig } from "./validate.js";
12
- function sleepMs(ms) {
13
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
14
- }
15
- function configHash(config) {
16
- return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
17
- }
18
- export function waitForPaneCommand(targetPane, expectedCommands, { attempts = 20, delayMs = 100, getCurrentCommand = getPaneCurrentCommand, sleep = sleepMs, } = {}) {
19
- const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
20
- for (let attempt = 0; attempt < attempts; attempt++) {
21
- try {
22
- const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
23
- if (current && allowed.has(current))
24
- return true;
25
- }
26
- catch {
27
- // Fall through to retry; tmux can briefly report transitional state.
28
- }
29
- if (attempt < attempts - 1) {
30
- sleep(delayMs);
31
- }
32
- }
33
- return false;
34
- }
35
- export function buildPaneMap(rows, dir, rootPaneId, splitPaneFn) {
36
- const rowSizes = computeSizes(rows);
37
- const rowSplitPercents = toSplitPercents(rowSizes);
38
- // Create all rows vertically first so each row spans the full width.
39
- const rowPaneIds = [rootPaneId];
40
- for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
41
- const splitFrom = rowPaneIds[rowIdx - 1];
42
- const newPaneId = splitPaneFn({
43
- targetPane: splitFrom,
44
- direction: "vertical",
45
- cwd: dir,
46
- percent: rowSplitPercents[rowIdx - 1],
47
- });
48
- rowPaneIds.push(newPaneId);
49
- }
50
- const paneMap = [];
51
- const firstPanesOfRows = new Set(rowPaneIds);
52
- for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
53
- const row = rows[rowIdx];
54
- const panes = row.panes ?? [];
55
- const rowPaneId = rowPaneIds[rowIdx];
56
- const rowPanes = [rowPaneId];
57
- const paneSizes = computeSizes(panes);
58
- const paneSplitPercents = toSplitPercents(paneSizes);
59
- for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
60
- const pane = panes[paneIdx];
61
- const targetPane = rowPanes[paneIdx - 1];
62
- const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
63
- const newPaneId = splitPaneFn({
64
- targetPane,
65
- direction: "horizontal",
66
- cwd: paneDir,
67
- percent: paneSplitPercents[paneIdx - 1],
68
- });
69
- rowPanes.push(newPaneId);
70
- }
71
- paneMap.push(rowPanes);
72
- }
73
- return { paneMap, firstPanesOfRows };
74
- }
75
- function loadLaunchConfig(dir) {
76
- let config;
77
- try {
78
- ({ config } = readConfig(dir));
79
- }
80
- catch (error) {
81
- if (error?.code === "ENOENT") {
82
- outputError(`No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`, "CONFIG_NOT_FOUND");
83
- }
84
- outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
85
- }
86
- const errors = validateConfig(config);
87
- if (errors.length > 0) {
88
- outputError(`Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`, "INVALID_CONFIG");
89
- }
90
- return config;
91
- }
92
- function runBeforeHook(command, dir) {
93
- if (!command)
94
- return;
95
- console.log(`Running: ${command}`);
96
- try {
97
- execSync(command, { cwd: dir, stdio: "inherit" });
98
- }
99
- catch {
100
- outputError(`The before hook failed: ${command}`, "BEFORE_HOOK_FAILED");
101
- }
102
- }
103
- export async function launch(targetDir, { json = false, attach = true } = {}) {
104
- const dir = resolve(targetDir ?? ".");
105
- const config = loadLaunchConfig(dir);
106
- const { name: fallbackName } = getSessionName(dir);
107
- const session = config.name ?? fallbackName;
108
- const rows = config.rows;
109
- const theme = config.theme ?? {};
110
- const team = config.team ?? null;
111
- runBeforeHook(config.before, dir);
112
- // If session already exists, check for config drift and attach
113
- if (hasSession(session)) {
114
- const currentHash = configHash(config);
115
- const storedHash = getSessionVariable(session, "@config_hash");
116
- const configChanged = Boolean(storedHash && currentHash !== storedHash);
117
- if (json) {
118
- console.log(JSON.stringify({ session, running: true, configChanged }));
119
- }
120
- else if (configChanged) {
121
- console.log(`Session "${session}" is running but ide.yml has changed.`);
122
- console.log(`Run "tmux-ide restart" to apply changes.`);
123
- }
124
- else {
125
- console.log(`Session "${session}" is already running. Attaching...`);
126
- }
127
- if (attach) {
128
- attachSession(session);
129
- }
130
- return;
131
- }
132
- // Get terminal dimensions
133
- const cols = process.stdout.columns ?? 200;
134
- const lines = process.stdout.rows ?? 50;
135
- // Create session with first pane
136
- const rootPaneId = createDetachedSession(session, dir, { cols, lines });
137
- // Set agent teams env var if team config is present
138
- if (team) {
139
- setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
140
- }
141
- const { paneMap, firstPanesOfRows } = buildPaneMap(rows, dir, rootPaneId, ({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent));
142
- const { focusPane, paneActions } = collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir);
143
- for (const action of paneActions) {
144
- if (action.title) {
145
- setPaneTitle(action.targetPane, action.title);
146
- }
147
- if (action.chdir) {
148
- sendLiteral(action.targetPane, `cd ${action.chdir}`);
149
- }
150
- for (const exportCommand of action.exports) {
151
- sendLiteral(action.targetPane, exportCommand);
152
- }
153
- if (action.command) {
154
- sendLiteral(action.targetPane, action.command);
155
- }
156
- }
157
- for (const command of buildSessionOptions(session, { theme })) {
158
- runSessionCommand(command);
159
- }
160
- // Store config hash for drift detection on re-launch
161
- setSessionVariable(session, "@config_hash", configHash(config));
162
- // Start background session monitor (port detection + agent status)
163
- const monitorScript = resolve(dirname(fileURLToPath(import.meta.url)), "lib", "session-monitor.js");
164
- startSessionMonitor(session, monitorScript);
165
- // Focus the correct pane
166
- selectPane(focusPane);
167
- // Launch summary
168
- const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
169
- console.log(`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`);
170
- // Attach
171
- if (attach) {
172
- attachSession(session);
173
- }
174
- }
@@ -1,2 +0,0 @@
1
- export declare function getByPath(obj: Record<string, any>, path: string): unknown;
2
- export declare function setByPath(obj: Record<string, any>, path: string, value: unknown): void;
@@ -1,17 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- export function getByPath(obj, path) {
3
- return path.split(".").reduce((o, k) => o?.[k], obj);
4
- }
5
- export function setByPath(obj, path, value) {
6
- const keys = path.split(".");
7
- const last = keys.pop();
8
- let i = 0;
9
- const target = keys.reduce((o, k) => {
10
- const nextKey = keys[i + 1] ?? last;
11
- if (o[k] === undefined)
12
- o[k] = /^\d+$/.test(nextKey) ? [] : {};
13
- i++;
14
- return o[k];
15
- }, obj);
16
- target[last] = value;
17
- }
@@ -1,29 +0,0 @@
1
- export declare class IdeError extends Error {
2
- code: string | undefined;
3
- exitCode: number;
4
- constructor(message: string, { code, exitCode, cause }?: {
5
- code?: string;
6
- exitCode?: number;
7
- cause?: Error;
8
- });
9
- toJSON(): {
10
- error: string;
11
- code: string | undefined;
12
- cause?: string;
13
- };
14
- }
15
- export declare class ConfigError extends IdeError {
16
- constructor(message: string, code: string, { cause }?: {
17
- cause?: Error;
18
- });
19
- }
20
- export declare class TmuxError extends IdeError {
21
- constructor(message: string, code: string, { cause }?: {
22
- cause?: Error;
23
- });
24
- }
25
- export declare class SessionError extends IdeError {
26
- constructor(message: string, code: string, { cause }?: {
27
- cause?: Error;
28
- });
29
- }
@@ -1,37 +0,0 @@
1
- export class IdeError extends Error {
2
- code;
3
- exitCode;
4
- constructor(message, { code, exitCode = 1, cause } = {}) {
5
- super(message, { cause });
6
- this.name = "IdeError";
7
- this.code = code;
8
- this.exitCode = exitCode;
9
- }
10
- toJSON() {
11
- const obj = {
12
- error: this.message,
13
- code: this.code,
14
- };
15
- if (this.cause)
16
- obj.cause = this.cause.message;
17
- return obj;
18
- }
19
- }
20
- export class ConfigError extends IdeError {
21
- constructor(message, code, { cause } = {}) {
22
- super(message, { code, exitCode: 1, cause });
23
- this.name = "ConfigError";
24
- }
25
- }
26
- export class TmuxError extends IdeError {
27
- constructor(message, code, { cause } = {}) {
28
- super(message, { code, exitCode: 1, cause });
29
- this.name = "TmuxError";
30
- }
31
- }
32
- export class SessionError extends IdeError {
33
- constructor(message, code, { cause } = {}) {
34
- super(message, { code, exitCode: 1, cause });
35
- this.name = "SessionError";
36
- }
37
- }
@@ -1,6 +0,0 @@
1
- import type { Pane, Row, PaneAction } from "../types.ts";
2
- export declare function buildPaneCommand(pane: Pane): string | null;
3
- export declare function collectPaneStartupPlan(rows: Row[], paneMap: string[][], firstPanesOfRows: Set<string>, dir: string): {
4
- focusPane: string;
5
- paneActions: PaneAction[];
6
- };
@@ -1,40 +0,0 @@
1
- import { resolve } from "node:path";
2
- export function buildPaneCommand(pane) {
3
- if (!pane.command)
4
- return null;
5
- return pane.command;
6
- }
7
- export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir) {
8
- let focusPane = paneMap[0][0];
9
- const paneActions = [];
10
- for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
11
- const row = rows[rowIdx];
12
- const panes = row.panes ?? [];
13
- for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
14
- const pane = panes[paneIdx];
15
- const tmuxPane = paneMap[rowIdx][paneIdx];
16
- const action = {
17
- targetPane: tmuxPane,
18
- title: pane.title ?? null,
19
- chdir: null,
20
- exports: [],
21
- command: null,
22
- };
23
- if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
24
- action.chdir = resolve(dir, pane.dir);
25
- }
26
- if (pane.env && typeof pane.env === "object") {
27
- action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
28
- }
29
- const command = buildPaneCommand(pane);
30
- if (command) {
31
- action.command = command;
32
- }
33
- if (pane.focus) {
34
- focusPane = tmuxPane;
35
- }
36
- paneActions.push(action);
37
- }
38
- }
39
- return { focusPane, paneActions };
40
- }
@@ -1,9 +0,0 @@
1
- import { IdeError } from "./errors.ts";
2
- import type { IdeConfig } from "../types.ts";
3
- export declare function printLayout(config: IdeConfig): void;
4
- export declare function outputError(message: string, code: string, { exitCode }?: {
5
- exitCode?: number;
6
- }): never;
7
- export declare function printCommandError(error: IdeError, { json }?: {
8
- json?: boolean;
9
- }): never;
@@ -1,65 +0,0 @@
1
- import { IdeError } from "./errors.js";
2
- export function printLayout(config) {
3
- const INNER = 40;
4
- const rows = config.rows ?? [];
5
- if (rows.length === 0)
6
- return;
7
- for (let r = 0; r < rows.length; r++) {
8
- const panes = rows[r].panes ?? [];
9
- const count = panes.length || 1;
10
- const widths = [];
11
- let remaining = INNER;
12
- for (let i = 0; i < count; i++) {
13
- const w = i < count - 1 ? Math.floor(INNER / count) : remaining;
14
- widths.push(w);
15
- remaining -= w;
16
- }
17
- // Top border or mid divider
18
- if (r === 0) {
19
- let top = " \u250c";
20
- for (let i = 0; i < count; i++) {
21
- top += "\u2500".repeat(widths[i]);
22
- top += i < count - 1 ? "\u252c" : "\u2510";
23
- }
24
- console.log(top);
25
- }
26
- else {
27
- console.log(" \u251c" + "\u2500".repeat(INNER + count - 1) + "\u2524");
28
- }
29
- // Content line
30
- const sizeLabel = rows[r].size ?? "";
31
- let line = " \u2502";
32
- for (let i = 0; i < count; i++) {
33
- const title = panes[i]?.title ?? "";
34
- const w = widths[i];
35
- const pad = Math.max(0, w - title.length);
36
- const left = Math.floor(pad / 2);
37
- const right = pad - left;
38
- line += " ".repeat(left) + title + " ".repeat(right) + "\u2502";
39
- }
40
- if (sizeLabel)
41
- line += " " + sizeLabel;
42
- console.log(line);
43
- // Bottom border (last row only)
44
- if (r === rows.length - 1) {
45
- let bot = " \u2514";
46
- for (let i = 0; i < count; i++) {
47
- bot += "\u2500".repeat(widths[i]);
48
- bot += i < count - 1 ? "\u2534" : "\u2518";
49
- }
50
- console.log(bot);
51
- }
52
- }
53
- }
54
- export function outputError(message, code, { exitCode = 1 } = {}) {
55
- throw new IdeError(message, { code, exitCode });
56
- }
57
- export function printCommandError(error, { json = false } = {}) {
58
- if (json) {
59
- console.error(JSON.stringify(error.toJSON(), null, 2));
60
- }
61
- else {
62
- console.error(error.message);
63
- }
64
- process.exit(error.exitCode ?? 1);
65
- }
@@ -1,12 +0,0 @@
1
- interface MonitorPane {
2
- id: string;
3
- pid: string;
4
- cmd?: string;
5
- title?: string;
6
- }
7
- export declare function computePortPanes(panes: MonitorPane[], { listeners, tree }?: {
8
- listeners?: Set<string>;
9
- tree?: Map<string, string>;
10
- }): Set<string>;
11
- export declare function computeAgentStates(panes: MonitorPane[]): Map<string, "busy" | "idle" | null>;
12
- export {};
@@ -1,160 +0,0 @@
1
- import { execFileSync } from "node:child_process";
2
- import { fileURLToPath } from "node:url";
3
- import { resolve } from "node:path";
4
- const INTERVAL = 1000;
5
- const SPINNERS = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠂⠒⠢⠆⠐⠠⠄◐◓◑◒|/\\-] /;
6
- // --- Port detection (pure helpers) ---
7
- function getListeningPids() {
8
- // Returns Set of PIDs that have a listening TCP port in range 1024-20000
9
- try {
10
- const raw = execFileSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPn"], {
11
- encoding: "utf-8",
12
- stdio: ["ignore", "pipe", "ignore"],
13
- });
14
- const pids = new Set();
15
- let currentPid = null;
16
- for (const line of raw.split("\n")) {
17
- if (line.startsWith("p")) {
18
- currentPid = line.slice(1);
19
- }
20
- else if (line.startsWith("n") && currentPid) {
21
- const match = line.match(/:(\d+)$/);
22
- if (match) {
23
- const port = parseInt(match[1], 10);
24
- if (port >= 1024 && port <= 20000)
25
- pids.add(currentPid);
26
- }
27
- }
28
- }
29
- return pids;
30
- }
31
- catch {
32
- return new Set();
33
- }
34
- }
35
- function getProcessTree() {
36
- // Returns Map<pid, ppid>
37
- try {
38
- const raw = execFileSync("ps", ["-axo", "pid=,ppid="], {
39
- encoding: "utf-8",
40
- stdio: ["ignore", "pipe", "ignore"],
41
- });
42
- const tree = new Map();
43
- for (const line of raw.trim().split("\n")) {
44
- const parts = line.trim().split(/\s+/);
45
- if (parts.length === 2)
46
- tree.set(parts[0], parts[1]);
47
- }
48
- return tree;
49
- }
50
- catch {
51
- return new Map();
52
- }
53
- }
54
- export function computePortPanes(panes, { listeners, tree } = {}) {
55
- // Walk up from each listening PID to find which pane owns it
56
- const resolvedListeners = listeners ?? getListeningPids();
57
- const resolvedTree = tree ?? getProcessTree();
58
- if (resolvedListeners.size === 0)
59
- return new Set();
60
- const panePids = new Map(panes.map((p) => [p.pid, p.id]));
61
- const result = new Set();
62
- for (const listenerPid of resolvedListeners) {
63
- let pid = listenerPid;
64
- while (pid && pid !== "0") {
65
- if (panePids.has(pid)) {
66
- result.add(panePids.get(pid));
67
- break;
68
- }
69
- pid = resolvedTree.get(pid);
70
- }
71
- }
72
- return result;
73
- }
74
- // --- Agent detection ---
75
- export function computeAgentStates(panes) {
76
- // Returns Map<paneId, "busy" | "idle" | null>
77
- const states = new Map();
78
- for (const pane of panes) {
79
- const cmd = (pane.cmd ?? "").toLowerCase();
80
- if (!cmd.includes("claude") && !cmd.includes("codex")) {
81
- states.set(pane.id, null);
82
- continue;
83
- }
84
- states.set(pane.id, SPINNERS.test(pane.title ?? "") ? "busy" : "idle");
85
- }
86
- return states;
87
- }
88
- // --- Main loop (only runs when executed directly) ---
89
- const isMainModule = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
90
- if (isMainModule) {
91
- const session = process.argv[2];
92
- if (!session)
93
- process.exit(1);
94
- function tmux(...args) {
95
- return execFileSync("tmux", args, { encoding: "utf-8" }).trim();
96
- }
97
- function tmuxSilent(...args) {
98
- try {
99
- return tmux(...args);
100
- }
101
- catch {
102
- return "";
103
- }
104
- }
105
- function sessionExists() {
106
- try {
107
- tmux("has-session", "-t", session);
108
- return true;
109
- }
110
- catch {
111
- return false;
112
- }
113
- }
114
- function hasClients() {
115
- return tmuxSilent("list-clients").length > 0;
116
- }
117
- function listPanes() {
118
- const raw = tmuxSilent("list-panes", "-t", session, "-F", "#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{pane_title}");
119
- if (!raw)
120
- return [];
121
- return raw.split("\n").map((line) => {
122
- const [id, pid, cmd, title] = line.split("\t");
123
- return { id: id, pid: pid, cmd, title };
124
- });
125
- }
126
- let lastState = "";
127
- function tick() {
128
- if (!sessionExists())
129
- process.exit(0);
130
- if (!hasClients())
131
- return; // skip when nobody is watching
132
- const panes = listPanes();
133
- if (panes.length === 0)
134
- return;
135
- const portPanes = computePortPanes(panes);
136
- const agentStates = computeAgentStates(panes);
137
- // Build state fingerprint for change detection
138
- const stateKey = panes
139
- .map((p) => {
140
- const port = portPanes.has(p.id) ? "1" : "0";
141
- const agent = agentStates.get(p.id) ?? "-";
142
- return `${p.id}:${port}:${agent}`;
143
- })
144
- .join("|");
145
- if (stateKey === lastState)
146
- return;
147
- // Apply changes
148
- for (const pane of panes) {
149
- const hasPort = portPanes.has(pane.id) ? "1" : "0";
150
- const agent = agentStates.get(pane.id);
151
- tmuxSilent("set-option", "-pqt", pane.id, "@has_port", hasPort);
152
- tmuxSilent("set-option", "-pqt", pane.id, "@agent_busy", agent === "busy" ? "1" : "0");
153
- tmuxSilent("set-option", "-pqt", pane.id, "@agent_idle", agent === "idle" ? "1" : "0");
154
- }
155
- tmuxSilent("refresh-client", "-S");
156
- lastState = stateKey;
157
- }
158
- setInterval(tick, INTERVAL);
159
- tick(); // run immediately
160
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Composable builders for tmux session configuration.
3
- * Each returns an array of tmux command arrays.
4
- */
5
- import type { ThemeConfig, TmuxCommand } from "../types.ts";
6
- export declare function buildSessionOptions(session: string, { theme }?: {
7
- theme?: ThemeConfig;
8
- }): TmuxCommand[];
9
- export declare function themeOptions(session: string, theme: ThemeConfig): TmuxCommand[];
10
- export declare function borderOptions(session: string, theme: ThemeConfig): TmuxCommand[];
11
- export declare function behaviorOptions(session: string): TmuxCommand[];
12
- export declare function statusBarOptions(session: string, theme: ThemeConfig): TmuxCommand[];
13
- export declare function keyBindings(): TmuxCommand[];
@@ -1,85 +0,0 @@
1
- /**
2
- * Composable builders for tmux session configuration.
3
- * Each returns an array of tmux command arrays.
4
- */
5
- export function buildSessionOptions(session, { theme = {} } = {}) {
6
- return [
7
- ...themeOptions(session, theme),
8
- ...borderOptions(session, theme),
9
- ...behaviorOptions(session),
10
- ...statusBarOptions(session, theme),
11
- ...keyBindings(),
12
- ];
13
- }
14
- export function themeOptions(session, theme) {
15
- const accent = theme.accent ?? "colour75";
16
- const border = theme.border ?? "colour238";
17
- const bg = theme.bg ?? "colour235";
18
- const fg = theme.fg ?? "colour248";
19
- return [
20
- ["set-option", "-t", session, "status-style", `bg=${bg},fg=${fg}`],
21
- ["set-option", "-t", session, "pane-border-style", `fg=${border}`],
22
- ["set-option", "-t", session, "pane-active-border-style", `fg=${accent}`],
23
- ];
24
- }
25
- export function borderOptions(session, theme) {
26
- const accent = theme.accent ?? "colour75";
27
- const border = theme.border ?? "colour238";
28
- const fg = theme.fg ?? "colour248";
29
- return [
30
- ["set-option", "-t", session, "pane-border-status", "top"],
31
- [
32
- "set-option",
33
- "-t",
34
- session,
35
- "pane-border-format",
36
- ` #{?pane_active,#[fg=${accent}#,bold]▸ #T #[fg=${fg}]#{pane_current_path},#[fg=${border}]· #T #{pane_current_path}} `,
37
- ],
38
- ];
39
- }
40
- export function behaviorOptions(session) {
41
- return [
42
- ["set-option", "-t", session, "mouse", "on"],
43
- ["set-option", "-t", session, "escape-time", "0"],
44
- ["set-option", "-t", session, "status-interval", "1"],
45
- ];
46
- }
47
- export function statusBarOptions(session, theme) {
48
- const accent = theme.accent ?? "colour75";
49
- const border = theme.border ?? "colour238";
50
- const fg = theme.fg ?? "colour248";
51
- // Pane tab components — each is a self-contained piece
52
- const agentIndicator = [
53
- `#{?#{==:#{@agent_busy},1},#[fg=${accent}]⏺ ,`,
54
- `#{?#{==:#{@agent_idle},1},#[fg=${border}]● ,}}`,
55
- ].join("");
56
- const portIndicator = `#{?#{==:#{@has_port},1},#[fg=green]⏺ ,}`;
57
- const paneStyle = `#{?pane_active,#[fg=${accent}],#[fg=${border}]}`;
58
- const paneTab = `${agentIndicator}${portIndicator}${paneStyle}#[range=pane|#{pane_id}] #T #[norange]#[default]`;
59
- const separator = `#{?loop_last_flag,,#[fg=${border}]│}`;
60
- return [
61
- [
62
- "set-option",
63
- "-t",
64
- session,
65
- "status-left",
66
- `#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] `,
67
- ],
68
- ["set-option", "-t", session, "status-left-length", "30"],
69
- [
70
- "set-option",
71
- "-t",
72
- session,
73
- "status-right",
74
- `#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d `,
75
- ],
76
- ["set-option", "-t", session, "status-justify", "centre"],
77
- ["set-option", "-t", session, "window-status-current-format", `#[fg=${accent},bold]●`],
78
- ["set-option", "-t", session, "window-status-format", `#[fg=${border}]○`],
79
- ["set-option", "-t", session, "status", "2"],
80
- ["set-option", "-t", session, "status-format[1]", ` #{P:${paneTab}${separator}}`],
81
- ];
82
- }
83
- export function keyBindings() {
84
- return [["bind-key", "-n", "MouseDown1StatusDefault", "select-pane", "-t", "="]];
85
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Compute absolute sizes for items where some have explicit sizes and others don't.
3
- * Items with `size` (e.g. "70%") keep their value; remaining space is split equally
4
- * among items without a size.
5
- */
6
- export declare function computeSizes(items: {
7
- size?: string;
8
- }[]): number[];
9
- /**
10
- * Convert absolute sizes (e.g. [70, 30]) to tmux split percentages for sequential splits.
11
- * Returns array of -p values (one per split after the first item).
12
- *
13
- * Each tmux split divides the current pane. The percentage given to -p is
14
- * the portion allocated to the NEW (bottom/right) pane.
15
- */
16
- export declare function toSplitPercents(sizes: number[]): number[];