@yeaft/webchat-agent 1.0.413 → 1.0.415

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.
Files changed (58) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/connection/index.js +12 -0
  21. package/context.js +1 -0
  22. package/index.js +19 -2
  23. package/llm-config-cli.js +24 -21
  24. package/local-runtime/server/client-protocol.js +14 -0
  25. package/local-runtime/server/context.js +3 -2
  26. package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
  27. package/local-runtime/server/handlers/agent-output.js +3 -0
  28. package/local-runtime/server/handlers/client-misc.js +21 -4
  29. package/local-runtime/server/handlers/client-workbench.js +222 -41
  30. package/local-runtime/server/workbench-correlation.js +184 -0
  31. package/local-runtime/server/workbench-route.js +180 -0
  32. package/local-runtime/server/ws-agent.js +4 -0
  33. package/local-runtime/server/ws-client.js +25 -3
  34. package/local-runtime/version.json +1 -1
  35. package/local-runtime/web/app.bundle.js +191 -135
  36. package/local-runtime/web/app.bundle.js.gz +0 -0
  37. package/local-runtime/web/index.html +2 -2
  38. package/local-runtime/web/style.bundle.css +1 -1
  39. package/local-runtime/web/style.bundle.css.gz +0 -0
  40. package/package.json +5 -1
  41. package/service/config.js +23 -2
  42. package/service/index.js +1 -0
  43. package/service/linux.js +3 -2
  44. package/terminal.js +167 -30
  45. package/workbench/file-ops.js +21 -20
  46. package/workbench/file-search.js +4 -3
  47. package/workbench/git-ops.js +23 -22
  48. package/workbench/request-routing.js +16 -0
  49. package/yeaft/cli.js +57 -1
  50. package/yeaft/config-api.js +138 -192
  51. package/yeaft/config-store.js +192 -0
  52. package/yeaft/config.js +3 -0
  53. package/yeaft/init.js +20 -7
  54. package/yeaft/sessions/feature-flag.js +15 -33
  55. package/yeaft/sessions/session-manifest.js +114 -10
  56. package/yeaft/stdio-protocol.js +57 -0
  57. package/yeaft/storage/atomic.js +43 -17
  58. package/yeaft/tools/process-runner.js +86 -13
