@termfleet/terminal 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/tmux-stream.d.ts +1 -0
- package/dist/tmux-stream.js +9 -3
- package/dist/tmux.d.ts +7 -1
- package/dist/tmux.js +28 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,3 +29,7 @@ Created sessions can carry an app-namespaced ownership mark; discovery does not
|
|
|
29
29
|
claim or terminate unmarked sessions. Terminal attachment is intentionally
|
|
30
30
|
separate from session termination: disposing a viewer only detaches its
|
|
31
31
|
ephemeral PTY client and releases all handles.
|
|
32
|
+
|
|
33
|
+
`createSession` accepts a structured `{ program, arguments }` command plus an
|
|
34
|
+
environment map. Arguments are passed directly without constructing a shell
|
|
35
|
+
command, and the credential bootstrap deletes itself before the command starts.
|
package/dist/tmux-stream.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type TmuxTerminalSocketOptions<TAuthorization = void> = {
|
|
|
13
13
|
cwd?: string;
|
|
14
14
|
errorLabel?: string;
|
|
15
15
|
onInput?: (data: string, authorization: TAuthorization | undefined) => Promise<void> | void;
|
|
16
|
+
readOnly?: boolean;
|
|
16
17
|
rows?: number;
|
|
17
18
|
socket: WebSocket;
|
|
18
19
|
terminalId: string;
|
package/dist/tmux-stream.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pty from "@homebridge/node-pty-prebuilt-multiarch";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { assertSession, getWindowSize } from "./tmux.js";
|
|
3
|
+
import { assertSession, getStatusLineCount, getWindowSize } from "./tmux.js";
|
|
4
4
|
export class TmuxTerminalAttachError extends Error {
|
|
5
5
|
phase;
|
|
6
6
|
constructor(phase, cause) {
|
|
@@ -25,8 +25,13 @@ export function attachTmuxTerminalSocket(options) {
|
|
|
25
25
|
catch (error) {
|
|
26
26
|
throw new TmuxTerminalAttachError("resolve", error);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Observers inherit the durable pane dimensions exactly. Even an ignore-size tmux client can
|
|
29
|
+
// become the size source when it is the only attached client, so accepting the browser viewport
|
|
30
|
+
// here would silently reflow someone else's terminal merely by opening a read-only mirror.
|
|
31
|
+
const cols = options.readOnly ? tmuxSize.width : options.cols ?? tmuxSize.width;
|
|
32
|
+
const rows = options.readOnly
|
|
33
|
+
? tmuxSize.height + getStatusLineCount(options.terminalId, options.tmuxSocket)
|
|
34
|
+
: options.rows ?? tmuxSize.height;
|
|
30
35
|
let reconciledPty;
|
|
31
36
|
try {
|
|
32
37
|
reconciledPty = spawnReconciledPty("tmux", [
|
|
@@ -34,6 +39,7 @@ export function attachTmuxTerminalSocket(options) {
|
|
|
34
39
|
"-T",
|
|
35
40
|
"RGB",
|
|
36
41
|
"attach-session",
|
|
42
|
+
...(options.readOnly ? ["-r"] : []),
|
|
37
43
|
"-t",
|
|
38
44
|
options.terminalId
|
|
39
45
|
], {
|
package/dist/tmux.d.ts
CHANGED
|
@@ -9,7 +9,12 @@ export type TmuxWindowSize = {
|
|
|
9
9
|
width: number;
|
|
10
10
|
height: number;
|
|
11
11
|
};
|
|
12
|
+
export type TmuxCommand = {
|
|
13
|
+
arguments?: string[];
|
|
14
|
+
program: string;
|
|
15
|
+
};
|
|
12
16
|
export type CreateSessionOptions = {
|
|
17
|
+
command?: TmuxCommand;
|
|
13
18
|
cwd?: string;
|
|
14
19
|
env?: Record<string, string>;
|
|
15
20
|
name: string;
|
|
@@ -21,7 +26,7 @@ export type CreateSessionOptions = {
|
|
|
21
26
|
export declare function assertTmux(): void;
|
|
22
27
|
export declare function targetForSession(sessionName: string): string;
|
|
23
28
|
export declare function assertSession(name: string, socket?: string): void;
|
|
24
|
-
export declare function createSession({ cwd, env, name, owner, ownerOption: ownershipOption, panes, socket }: CreateSessionOptions): Promise<{
|
|
29
|
+
export declare function createSession({ command, cwd, env, name, owner, ownerOption: ownershipOption, panes, socket }: CreateSessionOptions): Promise<{
|
|
25
30
|
name: string;
|
|
26
31
|
panes: number;
|
|
27
32
|
}>;
|
|
@@ -92,6 +97,7 @@ export declare function getWindowSize({ name, socket, window }: {
|
|
|
92
97
|
window?: number;
|
|
93
98
|
socket?: string;
|
|
94
99
|
}): TmuxWindowSize;
|
|
100
|
+
export declare function getStatusLineCount(name: string, socket?: string): number;
|
|
95
101
|
export declare function listWindowSizesAsync(socket?: string): Promise<Map<string, TmuxWindowSize>>;
|
|
96
102
|
export declare function setPaneStyle(target: string, style: string, socket?: string): Promise<void>;
|
|
97
103
|
export declare function clearPaneStyle(target: string, socket?: string): Promise<void>;
|
package/dist/tmux.js
CHANGED
|
@@ -40,7 +40,7 @@ export function assertSession(name, socket) {
|
|
|
40
40
|
// createWindowSession), so every tmux invocation here uses runAsync (spawn, not
|
|
41
41
|
// spawnSync) and the pane-ready poll below uses sleepAsync — see waitForPanesReady.
|
|
42
42
|
// Nothing in this function may block the loop.
|
|
43
|
-
export async function createSession({ cwd, env, name, owner, ownerOption: ownershipOption = ownerOption, panes, socket }) {
|
|
43
|
+
export async function createSession({ command, cwd, env, name, owner, ownerOption: ownershipOption = ownerOption, panes, socket }) {
|
|
44
44
|
assertTmux();
|
|
45
45
|
if (!name) {
|
|
46
46
|
throw new Error("--name is required.");
|
|
@@ -48,6 +48,9 @@ export async function createSession({ cwd, env, name, owner, ownerOption: owners
|
|
|
48
48
|
if (!Number.isInteger(panes) || panes < 1) {
|
|
49
49
|
throw new Error("--panes must be a positive integer.");
|
|
50
50
|
}
|
|
51
|
+
if (command !== undefined && !command.program) {
|
|
52
|
+
throw new Error("A command program is required.");
|
|
53
|
+
}
|
|
51
54
|
// Global server options must be in place before the first pane spawns —
|
|
52
55
|
// history-limit is read at pane creation. On a fresh dedicated `-L` socket no
|
|
53
56
|
// server exists yet and `set-option -g` will NOT start one (only commands like
|
|
@@ -58,11 +61,13 @@ export async function createSession({ cwd, env, name, owner, ownerOption: owners
|
|
|
58
61
|
const sessionEnv = env ?? {};
|
|
59
62
|
const bootstrapDirectories = [];
|
|
60
63
|
const paneBootstrapArgs = (pane) => {
|
|
64
|
+
const commandArgs = command === undefined ? [] : [command.program, ...(command.arguments ?? [])];
|
|
61
65
|
if (Object.keys(sessionEnv).length === 0)
|
|
62
|
-
return
|
|
66
|
+
return commandArgs;
|
|
63
67
|
const directory = mkdtempSync(join(tmpdir(), "tmux-session-env-"));
|
|
64
68
|
bootstrapDirectories.push(directory);
|
|
65
|
-
|
|
69
|
+
const shell = process.env.SHELL ?? "/bin/sh";
|
|
70
|
+
return writeSessionEnvironmentBootstrap(directory, pane, sessionEnv, commandArgs.length === 0 ? [shell, "-l"] : commandArgs);
|
|
66
71
|
};
|
|
67
72
|
try {
|
|
68
73
|
const startArgs = [...sessionServerOptionArgs(), "new-session", "-d", "-s", name];
|
|
@@ -271,10 +276,9 @@ function tmuxClientEnvironment(source = process.env) {
|
|
|
271
276
|
}
|
|
272
277
|
return environment;
|
|
273
278
|
}
|
|
274
|
-
function writeSessionEnvironmentBootstrap(directory, pane, environment) {
|
|
279
|
+
function writeSessionEnvironmentBootstrap(directory, pane, environment, command) {
|
|
275
280
|
const environmentPath = join(directory, `pane-${pane}.env`);
|
|
276
281
|
const wrapperPath = join(directory, `pane-${pane}.sh`);
|
|
277
|
-
const shell = process.env.SHELL ?? "/bin/sh";
|
|
278
282
|
const lines = Object.entries(environment).map(([name, value]) => {
|
|
279
283
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
280
284
|
throw new Error(`Invalid session environment variable name: ${JSON.stringify(name)}.`);
|
|
@@ -284,15 +288,17 @@ function writeSessionEnvironmentBootstrap(directory, pane, environment) {
|
|
|
284
288
|
writeFileSync(environmentPath, `${lines.join("\n")}\n`, { flag: "wx", mode: 0o600 });
|
|
285
289
|
writeFileSync(wrapperPath, [
|
|
286
290
|
"#!/bin/sh",
|
|
291
|
+
'environment_path="$1"',
|
|
292
|
+
"shift",
|
|
287
293
|
"set -a",
|
|
288
|
-
'. "$
|
|
294
|
+
'. "$environment_path"',
|
|
289
295
|
"set +a",
|
|
290
|
-
'rm -f -- "$
|
|
296
|
+
'rm -f -- "$environment_path" "$0"',
|
|
291
297
|
'rmdir -- "$(dirname -- "$0")" 2>/dev/null || true',
|
|
292
|
-
'exec "
|
|
298
|
+
'exec "$@"',
|
|
293
299
|
""
|
|
294
300
|
].join("\n"), { flag: "wx", mode: 0o700 });
|
|
295
|
-
return [wrapperPath, environmentPath,
|
|
301
|
+
return [wrapperPath, environmentPath, ...command];
|
|
296
302
|
}
|
|
297
303
|
function shellQuote(value) {
|
|
298
304
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
@@ -508,6 +514,19 @@ export function getWindowSize({ name, socket, window = 0 }) {
|
|
|
508
514
|
}
|
|
509
515
|
return { height, width };
|
|
510
516
|
}
|
|
517
|
+
export function getStatusLineCount(name, socket) {
|
|
518
|
+
assertSession(name, socket);
|
|
519
|
+
const value = run("tmux", tmuxArgs(socket, ["show-options", "-Aqv", "-t", name, "status"])).trim();
|
|
520
|
+
if (value === "off")
|
|
521
|
+
return 0;
|
|
522
|
+
if (value === "on")
|
|
523
|
+
return 1;
|
|
524
|
+
const lines = Number(value);
|
|
525
|
+
if (!Number.isInteger(lines) || lines < 0 || lines > 5) {
|
|
526
|
+
throw new Error(`Could not determine tmux status height for ${name}.`);
|
|
527
|
+
}
|
|
528
|
+
return lines;
|
|
529
|
+
}
|
|
511
530
|
// Every window-0 size in ONE async exec, keyed by session. The observe loop uses
|
|
512
531
|
// this to size windows off the event loop instead of a synchronous getWindowSize
|
|
513
532
|
// per window — that per-window fan-out blocked the provider loop under a large fleet
|