tmux-ide 1.0.0 → 1.2.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/src/init.js CHANGED
@@ -1,32 +1,24 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve, basename } from "node:path";
3
- import { writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve, basename, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
4
6
  import { detectStack, suggestConfig } from "./detect.js";
5
- import { printLayout } from "./lib/output.js";
7
+ import { outputError, printLayout } from "./lib/output.js";
6
8
 
7
9
  export async function init({ template, json } = {}) {
8
10
  const dir = process.cwd();
9
11
  const configPath = resolve(dir, "ide.yml");
10
12
 
11
13
  if (existsSync(configPath)) {
12
- if (json) {
13
- console.error(JSON.stringify({ error: "ide.yml already exists", code: "EXISTS" }));
14
- } else {
15
- console.error("ide.yml already exists in this directory");
16
- }
17
- process.exit(1);
14
+ outputError("ide.yml already exists in this directory", "EXISTS");
18
15
  }
19
16
 
20
17
  // If a specific template is requested, use it
21
18
  if (template) {
22
- const templatePath = resolve(import.meta.dirname, "..", "templates", `${template}.yml`);
19
+ const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
23
20
  if (!existsSync(templatePath)) {
24
- if (json) {
25
- console.error(JSON.stringify({ error: `Template "${template}" not found`, code: "NOT_FOUND" }));
26
- } else {
27
- console.error(`Template "${template}" not found. Available: default, nextjs, convex, vite, python, go, agent-team, agent-team-nextjs, agent-team-monorepo`);
28
- }
29
- process.exit(1);
21
+ outputError(`Template "${template}" not found`, "NOT_FOUND");
30
22
  }
31
23
 
32
24
  let content = readFileSync(templatePath, "utf-8");
@@ -64,7 +56,7 @@ export async function init({ template, json } = {}) {
64
56
  }
65
57
  } else {
66
58
  // Fallback to default template
67
- const templatePath = resolve(import.meta.dirname, "..", "templates", "default.yml");
59
+ const templatePath = resolve(__dirname, "..", "templates", "default.yml");
68
60
  let content = readFileSync(templatePath, "utf-8");
69
61
  content = content.replace(/^name: .+/m, `name: ${name}`);
70
62
  writeFileSync(configPath, content);
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");
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
@@ -1,238 +1,245 @@
1
- import { resolve } from "node:path";
1
+ import { resolve, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { execSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
3
5
  import { readConfig, getSessionName } from "./lib/yaml-io.js";
4
6
  import { computeSizes, toSplitPercents } from "./lib/sizes.js";
5
-
6
- function tmux(cmd) {
7
- execSync(`tmux ${cmd}`, { stdio: "inherit" });
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);
8
29
  }
9
30
 
10
- function tmuxQuiet(cmd) {
11
- try {
12
- execSync(`tmux ${cmd}`, { stdio: "ignore" });
13
- } catch {}
31
+ function configHash(config) {
32
+ return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
14
33
  }
15
34
 
16
- export async function launch(targetDir) {
17
- const dir = resolve(targetDir ?? ".");
18
-
19
- let config;
20
- try {
21
- ({ config } = readConfig(dir));
22
- } catch {
23
- console.error(`No ide.yml found in ${dir}. Run "tmux-ide init" to create one.`);
24
- process.exit(1);
25
- }
26
-
27
- // Validate config structure before building tmux session
28
- if (!Array.isArray(config.rows) || config.rows.length === 0) {
29
- console.error("Invalid ide.yml: 'rows' must be a non-empty array");
30
- console.error("Run: tmux-ide validate");
31
- process.exit(1);
32
- }
33
- for (const row of config.rows) {
34
- if (!Array.isArray(row.panes) || row.panes.length === 0) {
35
- console.error("Invalid ide.yml: each row must have a non-empty 'panes' array");
36
- console.error("Run: tmux-ide validate");
37
- process.exit(1);
38
- }
39
- }
40
-
41
- const session = config.name ?? getSessionName(dir);
42
- const rows = config.rows;
43
- const theme = config.theme ?? {};
44
- const team = config.team ?? null;
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()));
45
41
 
46
- // Run before hook
47
- if (config.before) {
48
- console.log(`Running: ${config.before}`);
42
+ for (let attempt = 0; attempt < attempts; attempt++) {
49
43
  try {
50
- execSync(config.before, { cwd: dir, stdio: "inherit" });
51
- } catch (e) {
52
- console.error(`"before" hook failed: ${config.before}`);
53
- process.exit(1);
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.
54
48
  }
55
- }
56
49
 
57
- // If session already exists, just attach to it
58
- try {
59
- execSync(`tmux has-session -t "${session}"`, { stdio: "ignore" });
60
- console.log(`Session "${session}" is already running. Attaching...`);
61
- tmux(`attach -t "${session}"`);
62
- return;
63
- } catch {
64
- // Session doesn't exist, continue with creation
50
+ if (attempt < attempts - 1) {
51
+ sleep(delayMs);
52
+ }
65
53
  }
66
54
 
67
- // Get terminal dimensions
68
- const cols = process.stdout.columns ?? 200;
69
- const lines = process.stdout.rows ?? 50;
70
-
71
- // Create session with first pane
72
- tmux(`new-session -d -s "${session}" -c "${dir}" -x ${cols} -y ${lines}`);
73
-
74
- // Set agent teams env var if team config is present
75
- if (team) {
76
- tmux(`set-environment -t "${session}" CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS 1`);
77
- }
55
+ return false;
56
+ }
78
57
 
79
- // Compute row sizes and convert to tmux split percentages
58
+ export function buildPaneMap(rows, dir, rootPaneId, splitPane) {
80
59
  const rowSizes = computeSizes(rows);
81
60
  const rowSplitPercents = toSplitPercents(rowSizes);
82
61
 
83
- // Phase 1: Create all rows (vertical splits) BEFORE any horizontal splits.
84
- // This ensures each row spans the full window width.
85
- const rowPaneIndices = [0]; // rowPaneIndices[i] = tmux pane index for row i
86
- let nextPaneIndex = 1;
87
-
62
+ // Create all rows vertically first so each row spans the full width.
63
+ const rowPaneIds = [rootPaneId];
88
64
  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++;
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);
95
73
  }
96
74
 
97
- // Phase 2: Create panes within each row (horizontal splits).
98
- // paneMap[row][col] = tmux pane index
99
75
  const paneMap = [];
100
- const firstPanesOfRows = new Set(rowPaneIndices);
76
+ const firstPanesOfRows = new Set(rowPaneIds);
101
77
 
102
78
  for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
103
79
  const row = rows[rowIdx];
104
80
  const panes = row.panes ?? [];
105
- const rowPane = rowPaneIndices[rowIdx];
106
- const rowPanes = [rowPane];
81
+ const rowPaneId = rowPaneIds[rowIdx];
82
+ const rowPanes = [rowPaneId];
107
83
 
108
- // Compute pane sizes within this row and convert to split percentages
109
84
  const paneSizes = computeSizes(panes);
110
85
  const paneSplitPercents = toSplitPercents(paneSizes);
111
86
 
112
- // Split this row's panes horizontally
113
87
  for (let paneIdx = 1; paneIdx < panes.length; paneIdx++) {
114
88
  const pane = panes[paneIdx];
115
89
  const targetPane = rowPanes[paneIdx - 1];
116
-
117
- // Per-pane working directory
118
90
  const paneDir = pane.dir ? resolve(dir, pane.dir) : dir;
119
-
120
- tmux(
121
- `split-window -t "${session}:0.${targetPane}" -h -c "${paneDir}" -p ${paneSplitPercents[paneIdx - 1]}`
122
- );
123
- rowPanes.push(nextPaneIndex);
124
- nextPaneIndex++;
91
+ const newPaneId = splitPane({
92
+ targetPane,
93
+ direction: "horizontal",
94
+ cwd: paneDir,
95
+ percent: paneSplitPercents[paneIdx - 1],
96
+ });
97
+ rowPanes.push(newPaneId);
125
98
  }
126
99
 
127
100
  paneMap.push(rowPanes);
128
101
  }
129
102
 
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;
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
+ );
143
117
  }
