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.
@@ -0,0 +1,49 @@
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 CHANGED
@@ -1,12 +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
- }
1
+ import { IdeError } from "./errors.js";
10
2
 
11
3
  export function printLayout(config) {
12
4
  const INNER = 40;
@@ -26,47 +18,51 @@ export function printLayout(config) {
26
18
 
27
19
  // Top border or mid divider
28
20
  if (r === 0) {
29
- let top = " ┌";
21
+ let top = " \u250c";
30
22
  for (let i = 0; i < count; i++) {
31
- top += "─".repeat(widths[i]);
32
- top += i < count - 1 ? "┬" : "┐";
23
+ top += "\u2500".repeat(widths[i]);
24
+ top += i < count - 1 ? "\u252c" : "\u2510";
33
25
  }
34
26
  console.log(top);
35
27
  } else {
36
- console.log(" ├" + "─".repeat(INNER + count - 1) + "┤");
28
+ console.log(" \u251c" + "\u2500".repeat(INNER + count - 1) + "\u2524");
37
29
  }
38
30
 
39
31
  // Content line
40
32
  const sizeLabel = rows[r].size ?? "";
41
- let line = " │";
33
+ let line = " \u2502";
42
34
  for (let i = 0; i < count; i++) {
43
35
  const title = panes[i]?.title ?? "";
44
36
  const w = widths[i];
45
37
  const pad = Math.max(0, w - title.length);
46
38
  const left = Math.floor(pad / 2);
47
39
  const right = pad - left;
48
- line += " ".repeat(left) + title + " ".repeat(right) + "│";
40
+ line += " ".repeat(left) + title + " ".repeat(right) + "\u2502";
49
41
  }
50
42
  if (sizeLabel) line += " " + sizeLabel;
51
43
  console.log(line);
52
44
 
53
45
  // Bottom border (last row only)
54
46
  if (r === rows.length - 1) {
55
- let bot = " └";
47
+ let bot = " \u2514";
56
48
  for (let i = 0; i < count; i++) {
57
- bot += "─".repeat(widths[i]);
58
- bot += i < count - 1 ? "┴" : "┘";
49
+ bot += "\u2500".repeat(widths[i]);
50
+ bot += i < count - 1 ? "\u2534" : "\u2518";
59
51
  }
60
52
  console.log(bot);
61
53
  }
62
54
  }
63
55
  }
64
56
 
