tmux-ide 1.0.0 → 1.1.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.
- package/README.md +84 -35
- package/bin/cli.js +127 -85
- package/package.json +26 -5
- package/scripts/postinstall.js +46 -7
- package/skill/SKILL.md +28 -12
- package/src/attach.js +7 -10
- package/src/config.js +94 -7
- package/src/detect.js +77 -23
- package/src/doctor.js +23 -14
- package/src/init.js +16 -18
- package/src/inspect.js +127 -0
- package/src/launch.js +171 -177
- package/src/lib/launch-plan.js +83 -0
- package/src/lib/output.js +21 -4
- package/src/lib/tmux.js +200 -0
- package/src/ls.js +2 -4
- package/src/restart.js +5 -12
- package/src/status.js +4 -22
- package/src/stop.js +10 -9
- package/src/validate.js +0 -16
- package/src/detect.test.js +0 -115
- package/src/integration.test.js +0 -94
- package/src/lib/dot-path.test.js +0 -67
- package/src/lib/sizes.test.js +0 -74
- package/src/validate.test.js +0 -141
package/src/inspect.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { readConfig } from "./lib/yaml-io.js";
|
|
3
|
+
import { validateConfig } from "./validate.js";
|
|
4
|
+
import { outputError } from "./lib/output.js";
|
|
5
|
+
import { getSessionState, listPanes } from "./lib/tmux.js";
|
|
6
|
+
|
|
7
|
+
export function buildInspection(dir, { config, configPath, running, panes }) {
|
|
8
|
+
const errors = validateConfig(config);
|
|
9
|
+
const rows = Array.isArray(config?.rows) ? config.rows : [];
|
|
10
|
+
const resolvedRows = rows.map((row, rowIndex) => ({
|
|
11
|
+
index: rowIndex,
|
|
12
|
+
size: row.size ?? null,
|
|
13
|
+
panes: (Array.isArray(row?.panes) ? row.panes : []).map((pane, paneIndex) => ({
|
|
14
|
+
index: paneIndex,
|
|
15
|
+
title: pane.title ?? null,
|
|
16
|
+
command: pane.command ?? null,
|
|
17
|
+
dir: pane.dir ?? ".",
|
|
18
|
+
size: pane.size ?? null,
|
|
19
|
+
focus: pane.focus === true,
|
|
20
|
+
role: pane.role ?? null,
|
|
21
|
+
task: pane.task ?? null,
|
|
22
|
+
env: pane.env ?? {},
|
|
23
|
+
})),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const focusPane =
|
|
27
|
+
resolvedRows
|
|
28
|
+
.flatMap((row) => row.panes.map((pane) => ({ row: row.index, pane })))
|
|
29
|
+
.find(({ pane }) => pane.focus) ?? null;
|
|
30
|
+
const session = config?.name ?? basename(dir);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
dir,
|
|
34
|
+
configPath,
|
|
35
|
+
valid: errors.length === 0,
|
|
36
|
+
errors,
|
|
37
|
+
session,
|
|
38
|
+
before: config?.before ?? null,
|
|
39
|
+
summary: {
|
|
40
|
+
rows: resolvedRows.length,
|
|
41
|
+
panes: resolvedRows.reduce((sum, row) => sum + row.panes.length, 0),
|
|
42
|
+
focus: focusPane ? `rows.${focusPane.row}.panes.${focusPane.pane.index}` : null,
|
|
43
|
+
},
|
|
44
|
+
team: config?.team ?? null,
|
|
45
|
+
theme: config?.theme ?? null,
|
|
46
|
+
focus: focusPane
|
|
47
|
+
? {
|
|
48
|
+
row: focusPane.row,
|
|
49
|
+
pane: focusPane.pane.index,
|
|
50
|
+
title: focusPane.pane.title,
|
|
51
|
+
}
|
|
52
|
+
: null,
|
|
53
|
+
rows: resolvedRows,
|
|
54
|
+
rawConfig: config,
|
|
55
|
+
tmux: {
|
|
56
|
+
running,
|
|
57
|
+
panes,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function inspect(targetDir, { json } = {}) {
|
|
63
|
+
const dir = resolve(targetDir ?? ".");
|
|
64
|
+
|
|
65
|
+
let config;
|
|
66
|
+
let configPath;
|
|
67
|
+
try {
|
|
68
|
+
({ config, configPath } = readConfig(dir));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const session = config?.name ?? basename(dir);
|
|
75
|
+
const state = getSessionState(session);
|
|
76
|
+
const panes = state.running ? listPanes(session) : [];
|
|
77
|
+
const data = buildInspection(dir, { config, configPath, running: state.running, panes });
|
|
78
|
+
|
|
79
|
+
if (json) {
|
|
80
|
+
console.log(JSON.stringify(data, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`Directory: ${data.dir}`);
|
|
85
|
+
console.log(`Config: ${data.configPath}`);
|
|
86
|
+
console.log(`Valid: ${data.valid ? "yes" : "no"}`);
|
|
87
|
+
console.log(`Session: ${data.session}`);
|
|
88
|
+
console.log(`Running: ${data.tmux.running ? "yes" : "no"}`);
|
|
89
|
+
console.log(`Rows: ${data.summary.rows}`);
|
|
90
|
+
console.log(`Panes: ${data.summary.panes}`);
|
|
91
|
+
console.log(`Team: ${data.team ? data.team.name : "disabled"}`);
|
|
92
|
+
|
|
93
|
+
if (data.focus) {
|
|
94
|
+
console.log(
|
|
95
|
+
`Focus: row ${data.focus.row}, pane ${data.focus.pane}${data.focus.title ? ` (${data.focus.title})` : ""}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!data.valid) {
|
|
100
|
+
console.log("\nValidation Errors:");
|
|
101
|
+
for (const error of data.errors) {
|
|
102
|
+
console.log(` - ${error}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log("\nResolved Layout:");
|
|
107
|
+
for (const row of data.rows) {
|
|
108
|
+
console.log(` Row ${row.index}${row.size ? ` (${row.size})` : ""}`);
|
|
109
|
+
for (const pane of row.panes) {
|
|
110
|
+
const parts = [];
|
|
111
|
+
if (pane.title) parts.push(pane.title);
|
|
112
|
+
if (pane.command) parts.push(`cmd=${pane.command}`);
|
|
113
|
+
if (pane.dir && pane.dir !== ".") parts.push(`dir=${pane.dir}`);
|
|
114
|
+
if (pane.role) parts.push(`role=${pane.role}`);
|
|
115
|
+
if (pane.focus) parts.push("focus");
|
|
116
|
+
console.log(` - pane ${pane.index}: ${parts.join(" | ") || "shell"}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (data.tmux.running && data.tmux.panes.length > 0) {
|
|
121
|
+
console.log("\nLive Panes:");
|
|
122
|
+
for (const pane of data.tmux.panes) {
|
|
123
|
+
const active = pane.active ? " (active)" : "";
|
|
124
|
+
console.log(` ${pane.index}: ${pane.title} [${pane.width}x${pane.height}]${active}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/launch.js
CHANGED
|
@@ -2,66 +2,159 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { readConfig, getSessionName } from "./lib/yaml-io.js";
|
|
4
4
|
import { computeSizes, toSplitPercents } from "./lib/sizes.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { outputError } from "./lib/output.js";
|
|
6
|
+
import { buildThemeOptions, collectPaneStartupPlan } from "./lib/launch-plan.js";
|
|
7
|
+
import {
|
|
8
|
+
attachSession,
|
|
9
|
+
createDetachedSession,
|
|
10
|
+
getPaneCurrentCommand,
|
|
11
|
+
hasSession,
|
|
12
|
+
runSessionCommand,
|
|
13
|
+
selectPane,
|
|
14
|
+
sendLiteral,
|
|
15
|
+
setPaneTitle,
|
|
16
|
+
setSessionEnvironment,
|
|
17
|
+
splitPane,
|
|
18
|
+
} from "./lib/tmux.js";
|
|
19
|
+
import { validateConfig } from "./validate.js";
|
|
20
|
+
|
|
21
|
+
function sleepMs(ms) {
|
|
22
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
8
23
|
}
|
|
9
24
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
25
|
+
export function waitForPaneCommand(
|
|
26
|
+
targetPane,
|
|
27
|
+
expectedCommands,
|
|
28
|
+
{ attempts = 20, delayMs = 100, getCurrentCommand = getPaneCurrentCommand, sleep = sleepMs } = {},
|
|
29
|
+
) {
|
|
30
|
+
const allowed = new Set(expectedCommands.map((command) => command.toLowerCase()));
|
|
31
|
+
|
|
32
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
const current = getCurrentCommand(targetPane)?.trim().toLowerCase();
|
|
35
|
+
if (allowed.has(current)) return true;
|
|
36
|
+
} catch {
|
|
37
|
+
// Fall through to retry; tmux can briefly report transitional state.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (attempt < attempts - 1) {
|
|
41
|
+
sleep(delayMs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
14
46
|
}
|
|
15
47
|
|
|
16
|
-
export
|
|
17
|
-
const
|
|
48
|
+
export function buildPaneMap(rows, dir, rootPaneId, splitPane) {
|
|
49
|
+
const rowSizes = computeSizes(rows);
|
|
50
|
+
const rowSplitPercents = toSplitPercents(rowSizes);
|
|
51
|
+
|
|
52
|
+
// Create all rows vertically first so each row spans the full width.
|
|
53
|
+
const rowPaneIds = [rootPaneId];
|
|
54
|
+
for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
|
|
55
|
+
const splitFrom = rowPaneIds[rowIdx - 1];
|
|
56
|
+
const newPaneId = splitPane({
|
|
57
|
+
targetPane: splitFrom,
|
|
58
|
+
direction: "vertical",
|
|
59
|
+
cwd: dir,
|
|
60
|
+
percent: rowSplitPercents[rowIdx - 1],
|
|
61
|
+
});
|
|
62
|
+
rowPaneIds.push(newPaneId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const paneMap = [];
|
|
66
|
+
const firstPanesOfRows = new Set(rowPaneIds);
|
|
18
67
|
|
|
68
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
69
|
+
const row = rows[rowIdx];
|
|
70
|
+
const panes = row.panes ?? [];
|
|
71
|
+
const rowPaneId = rowPaneIds[rowIdx];
|
|
72
|
+
const rowPanes = [rowPaneId];
|
|
73
|
+
|
|
74
|
+
const paneSizes = computeSizes(panes);
|
|
75
|
+
const paneSplitPercents = toSplitPercents(paneSizes);
|
|
76
|
+
|
|
77
|
+
for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
|
|
78
|
+
const pane = panes[paneIdx];
|
|
79
|
+
const targetPane = rowPanes[paneIdx - 1];
|
|
80
|
+
const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
|
|
81
|
+
const newPaneId = splitPane({
|
|
82
|
+
targetPane,
|
|
83
|
+
direction: "horizontal",
|
|
84
|
+
cwd: paneDir,
|
|
85
|
+
percent: paneSplitPercents[paneIdx - 1],
|
|
86
|
+
});
|
|
87
|
+
rowPanes.push(newPaneId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
paneMap.push(rowPanes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { paneMap, firstPanesOfRows };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadLaunchConfig(dir, { json } = {}) {
|
|
19
97
|
let config;
|
|
98
|
+
|
|
20
99
|
try {
|
|
21
100
|
({ config } = readConfig(dir));
|
|
22
|
-
} catch {
|
|
23
|
-
|
|
24
|
-
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code === "ENOENT") {
|
|
103
|
+
outputError(
|
|
104
|
+
`No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`,
|
|
105
|
+
"CONFIG_NOT_FOUND",
|
|
106
|
+
{ json },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
|
|
25
111
|
}
|
|
26
112
|
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
113
|
+
const errors = validateConfig(config);
|
|
114
|
+
if (errors.length > 0) {
|
|
115
|
+
outputError(
|
|
116
|
+
`Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`,
|
|
117
|
+
"INVALID_CONFIG",
|
|
118
|
+
{ json },
|
|
119
|
+
);
|
|
32
120
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
121
|
+
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runBeforeHook(command, dir, { json } = {}) {
|
|
126
|
+
if (!command) return;
|
|
127
|
+
|
|
128
|
+
console.log(`Running: ${command}`);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
execSync(command, { cwd: dir, stdio: "inherit" });
|
|
132
|
+
} catch {
|
|
133
|
+
const message = json
|
|
134
|
+
? `The "before" hook failed: ${command}`
|
|
135
|
+
: `The before hook failed: ${command}`;
|
|
136
|
+
outputError(message, "BEFORE_HOOK_FAILED", { json });
|
|
39
137
|
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function launch(targetDir, { json, attach = true } = {}) {
|
|
141
|
+
const dir = resolve(targetDir ?? ".");
|
|
142
|
+
const config = loadLaunchConfig(dir, { json });
|
|
40
143
|
|
|
41
144
|
const session = config.name ?? getSessionName(dir);
|
|
42
145
|
const rows = config.rows;
|
|
43
146
|
const theme = config.theme ?? {};
|
|
44
147
|
const team = config.team ?? null;
|
|
45
148
|
|
|
46
|
-
|
|
47
|
-
if (config.before) {
|
|
48
|
-
console.log(`Running: ${config.before}`);
|
|
49
|
-
try {
|
|
50
|
-
execSync(config.before, { cwd: dir, stdio: "inherit" });
|
|
51
|
-
} catch (e) {
|
|
52
|
-
console.error(`"before" hook failed: ${config.before}`);
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
149
|
+
runBeforeHook(config.before, dir, { json });
|
|
56
150
|
|
|
57
151
|
// If session already exists, just attach to it
|
|
58
|
-
|
|
59
|
-
execSync(`tmux has-session -t "${session}"`, { stdio: "ignore" });
|
|
152
|
+
if (hasSession(session)) {
|
|
60
153
|
console.log(`Session "${session}" is already running. Attaching...`);
|
|
61
|
-
|
|
154
|
+
if (attach) {
|
|
155
|
+
attachSession(session);
|
|
156
|
+
}
|
|
62
157
|
return;
|
|
63
|
-
} catch {
|
|
64
|
-
// Session doesn't exist, continue with creation
|
|
65
158
|
}
|
|
66
159
|
|
|
67
160
|
// Get terminal dimensions
|
|
@@ -69,170 +162,71 @@ export async function launch(targetDir) {
|
|
|
69
162
|
const lines = process.stdout.rows ?? 50;
|
|
70
163
|
|
|
71
164
|
// Create session with first pane
|
|
72
|
-
|
|
165
|
+
const rootPaneId = createDetachedSession(session, dir, { cols, lines });
|
|
73
166
|
|
|
74
167
|
// Set agent teams env var if team config is present
|
|
75
168
|
if (team) {
|
|
76
|
-
|
|
169
|
+
setSessionEnvironment(session, "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", "1");
|
|
77
170
|
}
|
|
78
171
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const rowPaneIndices = [0]; // rowPaneIndices[i] = tmux pane index for row i
|
|
86
|
-
let nextPaneIndex = 1;
|
|
87
|
-
|
|
88
|
-
for (let rowIdx = 1; rowIdx < rows.length; rowIdx++) {
|
|
89
|
-
const splitFrom = rowPaneIndices[rowIdx - 1];
|
|
90
|
-
tmux(
|
|
91
|
-
`split-window -t "${session}:0.${splitFrom}" -v -c "${dir}" -p ${rowSplitPercents[rowIdx - 1]}`
|
|
92
|
-
);
|
|
93
|
-
rowPaneIndices.push(nextPaneIndex);
|
|
94
|
-
nextPaneIndex++;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Phase 2: Create panes within each row (horizontal splits).
|
|
98
|
-
// paneMap[row][col] = tmux pane index
|
|
99
|
-
const paneMap = [];
|
|
100
|
-
const firstPanesOfRows = new Set(rowPaneIndices);
|
|
101
|
-
|
|
102
|
-
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
103
|
-
const row = rows[rowIdx];
|
|
104
|
-
const panes = row.panes ?? [];
|
|
105
|
-
const rowPane = rowPaneIndices[rowIdx];
|
|
106
|
-
const rowPanes = [rowPane];
|
|
107
|
-
|
|
108
|
-
// Compute pane sizes within this row and convert to split percentages
|
|
109
|
-
const paneSizes = computeSizes(panes);
|
|
110
|
-
const paneSplitPercents = toSplitPercents(paneSizes);
|
|
111
|
-
|
|
112
|
-
// Split this row's panes horizontally
|
|
113
|
-
for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
|
|
114
|
-
const pane = panes[paneIdx];
|
|
115
|
-
const targetPane = rowPanes[paneIdx - 1];
|
|
172
|
+
const { paneMap, firstPanesOfRows } = buildPaneMap(
|
|
173
|
+
rows,
|
|
174
|
+
dir,
|
|
175
|
+
rootPaneId,
|
|
176
|
+
({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent),
|
|
177
|
+
);
|
|
116
178
|
|
|
117
|
-
|
|
118
|
-
|
|
179
|
+
const { focusPane, leadPane, paneActions, teammateCommands } = collectPaneStartupPlan(
|
|
180
|
+
rows,
|
|
181
|
+
paneMap,
|
|
182
|
+
firstPanesOfRows,
|
|
183
|
+
dir,
|
|
184
|
+
team,
|
|
185
|
+
);
|
|
119
186
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
123
|
-
rowPanes.push(nextPaneIndex);
|
|
124
|
-
nextPaneIndex++;
|
|
187
|
+
for (const action of paneActions) {
|
|
188
|
+
if (action.title) {
|
|
189
|
+
setPaneTitle(action.targetPane, action.title);
|
|
125
190
|
}
|
|
126
191
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Build the command for a pane, transforming claude commands for team roles
|
|
131
|
-
function buildCommand(p) {
|
|
132
|
-
if (!p.command) return null;
|
|
133
|
-
if (!team || !p.role) return p.command;
|
|
134
|
-
|
|
135
|
-
if (p.role === "lead") {
|
|
136
|
-
return `claude --team "${team.name}"`;
|
|
137
|
-
} else if (p.role === "teammate") {
|
|
138
|
-
let cmd = `claude --teammate-mode in-process --team "${team.name}"`;
|
|
139
|
-
if (p.task) {
|
|
140
|
-
cmd += ` --task "${p.task.replace(/"/g, '\\"')}"`;
|
|
141
|
-
}
|
|
142
|
-
return cmd;
|
|
192
|
+
if (action.chdir) {
|
|
193
|
+
sendLiteral(action.targetPane, `cd ${action.chdir}`);
|
|
143
194
|
}
|
|
144
|
-
return p.command;
|
|
145
|
-
}
|
|
146
195
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const teammateCommands = [];
|
|
196
|
+
for (const exportCommand of action.exports) {
|
|
197
|
+
sendLiteral(action.targetPane, exportCommand);
|
|
198
|
+
}
|
|
151
199
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const panes = row.panes ?? [];
|
|
155
|
-
for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
|
|
156
|
-
const p = panes[paneIdx];
|
|
157
|
-
const tmuxPane = paneMap[rowIdx][paneIdx];
|
|
158
|
-
|
|
159
|
-
if (p.title) {
|
|
160
|
-
tmux(`select-pane -t "${session}:0.${tmuxPane}" -T "${p.title}"`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Per-pane directory for first panes of each row (created with project root)
|
|
164
|
-
if (p.dir && firstPanesOfRows.has(tmuxPane)) {
|
|
165
|
-
const paneDir = resolve(dir, p.dir);
|
|
166
|
-
tmux(`send-keys -t "${session}:0.${tmuxPane}" "cd ${paneDir}" C-m`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Environment variables
|
|
170
|
-
if (p.env && typeof p.env === "object") {
|
|
171
|
-
for (const [key, val] of Object.entries(p.env)) {
|
|
172
|
-
tmux(`send-keys -t "${session}:0.${tmuxPane}" "export ${key}=${val}" C-m`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const cmd = buildCommand(p);
|
|
177
|
-
if (cmd) {
|
|
178
|
-
if (team && p.role === "teammate") {
|
|
179
|
-
// Defer teammate launches to second pass
|
|
180
|
-
teammateCommands.push({ pane: tmuxPane, cmd });
|
|
181
|
-
} else {
|
|
182
|
-
tmux(`send-keys -t "${session}:0.${tmuxPane}" "${cmd}" C-m`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (p.focus) {
|
|
187
|
-
focusPane = tmuxPane;
|
|
188
|
-
}
|
|
200
|
+
if (action.command) {
|
|
201
|
+
sendLiteral(action.targetPane, action.command);
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
|
|
192
|
-
//
|
|
205
|
+
// Keep a second pass hook available for future staged startup behavior.
|
|
193
206
|
if (teammateCommands.length > 0) {
|
|
194
|
-
|
|
207
|
+
if (leadPane) {
|
|
208
|
+
waitForPaneCommand(leadPane, ["claude"]);
|
|
209
|
+
}
|
|
195
210
|
for (const { pane: p, cmd } of teammateCommands) {
|
|
196
|
-
|
|
211
|
+
sendLiteral(p, cmd);
|
|
197
212
|
}
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const bg = theme.bg ?? "colour235";
|
|
204
|
-
const fg = theme.fg ?? "colour248";
|
|
205
|
-
|
|
206
|
-
tmux(`set-option -t "${session}" pane-border-status top`);
|
|
207
|
-
tmux(
|
|
208
|
-
`set-option -t "${session}" pane-border-format " #{?pane_active,#[bold]▸,·} #T "`
|
|
209
|
-
);
|
|
210
|
-
tmux(`set-option -t "${session}" pane-border-style "fg=${border}"`);
|
|
211
|
-
tmux(`set-option -t "${session}" pane-active-border-style "fg=${accent}"`);
|
|
212
|
-
|
|
213
|
-
tmux(`set-option -t "${session}" status-style "bg=${bg},fg=${fg}"`);
|
|
214
|
-
tmux(
|
|
215
|
-
`set-option -t "${session}" status-left "#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] "`
|
|
216
|
-
);
|
|
217
|
-
tmux(`set-option -t "${session}" status-left-length 30`);
|
|
218
|
-
tmux(
|
|
219
|
-
`set-option -t "${session}" status-right "#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d "`
|
|
220
|
-
);
|
|
221
|
-
tmux(`set-option -t "${session}" status-justify centre`);
|
|
222
|
-
tmux(
|
|
223
|
-
`set-option -t "${session}" window-status-current-format "#[fg=${accent},bold]●"`
|
|
224
|
-
);
|
|
225
|
-
tmux(
|
|
226
|
-
`set-option -t "${session}" window-status-format "#[fg=${border}]○"`
|
|
227
|
-
);
|
|
215
|
+
for (const command of buildThemeOptions(session, theme)) {
|
|
216
|
+
runSessionCommand(command);
|
|
217
|
+
}
|
|
228
218
|
|
|
229
219
|
// Focus the correct pane
|
|
230
|
-
|
|
220
|
+
selectPane(focusPane);
|
|
231
221
|
|
|
232
222
|
// Launch summary
|
|
233
223
|
const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
|
|
234
|
-
console.log(
|
|
224
|
+
console.log(
|
|
225
|
+
`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`,
|
|
226
|
+
);
|
|
235
227
|
|
|
236
228
|
// Attach
|
|
237
|
-
|
|
229
|
+
if (attach) {
|
|
230
|
+
attachSession(session);
|
|
231
|
+
}
|
|
238
232
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function buildPaneCommand(pane, _team) {
|
|
4
|
+
if (!pane.command) return null;
|
|
5
|
+
return pane.command;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir, team) {
|
|
9
|
+
let focusPane = paneMap[0][0];
|
|
10
|
+
const teammateCommands = [];
|
|
11
|
+
const paneActions = [];
|
|
12
|
+
|
|
13
|
+
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
|
14
|
+
const row = rows[rowIdx];
|
|
15
|
+
const panes = row.panes ?? [];
|
|
16
|
+
|
|
17
|
+
for (let paneIdx = 0; paneIdx < panes.length; paneIdx++) {
|
|
18
|
+
const pane = panes[paneIdx];
|
|
19
|
+
const tmuxPane = paneMap[rowIdx][paneIdx];
|
|
20
|
+
const action = {
|
|
21
|
+
targetPane: tmuxPane,
|
|
22
|
+
title: pane.title ?? null,
|
|
23
|
+
chdir: null,
|
|
24
|
+
exports: [],
|
|
25
|
+
command: null,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (pane.dir && firstPanesOfRows.has(tmuxPane)) {
|
|
29
|
+
action.chdir = resolve(dir, pane.dir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (pane.env && typeof pane.env === "object") {
|
|
33
|
+
action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const command = buildPaneCommand(pane, team);
|
|
37
|
+
if (command) {
|
|
38
|
+
action.command = command;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (pane.focus) {
|
|
42
|
+
focusPane = tmuxPane;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
paneActions.push(action);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { focusPane, leadPane: null, paneActions, teammateCommands };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildThemeOptions(session, theme = {}) {
|
|
53
|
+
const accent = theme.accent ?? "colour75";
|
|
54
|
+
const border = theme.border ?? "colour238";
|
|
55
|
+
const bg = theme.bg ?? "colour235";
|
|
56
|
+
const fg = theme.fg ?? "colour248";
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
["set-option", "-t", session, "pane-border-status", "top"],
|
|
60
|
+
["set-option", "-t", session, "pane-border-format", " #{?pane_active,#[bold]▸,·} #T "],
|
|
61
|
+
["set-option", "-t", session, "pane-border-style", `fg=${border}`],
|
|
62
|
+
["set-option", "-t", session, "pane-active-border-style", `fg=${accent}`],
|
|
63
|
+
["set-option", "-t", session, "status-style", `bg=${bg},fg=${fg}`],
|
|
64
|
+
[
|
|
65
|
+
"set-option",
|
|
66
|
+
"-t",
|
|
67
|
+
session,
|
|
68
|
+
"status-left",
|
|
69
|
+
`#[fg=colour0,bg=${accent},bold] ${session.toUpperCase()} IDE #[default] `,
|
|
70
|
+
],
|
|
71
|
+
["set-option", "-t", session, "status-left-length", "30"],
|
|
72
|
+
[
|
|
73
|
+
"set-option",
|
|
74
|
+
"-t",
|
|
75
|
+
session,
|
|
76
|
+
"status-right",
|
|
77
|
+
`#[fg=colour243]%H:%M #[fg=${accent}]│ #[fg=${fg}]%b %d `,
|
|
78
|
+
],
|
|
79
|
+
["set-option", "-t", session, "status-justify", "centre"],
|
|
80
|
+
["set-option", "-t", session, "window-status-current-format", `#[fg=${accent},bold]●`],
|
|
81
|
+
["set-option", "-t", session, "window-status-format", `#[fg=${border}]○`],
|
|
82
|
+
];
|
|
83
|
+
}
|
package/src/lib/output.js
CHANGED
|
@@ -8,6 +8,17 @@ export function output(data, { json } = {}) {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export class CommandError extends Error {
|
|
12
|
+
constructor(message, { code, exitCode = 1, details, json = false } = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "CommandError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
this.details = details;
|
|
18
|
+
this.json = json;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export function printLayout(config) {
|
|
12
23
|
const INNER = 40;
|
|
13
24
|
const rows = config.rows ?? [];
|
|
@@ -62,11 +73,17 @@ export function printLayout(config) {
|
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
export function outputError(message, code, { json } = {}) {
|
|
76
|
+
export function outputError(message, code, { json, exitCode = 1, details } = {}) {
|
|
77
|
+
throw new CommandError(message, { code, exitCode, details, json });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function printCommandError(error, { json = error?.json ?? false } = {}) {
|
|
66
81
|
if (json) {
|
|
67
|
-
|
|
82
|
+
const payload = { error: error.message, code: error.code };
|
|
83
|
+
if (error.details !== undefined) payload.details = error.details;
|
|
84
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
68
85
|
} else {
|
|
69
|
-
console.error(message);
|
|
86
|
+
console.error(error.message);
|
|
70
87
|
}
|
|
71
|
-
process.exit(
|
|
88
|
+
process.exit(error.exitCode ?? 1);
|
|
72
89
|
}
|