tmux-ide 1.1.0 → 1.2.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 CHANGED
@@ -13,7 +13,8 @@ import { validate } from "../src/validate.js";
13
13
  import { detect } from "../src/detect.js";
14
14
  import { config } from "../src/config.js";
15
15
  import { restart } from "../src/restart.js";
16
- import { CommandError, printCommandError } from "../src/lib/output.js";
16
+ import { IdeError } from "../src/lib/errors.js";
17
+ import { printCommandError } from "../src/lib/output.js";
17
18
 
18
19
  const { positionals, values } = parseArgs({
19
20
  allowPositionals: true,
@@ -28,6 +29,7 @@ const { positionals, values } = parseArgs({
28
29
  write: { type: "boolean" },
29
30
  template: { type: "string" },
30
31
  name: { type: "string" },
32
+ verbose: { type: "boolean", default: false },
31
33
  help: { type: "boolean", short: "h" },
32
34
  version: { type: "boolean", short: "v" },
33
35
  },
@@ -57,6 +59,10 @@ if (values.version) {
57
59
  process.exit(0);
58
60
  }
59
61
 
62
+ if (values.verbose) {
63
+ globalThis.__tmuxIdeVerbose = true;
64
+ }
65
+
60
66
  const firstPositional = positionals[0];
61
67
  const hasKnownCommand = firstPositional ? knownCommands.has(firstPositional) : false;
62
68
  const command = hasKnownCommand ? firstPositional : "start";
@@ -102,6 +108,7 @@ ${bold("Flags:")}
102
108
  ${cyan("--json")} ${dim("Output as JSON (all commands)")}
103
109
  ${cyan("--template <name>")} ${dim("Use specific template for init")}
104
110
  ${cyan("--write")} ${dim("Write detected config to ide.yml")}
111
+ ${cyan("--verbose")} ${dim("Log all tmux commands (or set TMUX_IDE_DEBUG=1)")}
105
112
  ${cyan("-h, --help")} ${dim("Show usage")}
106
113
  ${cyan("-v, --version")} ${dim("Show version number")}`);
107
114
  }
@@ -195,14 +202,13 @@ try {
195
202
  break;
196
203
 
197
204
  default:
198
- throw new CommandError(`Unknown command: ${command}\nRun "tmux-ide help" for usage.`, {
205
+ throw new IdeError(`Unknown command: ${command}\nRun "tmux-ide help" for usage.`, {
199
206
  code: "USAGE",
200
207
  exitCode: 1,
201
- json,
202
208
  });
203
209
  }
204
210
  } catch (error) {
205
- if (error instanceof CommandError) {
211
+ if (error instanceof IdeError) {
206
212
  printCommandError(error, { json });
207
213
  } else {
208
214
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Turn any project into a tmux-powered terminal IDE with a simple ide.yml",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -164,6 +164,38 @@ tmux-ide config disable-team
164
164
 
165
165
  Removes the `team` config and all `role`/`task` fields from panes.
166
166
 
167
+ ## Session features (v1.2.0)
168
+
169
+ tmux-ide sessions include these built-in features:
170
+
171
+ ### Mouse support
172
+
173
+ Mouse is enabled by default. Users can click to focus panes, scroll with trackpad, and drag pane borders to resize.
174
+
175
+ ### Two-line status bar
176
+
177
+ ```
178
+ Line 0: MY-PROJECT IDE ● 14:30 │ Mar 17
179
+ Line 1: ⏺ Claude 1 │ ● Claude 2 │ ⏺ Dev Server │ Shell
180
+ ```
181
+
182
+ - Line 0: session name, window indicators, time/date
183
+ - Line 1: clickable pane tabs (click to switch panes)
184
+ - Green `⏺` next to panes with a running dev server (listening TCP port)
185
+ - Pulsing `⏺` next to panes where Claude/Codex is actively working
186
+ - Dim `●` next to panes where Claude/Codex is idle
187
+
188
+ ### Config drift detection
189
+
190
+ If `ide.yml` is edited while a session is running, `tmux-ide` warns the user and suggests `tmux-ide restart` to apply changes.
191
+
192
+ ### Debugging
193
+
194
+ ```bash
195
+ tmux-ide --verbose # Log all tmux commands to stderr
196
+ TMUX_IDE_DEBUG=1 tmux-ide # Same via env var
197
+ ```
198
+
167
199
  ## Programmatic CLI
168
200
 
169
201
  All commands support `--json` for structured output.
@@ -200,6 +232,7 @@ tmux-ide stop # Kill session
200
232
  tmux-ide restart # Stop and relaunch
201
233
  tmux-ide attach # Reattach
202
234
  tmux-ide init # Scaffold config
235
+ tmux-ide --verbose # Launch with tmux command tracing
203
236
  ```
204
237
 
205
238
  ## Modification workflow
@@ -207,6 +240,7 @@ tmux-ide init # Scaffold config
207
240
  1. Read: `tmux-ide config --json`
208
241
  2. Modify: `tmux-ide config set <path> <value>` or `add-pane`/`remove-pane`
209
242
  3. Validate: `tmux-ide validate --json`
243
+ 4. Apply: `tmux-ide restart` (needed if session is already running)
210
244
 
211
245
  ## Best practices
212
246
 
@@ -218,6 +252,7 @@ tmux-ide init # Scaffold config
218
252
  - Use `detect --json` first to understand the project stack
219
253
  - For agent teams: assign specific tasks to teammate panes so your prompts stay focused
220
254
  - The team lead should have `focus: true` for easy access
255
+ - Use `tmux-ide --verbose` or `TMUX_IDE_DEBUG=1` when debugging layout issues
221
256
 
222
257
  ## ide.yml format
223
258
 
package/src/attach.js CHANGED
@@ -3,15 +3,13 @@ import { getSessionName } from "./lib/yaml-io.js";
3
3
  import { outputError } from "./lib/output.js";
4
4
  import { attachSession, getSessionState } from "./lib/tmux.js";
5
5
 
6
- export async function attach(targetDir, { json } = {}) {
6
+ export async function attach(targetDir, { json: _json } = {}) {
7
7
  const dir = resolve(targetDir ?? ".");
8
- const session = getSessionName(dir);
8
+ const { name: session } = getSessionName(dir);
9
9
  const state = getSessionState(session);
10
10
 
11
11
  if (!state.running) {
12
- outputError(`Session "${session}" is not running. Start it with: tmux-ide`, "NOT_RUNNING", {
13
- json,
14
- });
12
+ outputError(`Session "${session}" is not running. Start it with: tmux-ide`, "NOT_RUNNING");
15
13
  return;
16
14
  }
17
15
 
package/src/config.js CHANGED
@@ -31,7 +31,7 @@ function dumpConfig(dir, { json }) {
31
31
  try {
32
32
  ({ config: cfg } = readConfig(dir));
33
33
  } catch (e) {
34
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
34
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
35
35
  return;
36
36
  }
37
37
 
@@ -46,7 +46,7 @@ function dumpConfig(dir, { json }) {
46
46
  function setConfig(dir, args, { json }) {
47
47
  const [dotpath, ...rest] = args;
48
48
  if (!dotpath || rest.length === 0) {
49
- outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE", { json });
49
+ outputError("Usage: tmux-ide config set <dotpath> <value>", "USAGE");
50
50
  return;
51
51
  }
52
52
 
@@ -54,12 +54,12 @@ function setConfig(dir, args, { json }) {
54
54
  try {
55
55
  ({ config: cfg } = readConfig(dir));
56
56
  } catch (e) {
57
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
57
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
58
58
  return;
59
59
  }
60
60
 
61
61
  if (!isConfigObject(cfg)) {
62
- outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
62
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
63
63
  return;
64
64
  }
65
65
 
@@ -85,7 +85,6 @@ function addPane(dir, args, { json }) {
85
85
  outputError(
86
86
  "Usage: tmux-ide config add-pane --row <N> --title <T> [--command <C>] [--size <S>]",
87
87
  "USAGE",
88
- { json },
89
88
  );
90
89
  return;
91
90
  }
@@ -94,30 +93,28 @@ function addPane(dir, args, { json }) {
94
93
  try {
95
94
  ({ config: cfg } = readConfig(dir));
96
95
  } catch (e) {
97
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
96
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
98
97
  return;
99
98
  }
100
99
 
101
100
  if (!Array.isArray(cfg?.rows)) {
102
- outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
101
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
103
102
  return;
104
103
  }
105
104
 
106
105
  const rowIdx = parseIndex(row);
107
106
  if (rowIdx == null) {
108
- outputError(`Invalid row index "${row}"`, "USAGE", { json });
107
+ outputError(`Invalid row index "${row}"`, "USAGE");
109
108
  return;
110
109
  }
111
110
 
112
111
  if (!cfg.rows[rowIdx]) {
113
- outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW", { json });
112
+ outputError(`Row ${rowIdx} does not exist`, "INVALID_ROW");
114
113
  return;
115
114
  }
116
115
 
117
116
  if (!Array.isArray(cfg.rows[rowIdx].panes)) {
118
- outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG", {
119
- json,
120
- });
117
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
121
118
  return;
122
119
  }
123
120
 
@@ -139,7 +136,7 @@ function addPane(dir, args, { json }) {
139
136
  function removePane(dir, args, { json }) {
140
137
  const { row, pane } = parseNamedArgs(args);
141
138
  if (row === undefined || pane === undefined) {
142
- outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE", { json });
139
+ outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
143
140
  return;
144
141
  }
145
142
 
@@ -147,31 +144,29 @@ function removePane(dir, args, { json }) {
147
144
  try {
148
145
  ({ config: cfg } = readConfig(dir));
149
146
  } catch (e) {
150
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
147
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
151
148
  return;
152
149
  }
153
150
 
154
151
  if (!Array.isArray(cfg?.rows)) {
155
- outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
152
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
156
153
  return;
157
154
  }
158
155
 
159
156
  const rowIdx = parseIndex(row);
160
157
  const paneIdx = parseIndex(pane);
161
158
  if (rowIdx == null || paneIdx == null) {
162
- outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE", { json });
159
+ outputError("Usage: tmux-ide config remove-pane --row <N> --pane <M>", "USAGE");
163
160
  return;
164
161
  }
165
162
 
166
163
  if (!Array.isArray(cfg.rows[rowIdx]?.panes)) {
167
- outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG", {
168
- json,
169
- });
164
+ outputError(`Invalid ide.yml: row ${rowIdx} panes must be an array`, "INVALID_CONFIG");
170
165
  return;
171
166
  }
172
167
 
173
168
  if (!cfg.rows[rowIdx].panes[paneIdx]) {
174
- outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE", { json });
169
+ outputError(`Pane ${paneIdx} in row ${rowIdx} does not exist`, "INVALID_PANE");
175
170
  return;
176
171
  }
177
172
 
@@ -192,17 +187,17 @@ function addRow(dir, args, { json }) {
192
187
  try {
193
188
  ({ config: cfg } = readConfig(dir));
194
189
  } catch (e) {
195
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
190
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
196
191
  return;
197
192
  }
198
193
 
199
194
  if (!isConfigObject(cfg)) {
200
- outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
195
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
201
196
  return;
202
197
  }
203
198
 
204
199
  if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
205
- outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
200
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
206
201
  return;
207
202
  }
208
203
 
@@ -228,17 +223,17 @@ function enableTeam(dir, args, { json }) {
228
223
  try {
229
224
  ({ config: cfg } = readConfig(dir));
230
225
  } catch (e) {
231
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
226
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
232
227
  return;
233
228
  }
234
229
 
235
230
  if (!isConfigObject(cfg)) {
236
- outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
231
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
237
232
  return;
238
233
  }
239
234
 
240
235
  if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
241
- outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
236
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
242
237
  return;
243
238
  }
244
239
 
@@ -261,7 +256,7 @@ function enableTeam(dir, args, { json }) {
261
256
  }
262
257
  if (!leadAssigned) {
263
258
  delete cfg.team;
264
- outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG", { json });
259
+ outputError("Cannot enable agent team: no Claude panes found", "INVALID_CONFIG");
265
260
  return;
266
261
  }
267
262
 
@@ -279,17 +274,17 @@ function disableTeam(dir, { json }) {
279
274
  try {
280
275
  ({ config: cfg } = readConfig(dir));
281
276
  } catch (e) {
282
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
277
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
283
278
  return;
284
279
  }
285
280
 
286
281
  if (!isConfigObject(cfg)) {
287
- outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG", { json });
282
+ outputError("Invalid ide.yml: config root must be an object", "INVALID_CONFIG");
288
283
  return;
289
284
  }
290
285
 
291
286
  if (cfg.rows !== undefined && !Array.isArray(cfg.rows)) {
292
- outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG", { json });
287
+ outputError("Invalid ide.yml: 'rows' must be an array", "INVALID_CONFIG");
293
288
  return;
294
289
  }
295
290
 
package/src/init.js CHANGED
@@ -11,20 +11,14 @@ export async function init({ template, json } = {}) {
11
11
  const configPath = resolve(dir, "ide.yml");
12
12
 
13
13
  if (existsSync(configPath)) {
14
- outputError("ide.yml already exists in this directory", "EXISTS", { json });
14
+ outputError("ide.yml already exists in this directory", "EXISTS");
15
15
  }
16
16
 
17
17
  // If a specific template is requested, use it
18
18
  if (template) {
19
19
  const templatePath = resolve(__dirname, "..", "templates", `${template}.yml`);
20
20
  if (!existsSync(templatePath)) {
21
- outputError(
22
- json
23
- ? `Template "${template}" not found`
24
- : `Template "${template}" not found. Available: default, nextjs, convex, vite, python, go, agent-team, agent-team-nextjs, agent-team-monorepo`,
25
- "NOT_FOUND",
26
- { json },
27
- );
21
+ outputError(`Template "${template}" not found`, "NOT_FOUND");
28
22
  }
29
23
 
30
24
  let content = readFileSync(templatePath, "utf-8");
package/src/inspect.js CHANGED
@@ -67,7 +67,7 @@ export async function inspect(targetDir, { json } = {}) {
67
67
  try {
68
68
  ({ config, configPath } = readConfig(dir));
69
69
  } catch (error) {
70
- outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
70
+ outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
71
71
  return;
72
72
  }
73
73
 
package/src/launch.js CHANGED
@@ -1,20 +1,26 @@
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
7
  import { outputError } from "./lib/output.js";
6
- import { buildThemeOptions, collectPaneStartupPlan } from "./lib/launch-plan.js";
8
+ import { collectPaneStartupPlan } from "./lib/launch-plan.js";
9
+ import { buildSessionOptions } from "./lib/session-options.js";
7
10
  import {
8
11
  attachSession,
9
12
  createDetachedSession,
10
13
  getPaneCurrentCommand,
14
+ getSessionVariable,
11
15
  hasSession,
12
16
  runSessionCommand,
13
17
  selectPane,
14
18
  sendLiteral,
15
19
  setPaneTitle,
16
20
  setSessionEnvironment,
21
+ setSessionVariable,
17
22
  splitPane,
23
+ startSessionMonitor,
18
24
  } from "./lib/tmux.js";
19
25
  import { validateConfig } from "./validate.js";
20
26
 
@@ -22,6 +28,10 @@ function sleepMs(ms) {
22
28
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23
29
  }
24
30
 
31
+ function configHash(config) {
32
+ return createHash("sha256").update(JSON.stringify(config)).digest("hex").slice(0, 12);
33
+ }
34
+
25
35
  export function waitForPaneCommand(
26
36
  targetPane,
27
37
  expectedCommands,
@@ -93,7 +103,7 @@ export function buildPaneMap(rows, dir, rootPaneId, splitPane) {
93
103
  return { paneMap, firstPanesOfRows };
94
104
  }
95
105
 
96
- function loadLaunchConfig(dir, { json } = {}) {
106
+ function loadLaunchConfig(dir) {
97
107
  let config;
98
108
 
99
109
  try {
@@ -103,11 +113,10 @@ function loadLaunchConfig(dir, { json } = {}) {
103
113
  outputError(
104
114
  `No ide.yml found in ${dir}. Run "tmux-ide init" or "tmux-ide detect --write" to create one.`,
105
115
  "CONFIG_NOT_FOUND",
106
- { json },
107
116
  );
108
117
  }
109
118
 
110
- outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR", { json });
119
+ outputError(`Cannot read ide.yml: ${error.message}`, "READ_ERROR");
111
120
  }
112
121
 
113
122
  const errors = validateConfig(config);
@@ -115,14 +124,13 @@ function loadLaunchConfig(dir, { json } = {}) {
115
124
  outputError(
116
125
  `Invalid ide.yml in ${dir}. Run "tmux-ide validate" for details.`,
117
126
  "INVALID_CONFIG",
118
- { json },
119
127
  );
120
128
  }
121
129
 
122
130
  return config;
123
131
  }
124
132
 
125
- function runBeforeHook(command, dir, { json } = {}) {
133
+ function runBeforeHook(command, dir) {
126
134
  if (!command) return;
127
135
 
128
136
  console.log(`Running: ${command}`);
@@ -130,27 +138,37 @@ function runBeforeHook(command, dir, { json } = {}) {
130
138
  try {
131
139
  execSync(command, { cwd: dir, stdio: "inherit" });
132
140
  } catch {
133
- const message = json
134
- ? `The "before" hook failed: ${command}`
135
- : `The before hook failed: ${command}`;
136
- outputError(message, "BEFORE_HOOK_FAILED", { json });
141
+ outputError(`The before hook failed: ${command}`, "BEFORE_HOOK_FAILED");
137
142
  }
138
143
  }
139
144
 
140
- export async function launch(targetDir, { json, attach = true } = {}) {
145
+ export async function launch(targetDir, { json = false, attach = true } = {}) {
141
146
  const dir = resolve(targetDir ?? ".");
142
- const config = loadLaunchConfig(dir, { json });
147
+ const config = loadLaunchConfig(dir);
143
148
 
144
- const session = config.name ?? getSessionName(dir);
149
+ const { name: fallbackName } = getSessionName(dir);
150
+ const session = config.name ?? fallbackName;
145
151
  const rows = config.rows;
146
152
  const theme = config.theme ?? {};
147
153
  const team = config.team ?? null;
148
154
 
149
- runBeforeHook(config.before, dir, { json });
155
+ runBeforeHook(config.before, dir);
150
156
 
151
- // If session already exists, just attach to it
157
+ // If session already exists, check for config drift and attach
152
158
  if (hasSession(session)) {
153
- console.log(`Session "${session}" is already running. Attaching...`);
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
+
154
172
  if (attach) {
155
173
  attachSession(session);
156
174
  }
@@ -176,13 +194,7 @@ export async function launch(targetDir, { json, attach = true } = {}) {
176
194
  ({ targetPane, direction, cwd, percent }) => splitPane(targetPane, direction, cwd, percent),
177
195
  );
178
196
 
179
- const { focusPane, leadPane, paneActions, teammateCommands } = collectPaneStartupPlan(
180
- rows,
181
- paneMap,
182
- firstPanesOfRows,
183
- dir,
184
- team,
185
- );
197
+ const { focusPane, paneActions } = collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir);
186
198
 
187
199
  for (const action of paneActions) {
188
200
  if (action.title) {
@@ -202,20 +214,21 @@ export async function launch(targetDir, { json, attach = true } = {}) {
202
214
  }
203
215
  }
204
216
 
205
- // Keep a second pass hook available for future staged startup behavior.
206
- if (teammateCommands.length > 0) {
207
- if (leadPane) {
208
- waitForPaneCommand(leadPane, ["claude"]);
209
- }
210
- for (const { pane: p, cmd } of teammateCommands) {
211
- sendLiteral(p, cmd);
212
- }
213
- }
214
-
215
- for (const command of buildThemeOptions(session, theme)) {
217
+ for (const command of buildSessionOptions(session, { theme })) {
216
218
  runSessionCommand(command);
217
219
  }
218
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
+
219
232
  // Focus the correct pane
220
233
  selectPane(focusPane);
221
234
 
@@ -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
+ }
@@ -1,13 +1,12 @@
1
1
  import { resolve } from "node:path";
2
2
 
3
- export function buildPaneCommand(pane, _team) {
3
+ export function buildPaneCommand(pane) {
4
4
  if (!pane.command) return null;
5
5
  return pane.command;
6
6
  }
7
7
 
8
- export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir, team) {
8
+ export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir) {
9
9
  let focusPane = paneMap[0][0];
10
- const teammateCommands = [];
11
10
  const paneActions = [];
12
11
 
13
12
  for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
@@ -33,7 +32,7 @@ export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir, tea
33
32
  action.exports = Object.entries(pane.env).map(([key, value]) => `export ${key}=${value}`);
34
33
  }
35
34
 
36
- const command = buildPaneCommand(pane, team);
35
+ const command = buildPaneCommand(pane);
37
36
  if (command) {
38
37
  action.command = command;
39
38
  }
@@ -46,38 +45,5 @@ export function collectPaneStartupPlan(rows, paneMap, firstPanesOfRows, dir, tea
46
45
  }
47
46
  }
48
47
 
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
- ];
48
+ return { focusPane, paneActions };
83
49
  }
package/src/lib/output.js CHANGED
@@ -1,23 +1,4 @@
1
- export function output(data, { json } = {}) {
2
- if (json) {
3
- console.log(JSON.stringify(data, null, 2));
4
- } else if (typeof data === "string") {
5
- console.log(data);
6
- } else {
7
- console.log(data);
8
- }
9
- }
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
- }
1
+ import { IdeError } from "./errors.js";
21
2
 
22
3
  export function printLayout(config) {
23
4
  const INNER = 40;
@@ -37,51 +18,49 @@ export function printLayout(config) {
37
18
 
38
19
  // Top border or mid divider
39
20
  if (r === 0) {
40
- let top = " ┌";
21
+ let top = " \u250c";
41
22
  for (let i = 0; i < count; i++) {
42
- top += "─".repeat(widths[i]);
43
- top += i < count - 1 ? "┬" : "┐";
23
+ top += "\u2500".repeat(widths[i]);
24
+ top += i < count - 1 ? "\u252c" : "\u2510";
44
25
  }
45
26
  console.log(top);
46
27
  } else {
47
- console.log(" ├" + "─".repeat(INNER + count - 1) + "┤");
28
+ console.log(" \u251c" + "\u2500".repeat(INNER + count - 1) + "\u2524");
48
29
  }
49
30
 
50
31
  // Content line
51
32
  const sizeLabel = rows[r].size ?? "";
52
- let line = " │";
33
+ let line = " \u2502";
53
34
  for (let i = 0; i < count; i++) {
54
35
  const title = panes[i]?.title ?? "";
55
36
  const w = widths[i];
56
37
  const pad = Math.max(0, w - title.length);
57
38
  const left = Math.floor(pad / 2);
58
39
  const right = pad - left;
59
- line += " ".repeat(left) + title + " ".repeat(right) + "│";
40
+ line += " ".repeat(left) + title + " ".repeat(right) + "\u2502";
60
41
  }
61
42
  if (sizeLabel) line += " " + sizeLabel;
62
43
  console.log(line);
63
44
 
64
45
  // Bottom border (last row only)
65
46
  if (r === rows.length - 1) {
66
- let bot = " └";
47
+ let bot = " \u2514";
67
48
  for (let i = 0; i < count; i++) {
68
- bot += "─".repeat(widths[i]);
69
- bot += i < count - 1 ? "┴" : "┘";
49
+ bot += "\u2500".repeat(widths[i]);
50
+ bot += i < count - 1 ? "\u2534" : "\u2518";
70
51
  }
71
52
  console.log(bot);
72
53
  }
73
54
  }
74
55
  }
75
56
 
76
- export function outputError(message, code, { json, exitCode = 1, details } = {}) {
77
- throw new CommandError(message, { code, exitCode, details, json });
57
+ export function outputError(message, code, { exitCode = 1 } = {}) {
58
+ throw new IdeError(message, { code, exitCode });
78
59
  }
79
60
 
80
- export function printCommandError(error, { json = error?.json ?? false } = {}) {
61
+ export function printCommandError(error, { json = false } = {}) {
81
62
  if (json) {
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));
63
+ console.error(JSON.stringify(error.toJSON(), null, 2));
85
64
  } else {
86
65
  console.error(error.message);
87
66
  }
@@ -0,0 +1,179 @@
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
+ }
@@ -0,0 +1,95 @@
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
+ }
package/src/lib/tmux.js CHANGED
@@ -1,4 +1,7 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { TmuxError } from "./errors.js";
3
+
4
+ const DEBUG = process.env.TMUX_IDE_DEBUG === "1";
2
5
 
3
6
  const SESSION_NOT_FOUND_PATTERNS = ["can't find session", "can't find window", "unknown target"];
4
7
 
@@ -9,14 +12,7 @@ const TMUX_UNAVAILABLE_PATTERNS = [
9
12
  "connection refused",
10
13
  ];
11
14
 
12
- export class TmuxError extends Error {
13
- constructor(message, code, cause) {
14
- super(message);
15
- this.name = "TmuxError";
16
- this.code = code;
17
- this.cause = cause;
18
- }
19
- }
15
+ export { TmuxError };
20
16
 
21
17
  export function getSessionState(session) {
22
18
  try {
@@ -44,7 +40,10 @@ export function hasSession(session) {
44
40
  runTmux(["has-session", "-t", session]);
45
41
  return true;
46
42
  } catch (error) {
47
- if (error instanceof TmuxError && error.code === "SESSION_NOT_FOUND") {
43
+ if (
44
+ error instanceof TmuxError &&
45
+ (error.code === "SESSION_NOT_FOUND" || error.code === "TMUX_UNAVAILABLE")
46
+ ) {
48
47
  return false;
49
48
  }
50
49
  throw error;
@@ -161,7 +160,47 @@ export function runSessionCommand(args) {
161
160
  runTmux(args, { stdio: "inherit" });
162
161
  }
163
162
 
163
+ export function startSessionMonitor(session, monitorScript) {
164
+ const child = spawn("node", [monitorScript, session], {
165
+ detached: true,
166
+ stdio: "ignore",
167
+ });
168
+ child.unref();
169
+ // Store PID as tmux session variable for later cleanup
170
+ runTmux(["set-option", "-t", session, "@monitor_pid", String(child.pid)]);
171
+ }
172
+
173
+ export function stopSessionMonitor(session) {
174
+ try {
175
+ const pid = runTmux(["show-option", "-gqvt", session, "@monitor_pid"], {
176
+ encoding: "utf-8",
177
+ }).trim();
178
+ if (pid) process.kill(parseInt(pid, 10));
179
+ } catch {
180
+ /* session or process already gone */
181
+ }
182
+ }
183
+
184
+ export function getSessionVariable(session, name) {
185
+ try {
186
+ const raw = runTmux(["show-option", "-gqvt", session, name], {
187
+ encoding: "utf-8",
188
+ });
189
+ return raw.trim() || null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ export function setSessionVariable(session, name, value) {
196
+ runTmux(["set-option", "-t", session, name, value]);
197
+ }
198
+
164
199
  function runTmux(args, options = {}) {
200
+ if (DEBUG || globalThis.__tmuxIdeVerbose) {
201
+ console.error(` [tmux] ${args.join(" ")}`);
202
+ }
203
+
165
204
  const execOptions = {
166
205
  stdio: ["ignore", "pipe", "pipe"],
167
206
  ...options,
@@ -178,18 +217,16 @@ function classifyTmuxError(error) {
178
217
  const detail = getErrorDetail(error).toLowerCase();
179
218
 
180
219
  if (SESSION_NOT_FOUND_PATTERNS.some((pattern) => detail.includes(pattern))) {
181
- return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", error);
220
+ return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", { cause: error });
182
221
  }
183
222
 
184
223
  if (TMUX_UNAVAILABLE_PATTERNS.some((pattern) => detail.includes(pattern))) {
185
- return new TmuxError(
186
- "tmux is unavailable or its socket is inaccessible",
187
- "TMUX_UNAVAILABLE",
188
- error,
189
- );
224
+ return new TmuxError("tmux is unavailable or its socket is inaccessible", "TMUX_UNAVAILABLE", {
225
+ cause: error,
226
+ });
190
227
  }
191
228
 
192
- return new TmuxError("tmux command failed", "TMUX_ERROR", error);
229
+ return new TmuxError("tmux command failed", "TMUX_ERROR", { cause: error });
193
230
  }
194
231
 
195
232
  function getErrorDetail(error) {
@@ -19,8 +19,8 @@ export function writeConfig(dir, config) {
19
19
  export function getSessionName(dir) {
20
20
  try {
21
21
  const { config } = readConfig(dir);
22
- return config.name ?? basename(dir);
22
+ return { name: config.name ?? basename(dir), source: config.name ? "config" : "fallback" };
23
23
  } catch {
24
- return basename(dir);
24
+ return { name: basename(dir), source: "fallback" };
25
25
  }
26
26
  }
package/src/restart.js CHANGED
@@ -5,7 +5,7 @@ import { killSession } from "./lib/tmux.js";
5
5
 
6
6
  export async function restart(targetDir, { json, attach } = {}) {
7
7
  const dir = resolve(targetDir ?? ".");
8
- const session = getSessionName(dir);
8
+ const { name: session } = getSessionName(dir);
9
9
  const result = killSession(session);
10
10
 
11
11
  if (result.stopped) {
package/src/status.js CHANGED
@@ -5,7 +5,7 @@ import { getSessionState, listPanes } from "./lib/tmux.js";
5
5
 
6
6
  export async function status(targetDir, { json } = {}) {
7
7
  const dir = resolve(targetDir ?? ".");
8
- const session = getSessionName(dir);
8
+ const { name: session } = getSessionName(dir);
9
9
  const configExists = existsSync(resolve(dir, "ide.yml"));
10
10
 
11
11
  const state = getSessionState(session);
package/src/stop.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import { resolve } from "node:path";
2
2
  import { getSessionName } from "./lib/yaml-io.js";
3
3
  import { outputError } from "./lib/output.js";
4
- import { killSession } from "./lib/tmux.js";
4
+ import { killSession, stopSessionMonitor } from "./lib/tmux.js";
5
5
 
6
6
  export async function stop(targetDir, { json } = {}) {
7
7
  const dir = resolve(targetDir ?? ".");
8
- const session = getSessionName(dir);
8
+ const { name: session } = getSessionName(dir);
9
+
10
+ // Stop the session monitor before killing the session
11
+ stopSessionMonitor(session);
12
+
9
13
  const result = killSession(session);
10
14
 
11
15
  if (result.stopped) {
@@ -17,8 +21,5 @@ export async function stop(targetDir, { json } = {}) {
17
21
  return;
18
22
  }
19
23
 
20
- outputError(`No active session "${session}" found`, "NOT_RUNNING", {
21
- json,
22
- exitCode: 1,
23
- });
24
+ outputError(`No active session "${session}" found`, "NOT_RUNNING");
24
25
  }
package/src/validate.js CHANGED
@@ -130,7 +130,7 @@ export async function validate(targetDir, { json } = {}) {
130
130
  try {
131
131
  ({ config } = readConfig(dir));
132
132
  } catch (e) {
133
- outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR", { json });
133
+ outputError(`Cannot read ide.yml: ${e.message}`, "READ_ERROR");
134
134
  return;
135
135
  }
136
136