65
- export function outputError(message, code, { json } = {}) {
57
+ export function outputError(message, code, { exitCode = 1 } = {}) {
58
+ throw new IdeError(message, { code, exitCode });
59
+ }
60
+
61
+ export function printCommandError(error, { json = false } = {}) {
66
62
  if (json) {
67
- console.error(JSON.stringify({ error: message, code }, null, 2));
63
+ console.error(JSON.stringify(error.toJSON(), null, 2));
68
64
  } else {
69
- console.error(message);
65
+ console.error(error.message);
70
66
  }
71
- process.exit(code ?? 1);
67
+ process.exit(error.exitCode ?? 1);
72
68
  }
@@ -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
+ }
@@ -0,0 +1,234 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { TmuxError } from "./errors.js";
3
+
4
+ const DEBUG = process.env.TMUX_IDE_DEBUG === "1";
5
+
6
+ const SESSION_NOT_FOUND_PATTERNS = ["can't find session", "can't find window", "unknown target"];
7
+
8
+ const TMUX_UNAVAILABLE_PATTERNS = [
9
+ "failed to connect to server",
10
+ "no server running",
11
+ "error connecting to",
12
+ "connection refused",
13
+ ];
14
+
15
+ export { TmuxError };
16
+
17
+ export function getSessionState(session) {
18
+ try {
19
+ runTmux(["has-session", "-t", session]);
20
+ return { running: true, reason: null };
21
+ } catch (error) {
22
+ if (error instanceof TmuxError) {
23
+ if (error.code === "SESSION_NOT_FOUND") {
24
+ return { running: false, reason: "SESSION_NOT_FOUND" };
25
+ }
26
+ if (error.code === "TMUX_UNAVAILABLE") {
27
+ return { running: false, reason: "TMUX_UNAVAILABLE" };
28
+ }
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ export function attachSession(session) {
35
+ runTmux(["attach", "-t", session], { stdio: "inherit" });
36
+ }
37
+
38
+ export function hasSession(session) {
39
+ try {
40
+ runTmux(["has-session", "-t", session]);
41
+ return true;
42
+ } catch (error) {
43
+ if (error instanceof TmuxError && error.code === "SESSION_NOT_FOUND") {
44
+ return false;
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ export function killSession(session) {
51
+ try {
52
+ runTmux(["kill-session", "-t", session]);
53
+ return { stopped: true, reason: null };
54
+ } catch (error) {
55
+ if (error instanceof TmuxError) {
56
+ if (error.code === "SESSION_NOT_FOUND") {
57
+ return { stopped: false, reason: "SESSION_NOT_FOUND" };
58
+ }
59
+ if (error.code === "TMUX_UNAVAILABLE") {
60
+ return { stopped: false, reason: "TMUX_UNAVAILABLE" };
61
+ }
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ export function listPanes(session) {
68
+ const raw = runTmux(
69
+ [
70
+ "list-panes",
71
+ "-t",
72
+ session,
73
+ "-F",
74
+ "#{pane_index}|#{pane_title}|#{pane_width}|#{pane_height}|#{pane_active}",
75
+ ],
76
+ { encoding: "utf-8" },
77
+ ).trim();
78
+
79
+ if (!raw) return [];
80
+
81
+ return raw.split("\n").map((line) => {
82
+ const [index, title, width, height, active] = line.split("|");
83
+ return {
84
+ index: Number.parseInt(index, 10),
85
+ title,
86
+ width: Number.parseInt(width, 10),
87
+ height: Number.parseInt(height, 10),
88
+ active: active === "1",
89
+ };
90
+ });
91
+ }
92
+
93
+ export function createDetachedSession(session, cwd, { cols, lines } = {}) {
94
+ return runTmux(
95
+ [
96
+ "new-session",
97
+ "-d",
98
+ "-P",
99
+ "-F",
100
+ "#{pane_id}",
101
+ "-s",
102
+ session,
103
+ "-c",
104
+ cwd,
105
+ "-x",
106
+ String(cols ?? 200),
107
+ "-y",
108
+ String(lines ?? 50),
109
+ ],
110
+ { encoding: "utf-8" },
111
+ ).trim();
112
+ }
113
+
114
+ export function setSessionEnvironment(session, key, value) {
115
+ runTmux(["set-environment", "-t", session, key, String(value)]);
116
+ }
117
+
118
+ export function splitPane(targetPane, direction, cwd, percent) {
119
+ return runTmux(
120
+ [
121
+ "split-window",
122
+ "-P",
123
+ "-F",
124
+ "#{pane_id}",
125
+ "-t",
126
+ targetPane,
127
+ direction === "vertical" ? "-v" : "-h",
128
+ "-c",
129
+ cwd,
130
+ "-p",
131
+ String(percent),
132
+ ],
133
+ { encoding: "utf-8" },
134
+ ).trim();
135
+ }
136
+
137
+ export function sendLiteral(targetPane, text) {
138
+ runTmux(["send-keys", "-t", targetPane, "-l", "--", text], { stdio: "inherit" });
139
+ runTmux(["send-keys", "-t", targetPane, "Enter"], { stdio: "inherit" });
140
+ }
141
+
142
+ export function getPaneCurrentCommand(targetPane) {
143
+ return runTmux(["display-message", "-p", "-t", targetPane, "#{pane_current_command}"], {
144
+ encoding: "utf-8",
145
+ }).trim();
146
+ }
147
+
148
+ export function selectPane(targetPane) {
149
+ runTmux(["select-pane", "-t", targetPane], { stdio: "inherit" });
150
+ }
151
+
152
+ export function setPaneTitle(targetPane, title) {
153
+ runTmux(["select-pane", "-t", targetPane, "-T", title], { stdio: "inherit" });
154
+ }
155
+
156
+ export function runSessionCommand(args) {
157
+ runTmux(args, { stdio: "inherit" });
158
+ }
159
+
160
+ export function startSessionMonitor(session, monitorScript) {
161
+ const child = spawn("node", [monitorScript, session], {
162
+ detached: true,
163
+ stdio: "ignore",
164
+ });
165
+ child.unref();
166
+ // Store PID as tmux session variable for later cleanup
167
+ runTmux(["set-option", "-t", session, "@monitor_pid", String(child.pid)]);
168
+ }
169
+
170
+ export function stopSessionMonitor(session) {
171
+ try {
172
+ const pid = runTmux(["show-option", "-gqvt", session, "@monitor_pid"], {
173
+ encoding: "utf-8",
174
+ }).trim();
175
+ if (pid) process.kill(parseInt(pid, 10));
176
+ } catch {
177
+ /* session or process already gone */
178
+ }
179
+ }
180
+
181
+ export function getSessionVariable(session, name) {
182
+ try {
183
+ const raw = runTmux(["show-option", "-gqvt", session, name], {
184
+ encoding: "utf-8",
185
+ });
186
+ return raw.trim() || null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
192
+ export function setSessionVariable(session, name, value) {
193
+ runTmux(["set-option", "-t", session, name, value]);
194
+ }
195
+
196
+ function runTmux(args, options = {}) {
197
+ if (DEBUG || globalThis.__tmuxIdeVerbose) {
198
+ console.error(` [tmux] ${args.join(" ")}`);
199
+ }
200
+
201
+ const execOptions = {
202
+ stdio: ["ignore", "pipe", "pipe"],
203
+ ...options,
204
+ };
205
+
206
+ try {
207
+ return execFileSync("tmux", args, execOptions);
208
+ } catch (error) {
209
+ throw classifyTmuxError(error);
210
+ }
211
+ }
212
+
213
+ function classifyTmuxError(error) {
214
+ const detail = getErrorDetail(error).toLowerCase();
215
+
216
+ if (SESSION_NOT_FOUND_PATTERNS.some((pattern) => detail.includes(pattern))) {
217
+ return new TmuxError("tmux session was not found", "SESSION_NOT_FOUND", { cause: error });
218
+ }
219
+
220
+ if (TMUX_UNAVAILABLE_PATTERNS.some((pattern) => detail.includes(pattern))) {
221
+ return new TmuxError("tmux is unavailable or its socket is inaccessible", "TMUX_UNAVAILABLE", {
222
+ cause: error,
223
+ });
224
+ }
225
+
226
+ return new TmuxError("tmux command failed", "TMUX_ERROR", { cause: error });
227
+ }
228
+
229
+ function getErrorDetail(error) {
230
+ const stderr = error?.stderr;
231
+ if (typeof stderr === "string" && stderr.length > 0) return stderr;
232
+ if (Buffer.isBuffer(stderr) && stderr.length > 0) return stderr.toString("utf-8");
233
+ return error?.message ?? "";
234
+ }
@@ -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/ls.js CHANGED
@@ -5,7 +5,7 @@ export async function ls({ json } = {}) {
5
5
  try {
6
6
  raw = execSync(
7
7
  'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
8
- { encoding: "utf-8" }
8
+ { encoding: "utf-8" },
9
9
  ).trim();
10
10
  } catch {
11
11
  if (json) {
@@ -35,8 +35,6 @@ export async function ls({ json } = {}) {
35
35
  console.log("─".repeat(54));
36
36
  for (const s of sessions) {
37
37
  const date = new Date(s.created).toLocaleString();
38
- console.log(
39
- s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no")
40
- );
38
+ console.log(s.name.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
41
39
  }
42
40
  }
package/src/restart.js CHANGED
@@ -1,23 +1,16 @@
1
1
  import { resolve } from "node:path";
2
- import { execSync } from "node:child_process";
3
2
  import { getSessionName } from "./lib/yaml-io.js";
4
3
  import { launch } from "./launch.js";
4
+ import { killSession } from "./lib/tmux.js";
5
5
 
6
- export async function restart(targetDir) {
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
+ const result = killSession(session);
9
10
 
10
- let wasRunning = false;
11
- try {
12
- execSync(`tmux kill-session -t "${session}"`, { stdio: "ignore" });
13
- wasRunning = true;
14
- } catch {
15
- // Session wasn't running — that's fine
16
- }
17
-
18
- if (wasRunning) {
11
+ if (result.stopped) {
19
12
  console.log(`Stopped session "${session}"`);
20
13
  }
21
14
 
22
- await launch(dir);
15
+ await launch(dir, { json, attach });
23
16
  }