@termfleet/terminal 0.1.1 → 0.1.3
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 +6 -0
- package/dist/local-terminal.d.ts +13 -0
- package/dist/local-terminal.js +39 -0
- package/dist/tmux-stream.d.ts +1 -0
- package/dist/tmux-stream.js +9 -3
- package/dist/tmux.d.ts +1 -0
- package/dist/tmux.js +13 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -30,6 +30,12 @@ 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
32
|
|
|
33
|
+
`@termfleet/terminal/local-terminal.js` opens a structured command in the
|
|
34
|
+
user's macOS Terminal without constructing AppleScript from caller data. Every
|
|
35
|
+
program, argument, working directory, and environment value is POSIX-quoted
|
|
36
|
+
before the fixed AppleScript receives it as a single argv value. Unsupported
|
|
37
|
+
platforms fail honestly instead of guessing a terminal emulator.
|
|
38
|
+
|
|
33
39
|
`createSession` accepts a structured `{ program, arguments }` command plus an
|
|
34
40
|
environment map. Arguments are passed directly without constructing a shell
|
|
35
41
|
command, and the credential bootstrap deletes itself before the command starts.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface LocalTerminalCommand {
|
|
2
|
+
program: string;
|
|
3
|
+
arguments?: readonly string[];
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export type LocalTerminalRunner = (program: string, arguments_: readonly string[]) => Promise<void>;
|
|
8
|
+
export interface OpenLocalTerminalOptions {
|
|
9
|
+
platform?: NodeJS.Platform;
|
|
10
|
+
runner?: LocalTerminalRunner;
|
|
11
|
+
}
|
|
12
|
+
export declare function terminalShellCommand(command: LocalTerminalCommand): string;
|
|
13
|
+
export declare function openLocalTerminal(command: LocalTerminalCommand, options?: OpenLocalTerminalOptions): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { runAsync } from "./internal/exec.js";
|
|
2
|
+
const APPLE_SCRIPT = [
|
|
3
|
+
"on run argv",
|
|
4
|
+
'tell application "Terminal"',
|
|
5
|
+
"activate",
|
|
6
|
+
"do script (item 1 of argv)",
|
|
7
|
+
"end tell",
|
|
8
|
+
"end run",
|
|
9
|
+
].join("\n");
|
|
10
|
+
export function terminalShellCommand(command) {
|
|
11
|
+
if (!command.program)
|
|
12
|
+
throw new Error("A terminal command program is required.");
|
|
13
|
+
const environment = Object.entries(command.env ?? {}).map(([name, value]) => {
|
|
14
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
15
|
+
throw new Error(`Invalid terminal environment variable name: ${JSON.stringify(name)}.`);
|
|
16
|
+
}
|
|
17
|
+
return `${name}=${value}`;
|
|
18
|
+
});
|
|
19
|
+
const launch = [
|
|
20
|
+
...(environment.length ? ["env", ...environment] : []),
|
|
21
|
+
command.program,
|
|
22
|
+
...(command.arguments ?? []),
|
|
23
|
+
].map(shellQuote).join(" ");
|
|
24
|
+
return command.cwd ? `cd ${shellQuote(command.cwd)} && exec ${launch}` : `exec ${launch}`;
|
|
25
|
+
}
|
|
26
|
+
export async function openLocalTerminal(command, options = {}) {
|
|
27
|
+
const platform = options.platform ?? process.platform;
|
|
28
|
+
if (platform !== "darwin") {
|
|
29
|
+
throw new Error(`Opening a local terminal is not supported on ${platform}.`);
|
|
30
|
+
}
|
|
31
|
+
const runner = options.runner ?? defaultRunner;
|
|
32
|
+
await runner("osascript", ["-e", APPLE_SCRIPT, terminalShellCommand(command)]);
|
|
33
|
+
}
|
|
34
|
+
async function defaultRunner(program, arguments_) {
|
|
35
|
+
await runAsync(program, [...arguments_], { timeoutMs: 5_000 });
|
|
36
|
+
}
|
|
37
|
+
function shellQuote(value) {
|
|
38
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
39
|
+
}
|
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
|
@@ -97,6 +97,7 @@ export declare function getWindowSize({ name, socket, window }: {
|
|
|
97
97
|
window?: number;
|
|
98
98
|
socket?: string;
|
|
99
99
|
}): TmuxWindowSize;
|
|
100
|
+
export declare function getStatusLineCount(name: string, socket?: string): number;
|
|
100
101
|
export declare function listWindowSizesAsync(socket?: string): Promise<Map<string, TmuxWindowSize>>;
|
|
101
102
|
export declare function setPaneStyle(target: string, style: string, socket?: string): Promise<void>;
|
|
102
103
|
export declare function clearPaneStyle(target: string, socket?: string): Promise<void>;
|
package/dist/tmux.js
CHANGED
|
@@ -514,6 +514,19 @@ export function getWindowSize({ name, socket, window = 0 }) {
|
|
|
514
514
|
}
|
|
515
515
|
return { height, width };
|
|
516
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
|
+
}
|
|
517
530
|
// Every window-0 size in ONE async exec, keyed by session. The observe loop uses
|
|
518
531
|
// this to size windows off the event loop instead of a synchronous getWindowSize
|
|
519
532
|
// per window — that per-window fan-out blocked the provider loop under a large fleet
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@termfleet/terminal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Framework-neutral terminal transport and durable tmux session substrate extracted from Termfleet.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pty",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"development": "./src/client.ts",
|
|
33
33
|
"default": "./dist/client.js"
|
|
34
34
|
},
|
|
35
|
+
"./local-terminal.js": {
|
|
36
|
+
"types": "./dist/local-terminal.d.ts",
|
|
37
|
+
"development": "./src/local-terminal.ts",
|
|
38
|
+
"default": "./dist/local-terminal.js"
|
|
39
|
+
},
|
|
35
40
|
"./tmux-stream.js": {
|
|
36
41
|
"types": "./dist/tmux-stream.d.ts",
|
|
37
42
|
"development": "./src/tmux-stream.ts",
|
|
@@ -48,7 +53,8 @@
|
|
|
48
53
|
],
|
|
49
54
|
"scripts": {
|
|
50
55
|
"build": "tsc -p tsconfig.json",
|
|
51
|
-
"
|
|
56
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
57
|
+
"prepack": "npm test"
|
|
52
58
|
},
|
|
53
59
|
"engines": {
|
|
54
60
|
"node": ">=20"
|