144
- return p.command;
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
+ );
145
128
  }
146
129
 
147
- // Send commands, set titles, handle env/focus/dir for first panes
148
- // Two-pass approach for teams: lead + non-teammates first, then teammates
149
- let focusPane = paneMap[0][0];
150
- const teammateCommands = [];
130
+ return config;
131
+ }
151
132
 
152
- for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
153
- const row = rows[rowIdx];
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
- }
189
- }
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");
190
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
+ }
191
171
 
192
- // Second pass: launch teammates after a brief delay for the lead to initialize
193
- if (teammateCommands.length > 0) {
194
- execSync("sleep 2");
195
- for (const { pane: p, cmd } of teammateCommands) {
196
- tmux(`send-keys -t "${session}:0.${p}" "${cmd}" C-m`);
172
+ if (attach) {
173
+ attachSession(session);
197
174
  }
175
+ return;
198
176
  }
199
177
 
200
- // Apply styling
201
- const accent = theme.accent ?? "colour75";
202
- const border = theme.border ?? "colour238";
203
- const bg = theme.bg ?? "colour235";
204
- const fg = theme.fg ?? "colour248";
178
+ // Get terminal dimensions
179
+ const cols = process.stdout.columns ?? 200;
180
+ const lines = process.stdout.rows ?? 50;
205
181
 
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}"`);
182
+ // Create session with first pane
183
+ const rootPaneId = createDetachedSession(session, dir, { cols, lines });
212
184
 
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]●"`
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),
224
195
  );
225
- tmux(
226
- `set-option -t "${session}" window-status-format "#[fg=${border}]○"`
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",
227
229
  );
230
+ startSessionMonitor(session, monitorScript);
228
231
 
229
232
  // Focus the correct pane
230
- tmux(`select-pane -t "${session}:0.${focusPane}"`);
233
+ selectPane(focusPane);
231
234
 
232
235
  // Launch summary
233
236
  const totalPanes = rows.reduce((sum, r) => sum + (r.panes?.length ?? 0), 0);
234
- console.log(`Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`);
237
+ console.log(
238
+ `Starting "${session}" (${rows.length} row${rows.length === 1 ? "" : "s"}, ${totalPanes} pane${totalPanes === 1 ? "" : "s"})...`,
239
+ );
235
240
 
236
241
  // Attach
237
- tmux(`attach -t "${session}"`);
242
+ if (attach) {
243
+ attachSession(session);
244
+ }
238
245
  }
@@ -0,0 +1,35 @@
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
+ }