@@ -0,0 +1,71 @@
1
+ export const BROWSER_PROTOCOL_VERSION = 1;
2
+ export const BROWSER_CONTROL_CHANNEL = 'browser.control.v1';
3
+ export const BROWSER_POINTER_CHANNEL = 'browser.pointer.v1';
4
+ export const BROWSER_STATE_CHANNEL = 'browser.state.v1';
5
+
6
+ function positiveInteger(value) {
7
+ return Number.isSafeInteger(value) && value > 0;
8
+ }
9
+
10
+ /**
11
+ * Per-producer sequence fencing. Reliable control and lossy pointer traffic
12
+ * intentionally have independent sequence spaces: pointer loss or reordering
13
+ * must never advance or block reliable keyboard/click/navigation actions.
14
+ */
15
+ export class ProducerSequenceState {
16
+ /** @param {{producerId:string, producerGeneration:number}} identity */
17
+ constructor({ producerId, producerGeneration }) {
18
+ if (typeof producerId !== 'string' || !producerId) throw new Error('producerId required');
19
+ if (!positiveInteger(producerGeneration)) throw new Error('producerGeneration must be a positive integer');
20
+ this.producerId = producerId;
21
+ this.producerGeneration = producerGeneration;
22
+ this.lastAcceptedControlSeq = 0;
23
+ this.lastAcceptedPointerSeq = 0;
24
+ }
25
+
26
+ #matches(envelope) {
27
+ return envelope?.producerId === this.producerId
28
+ && envelope?.producerGeneration === this.producerGeneration;
29
+ }
30
+
31
+ /**
32
+ * Accept only the next gap-free reliable sequence number.
33
+ * @returns {{accepted:boolean, code:string, expectedControlSeq:number}}
34
+ */
35
+ acceptControl(envelope) {
36
+ const expectedControlSeq = this.lastAcceptedControlSeq + 1;
37
+ if (!this.#matches(envelope)) return { accepted: false, code: 'producer_stale', expectedControlSeq };
38
+ if (!positiveInteger(envelope.controlSeq)) return { accepted: false, code: 'control_seq_invalid', expectedControlSeq };
39
+ if (envelope.controlSeq < expectedControlSeq) return { accepted: false, code: 'control_duplicate', expectedControlSeq };
40
+ if (envelope.controlSeq > expectedControlSeq) return { accepted: false, code: 'control_gap', expectedControlSeq };
41
+ this.lastAcceptedControlSeq = envelope.controlSeq;
42
+ return { accepted: true, code: 'accepted', expectedControlSeq: this.lastAcceptedControlSeq + 1 };
43
+ }
44
+
45
+ /**
46
+ * Accept any strictly newer lossy pointer sequence number. Gaps are expected.
47
+ * @returns {{accepted:boolean, code:string, pointerHighWater:number}}
48
+ */
49
+ acceptPointer(envelope) {
50
+ if (!this.#matches(envelope)) {
51
+ return { accepted: false, code: 'producer_stale', pointerHighWater: this.lastAcceptedPointerSeq };
52
+ }
53
+ if (!positiveInteger(envelope.pointerSeq)) {
54
+ return { accepted: false, code: 'pointer_seq_invalid', pointerHighWater: this.lastAcceptedPointerSeq };
55
+ }
56
+ if (envelope.pointerSeq <= this.lastAcceptedPointerSeq) {
57
+ return { accepted: false, code: 'pointer_stale', pointerHighWater: this.lastAcceptedPointerSeq };
58
+ }
59
+ this.lastAcceptedPointerSeq = envelope.pointerSeq;
60
+ return { accepted: true, code: 'accepted', pointerHighWater: this.lastAcceptedPointerSeq };
61
+ }
62
+
63
+ snapshot() {
64
+ return Object.freeze({
65
+ producerId: this.producerId,
66
+ producerGeneration: this.producerGeneration,
67
+ lastAcceptedControlSeq: this.lastAcceptedControlSeq,
68
+ lastAcceptedPointerSeq: this.lastAcceptedPointerSeq,
69
+ });
70
+ }
71
+ }
@@ -0,0 +1,132 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { normaliseBrowserRuntimeSection } from './config.js';
3
+ import { defaultBrowserCacheDir } from './browser-install.js';
4
+ import { probeBrowserRuntime } from './probe.js';
5
+ import { BrowserRuntimeError } from './errors.js';
6
+
7
+ /**
8
+ * Agent-local Browser Runtime lifecycle owner. Phase 0 exposes startup probe and
9
+ * capacity semantics only; Browser Sessions remain unavailable until the later
10
+ * owner-checked control-plane phase lands.
11
+ */
12
+ export class BrowserRuntimeService {
13
+ constructor({ yeaftDir, config, probe = probeBrowserRuntime } = {}) {
14
+ if (!yeaftDir) throw new Error('yeaftDir required');
15
+ this.yeaftDir = yeaftDir;
16
+ this.config = normaliseBrowserRuntimeSection(config);
17
+ this.config.cacheDir ||= defaultBrowserCacheDir(yeaftDir);
18
+ this.probe = probe;
19
+ this.sessions = new Map();
20
+ this.probeResult = null;
21
+ this.state = this.config.enabled ? 'unprobed' : 'disabled';
22
+ this.#probePromise = null;
23
+ this.#probeAbort = null;
24
+ this.#shutdownPromise = null;
25
+ }
26
+
27
+ #probePromise;
28
+ #probeAbort;
29
+ #shutdownPromise;
30
+
31
+ get enabled() { return this.config.enabled === true; }
32
+ get ready() { return this.state === 'ready' && this.probeResult?.ok === true; }
33
+
34
+ capabilities() {
35
+ // Phase 0 is a local spike only. Advertising the Phase 1 feature before
36
+ // owner-checked create/attach/signaling exists would expose a dead route.
37
+ return [];
38
+ }
39
+
40
+ async startupProbe() {
41
+ if (!this.enabled) return { ok: false, code: 'browser_runtime_disabled' };
42
+ if (this.#probePromise) return this.#probePromise;
43
+ this.state = 'probing';
44
+ this.#probeAbort = new AbortController();
45
+ this.#probePromise = this.probe({
46
+ executablePath: this.config.executablePath,
47
+ cacheDir: this.config.cacheDir,
48
+ headless: this.config.headless,
49
+ timeoutMs: this.config.startupProbeTimeoutMs,
50
+ profileParent: `${this.config.cacheDir}-profiles`,
51
+ signal: this.#probeAbort.signal,
52
+ }).then(result => {
53
+ this.probeResult = Object.freeze({ ...result });
54
+ this.state = result.ok ? 'ready' : 'unavailable';
55
+ return this.probeResult;
56
+ }).catch(error => {
57
+ this.probeResult = Object.freeze({
58
+ ok: false,
59
+ code: error?.code || 'browser_probe_failed',
60
+ safeError: String(error?.message || error).slice(0, 500),
61
+ });
62
+ this.state = 'unavailable';
63
+ return this.probeResult;
64
+ });
65
+ return this.#probePromise;
66
+ }
67
+
68
+ assertCanCreateSession() {
69
+ if (!this.ready) throw new BrowserRuntimeError('browser_runtime_unavailable');
70
+ if (this.sessions.size >= this.config.maxSessions) {
71
+ throw new BrowserRuntimeError('browser_session_limit');
72
+ }
73
+ }
74
+
75
+ /** Phase 0 test hook for lifecycle/capacity ownership. */
76
+ reserveSession(ownerUserId) {
77
+ this.assertCanCreateSession();
78
+ if (!ownerUserId) throw new BrowserRuntimeError('browser_owner_required');
79
+ const browserSessionId = randomUUID();
80
+ this.sessions.set(browserSessionId, Object.freeze({
81
+ browserSessionId,
82
+ ownerUserId,
83
+ state: 'reserved',
84
+ createdAt: Date.now(),
85
+ }));
86
+ return this.sessions.get(browserSessionId);
87
+ }
88
+
89
+ releaseSession(browserSessionId) {
90
+ return this.sessions.delete(browserSessionId);
91
+ }
92
+
93
+ snapshot() {
94
+ return Object.freeze({
95
+ enabled: this.enabled,
96
+ ready: this.ready,
97
+ state: this.state,
98
+ activeSessions: this.sessions.size,
99
+ maxSessions: this.config.maxSessions,
100
+ probe: this.probeResult,
101
+ });
102
+ }
103
+
104
+ async shutdown() {
105
+ if (this.#shutdownPromise) return this.#shutdownPromise;
106
+ this.#probeAbort?.abort(new BrowserRuntimeError('browser_runtime_shutdown'));
107
+ this.#shutdownPromise = Promise.resolve(this.#probePromise).catch(() => {}).then(() => {
108
+ this.sessions.clear();
109
+ this.state = 'closed';
110
+ });
111
+ return this.#shutdownPromise;
112
+ }
113
+ }
114
+
115
+ let runtime = null;
116
+
117
+ export function getBrowserRuntime() {
118
+ return runtime;
119
+ }
120
+
121
+ export async function bootBrowserRuntime(options) {
122
+ if (runtime) return runtime;
123
+ runtime = new BrowserRuntimeService(options);
124
+ await runtime.startupProbe();
125
+ return runtime;
126
+ }
127
+
128
+ export async function shutdownBrowserRuntime() {
129
+ const current = runtime;
130
+ runtime = null;
131
+ await current?.shutdown();
132
+ }
@@ -0,0 +1,233 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)][string]$NodePath,
3
+ [Parameter(Mandatory = $true)][string]$WorkerPath,
4
+ [Parameter(Mandatory = $true)][string]$ExecutablePath,
5
+ [int]$CleanupTimeoutMs = 500
6
+ )
7
+
8
+ $ErrorActionPreference = 'Stop'
9
+
10
+ Add-Type -TypeDefinition @'
11
+ using System;
12
+ using System.Runtime.InteropServices;
13
+ using System.Text;
14
+
15
+ public static class YeaftBrowserJob {
16
+ public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
17
+ public const int JobObjectBasicAccountingInformation = 1;
18
+ public const int JobObjectExtendedLimitInformation = 9;
19
+ public const uint PROCESS_SET_QUOTA = 0x0100;
20
+ public const uint PROCESS_TERMINATE = 0x0001;
21
+
22
+ [StructLayout(LayoutKind.Sequential)]
23
+ public struct IO_COUNTERS {
24
+ public UInt64 ReadOperationCount;
25
+ public UInt64 WriteOperationCount;
26
+ public UInt64 OtherOperationCount;
27
+ public UInt64 ReadTransferCount;
28
+ public UInt64 WriteTransferCount;
29
+ public UInt64 OtherTransferCount;
30
+ }
31
+
32
+ [StructLayout(LayoutKind.Sequential)]
33
+ public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
34
+ public Int64 PerProcessUserTimeLimit;
35
+ public Int64 PerJobUserTimeLimit;
36
+ public UInt32 LimitFlags;
37
+ public UIntPtr MinimumWorkingSetSize;
38
+ public UIntPtr MaximumWorkingSetSize;
39
+ public UInt32 ActiveProcessLimit;
40
+ public UIntPtr Affinity;
41
+ public UInt32 PriorityClass;
42
+ public UInt32 SchedulingClass;
43
+ }
44
+
45
+ [StructLayout(LayoutKind.Sequential)]
46
+ public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
47
+ public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
48
+ public IO_COUNTERS IoInfo;
49
+ public UIntPtr ProcessMemoryLimit;
50
+ public UIntPtr JobMemoryLimit;
51
+ public UIntPtr PeakProcessMemoryUsed;
52
+ public UIntPtr PeakJobMemoryUsed;
53
+ }
54
+
55
+ [StructLayout(LayoutKind.Sequential)]
56
+ public struct JOBOBJECT_BASIC_ACCOUNTING_INFORMATION {
57
+ public Int64 TotalUserTime;
58
+ public Int64 TotalKernelTime;
59
+ public Int64 ThisPeriodTotalUserTime;
60
+ public Int64 ThisPeriodTotalKernelTime;
61
+ public UInt32 TotalPageFaultCount;
62
+ public UInt32 TotalProcesses;
63
+ public UInt32 ActiveProcesses;
64
+ public UInt32 TotalTerminatedProcesses;
65
+ }
66
+
67
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
68
+ public static extern IntPtr CreateJobObject(IntPtr securityAttributes, string name);
69
+
70
+ [DllImport("kernel32.dll", SetLastError = true)]
71
+ [return: MarshalAs(UnmanagedType.Bool)]
72
+ public static extern bool SetInformationJobObject(
73
+ IntPtr job,
74
+ int infoClass,
75
+ IntPtr info,
76
+ UInt32 length
77
+ );
78
+
79
+ [DllImport("kernel32.dll", SetLastError = true)]
80
+ [return: MarshalAs(UnmanagedType.Bool)]
81
+ public static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
82
+
83
+ [DllImport("kernel32.dll", SetLastError = true)]
84
+ [return: MarshalAs(UnmanagedType.Bool)]
85
+ public static extern bool TerminateJobObject(IntPtr job, UInt32 exitCode);
86
+
87
+ [DllImport("kernel32.dll", SetLastError = true)]
88
+ [return: MarshalAs(UnmanagedType.Bool)]
89
+ public static extern bool QueryInformationJobObject(
90
+ IntPtr job,
91
+ int infoClass,
92
+ IntPtr info,
93
+ UInt32 length,
94
+ out UInt32 returnLength
95
+ );
96
+
97
+ [DllImport("kernel32.dll", SetLastError = true)]
98
+ public static extern IntPtr OpenProcess(UInt32 desiredAccess, bool inheritHandle, UInt32 processId);
99
+
100
+ [DllImport("kernel32.dll")]
101
+ [return: MarshalAs(UnmanagedType.Bool)]
102
+ public static extern bool CloseHandle(IntPtr handle);
103
+
104
+ public static string QuoteArgument(string value) {
105
+ if (value.Length > 0 && value.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0) {
106
+ return value;
107
+ }
108
+ var output = new StringBuilder();
109
+ output.Append('"');
110
+ var slashes = 0;
111
+ foreach (var ch in value) {
112
+ if (ch == '\\') {
113
+ slashes++;
114
+ } else if (ch == '"') {
115
+ output.Append('\\', slashes * 2 + 1);
116
+ output.Append('"');
117
+ slashes = 0;
118
+ } else {
119
+ output.Append('\\', slashes);
120
+ slashes = 0;
121
+ output.Append(ch);
122
+ }
123
+ }
124
+ output.Append('\\', slashes * 2);
125
+ output.Append('"');
126
+ return output.ToString();
127
+ }
128
+ }
129
+ '@
130
+
131
+ function Throw-Win32Error([string]$Operation) {
132
+ $code = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
133
+ throw "$Operation failed with Win32 error $code"
134
+ }
135
+
136
+ $job = [IntPtr]::Zero
137
+ $workerHandle = [IntPtr]::Zero
138
+ $worker = $null
139
+ $resultLine = $null
140
+ try {
141
+ $job = [YeaftBrowserJob]::CreateJobObject([IntPtr]::Zero, $null)
142
+ if ($job -eq [IntPtr]::Zero) { Throw-Win32Error 'CreateJobObject' }
143
+
144
+ $limit = New-Object YeaftBrowserJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION
145
+ $basicLimit = $limit.BasicLimitInformation
146
+ $basicLimit.LimitFlags = [YeaftBrowserJob]::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
147
+ $limit.BasicLimitInformation = $basicLimit
148
+ $limitSize = [Runtime.InteropServices.Marshal]::SizeOf($limit)
149
+ $limitPtr = [Runtime.InteropServices.Marshal]::AllocHGlobal($limitSize)
150
+ try {
151
+ [Runtime.InteropServices.Marshal]::StructureToPtr($limit, $limitPtr, $false)
152
+ if (-not [YeaftBrowserJob]::SetInformationJobObject(
153
+ $job,
154
+ [YeaftBrowserJob]::JobObjectExtendedLimitInformation,
155
+ $limitPtr,
156
+ [uint32]$limitSize
157
+ )) { Throw-Win32Error 'SetInformationJobObject' }
158
+ } finally {
159
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($limitPtr)
160
+ }
161
+
162
+ $start = New-Object System.Diagnostics.ProcessStartInfo
163
+ $start.FileName = $NodePath
164
+ $start.Arguments = [YeaftBrowserJob]::QuoteArgument($WorkerPath) + ' ' + [YeaftBrowserJob]::QuoteArgument($ExecutablePath)
165
+ $start.UseShellExecute = $false
166
+ $start.CreateNoWindow = $true
167
+ $start.RedirectStandardInput = $true
168
+ $start.RedirectStandardOutput = $true
169
+ $start.RedirectStandardError = $true
170
+
171
+ $worker = New-Object System.Diagnostics.Process
172
+ $worker.StartInfo = $start
173
+ if (-not $worker.Start()) { throw 'Windows version worker did not start' }
174
+ $workerHandle = [YeaftBrowserJob]::OpenProcess(
175
+ [YeaftBrowserJob]::PROCESS_SET_QUOTA -bor [YeaftBrowserJob]::PROCESS_TERMINATE,
176
+ $false,
177
+ [uint32]$worker.Id
178
+ )
179
+ if ($workerHandle -eq [IntPtr]::Zero) { Throw-Win32Error 'OpenProcess' }
180
+ if (-not [YeaftBrowserJob]::AssignProcessToJobObject($job, $workerHandle)) {
181
+ Throw-Win32Error 'AssignProcessToJobObject'
182
+ }
183
+
184
+ $worker.StandardInput.WriteLine('go')
185
+ $worker.StandardInput.Close()
186
+ $resultLine = $worker.StandardOutput.ReadLine()
187
+ if ([string]::IsNullOrWhiteSpace($resultLine)) {
188
+ if (-not $worker.HasExited) {
189
+ [void][YeaftBrowserJob]::TerminateJobObject($job, 1)
190
+ $worker.WaitForExit()
191
+ }
192
+ $workerError = $worker.StandardError.ReadToEnd()
193
+ throw "Windows version worker returned no result ($($worker.ExitCode)): $workerError"
194
+ }
195
+
196
+ if (-not [YeaftBrowserJob]::TerminateJobObject($job, 1)) {
197
+ Throw-Win32Error 'TerminateJobObject'
198
+ }
199
+
200
+ $accounting = New-Object YeaftBrowserJob+JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
201
+ $accountingSize = [Runtime.InteropServices.Marshal]::SizeOf($accounting)
202
+ $accountingPtr = [Runtime.InteropServices.Marshal]::AllocHGlobal($accountingSize)
203
+ try {
204
+ $deadline = [DateTime]::UtcNow.AddMilliseconds([Math]::Max(1, $CleanupTimeoutMs))
205
+ do {
206
+ [uint32]$returned = 0
207
+ if (-not [YeaftBrowserJob]::QueryInformationJobObject(
208
+ $job,
209
+ [YeaftBrowserJob]::JobObjectBasicAccountingInformation,
210
+ $accountingPtr,
211
+ [uint32]$accountingSize,
212
+ [ref]$returned
213
+ )) { Throw-Win32Error 'QueryInformationJobObject' }
214
+ $accounting = [Runtime.InteropServices.Marshal]::PtrToStructure(
215
+ $accountingPtr,
216
+ [type][YeaftBrowserJob+JOBOBJECT_BASIC_ACCOUNTING_INFORMATION]
217
+ )
218
+ if ($accounting.ActiveProcesses -eq 0) { break }
219
+ Start-Sleep -Milliseconds 10
220
+ } while ([DateTime]::UtcNow -lt $deadline)
221
+ if ($accounting.ActiveProcesses -ne 0) {
222
+ throw "Windows Browser Runtime job still has $($accounting.ActiveProcesses) active process(es)"
223
+ }
224
+ } finally {
225
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($accountingPtr)
226
+ }
227
+
228
+ [Console]::Out.WriteLine($resultLine)
229
+ } finally {
230
+ if ($workerHandle -ne [IntPtr]::Zero) { [void][YeaftBrowserJob]::CloseHandle($workerHandle) }
231
+ if ($job -ne [IntPtr]::Zero) { [void][YeaftBrowserJob]::CloseHandle($job) }
232
+ if ($null -ne $worker) { $worker.Dispose() }
233
+ }
@@ -0,0 +1,75 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ const MAX_OUTPUT_BYTES = 64 * 1024;
4
+
5
+ function writeResult(result) {
6
+ process.stdout.write(`${JSON.stringify(result)}\n`, () => process.exit(0));
7
+ }
8
+
9
+ function capture(chunks, chunk, state) {
10
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
11
+ const remaining = MAX_OUTPUT_BYTES - state.bytes;
12
+ if (remaining <= 0) {
13
+ state.truncated = true;
14
+ return;
15
+ }
16
+ const bounded = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
17
+ chunks.push(bounded);
18
+ state.bytes += bounded.length;
19
+ if (bounded.length !== buffer.length) state.truncated = true;
20
+ }
21
+
22
+ function run(executablePath) {
23
+ return new Promise(resolve => {
24
+ let child;
25
+ try {
26
+ child = spawn(executablePath, ['--version'], {
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ windowsHide: true,
29
+ });
30
+ } catch (error) {
31
+ resolve({ ok: false, error: String(error?.message || error) });
32
+ return;
33
+ }
34
+ const stdout = [];
35
+ const stderr = [];
36
+ const stdoutState = { bytes: 0, truncated: false };
37
+ const stderrState = { bytes: 0, truncated: false };
38
+ const stopOnOverflow = state => {
39
+ if (!state.truncated) return;
40
+ try { child.kill('SIGKILL'); } catch {}
41
+ };
42
+ child.stdout.on('data', chunk => {
43
+ capture(stdout, chunk, stdoutState);
44
+ stopOnOverflow(stdoutState);
45
+ });
46
+ child.stderr.on('data', chunk => {
47
+ capture(stderr, chunk, stderrState);
48
+ stopOnOverflow(stderrState);
49
+ });
50
+ let settled = false;
51
+ const finish = result => {
52
+ if (settled) return;
53
+ settled = true;
54
+ resolve(result);
55
+ };
56
+ child.once('error', error => finish({ ok: false, error: String(error?.message || error) }));
57
+ child.once('close', code => finish({
58
+ ok: true,
59
+ code: code ?? 1,
60
+ stdout: Buffer.concat(stdout).toString('utf8').replace(/\r/g, ''),
61
+ stderr: Buffer.concat(stderr).toString('utf8').replace(/\r/g, ''),
62
+ truncated: stdoutState.truncated || stderrState.truncated,
63
+ }));
64
+ });
65
+ }
66
+
67
+ let started = false;
68
+ process.stdin.setEncoding('utf8');
69
+ process.stdin.on('data', async chunk => {
70
+ if (started || !String(chunk).split(/\r?\n/).includes('go')) return;
71
+ started = true;
72
+ const result = await run(process.argv[2]);
73
+ writeResult(result);
74
+ });
75
+ process.stdin.resume();
@@ -0,0 +1,85 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { runProcess } from '../yeaft/tools/process-runner.js';
5
+
6
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
7
+ const WINDOWS_JOB_SCRIPT = join(MODULE_DIR, 'windows-version-job.ps1');
8
+ const WINDOWS_WORKER = join(MODULE_DIR, 'windows-version-worker.js');
9
+ const DEFAULT_WINDOWS_CLEANUP_MS = 500;
10
+
11
+ export function resolveWindowsPowerShell({ env = process.env, fileExists = existsSync } = {}) {
12
+ const systemRoot = env.SystemRoot || env.WINDIR || 'C:\\Windows';
13
+ const candidates = [
14
+ join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
15
+ 'powershell.exe',
16
+ ];
17
+ return candidates.find(candidate => candidate === 'powershell.exe' || fileExists(candidate));
18
+ }
19
+
20
+ function remaining(deadline, fallback) {
21
+ if (!Number.isFinite(deadline)) return fallback;
22
+ return Math.max(1, deadline - Date.now());
23
+ }
24
+
25
+ export async function readWindowsBrowserExecutableVersion(executablePath, {
26
+ signal = null,
27
+ terminationDeadline = null,
28
+ run = runProcess,
29
+ powershellPath = resolveWindowsPowerShell(),
30
+ nodePath = process.execPath,
31
+ workerPath = WINDOWS_WORKER,
32
+ jobScriptPath = WINDOWS_JOB_SCRIPT,
33
+ env = process.env,
34
+ } = {}) {
35
+ const cleanupMs = Number.isFinite(terminationDeadline)
36
+ ? Math.min(DEFAULT_WINDOWS_CLEANUP_MS, remaining(terminationDeadline, DEFAULT_WINDOWS_CLEANUP_MS))
37
+ : DEFAULT_WINDOWS_CLEANUP_MS;
38
+ const timeoutMs = remaining(terminationDeadline, 20_000);
39
+ const result = await run(powershellPath, [
40
+ '-NoLogo',
41
+ '-NoProfile',
42
+ '-NonInteractive',
43
+ '-ExecutionPolicy',
44
+ 'Bypass',
45
+ '-File',
46
+ jobScriptPath,
47
+ '-NodePath',
48
+ nodePath,
49
+ '-WorkerPath',
50
+ workerPath,
51
+ '-ExecutablePath',
52
+ executablePath,
53
+ '-CleanupTimeoutMs',
54
+ String(cleanupMs),
55
+ ], {
56
+ signal,
57
+ timeoutMs,
58
+ maxBytes: 128 * 1024,
59
+ env,
60
+ killGraceMs: 0,
61
+ gracefulTerminationDeadline: terminationDeadline,
62
+ terminationDeadline,
63
+ forceSettleMs: cleanupMs,
64
+ treeKillTimeoutMs: cleanupMs,
65
+ requireExitConfirmation: true,
66
+ });
67
+ if (result.code !== 0) {
68
+ const termination = result.terminationError ? ` ${result.terminationError}` : '';
69
+ throw new Error(`Managed Chrome version job failed (${result.code}): ${result.stderr.slice(0, 500)}${termination}`);
70
+ }
71
+
72
+ const line = result.stdout.trim().split(/\r?\n/).at(-1);
73
+ let payload;
74
+ try {
75
+ payload = JSON.parse(line);
76
+ } catch {
77
+ throw new Error(`Managed Chrome version job returned invalid output: ${result.stdout.slice(0, 500)}`);
78
+ }
79
+ if (!payload?.ok) throw new Error(`Managed Chrome version check failed: ${payload?.error || 'unknown worker error'}`);
80
+ if (payload.code !== 0) {
81
+ throw new Error(`Managed Chrome version check failed (${payload.code}): ${String(payload.stderr || '').slice(0, 200)}`);
82
+ }
83
+ if (payload.truncated) throw new Error('Managed Chrome version output exceeded its bounded capture');
84
+ return String(payload.stdout || '').trim();
85
+ }