@osdlc/agent-seat 0.0.0-stage → 0.7.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/CHANGELOG.md +133 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +181 -2
- package/SECURITY.md +41 -0
- package/dist/contract.d.ts +297 -0
- package/dist/contract.js +102 -0
- package/dist/harness.d.ts +305 -0
- package/dist/harness.js +610 -0
- package/dist/hosts/cmux.d.ts +67 -0
- package/dist/hosts/cmux.js +252 -0
- package/dist/hosts/herdr.d.ts +78 -0
- package/dist/hosts/herdr.js +324 -0
- package/dist/hosts/jetbrains.d.ts +98 -0
- package/dist/hosts/jetbrains.js +353 -0
- package/dist/hosts/orca.d.ts +67 -0
- package/dist/hosts/orca.js +292 -0
- package/dist/hosts/otty.d.ts +70 -0
- package/dist/hosts/otty.js +233 -0
- package/dist/hosts/registry.d.ts +50 -0
- package/dist/hosts/registry.js +100 -0
- package/dist/hosts/session-panes.d.ts +29 -0
- package/dist/hosts/session-panes.js +49 -0
- package/dist/hosts/terminal.d.ts +23 -0
- package/dist/hosts/terminal.js +47 -0
- package/dist/hosts/tmux.d.ts +145 -0
- package/dist/hosts/tmux.js +409 -0
- package/dist/hosts/vscode-client.d.ts +81 -0
- package/dist/hosts/vscode-client.js +124 -0
- package/dist/hosts/vscode.d.ts +35 -0
- package/dist/hosts/vscode.js +127 -0
- package/dist/hosts/wezterm.d.ts +78 -0
- package/dist/hosts/wezterm.js +405 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +61 -0
- package/dist/open-workspace.d.ts +9 -0
- package/dist/open-workspace.js +12 -0
- package/dist/util.d.ts +50 -0
- package/dist/util.js +108 -0
- package/package.json +76 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared reader for a host's registry: `<runtime>/hosts/<name>/*.json`,
|
|
3
|
+
* one file per running IDE or window, written by the extension or plugin that
|
|
4
|
+
* owns it. `<runtime>` is the caller's runtime directory (`runtimeDir`); the
|
|
5
|
+
* package picks none of its own, because the IDE companions must write to the
|
|
6
|
+
* same place.
|
|
7
|
+
*/
|
|
8
|
+
import { closeSync, fstatSync, openSync, readdirSync, readFileSync, statSync, } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { isWithin, parseJsonOrNull, realResolve } from "../util.js";
|
|
11
|
+
/** Where host `name` keeps its state under `runtimeDir`: `<runtimeDir>/hosts/<name>`. */
|
|
12
|
+
export function hostRegistryDir(runtimeDir, name) {
|
|
13
|
+
return join(runtimeDir, 'hosts', name);
|
|
14
|
+
}
|
|
15
|
+
/** Host `name`'s state directory under `opts`. */
|
|
16
|
+
export function registryDirOf(name, opts) {
|
|
17
|
+
return opts.registryDir ?? hostRegistryDir(opts.runtimeDir, name);
|
|
18
|
+
}
|
|
19
|
+
/** Why `stats` cannot be trusted under `uid`: owned by another user, or writable by group or others. */
|
|
20
|
+
function untrusted(stats, uid) {
|
|
21
|
+
if (stats.uid !== uid)
|
|
22
|
+
return `is owned by uid ${stats.uid}, not ${uid}`;
|
|
23
|
+
if ((stats.mode & 0o022) !== 0)
|
|
24
|
+
return 'is writable by group or others';
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Entries in `dir` that `parse` accepts and whose process is alive. A missing
|
|
29
|
+
* directory, an unreadable or malformed file, and a dead pid are all skipped.
|
|
30
|
+
*
|
|
31
|
+
* An entry carries an endpoint and a token, so a planted one would receive
|
|
32
|
+
* every session's argv and sent text. On POSIX the whole directory is skipped
|
|
33
|
+
* unless it belongs to the current user and is not group- or world-writable,
|
|
34
|
+
* and so is each file; `opts.onSkip` hears why. Windows has no uid to check.
|
|
35
|
+
*/
|
|
36
|
+
export function readHostRegistry(dir, parse, alive, opts = {}) {
|
|
37
|
+
const uid = opts.uid === undefined ? (process.getuid?.() ?? null) : opts.uid;
|
|
38
|
+
let names;
|
|
39
|
+
try {
|
|
40
|
+
if (uid !== null) {
|
|
41
|
+
const why = untrusted(statSync(dir), uid);
|
|
42
|
+
if (why !== null) {
|
|
43
|
+
opts.onSkip?.(`registry directory ${dir} ${why}`);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
names = readdirSync(dir).filter((n) => n.endsWith('.json'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
const path = join(dir, name);
|
|
55
|
+
let text;
|
|
56
|
+
let fd = null;
|
|
57
|
+
try {
|
|
58
|
+
// Checked on the open descriptor, so the file cannot be swapped between check and read.
|
|
59
|
+
fd = openSync(path, 'r');
|
|
60
|
+
const why = uid === null ? null : untrusted(fstatSync(fd), uid);
|
|
61
|
+
if (why !== null) {
|
|
62
|
+
opts.onSkip?.(`registry file ${path} ${why}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
text = readFileSync(fd, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (fd !== null)
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
const entry = parse(parseJsonOrNull(text));
|
|
75
|
+
if (entry !== null && alive(entry.pid))
|
|
76
|
+
entries.push(entry);
|
|
77
|
+
}
|
|
78
|
+
return entries;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The candidate with the deepest folder containing `cwd` (depth = length of the
|
|
82
|
+
* real path), the newest `startedAt` winning a tie, then the earliest in list
|
|
83
|
+
* order. Null when no folder contains `cwd`. How both IDE hosts pick a window.
|
|
84
|
+
*/
|
|
85
|
+
export function deepestContaining(cwd, candidates, folders, startedAt) {
|
|
86
|
+
let best = null;
|
|
87
|
+
for (const c of candidates) {
|
|
88
|
+
for (const folder of folders(c)) {
|
|
89
|
+
if (!isWithin(cwd, folder))
|
|
90
|
+
continue;
|
|
91
|
+
const depth = realResolve(folder).length;
|
|
92
|
+
if (best === null ||
|
|
93
|
+
depth > best.depth ||
|
|
94
|
+
(depth === best.depth && startedAt(c) > startedAt(best.c))) {
|
|
95
|
+
best = { c, depth };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return best?.c ?? null;
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host's memory of which pane belongs to which session:
|
|
3
|
+
* `<runtime>/hosts/<name>/sessions/<slug>.json` ({@link sessionIdSlug}), holding the session id,
|
|
4
|
+
* the host's own pane identifier (WezTerm's pane id, Orca's terminal handle, herdr's pane id)
|
|
5
|
+
* and the cwd it was opened in. The host writes it when it opens a pane for a session and resolves a
|
|
6
|
+
* later `send`/`find`/`focus`/`capture`/`close` for that session through it,
|
|
7
|
+
* so two sessions in one worktree reach two different panes.
|
|
8
|
+
*
|
|
9
|
+
* The identifier is only a hint: a host checks it against its own live
|
|
10
|
+
* listing before acting, because panes close and a restarted app reissues ids.
|
|
11
|
+
*/
|
|
12
|
+
/** What a host remembers about the pane it opened for a session. */
|
|
13
|
+
export interface RememberedPane {
|
|
14
|
+
/** The host's own pane identifier. */
|
|
15
|
+
pane: string;
|
|
16
|
+
/** The working directory the pane was opened in, so a reissued id can be told from the original. */
|
|
17
|
+
cwd: string;
|
|
18
|
+
}
|
|
19
|
+
/** The pane remembered for `sessionId`, or null. A file that names another id is ignored. */
|
|
20
|
+
export declare function recallSessionPane(dir: string, sessionId: string): RememberedPane | null;
|
|
21
|
+
/** The pane remembered for the request's session id, or null when it carries none or none was remembered. */
|
|
22
|
+
export declare function boundPane(dir: string, target: {
|
|
23
|
+
sessionId?: string | undefined;
|
|
24
|
+
}): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Remembers `paneId` (opened in `cwd`) for `sessionId`. A failure is reported on stderr and
|
|
27
|
+
* not thrown: the session still runs, and later requests for it resolve by `cwd`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function rememberSessionPane(dir: string, sessionId: string, paneId: string, cwd: string): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host's memory of which pane belongs to which session:
|
|
3
|
+
* `<runtime>/hosts/<name>/sessions/<slug>.json` ({@link sessionIdSlug}), holding the session id,
|
|
4
|
+
* the host's own pane identifier (WezTerm's pane id, Orca's terminal handle, herdr's pane id)
|
|
5
|
+
* and the cwd it was opened in. The host writes it when it opens a pane for a session and resolves a
|
|
6
|
+
* later `send`/`find`/`focus`/`capture`/`close` for that session through it,
|
|
7
|
+
* so two sessions in one worktree reach two different panes.
|
|
8
|
+
*
|
|
9
|
+
* The identifier is only a hint: a host checks it against its own live
|
|
10
|
+
* listing before acting, because panes close and a restarted app reissues ids.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { ensureDir, isRecord, readJsonOrNull, sessionIdSlug, writeJsonAtomic } from "../util.js";
|
|
14
|
+
/**
|
|
15
|
+
* `<dir>/sessions/<slug>.json`: one file per session, so concurrent launches never overwrite each
|
|
16
|
+
* other. The name is a slug with a 128-bit hash, not the id itself, so it is unique on
|
|
17
|
+
* case-insensitive file systems and valid on Windows.
|
|
18
|
+
*/
|
|
19
|
+
function filePath(dir, sessionId) {
|
|
20
|
+
return join(dir, 'sessions', `${sessionIdSlug(sessionId, 32)}.json`);
|
|
21
|
+
}
|
|
22
|
+
/** The pane remembered for `sessionId`, or null. A file that names another id is ignored. */
|
|
23
|
+
export function recallSessionPane(dir, sessionId) {
|
|
24
|
+
const data = readJsonOrNull(filePath(dir, sessionId));
|
|
25
|
+
if (!isRecord(data) || data['id'] !== sessionId)
|
|
26
|
+
return null;
|
|
27
|
+
const { pane, cwd } = data;
|
|
28
|
+
return typeof pane === 'string' && typeof cwd === 'string' ? { pane, cwd } : null;
|
|
29
|
+
}
|
|
30
|
+
/** The pane remembered for the request's session id, or null when it carries none or none was remembered. */
|
|
31
|
+
export function boundPane(dir, target) {
|
|
32
|
+
return target.sessionId === undefined
|
|
33
|
+
? null
|
|
34
|
+
: (recallSessionPane(dir, target.sessionId)?.pane ?? null);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remembers `paneId` (opened in `cwd`) for `sessionId`. A failure is reported on stderr and
|
|
38
|
+
* not thrown: the session still runs, and later requests for it resolve by `cwd`.
|
|
39
|
+
*/
|
|
40
|
+
export function rememberSessionPane(dir, sessionId, paneId, cwd) {
|
|
41
|
+
try {
|
|
42
|
+
ensureDir(join(dir, 'sessions'));
|
|
43
|
+
writeJsonAtomic(filePath(dir, sessionId), { id: sessionId, pane: paneId, cwd });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
47
|
+
process.stderr.write(`[host] could not remember the pane of session ${sessionId}: ${detail}\n`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The current terminal: run it right here and wait for it.
|
|
3
|
+
*
|
|
4
|
+
* An interactive process is the one thing that must NOT be captured — it
|
|
5
|
+
* owns the TTY until the person leaves it — so this runs through the command
|
|
6
|
+
* seam's STREAMING door (`streamRunner`; stdio fully
|
|
7
|
+
* inherited, resolves to `{exitCode}`) rather than a capture door. `req.argv`
|
|
8
|
+
* names a different binary on every call, so the injected dependency is a
|
|
9
|
+
* runner FACTORY — `(binary) => StreamRunner`, the same shape `streamRunner`
|
|
10
|
+
* itself has — not a runner bound to one binary the way `hosts/tmux.ts` and
|
|
11
|
+
* `hosts/cmux.ts` inject theirs (their binary, `tmux`/`cmux`, is fixed).
|
|
12
|
+
*/
|
|
13
|
+
import { type StreamRunner } from '@osdlc/command-seam';
|
|
14
|
+
import type { Host } from '../contract.ts';
|
|
15
|
+
/**
|
|
16
|
+
* The current-terminal host: runs the command right here with inherited stdio and waits for it.
|
|
17
|
+
* Needs no binary or app, runs on every platform, and is always available.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeTerminalHost(opts?: {
|
|
20
|
+
streamRunner?: ((binary: string) => StreamRunner) | undefined;
|
|
21
|
+
}): Host;
|
|
22
|
+
/** The production terminal host — `streamRunner` as its runner factory. */
|
|
23
|
+
export declare const terminalHost: Host;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The current terminal: run it right here and wait for it.
|
|
3
|
+
*
|
|
4
|
+
* An interactive process is the one thing that must NOT be captured — it
|
|
5
|
+
* owns the TTY until the person leaves it — so this runs through the command
|
|
6
|
+
* seam's STREAMING door (`streamRunner`; stdio fully
|
|
7
|
+
* inherited, resolves to `{exitCode}`) rather than a capture door. `req.argv`
|
|
8
|
+
* names a different binary on every call, so the injected dependency is a
|
|
9
|
+
* runner FACTORY — `(binary) => StreamRunner`, the same shape `streamRunner`
|
|
10
|
+
* itself has — not a runner bound to one binary the way `hosts/tmux.ts` and
|
|
11
|
+
* `hosts/cmux.ts` inject theirs (their binary, `tmux`/`cmux`, is fixed).
|
|
12
|
+
*/
|
|
13
|
+
import { streamRunner } from '@osdlc/command-seam';
|
|
14
|
+
import { onPath } from "../contract.js";
|
|
15
|
+
import { wrapperSession } from "../harness.js";
|
|
16
|
+
/**
|
|
17
|
+
* The current-terminal host: runs the command right here with inherited stdio and waits for it.
|
|
18
|
+
* Needs no binary or app, runs on every platform, and is always available.
|
|
19
|
+
*/
|
|
20
|
+
export function makeTerminalHost(opts = {}) {
|
|
21
|
+
const makeRunner = opts.streamRunner ?? streamRunner;
|
|
22
|
+
const terminal = (req) => {
|
|
23
|
+
const [bin, ...rest] = req.argv;
|
|
24
|
+
if (bin === undefined)
|
|
25
|
+
throw new Error('empty argv');
|
|
26
|
+
if (!onPath(bin))
|
|
27
|
+
throw new Error(`'${bin}' is not on PATH`);
|
|
28
|
+
return makeRunner(bin).run(rest, { cwd: req.cwd }).exitCode;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
name: 'terminal',
|
|
32
|
+
platforms: 'all',
|
|
33
|
+
available() {
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
terminal,
|
|
37
|
+
// `terminal` returns only once the agent exited, so the session-id lookup looks once. The
|
|
38
|
+
// host's own `terminal` part runs, so a host spread from this one with another
|
|
39
|
+
// `terminal` opens the session through that one.
|
|
40
|
+
session(req) {
|
|
41
|
+
const run = this.terminal ?? terminal;
|
|
42
|
+
return wrapperSession((r) => run.call(this, r), req, { hostWaited: true });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The production terminal host — `streamRunner` as its runner factory. */
|
|
47
|
+
export const terminalHost = makeTerminalHost();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux: a detached session on `cwd` running a command, found again by a name
|
|
3
|
+
* bound to the caller's session id; see `docs/planning/research/tmux.md`.
|
|
4
|
+
*
|
|
5
|
+
* Unlike orca/cmux, tmux has no app-level worktree registry and no `--name`
|
|
6
|
+
* the caller supplies — a session name is chosen here, once, and must be
|
|
7
|
+
* unique server-wide. A launch carries the caller's session id
|
|
8
|
+
* (`CommandRequest.sessionId`), so the name is derived from it
|
|
9
|
+
* ({@link tmuxIdSessionName}) and two sessions in one worktree get two tmux
|
|
10
|
+
* sessions. `send`/`find`/`focus`/`capture`/`close` take the same id and
|
|
11
|
+
* target that name; a request without an id (or a session opened before this
|
|
12
|
+
* binding existed) falls back to the name derived from `cwd`
|
|
13
|
+
* ({@link tmuxSessionName}).
|
|
14
|
+
*/
|
|
15
|
+
import { type CommandRunner } from '@osdlc/command-seam';
|
|
16
|
+
import type { CommandRequest, Host } from '../contract.ts';
|
|
17
|
+
/**
|
|
18
|
+
* The cwd-derived tmux session name: stable across repeated calls with the same
|
|
19
|
+
* `cwd`, and the name every session had before addressing by id. tmux session
|
|
20
|
+
* names must be unique server-wide and match `[A-Za-z0-9_-]` —
|
|
21
|
+
* `docs/planning/research/tmux.md`'s "Naming rules" verified that `.`/`:` are
|
|
22
|
+
* silently rewritten to `_`, which can then cause a LATER create to fail with
|
|
23
|
+
* `duplicate session` against the rewritten name instead of the one actually
|
|
24
|
+
* requested. Hashing `cwd` sidesteps that class of surprise entirely rather
|
|
25
|
+
* than trying to sanitize an arbitrary path into the safe character set.
|
|
26
|
+
*
|
|
27
|
+
* Used for a request with no session id and as the fallback for a session
|
|
28
|
+
* opened before names were bound to ids. Two launches WITHOUT an id for the
|
|
29
|
+
* SAME `cwd` still collide — the second `tmux new-session` fails with its own
|
|
30
|
+
* real "duplicate session" error, surfaced verbatim below.
|
|
31
|
+
*
|
|
32
|
+
* @experimental an `attach` part is planned to replace the tmux-specific attach helpers.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tmuxSessionName(cwd: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The tmux session name bound to a caller session id: `sd-` and {@link sessionIdSlug}, the
|
|
37
|
+
* id's first 12 characters with anything outside `[A-Za-z0-9_-]` rewritten to `_` (so tmux
|
|
38
|
+
* never rewrites it for us), then `-` and 8 hex digits of the sha256 of the whole id. The hash
|
|
39
|
+
* keeps apart ids that share their first 12 characters (`build-step-001`, `build-step-002`) or
|
|
40
|
+
* differ only in a rewritten character (`a.b`, `a_b`).
|
|
41
|
+
*
|
|
42
|
+
* The single source of the name: an attach must call this rather than rebuild it.
|
|
43
|
+
*
|
|
44
|
+
* @experimental an `attach` part is planned to replace the tmux-specific attach helpers.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tmuxIdSessionName(sessionId: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* The name of a visible run's session: `sd-` and the run id (anything outside `[A-Za-z0-9_-]`
|
|
49
|
+
* rewritten to `_`), then, when there is a caller session id (a run's several agent steps each
|
|
50
|
+
* get a session), `-` and the same id part as {@link tmuxIdSessionName}. A person finds it by
|
|
51
|
+
* the run id (`tmux ls`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function tmuxRunSessionName(runId: string, sessionId?: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Argv for `tmux new-session` — pure, so a test can pin it. `-d` (detached)
|
|
56
|
+
* is what makes this hands-off, the same category as orca/cmux's own managed
|
|
57
|
+
* window: `new-session` returns almost immediately once the session exists,
|
|
58
|
+
* long before whatever it runs finishes. `--` terminates tmux's own option
|
|
59
|
+
* parsing before `req.argv` (the harness's argv, possibly under an `env` prefix —
|
|
60
|
+
* see `wrapperSession` in `../harness.ts`) starts, the same guard `harnessArgv` itself applies one
|
|
61
|
+
* level down for the harness binary's own flags.
|
|
62
|
+
*/
|
|
63
|
+
export declare function tmuxCreateArgv(req: CommandRequest): string[];
|
|
64
|
+
/** Argv for the wait poll: prints `<pane_dead> <pane_dead_status>` for the session's pane. */
|
|
65
|
+
export declare function tmuxPaneStatusArgv(name: string): string[];
|
|
66
|
+
/**
|
|
67
|
+
* Argv for `load-buffer`, the first of the three invocations `send` makes, per
|
|
68
|
+
* `docs/planning/research/tmux.md`'s "Sending input" section (verified): `-`
|
|
69
|
+
* reads the buffer's content from stdin (`req.text`, passed as
|
|
70
|
+
* `CommandRunOptions.input`), so embedded newlines survive the paste instead of
|
|
71
|
+
* each arriving as an Enter, and a `text` starting with `-` is never parsed as
|
|
72
|
+
* a flag.
|
|
73
|
+
*/
|
|
74
|
+
export declare function tmuxLoadBufferArgv(name: string): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Argv for `paste-buffer`, the second invocation: `-p` pastes as if typed
|
|
77
|
+
* (bracketed paste, matching Orca's own send behavior), `-d` deletes the
|
|
78
|
+
* buffer immediately after so it never lingers for a later, unrelated send
|
|
79
|
+
* to collide with. `-t` addresses the pane by session `name`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function tmuxPasteBufferArgv(name: string): string[];
|
|
82
|
+
/** The third, separate `send-keys` call that submits what {@link tmuxPasteBufferArgv} just pasted. */
|
|
83
|
+
export declare function tmuxSendEnterArgv(name: string): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Argv for `tmux has-session -t <name>` — per `docs/planning/research/tmux.md`'s
|
|
86
|
+
* "Presence checks" section (verified): exits 0 when the session exists, 1
|
|
87
|
+
* when it does not. Simpler than `list-panes` and sufficient here, since a
|
|
88
|
+
* session name maps 1:1 to a pane.
|
|
89
|
+
*/
|
|
90
|
+
export declare function tmuxHasSessionArgv(name: string): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Argv for `tmux attach-session -t <name>` — the real, interactive attach
|
|
93
|
+
* a caller uses for a tmux-hosted session `find` (below) confirms is still
|
|
94
|
+
* live, instead of falling through to an engine-native resume that would risk
|
|
95
|
+
* opening a SECOND, competing session on top of a still-live one. Unlike orca/cmux, tmux has a genuine
|
|
96
|
+
* interactive attach primitive of its own — this is that mechanism (`attach-
|
|
97
|
+
* session` itself is standard, well-documented tmux behavior, not something
|
|
98
|
+
* this campaign's own research docs independently verified).
|
|
99
|
+
*
|
|
100
|
+
* Only correct when the attaching process is OUTSIDE any tmux client already
|
|
101
|
+
* — a caller reaches for {@link tmuxSwitchClientArgv} instead
|
|
102
|
+
* when `$TMUX` is set, since `attach-session` refuses to nest one client
|
|
103
|
+
* inside another.
|
|
104
|
+
*
|
|
105
|
+
* @experimental an `attach` part is planned to replace the tmux-specific attach helpers.
|
|
106
|
+
*/
|
|
107
|
+
export declare function tmuxAttachArgv(name: string): string[];
|
|
108
|
+
/**
|
|
109
|
+
* Argv for `tmux switch-client -t <name>` — what a caller uses
|
|
110
|
+
* instead of {@link tmuxAttachArgv} when the attaching process is ITSELF
|
|
111
|
+
* already inside a tmux client (`$TMUX` set, the common case for a tmux
|
|
112
|
+
* user): `attach-session` there refuses with "sessions should be nested with
|
|
113
|
+
* care, unset $TMUX to force" and exits 1 without attaching anything, while
|
|
114
|
+
* `switch-client` is tmux's own documented mechanism for moving the current
|
|
115
|
+
* client to a different session instead of nesting one inside another.
|
|
116
|
+
*
|
|
117
|
+
* @experimental an `attach` part is planned to replace the tmux-specific attach helpers.
|
|
118
|
+
*/
|
|
119
|
+
export declare function tmuxSwitchClientArgv(name: string): string[];
|
|
120
|
+
/** Argv for `tmux list-sessions`, one session name per line. */
|
|
121
|
+
export declare function tmuxListSessionsArgv(): string[];
|
|
122
|
+
/** Argv for `tmux capture-pane -p -t <name>`: prints the pane's visible text to stdout. */
|
|
123
|
+
export declare function tmuxCapturePaneArgv(name: string): string[];
|
|
124
|
+
/** Argv for `tmux kill-session -t <name>`: ends the session and signals what runs in it. */
|
|
125
|
+
export declare function tmuxKillSessionArgv(name: string): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Whether this process runs inside a tmux client (the `TMUX` environment variable is set).
|
|
128
|
+
*
|
|
129
|
+
* @experimental an `attach` part is planned to replace the tmux-specific attach helpers.
|
|
130
|
+
*/
|
|
131
|
+
export declare function insideTmux(): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* The tmux host: drives a detached tmux session per launch through the `tmux` binary, which must be on PATH.
|
|
134
|
+
* Runs on darwin and linux only (on Windows it is unavailable; run it inside WSL). Keeps no state of its own;
|
|
135
|
+
* sessions are found again by a name derived from the caller's session id.
|
|
136
|
+
*/
|
|
137
|
+
export declare function makeTmuxHost(opts: {
|
|
138
|
+
runner?: CommandRunner | undefined;
|
|
139
|
+
available?: (() => boolean) | undefined;
|
|
140
|
+
/** Defaults to {@link insideTmux}. */
|
|
141
|
+
inside?: (() => boolean) | undefined;
|
|
142
|
+
/** The wait poll's pause and clock (epoch ms); tests replace them. */
|
|
143
|
+
sleep?: ((ms: number) => void) | undefined;
|
|
144
|
+
now?: (() => number) | undefined;
|
|
145
|
+
}): Host;
|