@m4ike1/ion-tui 0.1.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.
- package/README.md +114 -0
- package/dist/alt-screen-search.d.ts +46 -0
- package/dist/alt-screen-search.d.ts.map +1 -0
- package/dist/alt-screen-search.js +282 -0
- package/dist/alt-screen-search.js.map +1 -0
- package/dist/autocomplete.d.ts +75 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +749 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/box.d.ts +24 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +133 -0
- package/dist/components/box.js.map +1 -0
- package/dist/components/cancellable-loader.d.ts +22 -0
- package/dist/components/cancellable-loader.d.ts.map +1 -0
- package/dist/components/cancellable-loader.js +35 -0
- package/dist/components/cancellable-loader.js.map +1 -0
- package/dist/components/editor.d.ts +266 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +2087 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +28 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +90 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +48 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +417 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +33 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +76 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +103 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +858 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +21 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +68 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +199 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts +53 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +206 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +60 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +252 -0
- package/dist/components/settings-list.js.map +1 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +23 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +111 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +90 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +51 -0
- package/dist/components/truncated-text.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +26 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/components/virtual-list.d.ts +51 -0
- package/dist/components/virtual-list.d.ts.map +1 -0
- package/dist/components/virtual-list.js +170 -0
- package/dist/components/virtual-list.js.map +1 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +2 -0
- package/dist/editor-component.js.map +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +110 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +366 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +331 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +184 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1174 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +44 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1285 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +59 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +12 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +42 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +404 -0
- package/dist/layout.js.map +1 -0
- package/dist/native-modifiers.d.ts +4 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +13 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/native-module-path.d.ts +7 -0
- package/dist/native-module-path.d.ts.map +1 -0
- package/dist/native-module-path.js +19 -0
- package/dist/native-module-path.js.map +1 -0
- package/dist/native-platform.d.ts +18 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/pty-host.d.ts +224 -0
- package/dist/pty-host.d.ts.map +1 -0
- package/dist/pty-host.js +347 -0
- package/dist/pty-host.js.map +1 -0
- package/dist/stdin-buffer.d.ts +56 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +366 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/terminal-colors.d.ts +10 -0
- package/dist/terminal-colors.d.ts.map +1 -0
- package/dist/terminal-colors.js +59 -0
- package/dist/terminal-colors.js.map +1 -0
- package/dist/terminal-image.d.ts +118 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +550 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +123 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +442 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui-alt-screen.d.ts +165 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +1515 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +39 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +594 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +431 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1142 -0
- package/dist/tui.js.map +1 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +25 -0
- package/dist/undo-stack.js.map +1 -0
- package/dist/utils.d.ts +98 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1200 -0
- package/dist/utils.js.map +1 -0
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +96 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/clipboard.h +170 -0
- package/native/darwin/README.md +22 -0
- package/native/darwin/build.sh +69 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/native/darwin/src/darwin-platform.m +93 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +89 -0
- package/native/win32/README.md +31 -0
- package/native/win32/build.mjs +232 -0
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-platform.js","sourceRoot":"","sources":["../src/native-platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAkBlD,sFAAsF;AACtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4C,CAAC;AAEpE,SAAS,wBAAwB,CAAC,QAAgB,EAAE,MAAM,GAAG,EAAE,EAAoC;IAClG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAC3B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,GAAG,QAAQ,IAAI,IAAI,EAAE,EACrB,GAAG,QAAQ,YAAY,MAAM,OAAO,CACpC,CAAC;IACF,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAE5D,KAAK,MAAM,UAAU,IAAI,yBAAyB,CAAC,UAAU,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAyC,CAAC;YAC9E,IAAI,OAAO,MAAM,EAAE,OAAO,KAAK,UAAU,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBACpF,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAA8B,CAAC,CAAC;gBACxD,OAAO,MAA8B,CAAC;YACvC,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,4CAA4C;QAC7C,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IACnC,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,UAAU,uBAAuB,GAAqC;IAC3E,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IACpF,OAAO,wBAAwB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAAA,CAClD;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,GAAgC;IACjE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,uBAAuB,EAAE,CAAC;IACnE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC3C,OAAO,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AAAA,CACjD","sourcesContent":["import { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { getNativeModuleCandidates } from \"./native-module-path.ts\";\n\nconst cjsRequire = createRequire(import.meta.url);\n\nexport type ModifierKey = \"shift\" | \"command\" | \"control\" | \"option\";\n\nexport interface NativeClipboard {\n\t/** Undefined means unavailable, null means no text; transfer failures reject. */\n\tgetText(): Promise<string | null | undefined>;\n\t/** Undefined means unavailable, null means no image; transfer failures reject. */\n\tgetImage(): Promise<Uint8Array | null | undefined>;\n\t/** Linux uses command-line tools to retain clipboard ownership instead. */\n\tsetText?(text: string): Promise<void>;\n}\n\ntype NativePlatformHelper = NativeClipboard & {\n\tenableVirtualTerminalInput?: () => boolean;\n\tisModifierPressed?: (name: ModifierKey) => boolean;\n};\n\n// Cache module loading, not display availability: a disconnected display can recover.\nconst helpers = new Map<string, NativePlatformHelper | undefined>();\n\nfunction loadNativePlatformHelper(platform: string, suffix = \"\"): NativePlatformHelper | undefined {\n\tconst arch = process.arch;\n\tif (arch !== \"x64\" && arch !== \"arm64\") return undefined;\n\tconst nativePath = path.join(\n\t\t\"native\",\n\t\tplatform,\n\t\t\"prebuilds\",\n\t\t`${platform}-${arch}`,\n\t\t`${platform}-platform${suffix}.node`,\n\t);\n\tif (helpers.has(nativePath)) return helpers.get(nativePath);\n\n\tfor (const modulePath of getNativeModuleCandidates(nativePath)) {\n\t\ttry {\n\t\t\tconst helper = cjsRequire(modulePath) as Partial<NativePlatformHelper> | null;\n\t\t\tif (typeof helper?.getText === \"function\" && typeof helper.getImage === \"function\") {\n\t\t\t\thelpers.set(nativePath, helper as NativePlatformHelper);\n\t\t\t\treturn helper as NativePlatformHelper;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try the next possible packaging location.\n\t\t}\n\t}\n\thelpers.set(nativePath, undefined);\n\treturn undefined;\n}\n\nexport function getNativePlatformHelper(): NativePlatformHelper | undefined {\n\tif (process.platform !== \"darwin\" && process.platform !== \"win32\") return undefined;\n\treturn loadNativePlatformHelper(process.platform);\n}\n\n/** Load a clipboard helper without opening the display until a read is requested. */\nexport function getNativeClipboard(): NativeClipboard | undefined {\n\tif (process.platform !== \"linux\") return getNativePlatformHelper();\n\tif (!process.env.DISPLAY) return undefined;\n\treturn loadNativePlatformHelper(\"linux\", \"-x11\");\n}\n"]}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY host spike (tui-overhaul 1.1-1.5).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the stock ion TUI inside a pseudoterminal and relays bytes
|
|
5
|
+
* bidirectionally between the PTY master and the session owner. The TUI
|
|
6
|
+
* itself stays raw-terminal plus alt-screen and is never rewritten for
|
|
7
|
+
* hosting: this mirrors the Claude Code seam where a separate server hosts
|
|
8
|
+
* the stock TUI under node-pty (see TUI.claude.md section 1.4).
|
|
9
|
+
*
|
|
10
|
+
* Seam rules (pinned by test/pty-host.test.ts):
|
|
11
|
+
* - The core TUI (index.ts, terminal.ts, tui.ts) never imports this module.
|
|
12
|
+
* - node-pty resolves at runtime with prebuilt-first lookup; when no binary
|
|
13
|
+
* is available the caller gets an actionable install hint and the plain
|
|
14
|
+
* terminal path keeps working.
|
|
15
|
+
*
|
|
16
|
+
* Transport decision (task 1.5): in-process handle. A Unix-socket transport
|
|
17
|
+
* would only pay off for reattach-across-process, which no consumer needs
|
|
18
|
+
* yet (the future session-share viewer attaches in-process). Sockets would
|
|
19
|
+
* add framing, auth, and lifecycle management for zero current benefit, so
|
|
20
|
+
* the host is a plain object owning its PTY child. Revisit when an
|
|
21
|
+
* out-of-process attacher arrives.
|
|
22
|
+
*
|
|
23
|
+
* Reattach replay is best-effort byte replay: the host journals PTY output
|
|
24
|
+
* in a bounded ring and, on reattach, nudges the child with a same-dims
|
|
25
|
+
* resize (SIGWINCH repaint, the setWinSize semantic) then replays everything
|
|
26
|
+
* journaled while detached. A terminal-emulator-backed screen dump is
|
|
27
|
+
* future work, not part of this spike.
|
|
28
|
+
*
|
|
29
|
+
* Minimum dimensions stay an open product question: the spike validates
|
|
30
|
+
* positive integers only. Layouts below ~40 columns are known to degrade
|
|
31
|
+
* (the 40x12 golden case pins bounded behavior, not beauty).
|
|
32
|
+
*/
|
|
33
|
+
/** Transport backing session handles. Pinned to in-process by the task 1.5 decision above. */
|
|
34
|
+
export declare const PTY_HOST_TRANSPORT = "in-process";
|
|
35
|
+
/** Default cap for the reattach replay journal. */
|
|
36
|
+
export declare const PTY_HOST_JOURNAL_LIMIT_BYTES: number;
|
|
37
|
+
/**
|
|
38
|
+
* Resync prefix used only when the journal overflowed while detached and
|
|
39
|
+
* replay can no longer continue the previous screen state.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PTY_RESYNC_SEQUENCE = "\u001B[2J\u001B[H";
|
|
42
|
+
/** Caller-specified PTY dimensions in character cells. */
|
|
43
|
+
export interface PtyDimensions {
|
|
44
|
+
cols: number;
|
|
45
|
+
rows: number;
|
|
46
|
+
}
|
|
47
|
+
/** Throw unless cols/rows are positive integers. Integers only: fractional cells do not exist. */
|
|
48
|
+
export declare function assertPtyDimensions(cols: number, rows: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* Minimal structural backend over a PTY child. Matches the node-pty
|
|
51
|
+
* surface the host needs without depending on node-pty types, so tests
|
|
52
|
+
* inject fakes and the core TUI never sees this module.
|
|
53
|
+
*/
|
|
54
|
+
export interface PtyBackend {
|
|
55
|
+
readonly pid: number;
|
|
56
|
+
onData(callback: (data: string) => void): void;
|
|
57
|
+
onExit(callback: (exitCode: number, signal: number | undefined) => void): void;
|
|
58
|
+
write(data: string): void;
|
|
59
|
+
resize(cols: number, rows: number): void;
|
|
60
|
+
kill(signal?: string): void;
|
|
61
|
+
/** Optional raw-mode mirror; real PTY slaves are raw by default so most backends omit this. */
|
|
62
|
+
setRawMode?(active: boolean): void;
|
|
63
|
+
}
|
|
64
|
+
/** Options passed to a backend spawn function. */
|
|
65
|
+
export interface PtySpawnOptions {
|
|
66
|
+
command: string;
|
|
67
|
+
args: string[];
|
|
68
|
+
cols: number;
|
|
69
|
+
rows: number;
|
|
70
|
+
cwd: string;
|
|
71
|
+
env: NodeJS.ProcessEnv;
|
|
72
|
+
name: string;
|
|
73
|
+
}
|
|
74
|
+
/** Injectable backend factory. Tests pass fakes; production passes the node-pty adapter. */
|
|
75
|
+
export type PtySpawnFn = (options: PtySpawnOptions) => PtyBackend;
|
|
76
|
+
/** Minimal structural view of the node-pty module (spawn only). */
|
|
77
|
+
export interface NodePtyModule {
|
|
78
|
+
spawn(file: string, args: string[], options: {
|
|
79
|
+
name: string;
|
|
80
|
+
cols: number;
|
|
81
|
+
rows: number;
|
|
82
|
+
cwd: string;
|
|
83
|
+
env: NodeJS.ProcessEnv;
|
|
84
|
+
}): {
|
|
85
|
+
pid: number;
|
|
86
|
+
onData(callback: (data: string) => void): void;
|
|
87
|
+
onExit(callback: (event: {
|
|
88
|
+
exitCode: number;
|
|
89
|
+
signal?: number;
|
|
90
|
+
}) => void): void;
|
|
91
|
+
write(data: string): void;
|
|
92
|
+
resize(cols: number, rows: number): void;
|
|
93
|
+
kill(signal?: string): void;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** node-pty resolved and ready: prebuilt-first lookup hit. */
|
|
97
|
+
export interface PtyResolutionSuccess {
|
|
98
|
+
available: true;
|
|
99
|
+
/** Which specifier resolved (explicit ION_PTY_MODULE override or "node-pty"). */
|
|
100
|
+
source: string;
|
|
101
|
+
spawn: PtySpawnFn;
|
|
102
|
+
}
|
|
103
|
+
/** node-pty unavailable: hint carries the actionable install instructions. */
|
|
104
|
+
export interface PtyResolutionFailure {
|
|
105
|
+
available: false;
|
|
106
|
+
hint: string;
|
|
107
|
+
}
|
|
108
|
+
export type PtyResolution = PtyResolutionSuccess | PtyResolutionFailure;
|
|
109
|
+
export interface ResolveNodePtyOptions {
|
|
110
|
+
/** Injectable loader for tests. Defaults to require relative to this module. */
|
|
111
|
+
loadModule?: (specifier: string) => unknown;
|
|
112
|
+
env?: NodeJS.ProcessEnv;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve node-pty at runtime, prebuilt-first (node-pty's own packaging
|
|
116
|
+
* prefers prebuilds via prebuild-install). Lookup order: explicit
|
|
117
|
+
* ION_PTY_MODULE override path first, then the "node-pty" package.
|
|
118
|
+
* Never throws: failure returns an actionable hint instead.
|
|
119
|
+
*/
|
|
120
|
+
export declare function resolveNodePty(options?: ResolveNodePtyOptions): PtyResolution;
|
|
121
|
+
/** Thrown by createPtyHost when node-pty is unavailable. Carries the install hint as message. */
|
|
122
|
+
export declare class PtyHostUnavailableError extends Error {
|
|
123
|
+
constructor(hint: string);
|
|
124
|
+
}
|
|
125
|
+
export interface PtyHostOptions {
|
|
126
|
+
/** Stock TUI binary to spawn inside the PTY (host never rewrites the TUI). */
|
|
127
|
+
command: string;
|
|
128
|
+
args?: string[];
|
|
129
|
+
cols: number;
|
|
130
|
+
rows: number;
|
|
131
|
+
cwd?: string;
|
|
132
|
+
env?: NodeJS.ProcessEnv;
|
|
133
|
+
/** TERM value for the PTY slave. */
|
|
134
|
+
name?: string;
|
|
135
|
+
/** Replay journal cap in bytes. Defaults to PTY_HOST_JOURNAL_LIMIT_BYTES. */
|
|
136
|
+
journalLimitBytes?: number;
|
|
137
|
+
/** Injected backend. Production callers omit this and go through createPtyHost. */
|
|
138
|
+
spawn: PtySpawnFn;
|
|
139
|
+
/** Convenience exit listener, equivalent to onExit(). */
|
|
140
|
+
onExit?: (exitCode: number, signal: number | undefined) => void;
|
|
141
|
+
}
|
|
142
|
+
export type CreatePtyHostOptions = Omit<PtyHostOptions, "spawn"> & {
|
|
143
|
+
spawn?: PtySpawnFn;
|
|
144
|
+
resolveOptions?: ResolveNodePtyOptions;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Create a host, resolving node-pty unless the caller injects a backend.
|
|
148
|
+
* Throws PtyHostUnavailableError (message = install hint) when no PTY
|
|
149
|
+
* binary resolves; callers fall back to the plain terminal path.
|
|
150
|
+
*/
|
|
151
|
+
export declare function createPtyHost(options: CreatePtyHostOptions): PtyHost;
|
|
152
|
+
/** Owner callback receiving PTY master output bytes verbatim. */
|
|
153
|
+
export type PtyOwnerCallback = (data: string) => void;
|
|
154
|
+
/** PTY child exit listener. */
|
|
155
|
+
export type PtyExitCallback = (exitCode: number, signal: number | undefined) => void;
|
|
156
|
+
/**
|
|
157
|
+
* In-process owner of one PTY-hosted TUI session. Byte relay is fully
|
|
158
|
+
* transparent in both directions: the host never interprets owner input
|
|
159
|
+
* (Ctrl+C arrives as 0x03, escape sequences pass through untouched) and
|
|
160
|
+
* forwards child output verbatim. Raw-mode passthrough mirrors the owner
|
|
161
|
+
* raw state to backends that support it; it never gates the byte stream.
|
|
162
|
+
*/
|
|
163
|
+
export declare class PtyHost {
|
|
164
|
+
private readonly backend;
|
|
165
|
+
private readonly journalLimit;
|
|
166
|
+
private owner;
|
|
167
|
+
private readonly exitListeners;
|
|
168
|
+
private colsValue;
|
|
169
|
+
private rowsValue;
|
|
170
|
+
private rawModeValue;
|
|
171
|
+
private disposedValue;
|
|
172
|
+
private exitedValue;
|
|
173
|
+
private exitCodeValue;
|
|
174
|
+
private journal;
|
|
175
|
+
private journalBytes;
|
|
176
|
+
private detachChunkIndex;
|
|
177
|
+
private detachOverflow;
|
|
178
|
+
constructor(options: PtyHostOptions);
|
|
179
|
+
get cols(): number;
|
|
180
|
+
get rows(): number;
|
|
181
|
+
get pid(): number;
|
|
182
|
+
get isAttached(): boolean;
|
|
183
|
+
get isDetached(): boolean;
|
|
184
|
+
get isDisposed(): boolean;
|
|
185
|
+
get exited(): boolean;
|
|
186
|
+
get exitCode(): number | undefined;
|
|
187
|
+
get rawMode(): boolean;
|
|
188
|
+
/** Initial owner attach. Throws when already attached or disposed. */
|
|
189
|
+
attach(owner: PtyOwnerCallback): void;
|
|
190
|
+
/** Owner disconnects; the PTY session keeps running and output is journaled. Idempotent. */
|
|
191
|
+
detach(): void;
|
|
192
|
+
/**
|
|
193
|
+
* New owner attaches and immediately receives everything journaled
|
|
194
|
+
* while detached (the current screen state, best-effort). A same-dims
|
|
195
|
+
* resize nudges the child to repaint first. Throws when an owner is
|
|
196
|
+
* already attached or the host is disposed.
|
|
197
|
+
*/
|
|
198
|
+
reattach(owner: PtyOwnerCallback): void;
|
|
199
|
+
/**
|
|
200
|
+
* Owner input reaches the PTY byte-transparent. Dropped while
|
|
201
|
+
* detached (no owner exists to produce input); throws after dispose.
|
|
202
|
+
*/
|
|
203
|
+
write(data: string): void;
|
|
204
|
+
/**
|
|
205
|
+
* Forward owner resize events to the PTY (setWinSize semantics); the
|
|
206
|
+
* stock TUI re-layouts to the new dimensions itself.
|
|
207
|
+
*/
|
|
208
|
+
resize(cols: number, rows: number): void;
|
|
209
|
+
/**
|
|
210
|
+
* Mirror owner raw-mode state to the backend when it supports it.
|
|
211
|
+
* Returns whether the backend honored the request. The byte stream
|
|
212
|
+
* is never gated on this flag.
|
|
213
|
+
*/
|
|
214
|
+
setRawMode(active: boolean): boolean;
|
|
215
|
+
/** Subscribe to PTY child exit. Returns an unsubscribe function. */
|
|
216
|
+
onExit(callback: PtyExitCallback): () => void;
|
|
217
|
+
/** Kill the PTY session and release the owner. Idempotent. */
|
|
218
|
+
dispose(): void;
|
|
219
|
+
private handleBackendData;
|
|
220
|
+
private handleBackendExit;
|
|
221
|
+
private appendJournal;
|
|
222
|
+
private throwIfDisposed;
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=pty-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pty-host.d.ts","sourceRoot":"","sources":["../src/pty-host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PTY host spike (tui-overhaul 1.1-1.5).\n *\n * Spawns the stock ion TUI inside a pseudoterminal and relays bytes\n * bidirectionally between the PTY master and the session owner. The TUI\n * itself stays raw-terminal plus alt-screen and is never rewritten for\n * hosting: this mirrors the Claude Code seam where a separate server hosts\n * the stock TUI under node-pty (see TUI.claude.md section 1.4).\n *\n * Seam rules (pinned by test/pty-host.test.ts):\n * - The core TUI (index.ts, terminal.ts, tui.ts) never imports this module.\n * - node-pty resolves at runtime with prebuilt-first lookup; when no binary\n * is available the caller gets an actionable install hint and the plain\n * terminal path keeps working.\n *\n * Transport decision (task 1.5): in-process handle. A Unix-socket transport\n * would only pay off for reattach-across-process, which no consumer needs\n * yet (the future session-share viewer attaches in-process). Sockets would\n * add framing, auth, and lifecycle management for zero current benefit, so\n * the host is a plain object owning its PTY child. Revisit when an\n * out-of-process attacher arrives.\n *\n * Reattach replay is best-effort byte replay: the host journals PTY output\n * in a bounded ring and, on reattach, nudges the child with a same-dims\n * resize (SIGWINCH repaint, the setWinSize semantic) then replays everything\n * journaled while detached. A terminal-emulator-backed screen dump is\n * future work, not part of this spike.\n *\n * Minimum dimensions stay an open product question: the spike validates\n * positive integers only. Layouts below ~40 columns are known to degrade\n * (the 40x12 golden case pins bounded behavior, not beauty).\n */\n\nimport { createRequire } from \"node:module\";\n\nconst moduleRequire = createRequire(import.meta.url);\n\n/** Transport backing session handles. Pinned to in-process by the task 1.5 decision above. */\nexport const PTY_HOST_TRANSPORT = \"in-process\";\n\n/** Default cap for the reattach replay journal. */\nexport const PTY_HOST_JOURNAL_LIMIT_BYTES = 256 * 1024;\n\n/**\n * Resync prefix used only when the journal overflowed while detached and\n * replay can no longer continue the previous screen state.\n */\nexport const PTY_RESYNC_SEQUENCE = \"\\x1b[2J\\x1b[H\";\n\n/** Caller-specified PTY dimensions in character cells. */\nexport interface PtyDimensions {\n\tcols: number;\n\trows: number;\n}\n\n/** Throw unless cols/rows are positive integers. Integers only: fractional cells do not exist. */\nexport function assertPtyDimensions(cols: number, rows: number): void {\n\tif (!Number.isInteger(cols) || cols < 1) {\n\t\tthrow new RangeError(`PTY cols must be a positive integer, got ${cols}`);\n\t}\n\tif (!Number.isInteger(rows) || rows < 1) {\n\t\tthrow new RangeError(`PTY rows must be a positive integer, got ${rows}`);\n\t}\n}\n\n/**\n * Minimal structural backend over a PTY child. Matches the node-pty\n * surface the host needs without depending on node-pty types, so tests\n * inject fakes and the core TUI never sees this module.\n */\nexport interface PtyBackend {\n\treadonly pid: number;\n\tonData(callback: (data: string) => void): void;\n\tonExit(callback: (exitCode: number, signal: number | undefined) => void): void;\n\twrite(data: string): void;\n\tresize(cols: number, rows: number): void;\n\tkill(signal?: string): void;\n\t/** Optional raw-mode mirror; real PTY slaves are raw by default so most backends omit this. */\n\tsetRawMode?(active: boolean): void;\n}\n\n/** Options passed to a backend spawn function. */\nexport interface PtySpawnOptions {\n\tcommand: string;\n\targs: string[];\n\tcols: number;\n\trows: number;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n\tname: string;\n}\n\n/** Injectable backend factory. Tests pass fakes; production passes the node-pty adapter. */\nexport type PtySpawnFn = (options: PtySpawnOptions) => PtyBackend;\n\n/** Minimal structural view of the node-pty module (spawn only). */\nexport interface NodePtyModule {\n\tspawn(\n\t\tfile: string,\n\t\targs: string[],\n\t\toptions: { name: string; cols: number; rows: number; cwd: string; env: NodeJS.ProcessEnv },\n\t): {\n\t\tpid: number;\n\t\tonData(callback: (data: string) => void): void;\n\t\tonExit(callback: (event: { exitCode: number; signal?: number }) => void): void;\n\t\twrite(data: string): void;\n\t\tresize(cols: number, rows: number): void;\n\t\tkill(signal?: string): void;\n\t};\n}\n\n/** node-pty resolved and ready: prebuilt-first lookup hit. */\nexport interface PtyResolutionSuccess {\n\tavailable: true;\n\t/** Which specifier resolved (explicit ION_PTY_MODULE override or \"node-pty\"). */\n\tsource: string;\n\tspawn: PtySpawnFn;\n}\n\n/** node-pty unavailable: hint carries the actionable install instructions. */\nexport interface PtyResolutionFailure {\n\tavailable: false;\n\thint: string;\n}\n\nexport type PtyResolution = PtyResolutionSuccess | PtyResolutionFailure;\n\nexport interface ResolveNodePtyOptions {\n\t/** Injectable loader for tests. Defaults to require relative to this module. */\n\tloadModule?: (specifier: string) => unknown;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction defaultLoadModule(specifier: string): unknown {\n\treturn moduleRequire(specifier);\n}\n\nfunction buildNodePtyHint(failures: string[]): string {\n\tconst lines = [\n\t\t\"node-pty is unavailable and the PTY host cannot start.\",\n\t\t\"Install it with `npm install node-pty` (prebuilt binaries are used when available;\",\n\t\t\"building from source needs python3, make, and g++).\",\n\t\t\"Set ION_PTY_MODULE to load node-pty from a custom path.\",\n\t\t\"The plain terminal path keeps working without it.\",\n\t];\n\tif (failures.length > 0) {\n\t\tlines.push(`Looked up: ${failures.join(\"; \")}`);\n\t}\n\treturn lines.join(\" \");\n}\n\nfunction adaptNodePty(moduleRef: NodePtyModule, options: PtySpawnOptions): PtyBackend {\n\tconst child = moduleRef.spawn(options.command, options.args, {\n\t\tname: options.name,\n\t\tcols: options.cols,\n\t\trows: options.rows,\n\t\tcwd: options.cwd,\n\t\tenv: options.env,\n\t});\n\treturn {\n\t\tpid: child.pid,\n\t\tonData: (callback) => child.onData(callback),\n\t\tonExit: (callback) => child.onExit((event) => callback(event.exitCode, event.signal)),\n\t\twrite: (data) => child.write(data),\n\t\tresize: (cols, rows) => child.resize(cols, rows),\n\t\tkill: (signal) => child.kill(signal),\n\t};\n}\n\n/**\n * Resolve node-pty at runtime, prebuilt-first (node-pty's own packaging\n * prefers prebuilds via prebuild-install). Lookup order: explicit\n * ION_PTY_MODULE override path first, then the \"node-pty\" package.\n * Never throws: failure returns an actionable hint instead.\n */\nexport function resolveNodePty(options: ResolveNodePtyOptions = {}): PtyResolution {\n\tconst loadModule = options.loadModule ?? defaultLoadModule;\n\tconst env = options.env ?? process.env;\n\tconst override = env.ION_PTY_MODULE?.trim();\n\tconst candidates = override ? [override, \"node-pty\"] : [\"node-pty\"];\n\tconst failures: string[] = [];\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst loaded = loadModule(candidate) as Partial<NodePtyModule> | undefined;\n\t\t\tif (loaded && typeof loaded.spawn === \"function\") {\n\t\t\t\tconst moduleRef = loaded as NodePtyModule;\n\t\t\t\treturn {\n\t\t\t\t\tavailable: true,\n\t\t\t\t\tsource: candidate,\n\t\t\t\t\tspawn: (spawnOptions) => adaptNodePty(moduleRef, spawnOptions),\n\t\t\t\t};\n\t\t\t}\n\t\t\tfailures.push(`${candidate}: module loaded but has no spawn() export`);\n\t\t} catch (error) {\n\t\t\tfailures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t}\n\treturn { available: false, hint: buildNodePtyHint(failures) };\n}\n\n/** Thrown by createPtyHost when node-pty is unavailable. Carries the install hint as message. */\nexport class PtyHostUnavailableError extends Error {\n\tconstructor(hint: string) {\n\t\tsuper(hint);\n\t\tthis.name = \"PtyHostUnavailableError\";\n\t}\n}\n\nexport interface PtyHostOptions {\n\t/** Stock TUI binary to spawn inside the PTY (host never rewrites the TUI). */\n\tcommand: string;\n\targs?: string[];\n\tcols: number;\n\trows: number;\n\tcwd?: string;\n\tenv?: NodeJS.ProcessEnv;\n\t/** TERM value for the PTY slave. */\n\tname?: string;\n\t/** Replay journal cap in bytes. Defaults to PTY_HOST_JOURNAL_LIMIT_BYTES. */\n\tjournalLimitBytes?: number;\n\t/** Injected backend. Production callers omit this and go through createPtyHost. */\n\tspawn: PtySpawnFn;\n\t/** Convenience exit listener, equivalent to onExit(). */\n\tonExit?: (exitCode: number, signal: number | undefined) => void;\n}\n\nexport type CreatePtyHostOptions = Omit<PtyHostOptions, \"spawn\"> & {\n\tspawn?: PtySpawnFn;\n\tresolveOptions?: ResolveNodePtyOptions;\n};\n\nfunction requireSpawn(resolveOptions?: ResolveNodePtyOptions): PtySpawnFn {\n\tconst resolution = resolveNodePty(resolveOptions);\n\tif (!resolution.available) {\n\t\tthrow new PtyHostUnavailableError(resolution.hint);\n\t}\n\treturn resolution.spawn;\n}\n\n/**\n * Create a host, resolving node-pty unless the caller injects a backend.\n * Throws PtyHostUnavailableError (message = install hint) when no PTY\n * binary resolves; callers fall back to the plain terminal path.\n */\nexport function createPtyHost(options: CreatePtyHostOptions): PtyHost {\n\tconst spawn = options.spawn ?? requireSpawn(options.resolveOptions);\n\treturn new PtyHost({ ...options, spawn });\n}\n\n/** Owner callback receiving PTY master output bytes verbatim. */\nexport type PtyOwnerCallback = (data: string) => void;\n\n/** PTY child exit listener. */\nexport type PtyExitCallback = (exitCode: number, signal: number | undefined) => void;\n\n/**\n * In-process owner of one PTY-hosted TUI session. Byte relay is fully\n * transparent in both directions: the host never interprets owner input\n * (Ctrl+C arrives as 0x03, escape sequences pass through untouched) and\n * forwards child output verbatim. Raw-mode passthrough mirrors the owner\n * raw state to backends that support it; it never gates the byte stream.\n */\nexport class PtyHost {\n\tprivate readonly backend: PtyBackend;\n\tprivate readonly journalLimit: number;\n\tprivate owner: PtyOwnerCallback | undefined;\n\tprivate readonly exitListeners = new Set<PtyExitCallback>();\n\tprivate colsValue: number;\n\tprivate rowsValue: number;\n\tprivate rawModeValue = false;\n\tprivate disposedValue = false;\n\tprivate exitedValue = false;\n\tprivate exitCodeValue: number | undefined;\n\tprivate journal: string[] = [];\n\tprivate journalBytes = 0;\n\tprivate detachChunkIndex: number | undefined;\n\tprivate detachOverflow = false;\n\n\tconstructor(options: PtyHostOptions) {\n\t\tassertPtyDimensions(options.cols, options.rows);\n\t\tthis.colsValue = options.cols;\n\t\tthis.rowsValue = options.rows;\n\t\tthis.journalLimit = options.journalLimitBytes ?? PTY_HOST_JOURNAL_LIMIT_BYTES;\n\t\tif (options.onExit) {\n\t\t\tthis.exitListeners.add(options.onExit);\n\t\t}\n\t\tthis.backend = options.spawn({\n\t\t\tcommand: options.command,\n\t\t\targs: options.args ? [...options.args] : [],\n\t\t\tcols: options.cols,\n\t\t\trows: options.rows,\n\t\t\tcwd: options.cwd ?? process.cwd(),\n\t\t\tenv: options.env ?? process.env,\n\t\t\tname: options.name ?? \"xterm-256color\",\n\t\t});\n\t\tthis.backend.onData((data) => this.handleBackendData(data));\n\t\tthis.backend.onExit((exitCode, signal) => this.handleBackendExit(exitCode, signal));\n\t}\n\n\tget cols(): number {\n\t\treturn this.colsValue;\n\t}\n\n\tget rows(): number {\n\t\treturn this.rowsValue;\n\t}\n\n\tget pid(): number {\n\t\treturn this.backend.pid;\n\t}\n\n\tget isAttached(): boolean {\n\t\treturn this.owner !== undefined;\n\t}\n\n\tget isDetached(): boolean {\n\t\treturn !this.disposedValue && this.owner === undefined;\n\t}\n\n\tget isDisposed(): boolean {\n\t\treturn this.disposedValue;\n\t}\n\n\tget exited(): boolean {\n\t\treturn this.exitedValue;\n\t}\n\n\tget exitCode(): number | undefined {\n\t\treturn this.exitCodeValue;\n\t}\n\n\tget rawMode(): boolean {\n\t\treturn this.rawModeValue;\n\t}\n\n\t/** Initial owner attach. Throws when already attached or disposed. */\n\tattach(owner: PtyOwnerCallback): void {\n\t\tthis.throwIfDisposed();\n\t\tif (this.owner) {\n\t\t\tthrow new Error(\"PtyHost already has an owner; detach first\");\n\t\t}\n\t\tthis.owner = owner;\n\t}\n\n\t/** Owner disconnects; the PTY session keeps running and output is journaled. Idempotent. */\n\tdetach(): void {\n\t\tthis.throwIfDisposed();\n\t\tthis.owner = undefined;\n\t\tthis.detachChunkIndex = this.journal.length;\n\t\tthis.detachOverflow = false;\n\t}\n\n\t/**\n\t * New owner attaches and immediately receives everything journaled\n\t * while detached (the current screen state, best-effort). A same-dims\n\t * resize nudges the child to repaint first. Throws when an owner is\n\t * already attached or the host is disposed.\n\t */\n\treattach(owner: PtyOwnerCallback): void {\n\t\tthis.throwIfDisposed();\n\t\tif (this.owner) {\n\t\t\tthrow new Error(\"PtyHost already has an owner; detach first\");\n\t\t}\n\t\tif (this.detachChunkIndex === undefined) {\n\t\t\tthis.owner = owner;\n\t\t\treturn;\n\t\t}\n\t\tconst replay = this.detachOverflow\n\t\t\t? PTY_RESYNC_SEQUENCE + this.journal.join(\"\")\n\t\t\t: this.journal.slice(this.detachChunkIndex).join(\"\");\n\t\tthis.detachChunkIndex = undefined;\n\t\tthis.detachOverflow = false;\n\t\tif (!this.exitedValue) {\n\t\t\tthis.backend.resize(this.colsValue, this.rowsValue);\n\t\t}\n\t\tthis.owner = owner;\n\t\tif (replay.length > 0) {\n\t\t\towner(replay);\n\t\t}\n\t}\n\n\t/**\n\t * Owner input reaches the PTY byte-transparent. Dropped while\n\t * detached (no owner exists to produce input); throws after dispose.\n\t */\n\twrite(data: string): void {\n\t\tthis.throwIfDisposed();\n\t\tif (!this.owner) return;\n\t\tthis.backend.write(data);\n\t}\n\n\t/**\n\t * Forward owner resize events to the PTY (setWinSize semantics); the\n\t * stock TUI re-layouts to the new dimensions itself.\n\t */\n\tresize(cols: number, rows: number): void {\n\t\tthis.throwIfDisposed();\n\t\tassertPtyDimensions(cols, rows);\n\t\tthis.colsValue = cols;\n\t\tthis.rowsValue = rows;\n\t\tif (!this.exitedValue) {\n\t\t\tthis.backend.resize(cols, rows);\n\t\t}\n\t}\n\n\t/**\n\t * Mirror owner raw-mode state to the backend when it supports it.\n\t * Returns whether the backend honored the request. The byte stream\n\t * is never gated on this flag.\n\t */\n\tsetRawMode(active: boolean): boolean {\n\t\tthis.throwIfDisposed();\n\t\tthis.rawModeValue = active;\n\t\tif (typeof this.backend.setRawMode === \"function\") {\n\t\t\tthis.backend.setRawMode(active);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Subscribe to PTY child exit. Returns an unsubscribe function. */\n\tonExit(callback: PtyExitCallback): () => void {\n\t\tthis.exitListeners.add(callback);\n\t\treturn () => {\n\t\t\tthis.exitListeners.delete(callback);\n\t\t};\n\t}\n\n\t/** Kill the PTY session and release the owner. Idempotent. */\n\tdispose(): void {\n\t\tif (this.disposedValue) return;\n\t\tthis.disposedValue = true;\n\t\tthis.owner = undefined;\n\t\tthis.exitListeners.clear();\n\t\ttry {\n\t\t\tthis.backend.kill();\n\t\t} catch {\n\t\t\t// Backend already gone; dispose still releases host state.\n\t\t}\n\t}\n\n\tprivate handleBackendData(data: string): void {\n\t\tthis.appendJournal(data);\n\t\tthis.owner?.(data);\n\t}\n\n\tprivate handleBackendExit(exitCode: number, signal: number | undefined): void {\n\t\tif (this.disposedValue) return;\n\t\tthis.exitedValue = true;\n\t\tthis.exitCodeValue = exitCode;\n\t\tfor (const listener of this.exitListeners) {\n\t\t\tlistener(exitCode, signal);\n\t\t}\n\t}\n\n\tprivate appendJournal(data: string): void {\n\t\tif (this.journalLimit <= 0 || data.length === 0) return;\n\t\tthis.journal.push(data);\n\t\tthis.journalBytes += data.length;\n\t\twhile (this.journal.length > 1 && this.journalBytes > this.journalLimit) {\n\t\t\tconst dropped = this.journal.shift() as string;\n\t\t\tthis.journalBytes -= dropped.length;\n\t\t\tif (this.detachChunkIndex !== undefined) {\n\t\t\t\tif (this.detachChunkIndex > 0) {\n\t\t\t\t\tthis.detachChunkIndex -= 1;\n\t\t\t\t} else {\n\t\t\t\t\tthis.detachOverflow = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate throwIfDisposed(): void {\n\t\tif (this.disposedValue) {\n\t\t\tthrow new Error(\"PtyHost is disposed\");\n\t\t}\n\t}\n}\n"]}
|
package/dist/pty-host.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY host spike (tui-overhaul 1.1-1.5).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the stock ion TUI inside a pseudoterminal and relays bytes
|
|
5
|
+
* bidirectionally between the PTY master and the session owner. The TUI
|
|
6
|
+
* itself stays raw-terminal plus alt-screen and is never rewritten for
|
|
7
|
+
* hosting: this mirrors the Claude Code seam where a separate server hosts
|
|
8
|
+
* the stock TUI under node-pty (see TUI.claude.md section 1.4).
|
|
9
|
+
*
|
|
10
|
+
* Seam rules (pinned by test/pty-host.test.ts):
|
|
11
|
+
* - The core TUI (index.ts, terminal.ts, tui.ts) never imports this module.
|
|
12
|
+
* - node-pty resolves at runtime with prebuilt-first lookup; when no binary
|
|
13
|
+
* is available the caller gets an actionable install hint and the plain
|
|
14
|
+
* terminal path keeps working.
|
|
15
|
+
*
|
|
16
|
+
* Transport decision (task 1.5): in-process handle. A Unix-socket transport
|
|
17
|
+
* would only pay off for reattach-across-process, which no consumer needs
|
|
18
|
+
* yet (the future session-share viewer attaches in-process). Sockets would
|
|
19
|
+
* add framing, auth, and lifecycle management for zero current benefit, so
|
|
20
|
+
* the host is a plain object owning its PTY child. Revisit when an
|
|
21
|
+
* out-of-process attacher arrives.
|
|
22
|
+
*
|
|
23
|
+
* Reattach replay is best-effort byte replay: the host journals PTY output
|
|
24
|
+
* in a bounded ring and, on reattach, nudges the child with a same-dims
|
|
25
|
+
* resize (SIGWINCH repaint, the setWinSize semantic) then replays everything
|
|
26
|
+
* journaled while detached. A terminal-emulator-backed screen dump is
|
|
27
|
+
* future work, not part of this spike.
|
|
28
|
+
*
|
|
29
|
+
* Minimum dimensions stay an open product question: the spike validates
|
|
30
|
+
* positive integers only. Layouts below ~40 columns are known to degrade
|
|
31
|
+
* (the 40x12 golden case pins bounded behavior, not beauty).
|
|
32
|
+
*/
|
|
33
|
+
import { createRequire } from "node:module";
|
|
34
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
35
|
+
/** Transport backing session handles. Pinned to in-process by the task 1.5 decision above. */
|
|
36
|
+
export const PTY_HOST_TRANSPORT = "in-process";
|
|
37
|
+
/** Default cap for the reattach replay journal. */
|
|
38
|
+
export const PTY_HOST_JOURNAL_LIMIT_BYTES = 256 * 1024;
|
|
39
|
+
/**
|
|
40
|
+
* Resync prefix used only when the journal overflowed while detached and
|
|
41
|
+
* replay can no longer continue the previous screen state.
|
|
42
|
+
*/
|
|
43
|
+
export const PTY_RESYNC_SEQUENCE = "\x1b[2J\x1b[H";
|
|
44
|
+
/** Throw unless cols/rows are positive integers. Integers only: fractional cells do not exist. */
|
|
45
|
+
export function assertPtyDimensions(cols, rows) {
|
|
46
|
+
if (!Number.isInteger(cols) || cols < 1) {
|
|
47
|
+
throw new RangeError(`PTY cols must be a positive integer, got ${cols}`);
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isInteger(rows) || rows < 1) {
|
|
50
|
+
throw new RangeError(`PTY rows must be a positive integer, got ${rows}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function defaultLoadModule(specifier) {
|
|
54
|
+
return moduleRequire(specifier);
|
|
55
|
+
}
|
|
56
|
+
function buildNodePtyHint(failures) {
|
|
57
|
+
const lines = [
|
|
58
|
+
"node-pty is unavailable and the PTY host cannot start.",
|
|
59
|
+
"Install it with `npm install node-pty` (prebuilt binaries are used when available;",
|
|
60
|
+
"building from source needs python3, make, and g++).",
|
|
61
|
+
"Set ION_PTY_MODULE to load node-pty from a custom path.",
|
|
62
|
+
"The plain terminal path keeps working without it.",
|
|
63
|
+
];
|
|
64
|
+
if (failures.length > 0) {
|
|
65
|
+
lines.push(`Looked up: ${failures.join("; ")}`);
|
|
66
|
+
}
|
|
67
|
+
return lines.join(" ");
|
|
68
|
+
}
|
|
69
|
+
function adaptNodePty(moduleRef, options) {
|
|
70
|
+
const child = moduleRef.spawn(options.command, options.args, {
|
|
71
|
+
name: options.name,
|
|
72
|
+
cols: options.cols,
|
|
73
|
+
rows: options.rows,
|
|
74
|
+
cwd: options.cwd,
|
|
75
|
+
env: options.env,
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
pid: child.pid,
|
|
79
|
+
onData: (callback) => child.onData(callback),
|
|
80
|
+
onExit: (callback) => child.onExit((event) => callback(event.exitCode, event.signal)),
|
|
81
|
+
write: (data) => child.write(data),
|
|
82
|
+
resize: (cols, rows) => child.resize(cols, rows),
|
|
83
|
+
kill: (signal) => child.kill(signal),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve node-pty at runtime, prebuilt-first (node-pty's own packaging
|
|
88
|
+
* prefers prebuilds via prebuild-install). Lookup order: explicit
|
|
89
|
+
* ION_PTY_MODULE override path first, then the "node-pty" package.
|
|
90
|
+
* Never throws: failure returns an actionable hint instead.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveNodePty(options = {}) {
|
|
93
|
+
const loadModule = options.loadModule ?? defaultLoadModule;
|
|
94
|
+
const env = options.env ?? process.env;
|
|
95
|
+
const override = env.ION_PTY_MODULE?.trim();
|
|
96
|
+
const candidates = override ? [override, "node-pty"] : ["node-pty"];
|
|
97
|
+
const failures = [];
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
try {
|
|
100
|
+
const loaded = loadModule(candidate);
|
|
101
|
+
if (loaded && typeof loaded.spawn === "function") {
|
|
102
|
+
const moduleRef = loaded;
|
|
103
|
+
return {
|
|
104
|
+
available: true,
|
|
105
|
+
source: candidate,
|
|
106
|
+
spawn: (spawnOptions) => adaptNodePty(moduleRef, spawnOptions),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
failures.push(`${candidate}: module loaded but has no spawn() export`);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { available: false, hint: buildNodePtyHint(failures) };
|
|
116
|
+
}
|
|
117
|
+
/** Thrown by createPtyHost when node-pty is unavailable. Carries the install hint as message. */
|
|
118
|
+
export class PtyHostUnavailableError extends Error {
|
|
119
|
+
constructor(hint) {
|
|
120
|
+
super(hint);
|
|
121
|
+
this.name = "PtyHostUnavailableError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function requireSpawn(resolveOptions) {
|
|
125
|
+
const resolution = resolveNodePty(resolveOptions);
|
|
126
|
+
if (!resolution.available) {
|
|
127
|
+
throw new PtyHostUnavailableError(resolution.hint);
|
|
128
|
+
}
|
|
129
|
+
return resolution.spawn;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Create a host, resolving node-pty unless the caller injects a backend.
|
|
133
|
+
* Throws PtyHostUnavailableError (message = install hint) when no PTY
|
|
134
|
+
* binary resolves; callers fall back to the plain terminal path.
|
|
135
|
+
*/
|
|
136
|
+
export function createPtyHost(options) {
|
|
137
|
+
const spawn = options.spawn ?? requireSpawn(options.resolveOptions);
|
|
138
|
+
return new PtyHost({ ...options, spawn });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* In-process owner of one PTY-hosted TUI session. Byte relay is fully
|
|
142
|
+
* transparent in both directions: the host never interprets owner input
|
|
143
|
+
* (Ctrl+C arrives as 0x03, escape sequences pass through untouched) and
|
|
144
|
+
* forwards child output verbatim. Raw-mode passthrough mirrors the owner
|
|
145
|
+
* raw state to backends that support it; it never gates the byte stream.
|
|
146
|
+
*/
|
|
147
|
+
export class PtyHost {
|
|
148
|
+
backend;
|
|
149
|
+
journalLimit;
|
|
150
|
+
owner;
|
|
151
|
+
exitListeners = new Set();
|
|
152
|
+
colsValue;
|
|
153
|
+
rowsValue;
|
|
154
|
+
rawModeValue = false;
|
|
155
|
+
disposedValue = false;
|
|
156
|
+
exitedValue = false;
|
|
157
|
+
exitCodeValue;
|
|
158
|
+
journal = [];
|
|
159
|
+
journalBytes = 0;
|
|
160
|
+
detachChunkIndex;
|
|
161
|
+
detachOverflow = false;
|
|
162
|
+
constructor(options) {
|
|
163
|
+
assertPtyDimensions(options.cols, options.rows);
|
|
164
|
+
this.colsValue = options.cols;
|
|
165
|
+
this.rowsValue = options.rows;
|
|
166
|
+
this.journalLimit = options.journalLimitBytes ?? PTY_HOST_JOURNAL_LIMIT_BYTES;
|
|
167
|
+
if (options.onExit) {
|
|
168
|
+
this.exitListeners.add(options.onExit);
|
|
169
|
+
}
|
|
170
|
+
this.backend = options.spawn({
|
|
171
|
+
command: options.command,
|
|
172
|
+
args: options.args ? [...options.args] : [],
|
|
173
|
+
cols: options.cols,
|
|
174
|
+
rows: options.rows,
|
|
175
|
+
cwd: options.cwd ?? process.cwd(),
|
|
176
|
+
env: options.env ?? process.env,
|
|
177
|
+
name: options.name ?? "xterm-256color",
|
|
178
|
+
});
|
|
179
|
+
this.backend.onData((data) => this.handleBackendData(data));
|
|
180
|
+
this.backend.onExit((exitCode, signal) => this.handleBackendExit(exitCode, signal));
|
|
181
|
+
}
|
|
182
|
+
get cols() {
|
|
183
|
+
return this.colsValue;
|
|
184
|
+
}
|
|
185
|
+
get rows() {
|
|
186
|
+
return this.rowsValue;
|
|
187
|
+
}
|
|
188
|
+
get pid() {
|
|
189
|
+
return this.backend.pid;
|
|
190
|
+
}
|
|
191
|
+
get isAttached() {
|
|
192
|
+
return this.owner !== undefined;
|
|
193
|
+
}
|
|
194
|
+
get isDetached() {
|
|
195
|
+
return !this.disposedValue && this.owner === undefined;
|
|
196
|
+
}
|
|
197
|
+
get isDisposed() {
|
|
198
|
+
return this.disposedValue;
|
|
199
|
+
}
|
|
200
|
+
get exited() {
|
|
201
|
+
return this.exitedValue;
|
|
202
|
+
}
|
|
203
|
+
get exitCode() {
|
|
204
|
+
return this.exitCodeValue;
|
|
205
|
+
}
|
|
206
|
+
get rawMode() {
|
|
207
|
+
return this.rawModeValue;
|
|
208
|
+
}
|
|
209
|
+
/** Initial owner attach. Throws when already attached or disposed. */
|
|
210
|
+
attach(owner) {
|
|
211
|
+
this.throwIfDisposed();
|
|
212
|
+
if (this.owner) {
|
|
213
|
+
throw new Error("PtyHost already has an owner; detach first");
|
|
214
|
+
}
|
|
215
|
+
this.owner = owner;
|
|
216
|
+
}
|
|
217
|
+
/** Owner disconnects; the PTY session keeps running and output is journaled. Idempotent. */
|
|
218
|
+
detach() {
|
|
219
|
+
this.throwIfDisposed();
|
|
220
|
+
this.owner = undefined;
|
|
221
|
+
this.detachChunkIndex = this.journal.length;
|
|
222
|
+
this.detachOverflow = false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* New owner attaches and immediately receives everything journaled
|
|
226
|
+
* while detached (the current screen state, best-effort). A same-dims
|
|
227
|
+
* resize nudges the child to repaint first. Throws when an owner is
|
|
228
|
+
* already attached or the host is disposed.
|
|
229
|
+
*/
|
|
230
|
+
reattach(owner) {
|
|
231
|
+
this.throwIfDisposed();
|
|
232
|
+
if (this.owner) {
|
|
233
|
+
throw new Error("PtyHost already has an owner; detach first");
|
|
234
|
+
}
|
|
235
|
+
if (this.detachChunkIndex === undefined) {
|
|
236
|
+
this.owner = owner;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const replay = this.detachOverflow
|
|
240
|
+
? PTY_RESYNC_SEQUENCE + this.journal.join("")
|
|
241
|
+
: this.journal.slice(this.detachChunkIndex).join("");
|
|
242
|
+
this.detachChunkIndex = undefined;
|
|
243
|
+
this.detachOverflow = false;
|
|
244
|
+
if (!this.exitedValue) {
|
|
245
|
+
this.backend.resize(this.colsValue, this.rowsValue);
|
|
246
|
+
}
|
|
247
|
+
this.owner = owner;
|
|
248
|
+
if (replay.length > 0) {
|
|
249
|
+
owner(replay);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Owner input reaches the PTY byte-transparent. Dropped while
|
|
254
|
+
* detached (no owner exists to produce input); throws after dispose.
|
|
255
|
+
*/
|
|
256
|
+
write(data) {
|
|
257
|
+
this.throwIfDisposed();
|
|
258
|
+
if (!this.owner)
|
|
259
|
+
return;
|
|
260
|
+
this.backend.write(data);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Forward owner resize events to the PTY (setWinSize semantics); the
|
|
264
|
+
* stock TUI re-layouts to the new dimensions itself.
|
|
265
|
+
*/
|
|
266
|
+
resize(cols, rows) {
|
|
267
|
+
this.throwIfDisposed();
|
|
268
|
+
assertPtyDimensions(cols, rows);
|
|
269
|
+
this.colsValue = cols;
|
|
270
|
+
this.rowsValue = rows;
|
|
271
|
+
if (!this.exitedValue) {
|
|
272
|
+
this.backend.resize(cols, rows);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Mirror owner raw-mode state to the backend when it supports it.
|
|
277
|
+
* Returns whether the backend honored the request. The byte stream
|
|
278
|
+
* is never gated on this flag.
|
|
279
|
+
*/
|
|
280
|
+
setRawMode(active) {
|
|
281
|
+
this.throwIfDisposed();
|
|
282
|
+
this.rawModeValue = active;
|
|
283
|
+
if (typeof this.backend.setRawMode === "function") {
|
|
284
|
+
this.backend.setRawMode(active);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
/** Subscribe to PTY child exit. Returns an unsubscribe function. */
|
|
290
|
+
onExit(callback) {
|
|
291
|
+
this.exitListeners.add(callback);
|
|
292
|
+
return () => {
|
|
293
|
+
this.exitListeners.delete(callback);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Kill the PTY session and release the owner. Idempotent. */
|
|
297
|
+
dispose() {
|
|
298
|
+
if (this.disposedValue)
|
|
299
|
+
return;
|
|
300
|
+
this.disposedValue = true;
|
|
301
|
+
this.owner = undefined;
|
|
302
|
+
this.exitListeners.clear();
|
|
303
|
+
try {
|
|
304
|
+
this.backend.kill();
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Backend already gone; dispose still releases host state.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
handleBackendData(data) {
|
|
311
|
+
this.appendJournal(data);
|
|
312
|
+
this.owner?.(data);
|
|
313
|
+
}
|
|
314
|
+
handleBackendExit(exitCode, signal) {
|
|
315
|
+
if (this.disposedValue)
|
|
316
|
+
return;
|
|
317
|
+
this.exitedValue = true;
|
|
318
|
+
this.exitCodeValue = exitCode;
|
|
319
|
+
for (const listener of this.exitListeners) {
|
|
320
|
+
listener(exitCode, signal);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
appendJournal(data) {
|
|
324
|
+
if (this.journalLimit <= 0 || data.length === 0)
|
|
325
|
+
return;
|
|
326
|
+
this.journal.push(data);
|
|
327
|
+
this.journalBytes += data.length;
|
|
328
|
+
while (this.journal.length > 1 && this.journalBytes > this.journalLimit) {
|
|
329
|
+
const dropped = this.journal.shift();
|
|
330
|
+
this.journalBytes -= dropped.length;
|
|
331
|
+
if (this.detachChunkIndex !== undefined) {
|
|
332
|
+
if (this.detachChunkIndex > 0) {
|
|
333
|
+
this.detachChunkIndex -= 1;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.detachOverflow = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throwIfDisposed() {
|
|
342
|
+
if (this.disposedValue) {
|
|
343
|
+
throw new Error("PtyHost is disposed");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=pty-host.js.map
|