@f5-sales-demo/xcsh 19.51.4 → 19.51.6
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 +5 -0
- package/package.json +8 -8
- package/src/browser/extension-bridge.ts +66 -59
- package/src/browser/provider.ts +3 -14
- package/src/cli/chrome-cli.ts +7 -145
- package/src/cli.ts +2 -10
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/context-add-wizard.ts +111 -12
- package/src/commands/chrome-host.ts +0 -97
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [19.51.5] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Sign + notarize the embedded native addon (`pi_natives.*.node`) in `build-sign-macos` **before** it is embedded into the compiled binary. xcsh extracts the embedded `.node` to `~/.xcsh/natives/<ver>/` at runtime; previously that extracted copy was ad-hoc-signed, so managed/MDM Macs blocked the `dlopen` ("could not verify it is free of malware"). The addon is now Developer-ID-signed with hardened runtime and notarized, so Gatekeeper's online check passes it without manual approval.
|
|
9
|
+
|
|
5
10
|
## [19.51.4] - 2026-06-26
|
|
6
11
|
|
|
7
12
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.51.
|
|
4
|
+
"version": "19.51.6",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
56
56
|
"@mozilla/readability": "^0.6",
|
|
57
|
-
"@f5-sales-demo/xcsh-stats": "19.51.
|
|
58
|
-
"@f5-sales-demo/pi-agent-core": "19.51.
|
|
59
|
-
"@f5-sales-demo/pi-ai": "19.51.
|
|
60
|
-
"@f5-sales-demo/pi-natives": "19.51.
|
|
61
|
-
"@f5-sales-demo/pi-resource-management": "19.51.
|
|
62
|
-
"@f5-sales-demo/pi-tui": "19.51.
|
|
63
|
-
"@f5-sales-demo/pi-utils": "19.51.
|
|
57
|
+
"@f5-sales-demo/xcsh-stats": "19.51.4",
|
|
58
|
+
"@f5-sales-demo/pi-agent-core": "19.51.4",
|
|
59
|
+
"@f5-sales-demo/pi-ai": "19.51.4",
|
|
60
|
+
"@f5-sales-demo/pi-natives": "19.51.4",
|
|
61
|
+
"@f5-sales-demo/pi-resource-management": "19.51.4",
|
|
62
|
+
"@f5-sales-demo/pi-tui": "19.51.4",
|
|
63
|
+
"@f5-sales-demo/pi-utils": "19.51.6",
|
|
64
64
|
"@sinclair/typebox": "^0.34",
|
|
65
65
|
"@xterm/headless": "^6.0",
|
|
66
66
|
"ajv": "^8.20",
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import type { Socket, UnixSocketListener } from "bun";
|
|
2
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
6
3
|
|
|
7
4
|
export interface ToolResult {
|
|
8
5
|
content: unknown;
|
|
@@ -59,21 +56,36 @@ export class PendingRequests {
|
|
|
59
56
|
|
|
60
57
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
/** Default loopback port for the extension WebSocket bridge. */
|
|
60
|
+
export const DEFAULT_PORT = 19222;
|
|
61
|
+
|
|
62
|
+
/** Resolve the bridge port from an explicit value, then `XCSH_BRIDGE_PORT`, then the default. */
|
|
63
|
+
export function resolvePort(port?: number): number {
|
|
64
|
+
if (typeof port === "number" && Number.isFinite(port) && port > 0) return port;
|
|
65
|
+
const env = Number(process.env.XCSH_BRIDGE_PORT);
|
|
66
|
+
if (Number.isFinite(env) && env > 0) return env;
|
|
67
|
+
return DEFAULT_PORT;
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
71
|
+
* Loopback WebSocket server bridging xcsh to the Chrome extension. Speaks JSON
|
|
72
|
+
* over WS frames: requests `{type:"tool_request",...}`, replies
|
|
73
|
+
* `{type:"tool_result",...}`, plus `{type:"ping"|"pong"}`. Tracks a single
|
|
74
|
+
* connected client (a new connection replaces the prior one) and correlates
|
|
75
|
+
* replies via {@link PendingRequests}.
|
|
69
76
|
*/
|
|
70
77
|
export class BridgeServer {
|
|
71
78
|
#pending = new PendingRequests();
|
|
72
|
-
#
|
|
73
|
-
#client:
|
|
79
|
+
#server: Server<undefined> | null = null;
|
|
80
|
+
#client: ServerWebSocket<undefined> | null = null;
|
|
74
81
|
#onConnected: Array<() => void> = [];
|
|
75
82
|
#onDisconnected: Array<() => void> = [];
|
|
76
83
|
|
|
84
|
+
/** The port the WebSocket server is listening on (0 = not bound). */
|
|
85
|
+
get port(): number {
|
|
86
|
+
return this.#server?.port ?? 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
get connected(): boolean {
|
|
78
90
|
return this.#client !== null;
|
|
79
91
|
}
|
|
@@ -86,44 +98,47 @@ export class BridgeServer {
|
|
|
86
98
|
this.#onDisconnected.push(cb);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
/** Bind the server to a
|
|
90
|
-
listen(
|
|
91
|
-
this.#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
/** Bind the WebSocket server to a loopback port. Called by {@link startBridgeServer}. */
|
|
102
|
+
listen(port: number, opts?: { skipOriginCheck?: boolean }): void {
|
|
103
|
+
this.#server = Bun.serve({
|
|
104
|
+
port,
|
|
105
|
+
hostname: "127.0.0.1",
|
|
106
|
+
fetch: (req, server) => {
|
|
107
|
+
// Validate the Origin header: only the xcsh Chrome extension may connect.
|
|
108
|
+
// This restores the access-control guarantee that the Unix socket's 0o600
|
|
109
|
+
// permissions previously provided.
|
|
110
|
+
if (!opts?.skipOriginCheck) {
|
|
111
|
+
const origin = req.headers.get("origin") ?? "";
|
|
112
|
+
const { EXTENSION_ID } = require("../cli/chrome-cli");
|
|
113
|
+
if (origin !== `chrome-extension://${EXTENSION_ID}`) {
|
|
114
|
+
return new Response("Forbidden", { status: 403 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (server.upgrade(req)) return undefined;
|
|
118
|
+
return new Response("xcsh bridge: WebSocket only", { status: 426 });
|
|
119
|
+
},
|
|
120
|
+
websocket: {
|
|
121
|
+
open: ws => {
|
|
122
|
+
// One client at a time: close any prior connection on a new connect.
|
|
123
|
+
if (this.#client && this.#client !== ws) this.#client.close();
|
|
124
|
+
this.#client = ws;
|
|
97
125
|
for (const cb of this.#onConnected) cb();
|
|
98
126
|
},
|
|
99
|
-
|
|
100
|
-
this.#
|
|
101
|
-
},
|
|
102
|
-
close: socket => {
|
|
103
|
-
this.#onClose(socket);
|
|
127
|
+
message: (ws, message) => {
|
|
128
|
+
this.#handleMessage(ws, message);
|
|
104
129
|
},
|
|
105
|
-
|
|
106
|
-
this.#onClose(
|
|
130
|
+
close: ws => {
|
|
131
|
+
this.#onClose(ws);
|
|
107
132
|
},
|
|
108
133
|
},
|
|
109
134
|
});
|
|
110
135
|
}
|
|
111
136
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
let idx = socket.data.buf.indexOf("\n");
|
|
115
|
-
while (idx !== -1) {
|
|
116
|
-
const line = socket.data.buf.slice(0, idx);
|
|
117
|
-
socket.data.buf = socket.data.buf.slice(idx + 1);
|
|
118
|
-
if (line.length > 0) this.#handleLine(socket, line);
|
|
119
|
-
idx = socket.data.buf.indexOf("\n");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
#handleLine(socket: Socket<BridgeData>, line: string): void {
|
|
137
|
+
#handleMessage(ws: ServerWebSocket<undefined>, message: string | Buffer): void {
|
|
138
|
+
const text = typeof message === "string" ? message : message.toString("utf8");
|
|
124
139
|
let msg: { type?: string; id?: string; content?: unknown; is_error?: boolean };
|
|
125
140
|
try {
|
|
126
|
-
msg = JSON.parse(
|
|
141
|
+
msg = JSON.parse(text);
|
|
127
142
|
} catch {
|
|
128
143
|
return;
|
|
129
144
|
}
|
|
@@ -133,50 +148,42 @@ export class BridgeServer {
|
|
|
133
148
|
is_error: msg.is_error === true,
|
|
134
149
|
});
|
|
135
150
|
} else if (msg.type === "ping") {
|
|
136
|
-
|
|
151
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
#onClose(
|
|
141
|
-
if (this.#client !==
|
|
155
|
+
#onClose(ws: ServerWebSocket<undefined>): void {
|
|
156
|
+
if (this.#client !== ws) return;
|
|
142
157
|
this.#client = null;
|
|
143
158
|
this.#pending.rejectAll(new Error("bridge client disconnected"));
|
|
144
159
|
for (const cb of this.#onDisconnected) cb();
|
|
145
160
|
}
|
|
146
161
|
|
|
147
|
-
#write(socket: Socket<BridgeData>, msg: unknown): void {
|
|
148
|
-
socket.write(`${JSON.stringify(msg)}\n`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
162
|
/** Send a `tool_request` to the connected client and await its `tool_result`. */
|
|
152
163
|
request(tool: string, params: unknown, timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise<ToolResult> {
|
|
153
164
|
const client = this.#client;
|
|
154
165
|
if (!client) return Promise.reject(new Error("bridge: no client connected"));
|
|
155
166
|
const { id, promise } = this.#pending.create(timeoutMs);
|
|
156
|
-
|
|
167
|
+
client.send(JSON.stringify({ type: "tool_request", id, tool, params }));
|
|
157
168
|
return promise;
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
async close(): Promise<void> {
|
|
161
172
|
this.#pending.rejectAll(new Error("bridge server closed"));
|
|
162
|
-
this.#client?.
|
|
173
|
+
this.#client?.close();
|
|
163
174
|
this.#client = null;
|
|
164
|
-
this.#
|
|
165
|
-
this.#
|
|
175
|
+
this.#server?.stop(true);
|
|
176
|
+
this.#server = null;
|
|
166
177
|
}
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
181
|
+
* Start the {@link BridgeServer} on the resolved loopback port (explicit arg,
|
|
182
|
+
* then `XCSH_BRIDGE_PORT`, then {@link DEFAULT_PORT}). The WebSocket transport
|
|
183
|
+
* needs no filesystem setup — Chrome connects directly to `ws://127.0.0.1:<port>`.
|
|
173
184
|
*/
|
|
174
|
-
export async function startBridgeServer(
|
|
175
|
-
const resolved = socketPath ?? path.join(os.homedir(), ".xcsh", "chrome-bridge.sock");
|
|
176
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
177
|
-
fs.rmSync(resolved, { force: true });
|
|
185
|
+
export async function startBridgeServer(port?: number, opts?: { skipOriginCheck?: boolean }): Promise<BridgeServer> {
|
|
178
186
|
const server = new BridgeServer();
|
|
179
|
-
server.listen(
|
|
180
|
-
fs.chmodSync(resolved, 0o600);
|
|
187
|
+
server.listen(resolvePort(port), opts);
|
|
181
188
|
return server;
|
|
182
189
|
}
|
package/src/browser/provider.ts
CHANGED
|
@@ -130,17 +130,6 @@ export async function selectProvider(
|
|
|
130
130
|
|
|
131
131
|
if (forced === "cdp") return new CdpBrowserProvider(settings);
|
|
132
132
|
|
|
133
|
-
// Transparent, idempotent native-host install: ensure the native-messaging
|
|
134
|
-
// manifest is present + correct on every init so the extension can connect.
|
|
135
|
-
// Removes the need for a manual `xcsh chrome setup` step. Best-effort — any
|
|
136
|
-
// failure surfaces as the actionable "extension not connected" error below.
|
|
137
|
-
try {
|
|
138
|
-
const { ensureNativeHostInstalled } = await import("../cli/chrome-cli");
|
|
139
|
-
ensureNativeHostInstalled();
|
|
140
|
-
} catch {
|
|
141
|
-
/* best-effort; never block init on setup */
|
|
142
|
-
}
|
|
143
|
-
|
|
144
133
|
try {
|
|
145
134
|
const server = opts?.bridgeServer ?? (await startBridgeServer());
|
|
146
135
|
const deadline = Date.now() + probeMs;
|
|
@@ -152,13 +141,13 @@ export async function selectProvider(
|
|
|
152
141
|
}
|
|
153
142
|
if (forced === "extension") {
|
|
154
143
|
// Don't tear down the bridge or fall back — fail with an actionable
|
|
155
|
-
// install path.
|
|
156
|
-
//
|
|
144
|
+
// install path. A no-connect almost always means the extension itself
|
|
145
|
+
// isn't installed/enabled (the bridge listens on loopback already).
|
|
157
146
|
const { WEB_STORE_URL } = await import("../cli/chrome-cli");
|
|
158
147
|
throw new Error(
|
|
159
148
|
`The xcsh Chrome extension is not connected, so deterministic console automation cannot run. ` +
|
|
160
149
|
`Install it from the Chrome Web Store and keep it enabled:\n ${WEB_STORE_URL}\n` +
|
|
161
|
-
`Then reload Chrome and retry. (Waited ${probeMs}ms
|
|
150
|
+
`Then reload Chrome and retry. (Waited ${probeMs}ms.)`,
|
|
162
151
|
);
|
|
163
152
|
}
|
|
164
153
|
await server.close();
|
package/src/cli/chrome-cli.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chrome CLI command handlers.
|
|
3
3
|
*
|
|
4
|
-
* Backs both `xcsh chrome [status|relaunch]` and the `/chrome` REPL slash
|
|
4
|
+
* Backs both `xcsh chrome [status|relaunch|setup]` and the `/chrome` REPL slash
|
|
5
5
|
* command. `renderStatus` is a pure formatter so it can be unit-tested without
|
|
6
6
|
* touching Chrome, settings, or the network.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import * as fs from "node:fs";
|
|
10
|
-
import * as os from "node:os";
|
|
11
|
-
import * as path from "node:path";
|
|
12
9
|
import { acquirePage, type BrowserProviderStatus, CdpBrowserProvider } from "../browser";
|
|
10
|
+
import { resolvePort } from "../browser/extension-bridge";
|
|
13
11
|
|
|
14
12
|
type Settings = { get(key: string): unknown };
|
|
15
13
|
|
|
16
14
|
export type ChromeAction = "status" | "relaunch" | "setup";
|
|
17
15
|
|
|
18
16
|
export const EXTENSION_ID = "klajkjdoehjidngligegnpknogmjjhkc";
|
|
19
|
-
export const EXTENSION_IDS = [EXTENSION_ID];
|
|
20
17
|
|
|
21
18
|
/**
|
|
22
19
|
* Baked-in Chrome Web Store URL for the xcsh console-automation extension.
|
|
@@ -25,141 +22,6 @@ export const EXTENSION_IDS = [EXTENSION_ID];
|
|
|
25
22
|
*/
|
|
26
23
|
export const WEB_STORE_URL = `https://chromewebstore.google.com/detail/${EXTENSION_ID}`;
|
|
27
24
|
|
|
28
|
-
const NATIVE_HOST_NAME = "com.xcsh.xcsh.chrome_host";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* How Chrome should invoke the native-messaging relay. A compiled `xcsh`
|
|
32
|
-
* binary resolves `chrome-host` as a subcommand directly (path=xcsh,
|
|
33
|
-
* args=["chrome-host"]). Under a dev run (`bun src/cli.ts …`) `process.execPath`
|
|
34
|
-
* is `bun`, which needs the entry script path to resolve the subcommand —
|
|
35
|
-
* otherwise Chrome launches `bun chrome-host`, which fails, and the bridge never
|
|
36
|
-
* comes up. This makes the manifest correct for BOTH invocations.
|
|
37
|
-
*/
|
|
38
|
-
/** Find a compiled `xcsh` binary on PATH (self-contained; survives Chrome's stripped env). */
|
|
39
|
-
function defaultResolveXcshBin(): string | null {
|
|
40
|
-
const dirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
41
|
-
for (const dir of dirs) {
|
|
42
|
-
const candidate = path.join(dir, "xcsh");
|
|
43
|
-
try {
|
|
44
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
45
|
-
return candidate;
|
|
46
|
-
} catch {
|
|
47
|
-
/* not on this PATH entry */
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function nativeHostInvocation(
|
|
54
|
-
argv = process.argv,
|
|
55
|
-
execPath = process.execPath,
|
|
56
|
-
resolveXcshBin: () => string | null = defaultResolveXcshBin,
|
|
57
|
-
): {
|
|
58
|
-
binPath: string;
|
|
59
|
-
args: string[];
|
|
60
|
-
} {
|
|
61
|
-
const base = path.basename(execPath).toLowerCase();
|
|
62
|
-
if (base.startsWith("bun")) {
|
|
63
|
-
// Chrome launches native hosts with a stripped environment; `bun <entry>.ts`
|
|
64
|
-
// then fails dependency resolution and the host exits immediately ("Native
|
|
65
|
-
// host has exited"). A compiled, self-contained xcsh binary survives — so
|
|
66
|
-
// prefer it for the relay whenever one is resolvable on PATH.
|
|
67
|
-
const xcsh = resolveXcshBin();
|
|
68
|
-
if (xcsh) return { binPath: xcsh, args: ["chrome-host"] };
|
|
69
|
-
const script = argv[1];
|
|
70
|
-
if (script && (script.endsWith(".ts") || script.endsWith(".js"))) {
|
|
71
|
-
return { binPath: execPath, args: [script, "chrome-host"] };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { binPath: execPath, args: ["chrome-host"] };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Idempotently ensure the native-messaging host manifest is installed and
|
|
79
|
-
* correct. Safe to call on every console-automation init — it only writes when
|
|
80
|
-
* the manifest is missing or differs, so it's transparent to the user and
|
|
81
|
-
* removes the need to run `xcsh chrome setup` by hand. Returns whether a write
|
|
82
|
-
* occurred and the manifest path.
|
|
83
|
-
*/
|
|
84
|
-
export function ensureNativeHostInstalled(opts?: {
|
|
85
|
-
platform?: NodeJS.Platform;
|
|
86
|
-
home?: string;
|
|
87
|
-
argv?: string[];
|
|
88
|
-
execPath?: string;
|
|
89
|
-
devExtensionId?: string;
|
|
90
|
-
}): { manifestPath: string; changed: boolean } {
|
|
91
|
-
const platform = opts?.platform ?? process.platform;
|
|
92
|
-
const home = opts?.home ?? os.homedir();
|
|
93
|
-
if (platform === "win32") return { manifestPath: "", changed: false };
|
|
94
|
-
const { binPath, args } = nativeHostInvocation(opts?.argv, opts?.execPath);
|
|
95
|
-
const dir = nativeHostDir(platform, home);
|
|
96
|
-
const manifestPath = path.join(dir, `${NATIVE_HOST_NAME}.json`);
|
|
97
|
-
// Dev affordance: an unpacked/local extension build loads under a different
|
|
98
|
-
// (key- or path-derived) id than the published one. Setting
|
|
99
|
-
// XCSH_DEV_EXTENSION_ID lets the native host also trust that build so you can
|
|
100
|
-
// iterate locally without editing this file. Ignored in normal use.
|
|
101
|
-
const devId = (opts?.devExtensionId ?? process.env.XCSH_DEV_EXTENSION_ID)?.trim();
|
|
102
|
-
const ids = devId && !EXTENSION_IDS.includes(devId) ? [...EXTENSION_IDS, devId] : EXTENSION_IDS;
|
|
103
|
-
const desired = JSON.stringify(
|
|
104
|
-
{
|
|
105
|
-
name: NATIVE_HOST_NAME,
|
|
106
|
-
description: "xcsh Chrome native-messaging host",
|
|
107
|
-
path: binPath,
|
|
108
|
-
args,
|
|
109
|
-
type: "stdio",
|
|
110
|
-
allowed_origins: ids.map(id => `chrome-extension://${id}/`),
|
|
111
|
-
},
|
|
112
|
-
null,
|
|
113
|
-
2,
|
|
114
|
-
);
|
|
115
|
-
let existing: string | null = null;
|
|
116
|
-
try {
|
|
117
|
-
existing = fs.readFileSync(manifestPath, "utf8");
|
|
118
|
-
} catch {
|
|
119
|
-
/* missing — will write */
|
|
120
|
-
}
|
|
121
|
-
if (existing === desired) return { manifestPath, changed: false };
|
|
122
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
123
|
-
fs.writeFileSync(manifestPath, desired);
|
|
124
|
-
return { manifestPath, changed: true };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function nativeHostDir(platform: NodeJS.Platform, home: string): string {
|
|
128
|
-
if (platform === "darwin")
|
|
129
|
-
return path.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
|
|
130
|
-
return path.join(home, ".config", "google-chrome", "NativeMessagingHosts"); // linux (win32 handled separately — out of slice scope; throw)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function writeNativeHostManifest(opts: {
|
|
134
|
-
platform?: NodeJS.Platform;
|
|
135
|
-
home?: string;
|
|
136
|
-
xcshBinPath: string;
|
|
137
|
-
extensionIds: string[];
|
|
138
|
-
write?: (p: string, c: string) => void;
|
|
139
|
-
}): { manifestPath: string } {
|
|
140
|
-
const platform = opts.platform ?? process.platform;
|
|
141
|
-
const home = opts.home ?? os.homedir();
|
|
142
|
-
if (platform === "win32") throw new Error("xcsh chrome setup on Windows is not in the vertical slice");
|
|
143
|
-
const dir = nativeHostDir(platform, home);
|
|
144
|
-
const manifestPath = path.join(dir, `${NATIVE_HOST_NAME}.json`);
|
|
145
|
-
const manifest = {
|
|
146
|
-
name: NATIVE_HOST_NAME,
|
|
147
|
-
description: "xcsh Chrome native-messaging host",
|
|
148
|
-
path: opts.xcshBinPath,
|
|
149
|
-
args: ["chrome-host"],
|
|
150
|
-
type: "stdio",
|
|
151
|
-
allowed_origins: opts.extensionIds.map(id => `chrome-extension://${id}/`),
|
|
152
|
-
};
|
|
153
|
-
const write =
|
|
154
|
-
opts.write ??
|
|
155
|
-
((p, c) => {
|
|
156
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
157
|
-
fs.writeFileSync(p, c);
|
|
158
|
-
});
|
|
159
|
-
write(manifestPath, JSON.stringify(manifest, null, 2));
|
|
160
|
-
return { manifestPath };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
25
|
export function renderStatus(s: BrowserProviderStatus): string {
|
|
164
26
|
const yn = (b: boolean) => (b ? "yes" : "no");
|
|
165
27
|
return [
|
|
@@ -180,12 +42,12 @@ export async function runChromeCommand(action: ChromeAction, settings: Settings)
|
|
|
180
42
|
const provider = new CdpBrowserProvider(settings);
|
|
181
43
|
if (action === "status") return renderStatus(await provider.status());
|
|
182
44
|
if (action === "setup") {
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
const
|
|
186
|
-
const state = changed ? "Installed" : "Verified (already current)";
|
|
45
|
+
// The extension connects directly over a loopback WebSocket — no native-messaging
|
|
46
|
+
// host manifest to install. Report the port so the user can point the extension at it.
|
|
47
|
+
const port = resolvePort();
|
|
187
48
|
return (
|
|
188
|
-
|
|
49
|
+
`The xcsh Chrome extension connects directly to xcsh over a loopback WebSocket on ` +
|
|
50
|
+
`ws://127.0.0.1:${port} (override with XCSH_BRIDGE_PORT).\n` +
|
|
189
51
|
`Install/keep the xcsh Chrome extension from the Web Store, then it can drive your real Chrome:\n ${WEB_STORE_URL}`
|
|
190
52
|
);
|
|
191
53
|
}
|
package/src/cli.ts
CHANGED
|
@@ -52,7 +52,6 @@ const commands: CommandEntry[] = [
|
|
|
52
52
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
53
53
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
54
54
|
{ name: "chrome", load: () => import("./commands/chrome").then(m => m.default) },
|
|
55
|
-
{ name: "chrome-host", load: () => import("./commands/chrome-host").then(m => m.default) },
|
|
56
55
|
{ name: "grep", load: () => import("./commands/grep").then(m => m.default) },
|
|
57
56
|
{ name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
|
|
58
57
|
{ name: "read", load: () => import("./commands/read").then(m => m.default) },
|
|
@@ -110,14 +109,7 @@ if (process.env.XCSH_SMOKE_TEST_SPECS === "1") {
|
|
|
110
109
|
process.exit(domainCount > 0 && categoryCount > 0 ? 0 : 1);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// collectors) for it: the relay never needs a locale, and running full init per
|
|
116
|
-
// launch both wastes work and floods logs with "Applied locale" during reconnect
|
|
117
|
-
// churn. Only the user-facing commands need language discovery.
|
|
118
|
-
if (process.argv[2] !== "chrome-host") {
|
|
119
|
-
const { discoverAndApplyLanguage } = await import("./discovery/language");
|
|
120
|
-
await discoverAndApplyLanguage();
|
|
121
|
-
}
|
|
112
|
+
const { discoverAndApplyLanguage } = await import("./discovery/language");
|
|
113
|
+
await discoverAndApplyLanguage();
|
|
122
114
|
|
|
123
115
|
await runCli(process.argv.slice(2));
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.51.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.51.6",
|
|
21
|
+
"commit": "2a072802e0f6e75cb5f1ada167c64fed52cd39b1",
|
|
22
|
+
"shortCommit": "2a07280",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.51.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
24
|
+
"tag": "v19.51.6",
|
|
25
|
+
"commitDate": "2026-06-27T02:46:04Z",
|
|
26
|
+
"buildDate": "2026-06-27T03:14:10.843Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.51.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/2a072802e0f6e75cb5f1ada167c64fed52cd39b1",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.51.6"
|
|
33
33
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Container, Input, matchesKey, Spacer, Text, TruncatedText } from "@f5-sales-demo/pi-tui";
|
|
2
2
|
import type { XCSHContext } from "../../services/xcsh-context";
|
|
3
|
-
import { deriveTenantFromUrl } from "../../services/xcsh-env";
|
|
3
|
+
import { deriveTenantFromUrl, isSensitiveEnvKey, XCSH_CONSOLE_PASSWORD, XCSH_USERNAME } from "../../services/xcsh-env";
|
|
4
4
|
import { theme } from "../theme/theme";
|
|
5
5
|
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
6
6
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -28,18 +28,57 @@ export function validateWizardName(name: string): string | null {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
type WizardStep =
|
|
31
|
+
type WizardStep =
|
|
32
|
+
| "url"
|
|
33
|
+
| "token"
|
|
34
|
+
| "name"
|
|
35
|
+
| "validating"
|
|
36
|
+
| "namespace"
|
|
37
|
+
| "username"
|
|
38
|
+
| "password"
|
|
39
|
+
| "confirm"
|
|
40
|
+
| "activate";
|
|
32
41
|
|
|
33
42
|
interface WizardState {
|
|
34
43
|
url: string;
|
|
35
44
|
token: string;
|
|
36
45
|
name: string;
|
|
37
46
|
namespace: string;
|
|
47
|
+
/** Web-console login username (optional) — stored as the XCSH_USERNAME env var. */
|
|
48
|
+
username: string;
|
|
49
|
+
/** Web-console login password (optional) — stored as the XCSH_CONSOLE_PASSWORD env var. */
|
|
50
|
+
password: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the XCSHContext the wizard will persist from its collected state.
|
|
55
|
+
*
|
|
56
|
+
* The optional web-console credentials become generic env vars (XCSH_USERNAME /
|
|
57
|
+
* XCSH_CONSOLE_PASSWORD); any secret-looking key (the password) is auto-marked
|
|
58
|
+
* sensitive so it is masked in `/context show` and redacted on export. Exported
|
|
59
|
+
* so the persistence shape is unit-testable without driving the TUI.
|
|
60
|
+
*/
|
|
61
|
+
export function buildWizardContext(state: WizardState): XCSHContext {
|
|
62
|
+
const context: XCSHContext = {
|
|
63
|
+
name: state.name,
|
|
64
|
+
apiUrl: state.url,
|
|
65
|
+
apiToken: state.token,
|
|
66
|
+
defaultNamespace: state.namespace,
|
|
67
|
+
};
|
|
68
|
+
const env: Record<string, string> = {};
|
|
69
|
+
if (state.username) env[XCSH_USERNAME] = state.username;
|
|
70
|
+
if (state.password) env[XCSH_CONSOLE_PASSWORD] = state.password;
|
|
71
|
+
if (Object.keys(env).length > 0) {
|
|
72
|
+
context.env = env;
|
|
73
|
+
const sensitiveKeys = Object.keys(env).filter(isSensitiveEnvKey);
|
|
74
|
+
if (sensitiveKeys.length > 0) context.sensitiveKeys = sensitiveKeys;
|
|
75
|
+
}
|
|
76
|
+
return context;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
export class ContextAddWizard extends Container {
|
|
41
80
|
#currentStep: WizardStep = "url";
|
|
42
|
-
#state: WizardState = { url: "", token: "", name: "", namespace: "default" };
|
|
81
|
+
#state: WizardState = { url: "", token: "", name: "", namespace: "default", username: "", password: "" };
|
|
43
82
|
#contentContainer: Container;
|
|
44
83
|
#inputField: Input | null = null;
|
|
45
84
|
#selectedIndex = 0;
|
|
@@ -104,6 +143,12 @@ export class ContextAddWizard extends Container {
|
|
|
104
143
|
case "namespace":
|
|
105
144
|
this.#renderNamespaceStep();
|
|
106
145
|
break;
|
|
146
|
+
case "username":
|
|
147
|
+
this.#renderUsernameStep();
|
|
148
|
+
break;
|
|
149
|
+
case "password":
|
|
150
|
+
this.#renderPasswordStep();
|
|
151
|
+
break;
|
|
107
152
|
case "confirm":
|
|
108
153
|
this.#renderConfirmStep();
|
|
109
154
|
break;
|
|
@@ -139,6 +184,7 @@ export class ContextAddWizard extends Container {
|
|
|
139
184
|
this.#contentContainer.addChild(new Spacer(1));
|
|
140
185
|
|
|
141
186
|
this.#inputField = new Input();
|
|
187
|
+
this.#inputField.setMasked(true);
|
|
142
188
|
this.#inputField.setValue(this.#state.token);
|
|
143
189
|
this.#contentContainer.addChild(this.#inputField);
|
|
144
190
|
this.#contentContainer.addChild(new Spacer(1));
|
|
@@ -288,8 +334,41 @@ export class ContextAddWizard extends Container {
|
|
|
288
334
|
this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to go back]"), 0, 0));
|
|
289
335
|
}
|
|
290
336
|
|
|
337
|
+
#renderUsernameStep(): void {
|
|
338
|
+
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step 6: Web-Console Username")));
|
|
339
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
340
|
+
this.#contentContainer.addChild(new Text("Enter the web-console login username (optional):", 0, 0));
|
|
341
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
342
|
+
|
|
343
|
+
this.#inputField = new Input();
|
|
344
|
+
this.#inputField.setValue(this.#state.username);
|
|
345
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
346
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
347
|
+
|
|
348
|
+
this.#contentContainer.addChild(
|
|
349
|
+
new Text(theme.fg("muted", "[Enter to continue (empty to skip), Esc to go back]"), 0, 0),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#renderPasswordStep(): void {
|
|
354
|
+
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step 7: Web-Console Password")));
|
|
355
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
356
|
+
this.#contentContainer.addChild(new Text("Enter the web-console login password (optional):", 0, 0));
|
|
357
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
358
|
+
|
|
359
|
+
this.#inputField = new Input();
|
|
360
|
+
this.#inputField.setMasked(true);
|
|
361
|
+
this.#inputField.setValue(this.#state.password);
|
|
362
|
+
this.#contentContainer.addChild(this.#inputField);
|
|
363
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
364
|
+
|
|
365
|
+
this.#contentContainer.addChild(
|
|
366
|
+
new Text(theme.fg("muted", "[Enter to continue (empty to skip), Esc to go back]"), 0, 0),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
291
370
|
#renderConfirmStep(): void {
|
|
292
|
-
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step
|
|
371
|
+
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step 8: Confirm")));
|
|
293
372
|
this.#contentContainer.addChild(new Spacer(1));
|
|
294
373
|
|
|
295
374
|
// Summary table
|
|
@@ -298,6 +377,12 @@ export class ContextAddWizard extends Container {
|
|
|
298
377
|
const masked = this.#state.token.length > 4 ? `${"*".repeat(8)}${this.#state.token.slice(-4)}` : "****";
|
|
299
378
|
this.#contentContainer.addChild(new Text(`Token: ${masked}`, 0, 0));
|
|
300
379
|
this.#contentContainer.addChild(new Text(`Namespace: ${this.#state.namespace}`, 0, 0));
|
|
380
|
+
if (this.#state.username) {
|
|
381
|
+
this.#contentContainer.addChild(new Text(`Username: ${this.#state.username}`, 0, 0));
|
|
382
|
+
}
|
|
383
|
+
if (this.#state.password) {
|
|
384
|
+
this.#contentContainer.addChild(new Text(`Console Password: ${"*".repeat(8)}`, 0, 0));
|
|
385
|
+
}
|
|
301
386
|
this.#contentContainer.addChild(new Spacer(1));
|
|
302
387
|
|
|
303
388
|
this.#contentContainer.addChild(new Text("Save this context?", 0, 0));
|
|
@@ -318,7 +403,7 @@ export class ContextAddWizard extends Container {
|
|
|
318
403
|
}
|
|
319
404
|
|
|
320
405
|
#renderActivateStep(): void {
|
|
321
|
-
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step
|
|
406
|
+
this.#contentContainer.addChild(new Text(theme.fg("contentAccent", "Step 9: Activate")));
|
|
322
407
|
this.#contentContainer.addChild(new Spacer(1));
|
|
323
408
|
this.#contentContainer.addChild(new Text("Activate this context now?", 0, 0));
|
|
324
409
|
this.#contentContainer.addChild(new Spacer(1));
|
|
@@ -436,6 +521,19 @@ export class ContextAddWizard extends Container {
|
|
|
436
521
|
}
|
|
437
522
|
case "namespace": {
|
|
438
523
|
this.#state.namespace = value || "default";
|
|
524
|
+
this.#currentStep = "username";
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case "username": {
|
|
528
|
+
// Trimmed: usernames never carry meaningful surrounding whitespace.
|
|
529
|
+
this.#state.username = value;
|
|
530
|
+
this.#currentStep = "password";
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "password": {
|
|
534
|
+
// Preserve the password exactly as typed — do NOT use the trimmed
|
|
535
|
+
// `value`, since a password may legitimately contain whitespace.
|
|
536
|
+
this.#state.password = this.#inputField.getValue();
|
|
439
537
|
this.#currentStep = "confirm";
|
|
440
538
|
this.#selectedIndex = 0;
|
|
441
539
|
break;
|
|
@@ -477,12 +575,7 @@ export class ContextAddWizard extends Container {
|
|
|
477
575
|
return;
|
|
478
576
|
}
|
|
479
577
|
case "activate": {
|
|
480
|
-
const context
|
|
481
|
-
name: this.#state.name,
|
|
482
|
-
apiUrl: this.#state.url,
|
|
483
|
-
apiToken: this.#state.token,
|
|
484
|
-
defaultNamespace: this.#state.namespace,
|
|
485
|
-
};
|
|
578
|
+
const context = buildWizardContext(this.#state);
|
|
486
579
|
this.#onCompleteCallback(context, this.#selectedIndex === 0);
|
|
487
580
|
return;
|
|
488
581
|
}
|
|
@@ -521,9 +614,15 @@ export class ContextAddWizard extends Container {
|
|
|
521
614
|
case "namespace":
|
|
522
615
|
this.#currentStep = "name";
|
|
523
616
|
break;
|
|
524
|
-
case "
|
|
617
|
+
case "username":
|
|
525
618
|
this.#currentStep = "namespace";
|
|
526
619
|
break;
|
|
620
|
+
case "password":
|
|
621
|
+
this.#currentStep = "username";
|
|
622
|
+
break;
|
|
623
|
+
case "confirm":
|
|
624
|
+
this.#currentStep = "password";
|
|
625
|
+
break;
|
|
527
626
|
case "activate":
|
|
528
627
|
this.#currentStep = "confirm";
|
|
529
628
|
break;
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `chrome-host` — native-messaging relay subcommand.
|
|
3
|
-
*
|
|
4
|
-
* Launched by Chrome as the native-messaging host. It is a pure relay between
|
|
5
|
-
* Chrome's stdio (native-messaging framing: 4-byte LE length + JSON) and xcsh's
|
|
6
|
-
* Unix socket at `~/.xcsh/chrome-bridge.sock` (newline-delimited JSON). No
|
|
7
|
-
* business logic lives here.
|
|
8
|
-
*/
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
12
|
-
import { decodeNm, encodeNm } from "../browser/native-messaging";
|
|
13
|
-
|
|
14
|
-
const SOCKET_PATH = process.env.XCSH_BRIDGE_SOCKET ?? join(homedir(), ".xcsh", "chrome-bridge.sock");
|
|
15
|
-
|
|
16
|
-
// Diagnostic relay tracing, OFF unless ~/.xcsh/chrome-host-debug exists. Security:
|
|
17
|
-
// writes to a 0600 file in the user's home, lifecycle + byte-counts ONLY — never
|
|
18
|
-
// message content — and never to stdout (that would corrupt the NM stream).
|
|
19
|
-
function dbg(msg: string): void {
|
|
20
|
-
try {
|
|
21
|
-
const fs = require("node:fs");
|
|
22
|
-
const home = process.env.HOME || homedir();
|
|
23
|
-
if (!fs.existsSync(join(home, ".xcsh", "chrome-host-debug"))) return;
|
|
24
|
-
const fd = fs.openSync(join(home, ".xcsh", "chrome-host.log"), "a", 0o600);
|
|
25
|
-
fs.appendFileSync(fd, `${new Date().toISOString()} pid=${process.pid} ${msg}\n`);
|
|
26
|
-
fs.closeSync(fd);
|
|
27
|
-
} catch {
|
|
28
|
-
/* logging must never break the relay */
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default class ChromeHost extends Command {
|
|
33
|
-
static description = "Native-messaging relay between Chrome and the xcsh bridge socket (internal)";
|
|
34
|
-
|
|
35
|
-
async run(): Promise<void> {
|
|
36
|
-
let socket: Awaited<ReturnType<typeof Bun.connect>> | undefined;
|
|
37
|
-
let socketBuffer = "";
|
|
38
|
-
dbg(`start sock=${SOCKET_PATH} argc=${process.argv.length}`);
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
dbg("Bun.connect: begin");
|
|
42
|
-
socket = await Bun.connect({
|
|
43
|
-
unix: SOCKET_PATH,
|
|
44
|
-
socket: {
|
|
45
|
-
data(_sock, chunk) {
|
|
46
|
-
// Socket → Chrome: NDJSON lines → native-messaging frames.
|
|
47
|
-
socketBuffer += new TextDecoder().decode(chunk);
|
|
48
|
-
let newlineIndex = socketBuffer.indexOf("\n");
|
|
49
|
-
while (newlineIndex !== -1) {
|
|
50
|
-
const line = socketBuffer.slice(0, newlineIndex);
|
|
51
|
-
socketBuffer = socketBuffer.slice(newlineIndex + 1);
|
|
52
|
-
if (line.length > 0) {
|
|
53
|
-
dbg(`socket→chrome ${line.length}B`);
|
|
54
|
-
process.stdout.write(encodeNm(JSON.parse(line)));
|
|
55
|
-
}
|
|
56
|
-
newlineIndex = socketBuffer.indexOf("\n");
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
close() {
|
|
60
|
-
dbg("bridge socket closed → exit");
|
|
61
|
-
process.exit(0);
|
|
62
|
-
},
|
|
63
|
-
error() {
|
|
64
|
-
dbg("bridge socket error → exit");
|
|
65
|
-
process.exit(0);
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
dbg("Bun.connect: resolved (connected)");
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// xcsh not running — write nothing, exit cleanly. The extension retries.
|
|
72
|
-
dbg(`Bun.connect: threw → exit: ${e instanceof Error ? e.message : String(e)}`);
|
|
73
|
-
process.exit(0);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Chrome stdin → socket: native-messaging frames → NDJSON lines.
|
|
77
|
-
dbg("reading chrome stdin");
|
|
78
|
-
let stdinBuffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
|
79
|
-
for await (const chunk of process.stdin) {
|
|
80
|
-
const bytes = new Uint8Array(chunk);
|
|
81
|
-
const next = new Uint8Array(stdinBuffer.length + bytes.length);
|
|
82
|
-
next.set(stdinBuffer, 0);
|
|
83
|
-
next.set(bytes, stdinBuffer.length);
|
|
84
|
-
const { messages, rest } = decodeNm(next);
|
|
85
|
-
stdinBuffer = rest;
|
|
86
|
-
for (const msg of messages) {
|
|
87
|
-
dbg(`chrome→socket ${JSON.stringify(msg).length}B`);
|
|
88
|
-
socket.write(`${JSON.stringify(msg)}\n`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Chrome closed stdin — tear down and exit.
|
|
93
|
-
dbg("chrome stdin EOF → exit");
|
|
94
|
-
socket.end();
|
|
95
|
-
process.exit(0);
|
|
96
|
-
}
|
|
97
|
-
}
|