@libtmux/workspace-cli 0.1.0-alpha.11
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/LICENSE +21 -0
- package/README.md +330 -0
- package/THIRD-PARTY-NOTICES.md +183 -0
- package/dist/app-w7cbtf0n.js +747 -0
- package/dist/completions-2a9fqwq3.js +282 -0
- package/dist/extensions-8v6mfszj.js +268 -0
- package/dist/index-0j386v40.js +7112 -0
- package/dist/main-1de529jm.js +37 -0
- package/dist/main-bfnr2nkj.js +3106 -0
- package/dist/main-c8atq2pj.js +470 -0
- package/dist/main-gh3pafqb.js +369 -0
- package/dist/main.js +35 -0
- package/dist/progress-n08zapg2.js +1649 -0
- package/dist/reference-ebfxh6yp.js +90 -0
- package/dist/tmux-wa2zn8z1.js +893 -0
- package/docs/command-reference.md +227 -0
- package/docs/commands.json +1125 -0
- package/docs/completions/_tmux-workspace +239 -0
- package/docs/completions/tmux-workspace.bash +257 -0
- package/docs/completions/tmux-workspace.fish +430 -0
- package/package.json +49 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalize,
|
|
3
|
+
openTerminal,
|
|
4
|
+
processRun
|
|
5
|
+
} from "./main-c8atq2pj.js";
|
|
6
|
+
import {
|
|
7
|
+
CliError,
|
|
8
|
+
OperationOutput,
|
|
9
|
+
colorEnabled,
|
|
10
|
+
emitJson,
|
|
11
|
+
expandPath,
|
|
12
|
+
mapping,
|
|
13
|
+
privatePath,
|
|
14
|
+
readDocument,
|
|
15
|
+
resolveWorkspace,
|
|
16
|
+
saveDocument,
|
|
17
|
+
scalarText,
|
|
18
|
+
styled,
|
|
19
|
+
write
|
|
20
|
+
} from "./main-gh3pafqb.js";
|
|
21
|
+
import {
|
|
22
|
+
__require
|
|
23
|
+
} from "./main-1de529jm.js";
|
|
24
|
+
|
|
25
|
+
// src/tmux.ts
|
|
26
|
+
import {
|
|
27
|
+
isTmuxName,
|
|
28
|
+
LibTmuxError,
|
|
29
|
+
Server
|
|
30
|
+
} from "libtmux";
|
|
31
|
+
import { stat } from "node:fs/promises";
|
|
32
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
33
|
+
import { basename, extname, resolve } from "node:path";
|
|
34
|
+
function option(value) {
|
|
35
|
+
return typeof value === "boolean" ? value ? "on" : "off" : scalarText(value);
|
|
36
|
+
}
|
|
37
|
+
function isColdEndpoint(error) {
|
|
38
|
+
if (!(error instanceof LibTmuxError))
|
|
39
|
+
return false;
|
|
40
|
+
const reason = error.message.replace(/^cannot reach tmux: /u, "");
|
|
41
|
+
return reason.startsWith("no server running on ") || reason.startsWith("error connecting to ") && reason.endsWith(" (No such file or directory)");
|
|
42
|
+
}
|
|
43
|
+
function isTmuxUnavailable(error) {
|
|
44
|
+
return error instanceof LibTmuxError && error.message.startsWith("cannot reach tmux");
|
|
45
|
+
}
|
|
46
|
+
function unreachableMessage(server, error) {
|
|
47
|
+
const endpoint = server.socketPath ?? server.socketName ?? "the default tmux socket";
|
|
48
|
+
return isColdEndpoint(error) ? `No tmux server is running on ${endpoint}` : "The tmux executable could not be run for this command";
|
|
49
|
+
}
|
|
50
|
+
function currentEndpoint(context) {
|
|
51
|
+
if (!context.env.TMUX)
|
|
52
|
+
return;
|
|
53
|
+
const match = /^(.*),([0-9]+),[0-9]+$/s.exec(context.env.TMUX);
|
|
54
|
+
if (!match?.[1])
|
|
55
|
+
throw new CliError("usage", "TMUX must name the current server as socket,pid,session", 2);
|
|
56
|
+
return { socketPath: match[1], pid: match[2] };
|
|
57
|
+
}
|
|
58
|
+
function connection(values, context) {
|
|
59
|
+
let path = values.socket_path ? expandPath(scalarText(values.socket_path), context) : undefined;
|
|
60
|
+
if (!path && !values.socket_name)
|
|
61
|
+
path = currentEndpoint(context)?.socketPath;
|
|
62
|
+
const options = {
|
|
63
|
+
environment: { ...context.env, TMUX: undefined, TMUX_PANE: undefined },
|
|
64
|
+
...path ? { socketPath: path } : {},
|
|
65
|
+
...values.socket_name && !values.socket_path ? { socketName: scalarText(values.socket_name) } : {},
|
|
66
|
+
...values.tmux_config_file ? { configFile: scalarText(values.tmux_config_file) } : {},
|
|
67
|
+
...values.colors ? { colors: values.colors } : {},
|
|
68
|
+
...context.env.TMUX_BIN ? { tmuxBin: context.env.TMUX_BIN } : {}
|
|
69
|
+
};
|
|
70
|
+
return new Server(options);
|
|
71
|
+
}
|
|
72
|
+
function terminalArguments(server) {
|
|
73
|
+
return [
|
|
74
|
+
server.tmuxBin,
|
|
75
|
+
...server.socketPath ? ["-S", server.socketPath] : server.socketName ? ["-L", server.socketName] : []
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function targetMismatch() {
|
|
79
|
+
return new CliError("usage", "This operation must target the current pane's tmux server", 2);
|
|
80
|
+
}
|
|
81
|
+
async function verifyCurrentServer(server, context, endpoint) {
|
|
82
|
+
const acquisition = context.signal ? { signal: context.signal } : {};
|
|
83
|
+
const [currentResult, selectedResult] = await Promise.allSettled([
|
|
84
|
+
connection({}, context).daemonIdentity(acquisition),
|
|
85
|
+
server.snapshot(acquisition)
|
|
86
|
+
]);
|
|
87
|
+
if (currentResult.status === "rejected") {
|
|
88
|
+
if (isTmuxUnavailable(currentResult.reason))
|
|
89
|
+
throw targetMismatch();
|
|
90
|
+
throw currentResult.reason;
|
|
91
|
+
}
|
|
92
|
+
if (selectedResult.status === "rejected") {
|
|
93
|
+
if (isTmuxUnavailable(selectedResult.reason))
|
|
94
|
+
throw targetMismatch();
|
|
95
|
+
throw selectedResult.reason;
|
|
96
|
+
}
|
|
97
|
+
const current = currentResult.value;
|
|
98
|
+
const selected = selectedResult.value;
|
|
99
|
+
if (current.pid !== endpoint.pid || current.pid !== selected.daemonIdentity.pid || current.startTime !== selected.daemonIdentity.startTime)
|
|
100
|
+
throw targetMismatch();
|
|
101
|
+
return selected;
|
|
102
|
+
}
|
|
103
|
+
async function currentSession(server, context) {
|
|
104
|
+
const endpoint = currentEndpoint(context);
|
|
105
|
+
if (!endpoint || !context.env.TMUX_PANE)
|
|
106
|
+
throw new CliError("usage", "This operation requires TMUX and TMUX_PANE from a current pane", 2);
|
|
107
|
+
const selected = await verifyCurrentServer(server, context, endpoint);
|
|
108
|
+
let pane;
|
|
109
|
+
try {
|
|
110
|
+
pane = selected.panes.oneOrUndefined({ id: context.env.TMUX_PANE });
|
|
111
|
+
} catch {
|
|
112
|
+
pane = undefined;
|
|
113
|
+
}
|
|
114
|
+
if (!pane?.session || !pane.format.pane_tty)
|
|
115
|
+
throw new CliError("usage", `TMUX_PANE does not name a pane with a terminal on this server: ${context.env.TMUX_PANE}`, 2);
|
|
116
|
+
return { session: pane.session, daemon: selected.daemonIdentity };
|
|
117
|
+
}
|
|
118
|
+
async function attachmentClient(server, context) {
|
|
119
|
+
const endpoint = currentEndpoint(context);
|
|
120
|
+
if (!endpoint) {
|
|
121
|
+
try {
|
|
122
|
+
const terminal = await openTerminal(context);
|
|
123
|
+
await terminal.close();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (context.signal?.aborted)
|
|
126
|
+
throw error;
|
|
127
|
+
throw new CliError("usage", "Attached load needs a controlling terminal; pass -d", 2);
|
|
128
|
+
}
|
|
129
|
+
return { mode: "attach" };
|
|
130
|
+
}
|
|
131
|
+
if (!context.env.TMUX_PANE) {
|
|
132
|
+
await verifyCurrentServer(server, context, endpoint);
|
|
133
|
+
return { mode: "switch" };
|
|
134
|
+
}
|
|
135
|
+
const { session } = await currentSession(server, context);
|
|
136
|
+
const query = await processRun([...terminalArguments(server), "display-message", "-p", "#{client_name}"], {
|
|
137
|
+
cwd: context.cwd,
|
|
138
|
+
env: context.env,
|
|
139
|
+
terminal: "input",
|
|
140
|
+
...context.signal ? { signal: context.signal } : {}
|
|
141
|
+
});
|
|
142
|
+
const name = query.stdout.trim();
|
|
143
|
+
const clients = (await server.snapshot(context.signal ? { signal: context.signal } : {})).clients;
|
|
144
|
+
if (query.code !== 0 || !clients.toArray().some((client) => client.name === name && client.session?.id === session.id))
|
|
145
|
+
throw new CliError("usage", "The current pane has no attached client; pass -d", 2);
|
|
146
|
+
return { mode: "switch", client: name };
|
|
147
|
+
}
|
|
148
|
+
async function options(target, value, signal) {
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
return;
|
|
151
|
+
for (const [name, data] of Object.entries(mapping(value, "options")))
|
|
152
|
+
await target.setOption(name, option(data), signal ? { signal } : undefined);
|
|
153
|
+
}
|
|
154
|
+
function envInteger(context, name) {
|
|
155
|
+
const raw = context.env[name];
|
|
156
|
+
if (raw === undefined || raw.trim() === "")
|
|
157
|
+
return;
|
|
158
|
+
const value = Number(raw);
|
|
159
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 65535)
|
|
160
|
+
throw new CliError("usage", `${name} must be an integer from 1 to 65535`, 2);
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function sessionDimensions(context) {
|
|
164
|
+
let width = envInteger(context, "TMUXP_DEFAULT_COLUMNS") ?? envInteger(context, "COLUMNS") ?? 80;
|
|
165
|
+
let height = envInteger(context, "TMUXP_DEFAULT_ROWS") ?? envInteger(context, "ROWS") ?? 24;
|
|
166
|
+
const detect = context.env.TMUXP_DETECT_TERMINAL_SIZE;
|
|
167
|
+
if (detect !== undefined && detect !== "1")
|
|
168
|
+
return {};
|
|
169
|
+
const stream = context.stdout;
|
|
170
|
+
if (stream.isTTY && stream.columns && stream.rows) {
|
|
171
|
+
width = stream.columns;
|
|
172
|
+
height = stream.rows;
|
|
173
|
+
}
|
|
174
|
+
width = envInteger(context, "COLUMNS") ?? width;
|
|
175
|
+
height = envInteger(context, "LINES") ?? height;
|
|
176
|
+
return { width, height };
|
|
177
|
+
}
|
|
178
|
+
async function send(pane, spec, context) {
|
|
179
|
+
let enter = spec.data.enter ?? true;
|
|
180
|
+
let before = spec.data.sleep_before ?? null;
|
|
181
|
+
let after = spec.data.sleep_after ?? null;
|
|
182
|
+
for (const command of spec.commands) {
|
|
183
|
+
context.signal?.throwIfAborted();
|
|
184
|
+
if (command.enter !== undefined)
|
|
185
|
+
enter = command.enter;
|
|
186
|
+
if (command.sleep_before !== undefined)
|
|
187
|
+
before = command.sleep_before;
|
|
188
|
+
if (command.sleep_after !== undefined)
|
|
189
|
+
after = command.sleep_after;
|
|
190
|
+
if (before)
|
|
191
|
+
await sleep(Number(before) * 1000, undefined, { signal: context.signal });
|
|
192
|
+
await pane.sendKeys((spec.suppress ? " " : "") + command.cmd, {
|
|
193
|
+
enter: Boolean(enter),
|
|
194
|
+
literal: true,
|
|
195
|
+
...context.signal ? { signal: context.signal } : {}
|
|
196
|
+
});
|
|
197
|
+
if (after)
|
|
198
|
+
await sleep(Number(after) * 1000, undefined, { signal: context.signal });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function ready(pane, context) {
|
|
202
|
+
const deadline = performance.now() + 2000;
|
|
203
|
+
while (performance.now() < deadline) {
|
|
204
|
+
const [x = "0", y = "0"] = (await pane.displayMessage("#{cursor_x};#{cursor_y}"))[0]?.split(";") ?? [];
|
|
205
|
+
if (Number(x) !== 0 || Number(y) !== 0)
|
|
206
|
+
return true;
|
|
207
|
+
await sleep(50, undefined, { signal: context.signal });
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function freeIndex(occupied, first) {
|
|
212
|
+
let index = first;
|
|
213
|
+
while (occupied.has(index)) {
|
|
214
|
+
if (index === 2147483647)
|
|
215
|
+
throw new CliError("invalid_workspace", `No free window index at or above ${first}`);
|
|
216
|
+
index++;
|
|
217
|
+
}
|
|
218
|
+
return index;
|
|
219
|
+
}
|
|
220
|
+
async function removePlaceholder(session, placeholder, result) {
|
|
221
|
+
const renumber = (await session.showResolvedOptions()).get("renumber-windows") === "on";
|
|
222
|
+
const previous = renumber ? (await session.showOptions()).get("renumber-windows") : undefined;
|
|
223
|
+
let failure;
|
|
224
|
+
try {
|
|
225
|
+
if (renumber)
|
|
226
|
+
await session.setOption("renumber-windows", "off");
|
|
227
|
+
await placeholder.kill();
|
|
228
|
+
const dropped = result.created_windows.indexOf(placeholder.id);
|
|
229
|
+
if (dropped >= 0)
|
|
230
|
+
result.created_window_names?.splice(dropped, 1);
|
|
231
|
+
result.created_windows = result.created_windows.filter((id) => id !== placeholder.id);
|
|
232
|
+
const removed = new Set(placeholder.panes.toArray().map((pane) => pane.id));
|
|
233
|
+
result.created_panes = result.created_panes.filter((id) => !removed.has(id));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
failure = error;
|
|
236
|
+
} finally {
|
|
237
|
+
if (renumber) {
|
|
238
|
+
try {
|
|
239
|
+
if (previous === undefined)
|
|
240
|
+
await session.unsetOption("renumber-windows");
|
|
241
|
+
else
|
|
242
|
+
await session.setOption("renumber-windows", previous);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
result.renumber_restore_error = error instanceof Error ? error.message : String(error);
|
|
245
|
+
failure ??= error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (failure)
|
|
250
|
+
throw failure;
|
|
251
|
+
}
|
|
252
|
+
async function unusableDirectories(spec) {
|
|
253
|
+
const declared = new Set;
|
|
254
|
+
if (spec.directory !== undefined)
|
|
255
|
+
declared.add(spec.directory);
|
|
256
|
+
for (const window of spec.windows)
|
|
257
|
+
for (const pane of window.panes)
|
|
258
|
+
if (pane.directory !== undefined)
|
|
259
|
+
declared.add(pane.directory);
|
|
260
|
+
const unusable = [];
|
|
261
|
+
for (const path of declared) {
|
|
262
|
+
const details = await stat(path).catch(() => {
|
|
263
|
+
return;
|
|
264
|
+
});
|
|
265
|
+
if (!details?.isDirectory())
|
|
266
|
+
unusable.push(path);
|
|
267
|
+
}
|
|
268
|
+
return unusable;
|
|
269
|
+
}
|
|
270
|
+
function missingWindows(spec, session) {
|
|
271
|
+
const available = new Map;
|
|
272
|
+
let spare = 0;
|
|
273
|
+
for (const window of session.windows.toArray()) {
|
|
274
|
+
if (window.name === null)
|
|
275
|
+
spare++;
|
|
276
|
+
else
|
|
277
|
+
available.set(window.name, (available.get(window.name) ?? 0) + 1);
|
|
278
|
+
}
|
|
279
|
+
const missing = [];
|
|
280
|
+
const unnamed = [];
|
|
281
|
+
for (const [ordinal, window] of spec.windows.entries()) {
|
|
282
|
+
if (window.name === undefined) {
|
|
283
|
+
unnamed.push(`#${String(ordinal + 1)}`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const held = available.get(window.name) ?? 0;
|
|
287
|
+
if (held > 0)
|
|
288
|
+
available.set(window.name, held - 1);
|
|
289
|
+
else
|
|
290
|
+
missing.push(window.name);
|
|
291
|
+
}
|
|
292
|
+
spare += [...available.values()].reduce((total, count) => total + count, 0);
|
|
293
|
+
return [...missing, ...unnamed.slice(spare)];
|
|
294
|
+
}
|
|
295
|
+
function markRemoved(result) {
|
|
296
|
+
result.session_removed = true;
|
|
297
|
+
result.completed_stages.push("session-removed");
|
|
298
|
+
result.created_windows = [];
|
|
299
|
+
result.created_panes = [];
|
|
300
|
+
delete result.created_window_names;
|
|
301
|
+
}
|
|
302
|
+
async function discardSession(server, result) {
|
|
303
|
+
if (!result.session_id || result.session_removed)
|
|
304
|
+
return;
|
|
305
|
+
const id = result.session_id;
|
|
306
|
+
try {
|
|
307
|
+
const session = (await server.snapshot({ signal: AbortSignal.timeout(5000) })).sessions.oneOrUndefined({ id });
|
|
308
|
+
if (session)
|
|
309
|
+
await session.kill();
|
|
310
|
+
markRemoved(result);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
result.session_removal_error = error instanceof Error ? error.message : String(error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function create(server, spec, context, output, result, existing, futureIndexes = new Set) {
|
|
316
|
+
const inputIndex = result.input_index;
|
|
317
|
+
const acquisition = context.signal ? { signal: context.signal } : {};
|
|
318
|
+
context.signal?.throwIfAborted();
|
|
319
|
+
result.stage = existing ? "append" : "creating-session";
|
|
320
|
+
const { width, height } = sessionDimensions(context);
|
|
321
|
+
const session = existing ?? await server.newSession({
|
|
322
|
+
name: spec.name,
|
|
323
|
+
...spec.directory ? { startDirectory: spec.directory } : {},
|
|
324
|
+
...width !== undefined ? { width } : {},
|
|
325
|
+
...height !== undefined ? { height } : {},
|
|
326
|
+
...context.signal ? { signal: context.signal } : {}
|
|
327
|
+
});
|
|
328
|
+
result.session_id = session.id;
|
|
329
|
+
result.session_name = session.name ?? spec.name;
|
|
330
|
+
if (!existing) {
|
|
331
|
+
const bootstrap = session.windows.at(0);
|
|
332
|
+
result.created_windows.push(bootstrap.id);
|
|
333
|
+
(result.created_window_names ??= []).push(bootstrap.name ?? String(bootstrap.index));
|
|
334
|
+
result.created_panes.push(...bootstrap.panes.toArray().map((pane) => pane.id));
|
|
335
|
+
}
|
|
336
|
+
result.completed_stages.push(existing ? "session-resolved" : "session-created");
|
|
337
|
+
if (!existing)
|
|
338
|
+
await output.event("session-created", {
|
|
339
|
+
input_index: inputIndex,
|
|
340
|
+
session_id: session.id,
|
|
341
|
+
session_name: session.name
|
|
342
|
+
});
|
|
343
|
+
if (spec.bootstrap) {
|
|
344
|
+
result.stage = "before-script";
|
|
345
|
+
try {
|
|
346
|
+
await output.event("script-started", { input_index: inputIndex });
|
|
347
|
+
let child;
|
|
348
|
+
try {
|
|
349
|
+
child = await processRun(spec.bootstrap, {
|
|
350
|
+
cwd: spec.directory ?? context.cwd,
|
|
351
|
+
env: context.env,
|
|
352
|
+
...context.signal ? { signal: context.signal } : {},
|
|
353
|
+
output: async (stream, text) => {
|
|
354
|
+
const handled = await output.event("script-output", {
|
|
355
|
+
input_index: inputIndex,
|
|
356
|
+
stream,
|
|
357
|
+
text
|
|
358
|
+
});
|
|
359
|
+
if (output.mode === "human" && !handled)
|
|
360
|
+
await write(stream === "stdout" ? context.stdout : context.stderr, text, context.signal);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error instanceof CliError)
|
|
365
|
+
throw error;
|
|
366
|
+
throw new CliError("script_failed", `before_script could not run: ${spec.bootstrap[0]}`);
|
|
367
|
+
}
|
|
368
|
+
await output.event("script-completed", {
|
|
369
|
+
input_index: inputIndex,
|
|
370
|
+
child_status: child.code,
|
|
371
|
+
truncated: child.truncated.stdout || child.truncated.stderr
|
|
372
|
+
});
|
|
373
|
+
result.script_output = child;
|
|
374
|
+
if (child.code !== 0) {
|
|
375
|
+
const detail = child.stderr.trim();
|
|
376
|
+
throw new CliError("script_failed", `before_script exited with status ${child.code}${detail ? ": " + detail : ""}`);
|
|
377
|
+
}
|
|
378
|
+
result.completed_stages.push("before-script");
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (!existing && !context.signal?.aborted) {
|
|
381
|
+
await session.kill();
|
|
382
|
+
markRemoved(result);
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
result.stage = "session-options";
|
|
388
|
+
const declared = spec.data.options === undefined ? {} : mapping(spec.data.options, "options");
|
|
389
|
+
const sessionScoped = {};
|
|
390
|
+
const windowScoped = {};
|
|
391
|
+
if (Object.keys(declared).length > 0) {
|
|
392
|
+
const windowOptions = await server.showGlobalOptions("window", acquisition);
|
|
393
|
+
for (const [name, value] of Object.entries(declared))
|
|
394
|
+
if (windowOptions.has(name))
|
|
395
|
+
windowScoped[name] = value;
|
|
396
|
+
else
|
|
397
|
+
sessionScoped[name] = value;
|
|
398
|
+
}
|
|
399
|
+
await options(session, sessionScoped, context.signal);
|
|
400
|
+
for (const [name, value] of Object.entries(spec.data.global_options ? mapping(spec.data.global_options) : {}))
|
|
401
|
+
await server.setGlobalOption("session", name, option(value), context.signal ? { signal: context.signal } : undefined);
|
|
402
|
+
for (const [name, value] of Object.entries(spec.environment))
|
|
403
|
+
await session.setEnvironment(name, value, context.signal ? { signal: context.signal } : undefined);
|
|
404
|
+
const wait = spec.readiness !== "never" && spec.windows.some((window) => window.panes.some((pane) => !pane.shell && pane.commands.length > 0));
|
|
405
|
+
result.completed_stages.push("session-options");
|
|
406
|
+
result.stage = "window-allocation";
|
|
407
|
+
const current = (await server.snapshot(acquisition)).sessions.one({ id: session.id });
|
|
408
|
+
const placeholder = existing ? undefined : current.windows.toArray().find((window) => window.id === session.windows.at(0)?.id);
|
|
409
|
+
const occupied = new Set(current.windows.toArray().filter((window) => window.id !== placeholder?.id).map((window) => Number(window.index)));
|
|
410
|
+
const reserved = new Set([...occupied, ...futureIndexes]);
|
|
411
|
+
for (const window of spec.windows) {
|
|
412
|
+
if (window.index === undefined)
|
|
413
|
+
continue;
|
|
414
|
+
if (occupied.has(window.index))
|
|
415
|
+
throw new CliError("invalid_workspace", `Window index ${window.index} already exists`);
|
|
416
|
+
reserved.add(window.index);
|
|
417
|
+
}
|
|
418
|
+
let next = spec.windows.some((window) => window.index === undefined) ? Number((await session.showResolvedOptions(acquisition)).get("base-index") ?? "0") : 0;
|
|
419
|
+
const indexes = spec.windows.map((window) => {
|
|
420
|
+
if (window.index !== undefined)
|
|
421
|
+
return window.index;
|
|
422
|
+
next = freeIndex(reserved, next);
|
|
423
|
+
reserved.add(next);
|
|
424
|
+
return next;
|
|
425
|
+
});
|
|
426
|
+
if (placeholder) {
|
|
427
|
+
const spare = freeIndex(reserved, 0);
|
|
428
|
+
if (Number(placeholder.index) !== spare)
|
|
429
|
+
await placeholder.move({ index: spare });
|
|
430
|
+
}
|
|
431
|
+
let focusWindow;
|
|
432
|
+
for (const [windowIndex, desired] of spec.windows.entries()) {
|
|
433
|
+
context.signal?.throwIfAborted();
|
|
434
|
+
const first = desired.panes[0];
|
|
435
|
+
result.stage = "creating-window";
|
|
436
|
+
const window = await session.newWindow({
|
|
437
|
+
index: indexes[windowIndex],
|
|
438
|
+
...desired.name ? { name: desired.name } : {},
|
|
439
|
+
...first.directory ? { startDirectory: first.directory } : {},
|
|
440
|
+
...first.shell ? { shellCommand: first.shell } : {},
|
|
441
|
+
environment: first.environment,
|
|
442
|
+
...context.signal ? { signal: context.signal } : {}
|
|
443
|
+
});
|
|
444
|
+
result.created_windows.push(window.id);
|
|
445
|
+
(result.created_window_names ??= []).push(window.name ?? String(indexes[windowIndex]));
|
|
446
|
+
result.created_panes.push(window.panes.at(0).id);
|
|
447
|
+
if (windowIndex === 0 && placeholder) {
|
|
448
|
+
result.stage = "bootstrap-removal";
|
|
449
|
+
await removePlaceholder(session, placeholder, result);
|
|
450
|
+
}
|
|
451
|
+
result.stage = "window-options";
|
|
452
|
+
await options(window, windowScoped, context.signal);
|
|
453
|
+
await options(window, desired.data.options, context.signal);
|
|
454
|
+
await output.event("window-created", {
|
|
455
|
+
input_index: inputIndex,
|
|
456
|
+
session_id: session.id,
|
|
457
|
+
window_id: window.id,
|
|
458
|
+
window_name: window.name,
|
|
459
|
+
window_index: windowIndex + 1,
|
|
460
|
+
pane_total: desired.panes.length
|
|
461
|
+
});
|
|
462
|
+
let previous = window.panes.at(0);
|
|
463
|
+
let focusPane;
|
|
464
|
+
const panes = [previous];
|
|
465
|
+
for (const [paneIndex, paneSpec] of desired.panes.entries()) {
|
|
466
|
+
if (paneIndex === 0)
|
|
467
|
+
continue;
|
|
468
|
+
context.signal?.throwIfAborted();
|
|
469
|
+
result.stage = "creating-pane";
|
|
470
|
+
const pane = await previous.split({
|
|
471
|
+
...paneSpec.directory ? { startDirectory: paneSpec.directory } : {},
|
|
472
|
+
...paneSpec.shell ? { shellCommand: paneSpec.shell } : {},
|
|
473
|
+
environment: paneSpec.environment,
|
|
474
|
+
...context.signal ? { signal: context.signal } : {}
|
|
475
|
+
});
|
|
476
|
+
result.created_panes.push(pane.id);
|
|
477
|
+
previous = pane;
|
|
478
|
+
panes.push(pane);
|
|
479
|
+
await window.selectLayout("tiled");
|
|
480
|
+
}
|
|
481
|
+
if (desired.data.layout)
|
|
482
|
+
await window.selectLayout(scalarText(desired.data.layout));
|
|
483
|
+
for (const [paneIndex, paneSpec] of desired.panes.entries()) {
|
|
484
|
+
context.signal?.throwIfAborted();
|
|
485
|
+
const pane = panes[paneIndex];
|
|
486
|
+
await output.event("pane-created", {
|
|
487
|
+
input_index: inputIndex,
|
|
488
|
+
session_id: session.id,
|
|
489
|
+
pane_id: pane.id,
|
|
490
|
+
window_id: window.id,
|
|
491
|
+
pane_index: paneIndex + 1
|
|
492
|
+
});
|
|
493
|
+
if (paneSpec.commands.length > 0 && !paneSpec.shell && wait && !await ready(pane, context))
|
|
494
|
+
await output.event("warning", {
|
|
495
|
+
input_index: inputIndex,
|
|
496
|
+
pane_id: pane.id,
|
|
497
|
+
message: "Pane readiness timed out; sending commands"
|
|
498
|
+
});
|
|
499
|
+
result.stage = "pane-commands";
|
|
500
|
+
await send(pane, paneSpec, context);
|
|
501
|
+
await output.event("pane-completed", {
|
|
502
|
+
input_index: inputIndex,
|
|
503
|
+
session_id: session.id,
|
|
504
|
+
pane_id: pane.id,
|
|
505
|
+
window_id: window.id,
|
|
506
|
+
pane_index: paneIndex + 1
|
|
507
|
+
});
|
|
508
|
+
if (paneSpec.data.focus)
|
|
509
|
+
focusPane = pane;
|
|
510
|
+
}
|
|
511
|
+
await (focusPane ?? panes.at(-1)).select();
|
|
512
|
+
result.stage = "window-options-after";
|
|
513
|
+
await options(window, desired.data.options_after, context.signal);
|
|
514
|
+
await output.event("window-completed", {
|
|
515
|
+
input_index: inputIndex,
|
|
516
|
+
session_id: session.id,
|
|
517
|
+
window_id: window.id,
|
|
518
|
+
window_index: windowIndex + 1
|
|
519
|
+
});
|
|
520
|
+
if (desired.data.focus || !existing && !focusWindow)
|
|
521
|
+
focusWindow = window;
|
|
522
|
+
}
|
|
523
|
+
if (focusWindow)
|
|
524
|
+
await focusWindow.select();
|
|
525
|
+
result.completed_stages.push("windows-created");
|
|
526
|
+
return (await server.snapshot(acquisition)).sessions.one({ id: session.id });
|
|
527
|
+
}
|
|
528
|
+
async function promptChoice(context, message, choices) {
|
|
529
|
+
await write(context.stdout, message, context.signal);
|
|
530
|
+
const { createInterface } = await import("node:readline");
|
|
531
|
+
const rl = createInterface({ input: context.stdin, terminal: false });
|
|
532
|
+
try {
|
|
533
|
+
for await (const line of rl) {
|
|
534
|
+
const answer = line.trim().toLowerCase().slice(0, 1);
|
|
535
|
+
return choices.includes(answer) ? answer : choices[0];
|
|
536
|
+
}
|
|
537
|
+
return choices[0];
|
|
538
|
+
} finally {
|
|
539
|
+
rl.close();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function load(request, context) {
|
|
543
|
+
if (request.values.colors === 88)
|
|
544
|
+
throw new CliError("usage", "tmux 3.2a+ rejects the legacy 88-color flag (-8); remove it or use -2 for 256 colors", 2);
|
|
545
|
+
if (request.mode !== "human" && !request.values.detached && !request.values.append)
|
|
546
|
+
throw new CliError("usage", "Machine load requires -d or an explicit append operation", 2);
|
|
547
|
+
let appendRequested = Boolean(request.values.append) && !request.values.detached;
|
|
548
|
+
sessionDimensions(context);
|
|
549
|
+
const progress = request.mode === "human" && context.stderr.isTTY ? (await import("./progress-n08zapg2.js")).LoadProgress.create(request, context) : undefined;
|
|
550
|
+
const inputs = [];
|
|
551
|
+
let bridge;
|
|
552
|
+
const files = request.values.workspace_files;
|
|
553
|
+
for (const [index, input] of files.entries()) {
|
|
554
|
+
try {
|
|
555
|
+
const path = await resolveWorkspace(input, context);
|
|
556
|
+
const document = await readDocument(path);
|
|
557
|
+
const override = index === files.length - 1 && request.values.new_session_name ? scalarText(request.values.new_session_name) : undefined;
|
|
558
|
+
if (["plugins", "workspace_builder", "workspace_builder_paths"].some((key) => Object.hasOwn(document, key)))
|
|
559
|
+
bridge ??= await import("./extensions-8v6mfszj.js");
|
|
560
|
+
const extension = await bridge?.extensionPlan(document, path, context, appendRequested, override);
|
|
561
|
+
inputs.push(extension ? { path, kind: "extension", spec: extension } : { path, kind: "native", spec: normalize(document, path, context, override) });
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof CliError)
|
|
564
|
+
throw error;
|
|
565
|
+
throw new CliError("invalid_workspace", error instanceof Error ? error.message : String(error));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const python = inputs.some((input) => input.kind === "extension") ? await bridge.extensionRuntime(context) : undefined;
|
|
569
|
+
const server = connection(request.values, context);
|
|
570
|
+
try {
|
|
571
|
+
await server.validateLayouts(inputs.flatMap((input) => input.kind === "native" ? input.spec.windows.flatMap((window) => window.data.layout ? [{ layout: scalarText(window.data.layout), panes: window.panes.length }] : []) : []), context.signal ? { signal: context.signal } : {});
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (!(error instanceof TypeError))
|
|
574
|
+
throw error;
|
|
575
|
+
throw new CliError("invalid_workspace", error.message);
|
|
576
|
+
}
|
|
577
|
+
let attached = request.mode === "human" && !request.values.detached && !appendRequested;
|
|
578
|
+
let client = attached ? await attachmentClient(server, context) : undefined;
|
|
579
|
+
const interactive = attached && !request.values.answer_yes && Boolean(context.stdin.isTTY);
|
|
580
|
+
let declinedReuse = false;
|
|
581
|
+
if (attached) {
|
|
582
|
+
const lastName = inputs.at(-1).spec.name;
|
|
583
|
+
let existingLast = false;
|
|
584
|
+
try {
|
|
585
|
+
existingLast = (await server.snapshot(context.signal ? { signal: context.signal } : {})).sessions.oneOrUndefined({ name: lastName }) !== undefined;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (!isColdEndpoint(error))
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
if (existingLast) {
|
|
591
|
+
const answer = interactive ? await promptChoice(context, `${lastName} is already running. Attach? [Y/n] `, ["y", "n"]) : "y";
|
|
592
|
+
if (answer === "n") {
|
|
593
|
+
attached = false;
|
|
594
|
+
client = undefined;
|
|
595
|
+
declinedReuse = true;
|
|
596
|
+
}
|
|
597
|
+
} else if (client?.mode === "switch") {
|
|
598
|
+
const answer = interactive ? await promptChoice(context, "Already inside tmux: switch (y), load detached (n), or append (a)? [y/n/a] ", ["y", "n", "a"]) : "y";
|
|
599
|
+
if (answer === "n") {
|
|
600
|
+
attached = false;
|
|
601
|
+
client = undefined;
|
|
602
|
+
} else if (answer === "a") {
|
|
603
|
+
attached = false;
|
|
604
|
+
client = undefined;
|
|
605
|
+
appendRequested = true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
let append;
|
|
610
|
+
let borrowed;
|
|
611
|
+
const futureIndexes = new Set;
|
|
612
|
+
if (appendRequested) {
|
|
613
|
+
borrowed = await currentSession(server, context);
|
|
614
|
+
append = borrowed.session;
|
|
615
|
+
const reserved = new Set(append.windows.toArray().map((window) => Number(window.index)));
|
|
616
|
+
for (const input of inputs) {
|
|
617
|
+
if (input.kind !== "native")
|
|
618
|
+
continue;
|
|
619
|
+
for (const window of input.spec.windows)
|
|
620
|
+
if (window.index !== undefined) {
|
|
621
|
+
if (reserved.has(window.index))
|
|
622
|
+
throw new CliError("invalid_workspace", `Append cannot replace existing window index ${window.index}`);
|
|
623
|
+
reserved.add(window.index);
|
|
624
|
+
futureIndexes.add(window.index);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const output = new OperationOutput("load", request.mode, context.stdout, context.signal, async (event, data) => {
|
|
629
|
+
const level = event === "failed" ? "error" : event === "warning" ? "warning" : ["started", "completed", "script-output", "workspace-completed"].includes(event) ? "info" : "debug";
|
|
630
|
+
const echo = event === "failed" ? false : request.mode !== "human" || event !== "script-output";
|
|
631
|
+
if (echo && context.diagnostics?.accepts(level))
|
|
632
|
+
await progress?.clear();
|
|
633
|
+
await context.diagnostics?.record(level, event, data, echo);
|
|
634
|
+
return progress?.event(event, data);
|
|
635
|
+
});
|
|
636
|
+
const results = [];
|
|
637
|
+
await output.event("started", { inputs: inputs.length });
|
|
638
|
+
try {
|
|
639
|
+
for (const [index, input] of inputs.entries()) {
|
|
640
|
+
await output.event("workspace-started", {
|
|
641
|
+
input_index: index,
|
|
642
|
+
input: privatePath(input.path, context),
|
|
643
|
+
session_name: input.spec.name,
|
|
644
|
+
...input.kind === "native" ? {
|
|
645
|
+
window_total: input.spec.windows.length,
|
|
646
|
+
session_pane_total: input.spec.windows.reduce((total, window) => total + window.panes.length, 0)
|
|
647
|
+
} : {}
|
|
648
|
+
});
|
|
649
|
+
if (input.kind === "native") {
|
|
650
|
+
for (const message of input.spec.warnings)
|
|
651
|
+
await output.event("warning", { input_index: index, message });
|
|
652
|
+
for (const path of await unusableDirectories(input.spec))
|
|
653
|
+
await output.event("warning", {
|
|
654
|
+
input_index: index,
|
|
655
|
+
message: `start_directory is not a directory, tmux will fall back to $HOME: ${path}`
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const result = {
|
|
659
|
+
input_index: index,
|
|
660
|
+
input: privatePath(input.path, context),
|
|
661
|
+
session_name: input.spec.name,
|
|
662
|
+
reused: false,
|
|
663
|
+
appended: Boolean(append),
|
|
664
|
+
stage: "session-lookup",
|
|
665
|
+
completed_stages: [],
|
|
666
|
+
created_windows: [],
|
|
667
|
+
created_panes: []
|
|
668
|
+
};
|
|
669
|
+
results.push(result);
|
|
670
|
+
if (input.kind === "native") {
|
|
671
|
+
for (const window of input.spec.windows)
|
|
672
|
+
if (window.index !== undefined)
|
|
673
|
+
futureIndexes.delete(window.index);
|
|
674
|
+
}
|
|
675
|
+
let session;
|
|
676
|
+
if (input.kind === "extension") {
|
|
677
|
+
session = await bridge.buildExtension(python, input.spec, input.path, server, context, output, result, borrowed);
|
|
678
|
+
} else {
|
|
679
|
+
let existing;
|
|
680
|
+
if (!append) {
|
|
681
|
+
try {
|
|
682
|
+
existing = (await server.snapshot(context.signal ? { signal: context.signal } : {})).sessions.oneOrUndefined({ name: input.spec.name });
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (!isColdEndpoint(error))
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
result.reused = existing !== undefined;
|
|
689
|
+
if (existing) {
|
|
690
|
+
session = existing;
|
|
691
|
+
const skipCompare = declinedReuse && index === inputs.length - 1;
|
|
692
|
+
const missing = skipCompare ? [] : missingWindows(input.spec, existing);
|
|
693
|
+
if (missing.length > 0) {
|
|
694
|
+
result.session_id = existing.id;
|
|
695
|
+
result.session_name = existing.name ?? input.spec.name;
|
|
696
|
+
result.missing_windows = missing;
|
|
697
|
+
throw new CliError("session_mismatch", `Session ${input.spec.name} is already running and does not hold ${missing.join(", ")}; it was left as it is`);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
try {
|
|
701
|
+
session = await create(server, input.spec, context, output, result, append, futureIndexes);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (!append && !context.signal?.aborted)
|
|
704
|
+
await discardSession(server, result);
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
result.session_id = session.id;
|
|
710
|
+
result.session_name = session.name ?? input.spec.name;
|
|
711
|
+
result.stage = "completed";
|
|
712
|
+
await output.event("workspace-completed", result);
|
|
713
|
+
if (request.mode === "human") {
|
|
714
|
+
const color = colorEnabled(request.mode, request.values.color, context.env, Boolean(context.stdout.isTTY));
|
|
715
|
+
await write(context.stdout, `${styled("success", result.reused ? "Reused" : append ? "Appended" : "Loaded", color)} ${styled("subject", session.name, color)}
|
|
716
|
+
`, context.signal);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const last = results.at(-1);
|
|
720
|
+
if (attached && last?.session_id) {
|
|
721
|
+
last.stage = client?.mode === "switch" ? "switching-client" : "attaching";
|
|
722
|
+
const args = terminalArguments(server);
|
|
723
|
+
if (request.values.colors === 256)
|
|
724
|
+
args.push("-2");
|
|
725
|
+
args.push(...client?.mode === "switch" ? client.client ? ["switch-client", "-c", client.client] : ["switch-client"] : ["attach-session"], "-t", last.session_id);
|
|
726
|
+
const child = await processRun(args, {
|
|
727
|
+
cwd: context.cwd,
|
|
728
|
+
env: context.env,
|
|
729
|
+
terminal: client?.mode === "switch" ? client.client ? "input" : false : true,
|
|
730
|
+
...context.signal ? { signal: context.signal } : {}
|
|
731
|
+
});
|
|
732
|
+
if (child.code !== 0)
|
|
733
|
+
throw new CliError("tmux_failed", `tmux attachment failed${child.stderr.trim() ? ": " + child.stderr.trim() : ""}`, child.code);
|
|
734
|
+
last.stage = "completed";
|
|
735
|
+
}
|
|
736
|
+
await output.result({ status: "ok", results, errors: [] });
|
|
737
|
+
return 0;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (output.isFinished)
|
|
740
|
+
throw error;
|
|
741
|
+
const interrupted = context.signal?.aborted;
|
|
742
|
+
const changed = results.some((result) => result.effects_unknown || result.stage === "completed" || result.session_id && !result.session_removed && !result.missing_windows);
|
|
743
|
+
const unreachable = !(error instanceof CliError) && isTmuxUnavailable(error);
|
|
744
|
+
const code = interrupted ? "interrupted" : error instanceof CliError ? error.code : unreachable ? "tmux_unavailable" : "tmux_failed";
|
|
745
|
+
const kept = results.flatMap((result) => result.session_id && !result.session_removed ? result.created_window_names ?? [] : []);
|
|
746
|
+
const message = (unreachable ? unreachableMessage(server, error) : error instanceof Error ? error.message : String(error)) + (kept.length > 0 ? `. Windows kept: ${kept.join(", ")}` : "");
|
|
747
|
+
await output.result({
|
|
748
|
+
status: changed ? "partial" : "error",
|
|
749
|
+
results,
|
|
750
|
+
errors: [
|
|
751
|
+
{
|
|
752
|
+
code,
|
|
753
|
+
message,
|
|
754
|
+
input_index: results.at(-1)?.input_index ?? 0,
|
|
755
|
+
failed_stage: results.at(-1)?.stage ?? "started"
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
});
|
|
759
|
+
if (request.mode === "human") {
|
|
760
|
+
for (const result of results)
|
|
761
|
+
if (result.session_id && !result.session_removed)
|
|
762
|
+
await write(context.stderr, `Session ${styled("subject", result.session_name, false)} remains available (${result.stage})${result.created_window_names?.length ? `; windows kept: ${result.created_window_names.join(", ")}` : ""}.
|
|
763
|
+
`, context.signal);
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
await write(context.stderr, JSON.stringify({ schema_version: 1, code, message }) + `
|
|
767
|
+
`, context.signal).catch(() => {});
|
|
768
|
+
return interrupted ? 130 : 1;
|
|
769
|
+
} finally {
|
|
770
|
+
await progress?.clear(AbortSignal.timeout(100)).catch(() => {});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function addressable(name) {
|
|
774
|
+
return isTmuxName(name);
|
|
775
|
+
}
|
|
776
|
+
var requireAddressableName = (kind, name) => {
|
|
777
|
+
if (name !== null && !addressable(name))
|
|
778
|
+
throw new CliError("invalid_workspace", `The ${kind} named ${name} cannot be captured: tmux reads "." and ":" as target separators, so a workspace could not name it again`);
|
|
779
|
+
};
|
|
780
|
+
async function freezeSession(server, request, context) {
|
|
781
|
+
const name = request.values.session_name;
|
|
782
|
+
const explicit = name !== null && name !== undefined;
|
|
783
|
+
if (!explicit && context.env.TMUX)
|
|
784
|
+
return (await currentSession(server, context)).session;
|
|
785
|
+
if (explicit)
|
|
786
|
+
requireAddressableName("session", scalarText(name));
|
|
787
|
+
const acquisition = context.signal ? { signal: context.signal } : {};
|
|
788
|
+
let sessions = [];
|
|
789
|
+
try {
|
|
790
|
+
sessions = (await server.snapshot(acquisition)).sessions.toArray();
|
|
791
|
+
} catch (error) {
|
|
792
|
+
if (!isColdEndpoint(error))
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
795
|
+
if (explicit) {
|
|
796
|
+
const target = scalarText(name);
|
|
797
|
+
const session = sessions.find((item) => item.name === target || item.id === target);
|
|
798
|
+
if (!session)
|
|
799
|
+
throw new CliError("session_not_found", `Session not found: ${target}`);
|
|
800
|
+
return session;
|
|
801
|
+
}
|
|
802
|
+
if (sessions.length === 1)
|
|
803
|
+
return sessions[0];
|
|
804
|
+
if (!sessions.length)
|
|
805
|
+
throw new CliError("session_not_found", "No live sessions to capture");
|
|
806
|
+
throw new CliError("usage", "Several sessions are available; specify a session name or ID to freeze", 2);
|
|
807
|
+
}
|
|
808
|
+
async function freeze(request, context) {
|
|
809
|
+
const server = connection(request.values, context);
|
|
810
|
+
try {
|
|
811
|
+
return await capture(server, request, context);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error instanceof CliError || context.signal?.aborted)
|
|
814
|
+
throw error;
|
|
815
|
+
if (isTmuxUnavailable(error))
|
|
816
|
+
throw new CliError("tmux_unavailable", unreachableMessage(server, error));
|
|
817
|
+
throw new CliError("tmux_failed", error instanceof Error ? error.message : String(error));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function capture(server, request, context) {
|
|
821
|
+
const session = await freezeSession(server, request, context);
|
|
822
|
+
const acquisition = context.signal ? { signal: context.signal } : {};
|
|
823
|
+
requireAddressableName("session", session.name);
|
|
824
|
+
const ordinaryShells = new Set([
|
|
825
|
+
"sh",
|
|
826
|
+
"bash",
|
|
827
|
+
"zsh",
|
|
828
|
+
"dash",
|
|
829
|
+
"ash",
|
|
830
|
+
"ksh",
|
|
831
|
+
"mksh",
|
|
832
|
+
"fish",
|
|
833
|
+
"csh",
|
|
834
|
+
"tcsh"
|
|
835
|
+
]);
|
|
836
|
+
const defaultShell = basename((await session.showResolvedOptions(acquisition)).get("default-shell") ?? "");
|
|
837
|
+
const isDefaultShell = (command) => command === defaultShell || ordinaryShells.has(command);
|
|
838
|
+
const windows = [];
|
|
839
|
+
for (const window of session.windows.toArray()) {
|
|
840
|
+
requireAddressableName("window", window.name);
|
|
841
|
+
const panes = window.panes.toArray().map((pane) => ({
|
|
842
|
+
...pane.currentCommand !== null && !isDefaultShell(pane.currentCommand) ? { shell_command: [pane.currentCommand] } : {},
|
|
843
|
+
start_directory: pane.currentPath ?? context.cwd,
|
|
844
|
+
...pane.active === true ? { focus: true } : {}
|
|
845
|
+
}));
|
|
846
|
+
const optionsAfter = Object.fromEntries(await window.showOptions(acquisition));
|
|
847
|
+
windows.push({
|
|
848
|
+
window_name: window.name,
|
|
849
|
+
window_index: Number(window.index),
|
|
850
|
+
layout: window.layout,
|
|
851
|
+
options_after: optionsAfter,
|
|
852
|
+
panes,
|
|
853
|
+
...window.active === true ? { focus: true } : {}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
const sessionOptions = Object.fromEntries(await session.showOptions(acquisition));
|
|
857
|
+
delete sessionOptions["default-size"];
|
|
858
|
+
const document = {
|
|
859
|
+
session_name: session.name,
|
|
860
|
+
windows,
|
|
861
|
+
options: sessionOptions
|
|
862
|
+
};
|
|
863
|
+
const destination = request.values.save_to ? resolve(context.cwd, expandPath(scalarText(request.values.save_to), context)) : undefined;
|
|
864
|
+
const format = scalarText(request.values.workspace_format ?? (destination && extname(destination).toLowerCase() === ".json" ? "json" : "yaml"));
|
|
865
|
+
if (!destination && request.mode === "human")
|
|
866
|
+
throw new CliError("usage", "Specify --save-to or choose --json/--ndjson capture output", 2);
|
|
867
|
+
if (destination)
|
|
868
|
+
await saveDocument(document, destination, format, Boolean(request.values.force), context.signal);
|
|
869
|
+
if (request.mode === "json")
|
|
870
|
+
await emitJson(context.stdout, destination ? {
|
|
871
|
+
schema_version: 1,
|
|
872
|
+
command: "freeze",
|
|
873
|
+
status: "ok",
|
|
874
|
+
destination: privatePath(destination, context),
|
|
875
|
+
format
|
|
876
|
+
} : document, true, context.signal);
|
|
877
|
+
else if (request.mode === "ndjson")
|
|
878
|
+
await emitJson(context.stdout, {
|
|
879
|
+
schema_version: 1,
|
|
880
|
+
command: "freeze",
|
|
881
|
+
status: "ok",
|
|
882
|
+
...destination ? { destination: privatePath(destination, context), format } : { workspace: document }
|
|
883
|
+
}, false, context.signal);
|
|
884
|
+
else if (!request.values.quiet)
|
|
885
|
+
await write(context.stdout, `Saved ${destination}
|
|
886
|
+
`, context.signal);
|
|
887
|
+
return 0;
|
|
888
|
+
}
|
|
889
|
+
export {
|
|
890
|
+
connection,
|
|
891
|
+
freeze,
|
|
892
|
+
load
|
|
893
|
+
};
|