tmux-ide 1.2.0 → 1.3.1
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.
- package/bin/cli.js +14 -14
- package/dist/attach.d.ts +3 -0
- package/dist/attach.js +14 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +269 -0
- package/dist/detect.d.ts +15 -0
- package/dist/detect.js +228 -0
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +66 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +67 -0
- package/dist/inspect.d.ts +54 -0
- package/dist/inspect.js +118 -0
- package/dist/launch.d.ts +22 -0
- package/dist/launch.js +174 -0
- package/dist/lib/dot-path.d.ts +2 -0
- package/dist/lib/dot-path.js +17 -0
- package/dist/lib/errors.d.ts +29 -0
- package/dist/lib/errors.js +37 -0
- package/dist/lib/launch-plan.d.ts +6 -0
- package/dist/lib/launch-plan.js +40 -0
- package/dist/lib/output.d.ts +9 -0
- package/dist/lib/output.js +65 -0
- package/dist/lib/session-monitor.d.ts +12 -0
- package/dist/lib/session-monitor.js +160 -0
- package/dist/lib/session-options.d.ts +13 -0
- package/dist/lib/session-options.js +85 -0
- package/dist/lib/sizes.d.ts +16 -0
- package/dist/lib/sizes.js +36 -0
- package/dist/lib/tmux.d.ts +42 -0
- package/dist/lib/tmux.js +225 -0
- package/dist/lib/yaml-io.d.ts +10 -0
- package/dist/lib/yaml-io.js +24 -0
- package/dist/ls.d.ts +3 -0
- package/dist/ls.js +35 -0
- package/dist/restart.d.ts +4 -0
- package/dist/restart.js +13 -0
- package/dist/status.d.ts +3 -0
- package/dist/status.js +29 -0
- package/dist/stop.d.ts +3 -0
- package/dist/stop.js +21 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +172 -0
- package/package.json +16 -10
- package/src/attach.js +0 -17
- package/src/config.js +0 -341
- package/src/detect.js +0 -232
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/inspect.js +0 -127
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/output.js +0 -68
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/session-options.js +0 -95
- package/src/lib/sizes.js +0 -36
- package/src/lib/tmux.js +0 -234
- package/src/lib/yaml-io.js +0 -26
- package/src/ls.js +0 -40
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- package/src/validate.js +0 -154
package/src/launch.js
DELETED
|
@@ -1,245 +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 {
|
|
11
|
-
attachSession,
|
|
12
|
-
createDetachedSession,
|
|
13
|
-
getPaneCurrentCommand,
|
|
14
|
-
getSessionVariable,
|
|
15
|
-
hasSession,
|
|
16
|
-
runSessionCommand,
|
|
17
|
-
selectPane,
|
|
18
|
-
sendLiteral,
|
|
19
|
-
setPaneTitle,
|
|
20
|
-
setSessionEnvironment,
|
|
21
|
-
setSessionVariable,
|
|
22
|
-
splitPane,
|
|
23
|
-
startSessionMonitor,
|
|
24
|
-
} from "./lib/tmux.js";
|
|
25
|
-
import { validateConfig } from "./validate.js";
|
|
26
|
-
|
|
27
|
-
function sleepMs(ms) {
|
|
28
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function configHash(config) {
|
|
32
|
-
return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function waitForPaneCommand(
|
|
36
|
-
targetPane,
|
|
37
|
-
expectedCommands,
|
|
38
|
-
{ attempts = 20, delayMs = 100, getCurrentCommand = getPaneCurrentCommand, sleep = sleepMs } = {},
|
|
39
|
-
) {
|
|
40
|
-
const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
|
|
41
|
-
|
|
42
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
43
|
-
try {
|
|
44
|
-
const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
|
|
45
|
-
if (allowed.has(current)) return true;
|
|
46
|
-
} catch {
|
|
47
|
-
// Fall through to retry; tmux can briefly report transitional state.
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (attempt < attempts - 1) {
|
|
51
|
-
sleep(delayMs);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function buildPaneMap(rows, dir, rootPaneId, splitPane) {
|
|
59
|
-
const rowSizes = computeSizes(rows);
|
|
60
|
-
const rowSplitPercents = toSplitPercents(rowSizes);
|
|
61
|
-
|
|
62
|
-
// Create all rows vertically first so each row spans the full width.
|
|
63
|
-
const rowPaneIds = [rootPaneId];
|
|
64
|
-
for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
|
|
65
|
-
const splitFrom = rowPaneIds[rowIdx - 1];
|
|
66
|
-
const newPaneId = splitPane({
|
|
67
|
-
targetPane: splitFrom,
|
|
68
|
-
direction: "vertical",
|
|
69
|
-
cwd: dir,
|
|
70
|
-
percent: rowSplitPercents[rowIdx - 1],
|
|
71
|
-
});
|
|
72
|
-
rowPaneIds.push(newPaneId);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const paneMap = [];
|
|
76
|
-
const firstPanesOfRows = new Set(rowPaneIds);
|
|
77
|
-
|
|
78
|
-
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
79
|
-
const row = rows[rowIdx];
|
|
80
|
-
const panes = row.panes ?? [];
|
|
81
|
-
const rowPaneId = rowPaneIds[rowIdx];
|
|
82
|
-
const rowPanes = [rowPaneId];
|
|
83
|
-
|
|
84
|
-
const paneSizes = computeSizes(panes);
|
|
85
|
-
const paneSplitPercents = toSplitPercents(paneSizes);
|
|
86
|
-
|
|
87
|
-
for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
|
|
88
|
-
const pane = panes[paneIdx];
|
|
89
|
-
const targetPane = rowPanes[paneIdx - 1];
|
|
90
|
-
const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
|
|
91
|
-
const newPaneId = splitPane({
|
|
92
|
-
targetPane,
|
|
93
|
-
direction: "horizontal",
|
|
94
|
-
cwd: paneDir,
|
|
95
|
-
percent: paneSplitPercents[paneIdx - 1],
|
|
96
|
-
});
|
|
97
|
-
rowPanes.push(newPaneId);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
paneMap.push(rowPanes);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { paneMap, firstPanesOfRows };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function loadLaunchConfig(dir) {
|
|
107
|
-
let config;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
({ config } = readConfig(dir));
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (error?.code === "ENOENT") {
|
|
113
|
-
outputError(
|
|
114
|
-
`No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`,
|
|
115
|
-
"CONFIG_NOT_FOUND",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const errors = validateConfig(config);
|
|
123
|
-
if (errors.length > 0) {
|
|
124
|
-
outputError(
|
|
125
|
-
`Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`,
|
|
126
|
-
"INVALID_CONFIG",
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return config;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function runBeforeHook(command, dir) {
|
|
134
|
-
if (!command) return;
|
|
135
|
-
|
|
136
|
-
console.log(`Running: ${command}`);
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
execSync(command, { cwd: dir, stdio: "inherit" });
|
|
140
|
-
} catch {
|
|
141
|
-
outputError(`The before hook failed: ${command}`, "BEFORE_HOOK_FAILED");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function launch(targetDir, { json = false, attach = true } = {}) {
|
|
146
|
-
const dir = resolve(targetDir ?? ".");
|
|
147
|
-
const config = loadLaunchConfig(dir);
|
|
148
|
-
|
|
149
|
-
const { name: fallbackName } = getSessionName(dir);
|
|
150
|
-
const session = config.name ?? fallbackName;
|
|
151
|
-
const rows = config.rows;
|
|
152
|
-
const theme = config.theme ?? {};
|
|
153
|
-
const team = config.team ?? null;
|
|
154
|
-
|
|
155
|
-
runBeforeHook(config.before, dir);
|
|
156
|
-
|
|
157
|
-
// If session already exists, check for config drift and attach
|
|
158
|
-
if (hasSession(session)) {
|
|
159
|
-
const currentHash = configHash(config);
|
|
160
|
-
const storedHash = getSessionVariable(session, "@config_hash");
|
|
161
|
-
const configChanged = Boolean(storedHash && currentHash !== storedHash);
|
|
162
|
-
|
|
163
|
-
if (json) {
|
|
164
|
-
console.log(JSON.stringify({ session, running: true, configChanged }));
|
|
165
|
-
} else if (configChanged) {
|
|
166
|
-
console.log(`Session "${session}" is running but ide.yml has changed.`);
|
|
167
|
-
console.log(`Run "tmux-ide restart" to apply changes.`);
|
|
168
|
-
} else {
|
|
169
|
-
console.log(`Session "${session}" is already running. Attaching...`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (attach) {
|
|
173
|
-
attachSession(session);
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Get terminal dimensions
|
|
179
|
-
const cols = process.stdout.columns ?? 200;
|
|
180
|
-
const lines = process.stdout.rows ?? 50;
|
|
181
|
-
|
|
182
|
-
// Create session with first pane
|
|
183
|
-
const rootPaneId = createDetachedSession(session, dir, { cols, lines });
|
|
184
|
-
|
|
185
|
-
// Set agent teams env var if team config is present
|
|
186
|
-
if (team) {
|
|
187
|
-
setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const { paneMap, firstPanesOfRows } = buildPaneMap(
|
|
191
|
-
rows,
|
|
192
|
-
dir,
|
|
193
|
-
rootPaneId,
|
|
194
|
-
({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent),
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const { focusPane, paneActions } = collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir);
|
|
198
|
-
|
|
199
|
-
for (const action of paneActions) {
|
|
200
|
-
if (action.title) {
|
|
201
|
-
setPaneTitle(action.targetPane, action.title);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (action.chdir) {
|
|
205
|
-
sendLiteral(action.targetPane, `cd ${action.chdir}`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (const exportCommand of action.exports) {
|
|
209
|
-
sendLiteral(action.targetPane, exportCommand);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (action.command) {
|
|
213
|
-
sendLiteral(action.targetPane, action.command);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
for (const command of buildSessionOptions(session, { theme })) {
|
|
218
|
-
runSessionCommand(command);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Store config hash for drift detection on re-launch
|
|
222
|
-
setSessionVariable(session, "@config_hash", configHash(config));
|
|
223
|
-
|
|
224
|
-
// Start background session monitor (port detection + agent status)
|
|
225
|
-
const monitorScript = resolve(
|
|
226
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
227
|
-
"lib",
|
|
228
|
-
"session-monitor.js",
|
|
229
|
-
);
|
|
230
|
-
startSessionMonitor(session, monitorScript);
|
|
231
|
-
|
|
232
|
-
// Focus the correct pane
|
|
233
|
-
selectPane(focusPane);
|
|
234
|
-
|
|
235
|
-
// Launch summary
|
|
236
|
-
const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
|
|
237
|
-
console.log(
|
|
238
|
-
`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`,
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
// Attach
|
|
242
|
-
if (attach) {
|
|
243
|
-
attachSession(session);
|
|
244
|
-
}
|
|
245
|
-
}
|
package/src/lib/dot-path.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function getByPath(obj, path) {
|
|
2
|
-
return path.split(".").reduce((o, k) => o?.[k], obj);
|
|
3
|
-
}
|
|
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) o[k] = /^\d+$/.test(nextKey) ? [] : {};
|
|
12
|
-
i++;
|
|
13
|
-
return o[k];
|
|
14
|
-
}, obj);
|
|
15
|
-
target[last] = value;
|
|
16
|
-
}
|
package/src/lib/errors.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export class IdeError extends Error {
|
|
2
|
-
constructor(message, { code, exitCode = 1, cause } = {}) {
|
|
3
|
-
super(message, { cause });
|
|
4
|
-
this.name = "IdeError";
|
|
5
|
-
this.code = code;
|
|
6
|
-
this.exitCode = exitCode;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
toJSON() {
|
|
10
|
-
const obj = { error: this.message, code: this.code };
|
|
11
|
-
if (this.cause) obj.cause = this.cause.message;
|
|
12
|
-
return obj;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class ConfigError extends IdeError {
|
|
17
|
-
constructor(message, code, { cause } = {}) {
|
|
18
|
-
super(message, { code, exitCode: 1, cause });
|
|
19
|
-
this.name = "ConfigError";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class TmuxError extends IdeError {
|
|
24
|
-
constructor(message, code, { cause } = {}) {
|
|
25
|
-
super(message, { code, exitCode: 1, cause });
|
|
26
|
-
this.name = "TmuxError";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class SessionError extends IdeError {
|
|
31
|
-
constructor(message, code, { cause } = {}) {
|
|
32
|
-
super(message, { code, exitCode: 1, cause });
|
|
33
|
-
this.name = "SessionError";
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/lib/launch-plan.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
|
|
3
|
-
export function buildPaneCommand(pane) {
|
|
4
|
-
if (!pane.command) return null;
|
|
5
|
-
return pane.command;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir) {
|
|
9
|
-
let focusPane = paneMap[0][0];
|
|
10
|
-
const paneActions = [];
|
|
11
|
-
|
|
12
|
-
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
13
|
-
const row = rows[rowIdx];
|
|
14
|
-
const panes = row.panes ?? [];
|
|
15
|
-
|
|
16
|
-
for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
|
|
17
|
-
const pane = panes[paneIdx];
|
|
18
|
-
const tmuxPane = paneMap[rowIdx][paneIdx];
|
|
19
|
-
const action = {
|
|
20
|
-
targetPane: tmuxPane,
|
|
21
|
-
title: pane.title ?? null,
|
|
22
|
-
chdir: null,
|
|
23
|
-
exports: [],
|
|
24
|
-
command: null,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
|
|
28
|
-
action.chdir = resolve(dir, pane.dir);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (pane.env && typeof pane.env === "object") {
|
|
32
|
-
action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const command = buildPaneCommand(pane);
|
|
36
|
-
if (command) {
|
|
37
|
-
action.command = command;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (pane.focus) {
|
|
41
|
-
focusPane = tmuxPane;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
paneActions.push(action);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { focusPane, paneActions };
|
|
49
|
-
}
|
package/src/lib/output.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { IdeError } from "./errors.js";
|
|
2
|
-
|
|
3
|
-
export function printLayout(config) {
|
|
4
|
-
const INNER = 40;
|
|
5
|
-
const rows = config.rows ?? [];
|
|
6
|
-
if (rows.length === 0) return;
|
|
7
|
-
|
|
8
|
-
for (let r = 0; r < rows.length; r++) {
|
|
9
|
-
const panes = rows[r].panes ?? [];
|
|
10
|
-
const count = panes.length || 1;
|
|
11
|
-
const widths = [];
|
|
12
|
-
let remaining = INNER;
|
|
13
|
-
for (let i = 0; i < count; i++) {
|
|
14
|
-
const w = i < count - 1 ? Math.floor(INNER / count) : remaining;
|
|
15
|
-
widths.push(w);
|
|
16
|
-
remaining -= w;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Top border or mid divider
|
|
20
|
-
if (r === 0) {
|
|
21
|
-
let top = " \u250c";
|
|
22
|
-
for (let i = 0; i < count; i++) {
|
|
23
|
-
top += "\u2500".repeat(widths[i]);
|
|
24
|
-
top += i < count - 1 ? "\u252c" : "\u2510";
|
|
25
|
-
}
|
|
26
|
-
console.log(top);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(" \u251c" + "\u2500".repeat(INNER + count - 1) + "\u2524");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Content line
|
|
32
|
-
const sizeLabel = rows[r].size ?? "";
|
|
33
|
-
let line = " \u2502";
|
|
34
|
-
for (let i = 0; i < count; i++) {
|
|
35
|
-
const title = panes[i]?.title ?? "";
|
|
36
|
-
const w = widths[i];
|
|
37
|
-
const pad = Math.max(0, w - title.length);
|
|
38
|
-
const left = Math.floor(pad / 2);
|
|
39
|
-
const right = pad - left;
|
|
40
|
-
line += " ".repeat(left) + title + " ".repeat(right) + "\u2502";
|
|
41
|
-
}
|
|
42
|
-
if (sizeLabel) line += " " + sizeLabel;
|
|
43
|
-
console.log(line);
|
|
44
|
-
|
|
45
|
-
// Bottom border (last row only)
|
|
46
|
-
if (r === rows.length - 1) {
|
|
47
|
-
let bot = " \u2514";
|
|
48
|
-
for (let i = 0; i < count; i++) {
|
|
49
|
-
bot += "\u2500".repeat(widths[i]);
|
|
50
|
-
bot += i < count - 1 ? "\u2534" : "\u2518";
|
|
51
|
-
}
|
|
52
|
-
console.log(bot);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function outputError(message, code, { exitCode = 1 } = {}) {
|
|
58
|
-
throw new IdeError(message, { code, exitCode });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function printCommandError(error, { json = false } = {}) {
|
|
62
|
-
if (json) {
|
|
63
|
-
console.error(JSON.stringify(error.toJSON(), null, 2));
|
|
64
|
-
} else {
|
|
65
|
-
console.error(error.message);
|
|
66
|
-
}
|
|
67
|
-
process.exit(error.exitCode ?? 1);
|
|
68
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const INTERVAL = 1000;
|
|
6
|
-
const SPINNERS = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠂⠒⠢⠆⠐⠠⠄◐◓◑◒|/\\-] /;
|
|
7
|
-
|
|
8
|
-
// --- Port detection (pure helpers) ---
|
|
9
|
-
|
|
10
|
-
function getListeningPids() {
|
|
11
|
-
// Returns Set of PIDs that have a listening TCP port in range 1024-20000
|
|
12
|
-
try {
|
|
13
|
-
const raw = execFileSync("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPn"], {
|
|
14
|
-
encoding: "utf-8",
|
|
15
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
-
});
|
|
17
|
-
const pids = new Set();
|
|
18
|
-
let currentPid = null;
|
|
19
|
-
for (const line of raw.split("\n")) {
|
|
20
|
-
if (line.startsWith("p")) {
|
|
21
|
-
currentPid = line.slice(1);
|
|
22
|
-
} else if (line.startsWith("n") && currentPid) {
|
|
23
|
-
const match = line.match(/:(\d+)$/);
|
|
24
|
-
if (match) {
|
|
25
|
-
const port = parseInt(match[1], 10);
|
|
26
|
-
if (port >= 1024 && port <= 20000) pids.add(currentPid);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return pids;
|
|
31
|
-
} catch {
|
|
32
|
-
return new Set();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getProcessTree() {
|
|
37
|
-
// Returns Map<pid, ppid>
|
|
38
|
-
try {
|
|
39
|
-
const raw = execFileSync("ps", ["-axo", "pid=,ppid="], {
|
|
40
|
-
encoding: "utf-8",
|
|
41
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
42
|
-
});
|
|
43
|
-
const tree = new Map();
|
|
44
|
-
for (const line of raw.trim().split("\n")) {
|
|
45
|
-
const parts = line.trim().split(/\s+/);
|
|
46
|
-
if (parts.length === 2) tree.set(parts[0], parts[1]);
|
|
47
|
-
}
|
|
48
|
-
return tree;
|
|
49
|
-
} catch {
|
|
50
|
-
return new Map();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function computePortPanes(panes, { listeners, tree } = {}) {
|
|
55
|
-
// Walk up from each listening PID to find which pane owns it
|
|
56
|
-
if (!listeners) listeners = getListeningPids();
|
|
57
|
-
if (!tree) tree = getProcessTree();
|
|
58
|
-
if (listeners.size === 0) return new Set();
|
|
59
|
-
|
|
60
|
-
const panePids = new Map(panes.map((p) => [p.pid, p.id]));
|
|
61
|
-
const result = new Set();
|
|
62
|
-
|
|
63
|
-
for (const listenerPid of listeners) {
|
|
64
|
-
let pid = listenerPid;
|
|
65
|
-
while (pid && pid !== "0") {
|
|
66
|
-
if (panePids.has(pid)) {
|
|
67
|
-
result.add(panePids.get(pid));
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
pid = tree.get(pid);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// --- Agent detection ---
|
|
77
|
-
|
|
78
|
-
export function computeAgentStates(panes) {
|
|
79
|
-
// Returns Map<paneId, "busy" | "idle" | null>
|
|
80
|
-
const states = new Map();
|
|
81
|
-
for (const pane of panes) {
|
|
82
|
-
const cmd = (pane.cmd ?? "").toLowerCase();
|
|
83
|
-
if (!cmd.includes("claude") && !cmd.includes("codex")) {
|
|
84
|
-
states.set(pane.id, null);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
states.set(pane.id, SPINNERS.test(pane.title ?? "") ? "busy" : "idle");
|
|
88
|
-
}
|
|
89
|
-
return states;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// --- Main loop (only runs when executed directly) ---
|
|
93
|
-
|
|
94
|
-
const isMainModule = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
95
|
-
|
|
96
|
-
if (isMainModule) {
|
|
97
|
-
const session = process.argv[2];
|
|
98
|
-
if (!session) process.exit(1);
|
|
99
|
-
|
|
100
|
-
function tmux(...args) {
|
|
101
|
-
return execFileSync("tmux", args, { encoding: "utf-8" }).trim();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function tmuxSilent(...args) {
|
|
105
|
-
try {
|
|
106
|
-
return tmux(...args);
|
|
107
|
-
} catch {
|
|
108
|
-
return "";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function sessionExists() {
|
|
113
|
-
try {
|
|
114
|
-
tmux("has-session", "-t", session);
|
|
115
|
-
return true;
|
|
116
|
-
} catch {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function hasClients() {
|
|
122
|
-
return tmuxSilent("list-clients").length > 0;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function listPanes() {
|
|
126
|
-
const raw = tmuxSilent(
|
|
127
|
-
"list-panes",
|
|
128
|
-
"-t",
|
|
129
|
-
session,
|
|
130
|
-
"-F",
|
|
131
|
-
"#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{pane_title}",
|
|
132
|
-
);
|
|
133
|
-
if (!raw) return [];
|
|
134
|
-
return raw.split("\n").map((line) => {
|
|
135
|
-
const [id, pid, cmd, title] = line.split("\t");
|
|
136
|
-
return { id, pid, cmd, title };
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let lastState = "";
|
|
141
|
-
|
|
142
|
-
function tick() {
|
|
143
|
-
if (!sessionExists()) process.exit(0);
|
|
144
|
-
if (!hasClients()) return; // skip when nobody is watching
|
|
145
|
-
|
|
146
|
-
const panes = listPanes();
|
|
147
|
-
if (panes.length === 0) return;
|
|
148
|
-
|
|
149
|
-
const portPanes = computePortPanes(panes);
|
|
150
|
-
const agentStates = computeAgentStates(panes);
|
|
151
|
-
|
|
152
|
-
// Build state fingerprint for change detection
|
|
153
|
-
const stateKey = panes
|
|
154
|
-
.map((p) => {
|
|
155
|
-
const port = portPanes.has(p.id) ? "1" : "0";
|
|
156
|
-
const agent = agentStates.get(p.id) ?? "-";
|
|
157
|
-
return `${p.id}:${port}:${agent}`;
|
|
158
|
-
})
|
|
159
|
-
.join("|");
|
|
160
|
-
|
|
161
|
-
if (stateKey === lastState) return;
|
|
162
|
-
|
|
163
|
-
// Apply changes
|
|
164
|
-
for (const pane of panes) {
|
|
165
|
-
const hasPort = portPanes.has(pane.id) ? "1" : "0";
|
|
166
|
-
const agent = agentStates.get(pane.id);
|
|
167
|
-
|
|
168
|
-
tmuxSilent("set-option", "-pqt", pane.id, "@has_port", hasPort);
|
|
169
|
-
tmuxSilent("set-option", "-pqt", pane.id, "@agent_busy", agent === "busy" ? "1" : "0");
|
|
170
|
-
tmuxSilent("set-option", "-pqt", pane.id, "@agent_idle", agent === "idle" ? "1" : "0");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
tmuxSilent("refresh-client", "-S");
|
|
174
|
-
lastState = stateKey;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
setInterval(tick, INTERVAL);
|
|
178
|
-
tick(); // run immediately
|
|
179
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composable builders for tmux session configuration.
|
|
3
|
-
* Each returns an array of tmux command arrays.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export function buildSessionOptions(session, { theme = {} } = {}) {
|
|
7
|
-
return [
|
|
8
|
-
...themeOptions(session, theme),
|
|
9
|
-
...borderOptions(session, theme),
|
|
10
|
-
...behaviorOptions(session),
|
|
11
|
-
...statusBarOptions(session, theme),
|
|
12
|
-
...keyBindings(),
|
|
13
|
-
];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function themeOptions(session, theme) {
|
|
17
|
-
const accent = theme.accent ?? "colour75";
|
|
18
|
-
const border = theme.border ?? "colour238";
|
|
19
|
-
const bg = theme.bg ?? "colour235";
|
|
20
|
-
const fg = theme.fg ?? "colour248";
|
|
21
|
-
|
|
22
|
-
return [
|
|
23
|
-
["set-option", "-t", session, "status-style", `bg=${bg},fg=${fg}`],
|
|
24
|
-
["set-option", "-t", session, "pane-border-style", `fg=${border}`],
|
|
25
|
-
["set-option", "-t", session, "pane-active-border-style", `fg=${accent}`],
|
|
26
|
-
];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function borderOptions(session, theme) {
|
|
30
|
-
const accent = theme.accent ?? "colour75";
|
|
31
|
-
const border = theme.border ?? "colour238";
|
|
32
|
-
const fg = theme.fg ?? "colour248";
|
|
33
|
-
|
|
34
|
-
return [
|
|
35
|
-
["set-option", "-t", session, "pane-border-status", "top"],
|
|
36
|
-
[
|
|
37
|
-
"set-option",
|
|
38
|
-
"-t",
|
|
39
|
-
session,
|
|
40
|
-
"pane-border-format",
|
|
41
|
-
` #{?pane_active,#[fg=${accent}#,bold]▸ #T #[fg=${fg}]#{pane_current_path},#[fg=${border}]· #T #{pane_current_path}} `,
|
|
42
|
-
],
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function behaviorOptions(session) {
|
|
47
|
-
return [
|
|
48
|
-
["set-option", "-t", session, "mouse", "on"],
|
|
49
|
-
["set-option", "-t", session, "escape-time", "0"],
|
|
50
|
-
["set-option", "-t", session, "status-interval", "1"],
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function statusBarOptions(session, theme) {
|
|
55
|
-
const accent = theme.accent ?? "colour75";
|
|
56
|
-
const border = theme.border ?? "colour238";
|
|
57
|
-
const fg = theme.fg ?? "colour248";
|
|
58
|
-
|
|
59
|
-
// Pane tab components — each is a self-contained piece
|
|
60
|
-
const agentIndicator = [
|
|
61
|
-
`#{?#{==:#{@agent_busy},1},#[fg=${accent}]⏺ ,`,
|
|
62
|
-
`#{?#{==:#{@agent_idle},1},#[fg=${border}]● ,}}`,
|
|
63
|
-
].join("");
|
|
64
|
-
const portIndicator = `#{?#{==:#{@has_port},1},#[fg=green]⏺ ,}`;
|
|
65
|
-
const paneStyle = `#{?pane_active,#[fg=${accent}],#[fg=${border}]}`;
|
|
66
|
-
const paneTab = `${agentIndicator}${portIndicator}${paneStyle}#[range=pane|#{pane_id}] #T #[norange]#[default]`;
|
|
67
|
-
const separator = `#{?loop_last_flag,,#[fg=${border}]│}`;
|
|
68
|
-
|
|
69
|
-
return [
|
|
70
|
-
[
|
|
71
|
-
"set-option",
|
|
72
|
-
"-t",
|
|
73
|
-
session,
|
|
74
|
-
"status-left",
|
|
75
|
-
`#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] `,
|
|
76
|
-
],
|
|
77
|
-
["set-option", "-t", session, "status-left-length", "30"],
|
|
78
|
-
[
|
|
79
|
-
"set-option",
|
|
80
|
-
"-t",
|
|
81
|
-
session,
|
|
82
|
-
"status-right",
|
|
83
|
-
`#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d `,
|
|
84
|
-
],
|
|
85
|
-
["set-option", "-t", session, "status-justify", "centre"],
|
|
86
|
-
["set-option", "-t", session, "window-status-current-format", `#[fg=${accent},bold]●`],
|
|
87
|
-
["set-option", "-t", session, "window-status-format", `#[fg=${border}]○`],
|
|
88
|
-
["set-option", "-t", session, "status", "2"],
|
|
89
|
-
["set-option", "-t", session, "status-format[1]", ` #{P:${paneTab}${separator}}`],
|
|
90
|
-
];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function keyBindings() {
|
|
94
|
-
return [["bind-key", "-n", "MouseDown1StatusDefault", "select-pane", "-t", "="]];
|
|
95
|
-
}
|