@harness-mix/cli 0.2.3 → 0.2.4

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 (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +469 -467
  3. package/output/native-build/desktop-controller.mjs +1 -1
  4. package/output/native-build/renderer-extension.js +23 -4
  5. package/package.json +11 -9
  6. package/scripts/antigravity-adapter-test.cjs +647 -626
  7. package/scripts/codex-adapter-test.cjs +162 -127
  8. package/scripts/collaboration-test.cjs +274 -262
  9. package/scripts/jsonl-stdin-test.cjs +40 -31
  10. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  11. package/scripts/native-acp-depth-test.cjs +30 -5
  12. package/scripts/native-update-apply-test.cjs +269 -215
  13. package/scripts/native-update.cjs +78 -0
  14. package/scripts/native-vendor-adapters-test.cjs +196 -154
  15. package/scripts/salvage-rollout-writes.cjs +72 -0
  16. package/scripts/zcode-adapter-test.cjs +329 -0
  17. package/scripts/zcode-live-probe.cjs +66 -0
  18. package/src/main/adapters/antigravity.js +1428 -1418
  19. package/src/main/adapters/codex.js +656 -649
  20. package/src/main/adapters/native-acp-command.js +51 -48
  21. package/src/main/adapters/native-acp.js +47 -12
  22. package/src/main/adapters/qoder.js +12 -8
  23. package/src/main/adapters/zcode.js +921 -10
  24. package/src/main/host/collaboration.js +723 -715
  25. package/src/main/host/jsonl.js +130 -120
  26. package/src/main/native/config.js +9 -9
  27. package/src/main/native/launcher.js +252 -237
  28. package/src/main/native/process-utils.js +157 -57
  29. package/src/main/native/protocol.js +1221 -1187
  30. package/src/main/native/update-state.js +123 -110
  31. package/src/main/native/updater.js +460 -394
  32. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  33. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
  34. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
@@ -1,237 +1,252 @@
1
- const fs = require('node:fs');
2
- const path = require('node:path');
3
- const net = require('node:net');
4
- const crypto = require('node:crypto');
5
- const { spawn, spawnSync, execFileSync } = require('node:child_process');
6
- const { nativePaths, nativeEnvironment, saveNativeSettings } = require('./config');
7
- const { runUpdateFlow, reexecLauncher } = require('./updater');
8
- const { markBootOk, pidAlive } = require('./update-state');
9
- const { inspectPosix, assertDesktopStopped } = require('./platform');
10
- const {
11
- evaluateDesktopCompatibility,
12
- enforceDesktopCompatibility,
13
- } = require('./compatibility');
14
-
15
- const REPO_ROOT = path.resolve(__dirname, '../../..');
16
- const LIVE_HOST_HEARTBEAT_MS = 15000;
17
-
18
- function isCodexTaskEnvironment(env = process.env) {
19
- return Boolean(
20
- env.CODEX_THREAD_ID ||
21
- env.CODEX_SESSION_ID ||
22
- env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === 'Codex Desktop'
23
- );
24
- }
25
-
26
- function readLiveHostInstance(dataDir, {
27
- now = Date.now(),
28
- isPidAlive = pidAlive,
29
- maxAgeMs = LIVE_HOST_HEARTBEAT_MS,
30
- } = {}) {
31
- try {
32
- const instance = JSON.parse(fs.readFileSync(path.join(dataDir, 'runtime', 'instance.json'), 'utf8'));
33
- const age = now - Number(instance.beatAt || 0);
34
- if (
35
- instance.mode !== 'native-host' ||
36
- !Number.isInteger(instance.pid) ||
37
- instance.pid <= 0 ||
38
- age < 0 ||
39
- age > maxAgeMs ||
40
- !isPidAlive(instance.pid)
41
- ) return null;
42
- return instance;
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- function cacheCodexRuntime(resources, cache) {
49
- fs.mkdirSync(cache, { recursive: true });
50
- // The CLI resolves helper executables next to itself, including code mode.
51
- // Repair existing caches as well as preparing a new version.
52
- for (const entry of fs.readdirSync(resources, { withFileTypes: true })) {
53
- if (!entry.isFile() || !/^(?:codex(?:-.+)?|rg)\.exe$/i.test(entry.name)) continue;
54
- const source = path.join(resources, entry.name);
55
- const destination = path.join(cache, entry.name);
56
- if (!fs.existsSync(destination) || fs.statSync(destination).size !== fs.statSync(source).size) {
57
- fs.copyFileSync(source, destination);
58
- }
59
- }
60
- return path.join(cache, 'codex.exe');
61
- }
62
-
63
- function powershell(source) {
64
- return execFileSync('pwsh.exe', ['-NoLogo', '-NoProfile', '-Command', source], { encoding: 'utf8', windowsHide: true, timeout: 20000 }).trim();
65
- }
66
-
67
- // Match the verified installation executable; never terminate unrelated apps.
68
- function stopDesktopProcesses(installation) {
69
- if (process.platform !== 'win32') return assertDesktopStopped(installation);
70
- const target = installation.executable.replace(/'/g, "''");
71
- powershell(`Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq '${target}' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`);
72
- }
73
-
74
- // Reports a live host instance and reaps leftovers from crashed or force-killed
75
- // sessions before a new desktop is activated. Only processes that belong to this
76
- // project (shim binary path or our known entry scripts) are touched.
77
- function sweepLeftovers(root, dataDir) {
78
- if (process.platform !== 'win32') return;
79
- try {
80
- const instanceFile = path.join(dataDir, 'runtime', 'instance.json');
81
- let instance = null;
82
- try { instance = JSON.parse(fs.readFileSync(instanceFile, 'utf8')); } catch { /* no live instance record */ }
83
- if (instance && instance.pid && pidAlive(instance.pid) && Date.now() - Number(instance.beatAt || 0) < 30000) {
84
- console.warn(`[Harness Mix] 另一宿主实例(pid ${instance.pid})心跳仍在;继续清扫其残留。`);
85
- }
86
- const shimDir = path.join(root, 'output', 'native-build').replace(/'/g, "''");
87
- const query = `Get-CimInstance Win32_Process | Where-Object { ($_.ExecutablePath -like '${shimDir}\\harness-mix-*.exe') -or ($_.Name -eq 'node.exe' -and ($_.CommandLine -like '*native-host.cjs*' -or $_.CommandLine -like '*desktop-controller.mjs*')) } | Select-Object -ExpandProperty ProcessId`;
88
- const output = powershell(query);
89
- const start = Date.now();
90
- const pids = output.split(/\s+/)
91
- .map(value => Number.parseInt(value, 10))
92
- .filter(pid => Number.isInteger(pid) && pid > 0 && pid !== process.pid && pid !== process.ppid);
93
- for (const pid of pids) spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
94
- if (pids.length) console.log(`[Harness Mix] 启动前清扫残留进程 ${pids.length} 个(${Date.now() - start}ms)`);
95
- } catch (error) {
96
- console.warn(`[Harness Mix] 残留进程清扫失败(不影响启动):${error.message}`);
97
- }
98
- }
99
-
100
- // Stream a file into a sha256 digest. Avoids loading multi-hundred-MB binaries
101
- // into a single buffer via Buffer.allocUnsafe, which Node rejects with
102
- // ERR_MEMORY_ALLOCATION_FAILED on 64-bit Windows once the file outgrows the
103
- // internal pool size (Codex Desktop's bundled codex.exe is ~295 MB).
104
- function sha256OfFile(filePath) {
105
- return new Promise((resolve, reject) => {
106
- const hash = crypto.createHash('sha256');
107
- const stream = fs.createReadStream(filePath, { highWaterMark: 1 << 20 });
108
- stream.on('data', chunk => hash.update(chunk));
109
- stream.on('end', () => resolve(hash.digest('hex')));
110
- stream.on('error', reject);
111
- });
112
- }
113
- async function inspect() {
114
- if (process.platform !== 'win32') return inspectPosix();
115
- const installation = JSON.parse(powershell("$p = Get-AppxPackage -Name OpenAI.Codex | Select-Object -First 1; if (-not $p) { throw 'Codex Desktop not installed' }; $m = Get-AppxPackageManifest $p; @{ root=$p.InstallLocation; fullName=$p.PackageFullName; appId=($p.PackageFamilyName + '!' + @($m.Package.Applications.Application)[0].Id); version=$p.Version.ToString() } | ConvertTo-Json -Compress"));
116
- const packaged = path.join(installation.root, 'app/resources/codex.exe');
117
- if (!fs.existsSync(packaged)) throw new Error('Packaged Codex CLI not found');
118
- // An executable outside WindowsApps avoids package ACL/activation constraints.
119
- const digest = (await sha256OfFile(packaged)).slice(0, 16);
120
- const cache = path.join(process.env.LOCALAPPDATA, 'Harness Mix/codex', digest);
121
- const stock = cacheCodexRuntime(path.dirname(packaged), cache);
122
- return { ...installation, stock, executable: path.join(installation.root, 'app/ChatGPT.exe') };
123
- }
124
- async function freePort() {
125
- const server = net.createServer();
126
- await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
127
- const port = server.address().port;
128
- await new Promise(resolve => server.close(resolve));
129
- return port;
130
- }
131
- async function launch(args = []) {
132
- const root = REPO_ROOT;
133
- const flags = new Set(args);
134
- const unknown = args.filter(arg => !['--check', '--update', '--no-update', '--restart'].includes(arg));
135
- if (unknown.length) throw new Error('Unsupported Launcher arguments');
136
- const dataDir = nativeEnvironment().HARNESSMIX_DATA_DIR;
137
- if (!flags.has('--check') && isCodexTaskEnvironment()) {
138
- throw new Error('拒绝从 Codex 任务内部启动或重启 Harness Mix:这会终止当前 Codex Desktop 和正在执行的任务。请在 Codex 外部终端运行 Start-Codex.cmd 或 npm start。');
139
- }
140
- if (flags.has('--update')) {
141
- const outcome = await runUpdateFlow({
142
- root, dataDir, mode: 'apply',
143
- stopDesktop: async () => stopDesktopProcesses(await inspect()),
144
- });
145
- if (outcome.updated || outcome.repaired || outcome.rolledBack) {
146
- console.log('[Harness Mix] 更新流程完成,重新运行 npm start 生效。');
147
- }
148
- return;
149
- }
150
- if (!flags.has('--check') && !flags.has('--restart')) {
151
- const live = readLiveHostInstance(dataDir);
152
- if (live) {
153
- console.log(`[Harness Mix] 已在运行(Host PID ${live.pid});不会重启当前 Codex Desktop。需要显式重启时使用 npm start -- --restart。`);
154
- return;
155
- }
156
- }
157
- const installation = await inspect();
158
- const compatibility = installation.version === 'unknown'
159
- ? { state: 'unverified', desktopVersion: 'unknown', evidence: null }
160
- : evaluateDesktopCompatibility(installation.version);
161
- const paths = nativePaths();
162
- for (const file of Object.values(paths)) if (!fs.existsSync(file)) throw new Error(`Missing ${file}; run npm run build:native`);
163
- if (flags.has('--check')) {
164
- console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\ncodex_mode=official-passthrough\ncodex_managed_route=codex-harness\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
165
- return;
166
- }
167
- const skipUpdate = flags.has('--no-update') || process.env.HARNESS_MIX_AUTO_UPDATE === '0';
168
- if (!skipUpdate) {
169
- const outcome = await runUpdateFlow({
170
- root, dataDir, mode: 'apply',
171
- stopDesktop: async () => stopDesktopProcesses(installation),
172
- });
173
- if (outcome.restartRequired) {
174
- if (process.env.HARNESS_MIX_UPDATED === '1') throw new Error('更新后重启循环,已停止:请手动运行 npm run check:native 检查安装');
175
- console.log('[Harness Mix] 代码已更新,重新加载启动器…');
176
- process.exitCode = await reexecLauncher(root, args);
177
- return;
178
- }
179
- }
180
- enforceDesktopCompatibility(compatibility);
181
- if (compatibility.state !== 'verified') {
182
- console.warn(`[Harness Mix] Codex Desktop ${installation.version} compatibility is ${compatibility.state}; protocol checks continue, but full restarted Desktop acceptance is not recorded.`);
183
- }
184
- const env = nativeEnvironment();
185
- // Retire the previous runtime first: it still holds the data directory open
186
- // and would otherwise write over live sessions and accounts.
187
- console.log('[Harness Mix] Restarting Codex Desktop: official Codex passthrough + Harness Mix routes.');
188
- stopDesktopProcesses(installation);
189
- await new Promise(resolve => setTimeout(resolve, 1000));
190
- sweepLeftovers(root, dataDir);
191
- saveNativeSettings(env);
192
- fs.writeFileSync(path.join(path.dirname(paths.shim), 'node-path.txt'), process.execPath);
193
- fs.writeFileSync(path.join(path.dirname(paths.shim), 'stock-path.txt'), installation.stock);
194
- const port = await freePort();
195
- const attachmentPort = await freePort();
196
- const nonce = crypto.randomBytes(16).toString('hex');
197
- const overrides = { CODEX_CLI_PATH: paths.shim, HARNESSMIX_STOCK_CODEX_PATH: installation.stock,
198
- HARNESSMIX_DATA_DIR: env.HARNESSMIX_DATA_DIR, HARNESS_MIX_NODE_PATH: process.execPath,
199
- HARNESSMIX_DEFAULT_AGENT: 'codex' };
200
- const block = Buffer.from(Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le').toString('base64');
201
- let desktop;
202
- let pid;
203
- if (process.platform === 'win32') {
204
- pid = execFileSync(paths.activation, [installation.fullName, installation.appId, block, `--remote-debugging-port=${port}`], { encoding: 'utf8', windowsHide: true }).trim();
205
- } else {
206
- desktop = spawn(installation.executable, [`--remote-debugging-port=${port}`, '--remote-debugging-address=127.0.0.1'],
207
- { env: { ...env, ...overrides }, stdio: 'ignore' });
208
- await new Promise((resolve, reject) => { desktop.once('spawn', resolve); desktop.once('error', reject); });
209
- pid = desktop.pid;
210
- }
211
- console.log(`[Harness Mix] Desktop PID ${pid}, CDP ${port}`);
212
- const controller = spawn(process.execPath, [paths.controller, '--renderer-cdp-endpoint', `http://127.0.0.1:${port}`,
213
- '--renderer', paths.renderer, '--default-agent', 'codex', '--attachment-port', String(attachmentPort), '--attachment-nonce', nonce],
214
- { env, stdio: 'inherit', windowsHide: true });
215
- controller.on('error', error => { console.error(error.message); process.exitCode = 1; });
216
- controller.on('exit', code => { process.exitCode = code || 0; });
217
- if (desktop) {
218
- desktop.once('exit', () => controller.kill('SIGTERM'));
219
- // The GUI may remain open after controller failure; do not hold the launcher alive.
220
- controller.once('exit', () => desktop.unref());
221
- const stop = () => { controller.kill('SIGTERM'); desktop.kill('SIGTERM'); };
222
- process.once('SIGINT', stop);
223
- process.once('SIGTERM', stop);
224
- }
225
- // A healthy boot = the controller still alive after 20s; then a freshly applied
226
- // update is marked good and the crash-loop counter resets.
227
- const bootTimer = setTimeout(() => { if (controller.exitCode === null) markBootOk(dataDir); }, 20000);
228
- controller.on('exit', () => clearTimeout(bootTimer));
229
- return controller;
230
- }
231
- module.exports = {
232
- inspect,
233
- launch,
234
- cacheCodexRuntime,
235
- isCodexTaskEnvironment,
236
- readLiveHostInstance,
237
- };
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const net = require('node:net');
4
+ const crypto = require('node:crypto');
5
+ const { spawn, spawnSync, execFileSync } = require('node:child_process');
6
+ const { nativePaths, nativeEnvironment, saveNativeSettings } = require('./config');
7
+ const { runUpdateFlow, reexecLauncher } = require('./updater');
8
+ const { markBootOk, pidAlive } = require('./update-state');
9
+ const { inspectPosix, assertDesktopStopped } = require('./platform');
10
+ const {
11
+ evaluateDesktopCompatibility,
12
+ enforceDesktopCompatibility,
13
+ } = require('./compatibility');
14
+
15
+ const REPO_ROOT = path.resolve(__dirname, '../../..');
16
+ const LIVE_HOST_HEARTBEAT_MS = 15000;
17
+
18
+ function isCodexTaskEnvironment(env = process.env) {
19
+ return Boolean(
20
+ env.CODEX_THREAD_ID ||
21
+ env.CODEX_SESSION_ID ||
22
+ env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === 'Codex Desktop'
23
+ );
24
+ }
25
+
26
+ function readLiveHostInstance(dataDir, {
27
+ now = Date.now(),
28
+ isPidAlive = pidAlive,
29
+ maxAgeMs = LIVE_HOST_HEARTBEAT_MS,
30
+ } = {}) {
31
+ try {
32
+ const instance = JSON.parse(fs.readFileSync(path.join(dataDir, 'runtime', 'instance.json'), 'utf8'));
33
+ const age = now - Number(instance.beatAt || 0);
34
+ if (
35
+ instance.mode !== 'native-host' ||
36
+ !Number.isInteger(instance.pid) ||
37
+ instance.pid <= 0 ||
38
+ age < 0 ||
39
+ age > maxAgeMs ||
40
+ !isPidAlive(instance.pid)
41
+ ) return null;
42
+ return instance;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function cacheCodexRuntime(resources, cache) {
49
+ fs.mkdirSync(cache, { recursive: true });
50
+ // The CLI resolves helper executables next to itself, including code mode.
51
+ // Repair existing caches as well as preparing a new version.
52
+ for (const entry of fs.readdirSync(resources, { withFileTypes: true })) {
53
+ if (!entry.isFile() || !/^(?:codex(?:-.+)?|rg)\.exe$/i.test(entry.name)) continue;
54
+ const source = path.join(resources, entry.name);
55
+ const destination = path.join(cache, entry.name);
56
+ if (!fs.existsSync(destination) || fs.statSync(destination).size !== fs.statSync(source).size) {
57
+ fs.copyFileSync(source, destination);
58
+ }
59
+ }
60
+ return path.join(cache, 'codex.exe');
61
+ }
62
+
63
+ // PowerShell 7 (pwsh.exe) is preferred but not preinstalled on stock Windows;
64
+ // fall back to Windows PowerShell 5.1 Get-AppxPackage/Get-CimInstance work in
65
+ // both. Only a missing binary (ENOENT) falls through: a failed command must
66
+ // surface, not silently retry in the other shell.
67
+ function powershell(source) {
68
+ const args = ['-NoLogo', '-NoProfile', '-Command', source];
69
+ let lastError = null;
70
+ for (const bin of ['pwsh.exe', 'powershell.exe']) {
71
+ try {
72
+ return execFileSync(bin, args, { encoding: 'utf8', windowsHide: true, timeout: 20000 }).trim();
73
+ } catch (error) {
74
+ if (error.code !== 'ENOENT') throw error;
75
+ lastError = error;
76
+ }
77
+ }
78
+ throw new Error(`未找到可用的 PowerShell(pwsh.exe / powershell.exe):${lastError.message}`);
79
+ }
80
+
81
+ // Match the verified installation executable; never terminate unrelated apps.
82
+ function stopDesktopProcesses(installation) {
83
+ if (process.platform !== 'win32') return assertDesktopStopped(installation);
84
+ const target = installation.executable.replace(/'/g, "''");
85
+ powershell(`Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -eq '${target}' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`);
86
+ }
87
+
88
+ // Reports a live host instance and reaps leftovers from crashed or force-killed
89
+ // sessions before a new desktop is activated. Only processes that belong to this
90
+ // project (shim binary path or our known entry scripts) are touched.
91
+ function sweepLeftovers(root, dataDir) {
92
+ if (process.platform !== 'win32') return;
93
+ try {
94
+ const instanceFile = path.join(dataDir, 'runtime', 'instance.json');
95
+ let instance = null;
96
+ try { instance = JSON.parse(fs.readFileSync(instanceFile, 'utf8')); } catch { /* no live instance record */ }
97
+ if (instance && instance.pid && pidAlive(instance.pid) && Date.now() - Number(instance.beatAt || 0) < 30000) {
98
+ console.warn(`[Harness Mix] 另一宿主实例(pid ${instance.pid})心跳仍在;继续清扫其残留。`);
99
+ }
100
+ const shimDir = path.join(root, 'output', 'native-build').replace(/'/g, "''");
101
+ const query = `Get-CimInstance Win32_Process | Where-Object { ($_.ExecutablePath -like '${shimDir}\\harness-mix-*.exe') -or ($_.Name -eq 'node.exe' -and ($_.CommandLine -like '*native-host.cjs*' -or $_.CommandLine -like '*desktop-controller.mjs*')) } | Select-Object -ExpandProperty ProcessId`;
102
+ const output = powershell(query);
103
+ const start = Date.now();
104
+ const pids = output.split(/\s+/)
105
+ .map(value => Number.parseInt(value, 10))
106
+ .filter(pid => Number.isInteger(pid) && pid > 0 && pid !== process.pid && pid !== process.ppid);
107
+ for (const pid of pids) spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
108
+ if (pids.length) console.log(`[Harness Mix] 启动前清扫残留进程 ${pids.length} 个(${Date.now() - start}ms)`);
109
+ } catch (error) {
110
+ console.warn(`[Harness Mix] 残留进程清扫失败(不影响启动):${error.message}`);
111
+ }
112
+ }
113
+
114
+ // Stream a file into a sha256 digest. Avoids loading multi-hundred-MB binaries
115
+ // into a single buffer via Buffer.allocUnsafe, which Node rejects with
116
+ // ERR_MEMORY_ALLOCATION_FAILED on 64-bit Windows once the file outgrows the
117
+ // internal pool size (Codex Desktop's bundled codex.exe is ~295 MB).
118
+ function sha256OfFile(filePath) {
119
+ return new Promise((resolve, reject) => {
120
+ const hash = crypto.createHash('sha256');
121
+ const stream = fs.createReadStream(filePath, { highWaterMark: 1 << 20 });
122
+ stream.on('data', chunk => hash.update(chunk));
123
+ stream.on('end', () => resolve(hash.digest('hex')));
124
+ stream.on('error', reject);
125
+ });
126
+ }
127
+ async function inspect() {
128
+ if (process.platform !== 'win32') return inspectPosix();
129
+ const installation = JSON.parse(powershell("$p = Get-AppxPackage -Name OpenAI.Codex | Select-Object -First 1; if (-not $p) { throw 'Codex Desktop not installed' }; $m = Get-AppxPackageManifest $p; @{ root=$p.InstallLocation; fullName=$p.PackageFullName; appId=($p.PackageFamilyName + '!' + @($m.Package.Applications.Application)[0].Id); version=$p.Version.ToString() } | ConvertTo-Json -Compress"));
130
+ const packaged = path.join(installation.root, 'app/resources/codex.exe');
131
+ if (!fs.existsSync(packaged)) throw new Error('Packaged Codex CLI not found');
132
+ // An executable outside WindowsApps avoids package ACL/activation constraints.
133
+ const digest = (await sha256OfFile(packaged)).slice(0, 16);
134
+ const cache = path.join(process.env.LOCALAPPDATA, 'Harness Mix/codex', digest);
135
+ const stock = cacheCodexRuntime(path.dirname(packaged), cache);
136
+ return { ...installation, stock, executable: path.join(installation.root, 'app/ChatGPT.exe') };
137
+ }
138
+ async function freePort() {
139
+ const server = net.createServer();
140
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
141
+ const port = server.address().port;
142
+ await new Promise(resolve => server.close(resolve));
143
+ return port;
144
+ }
145
+ async function launch(args = []) {
146
+ const root = REPO_ROOT;
147
+ const flags = new Set(args);
148
+ const unknown = args.filter(arg => !['--check', '--update', '--no-update', '--restart'].includes(arg));
149
+ if (unknown.length) throw new Error('Unsupported Launcher arguments');
150
+ const dataDir = nativeEnvironment().HARNESSMIX_DATA_DIR;
151
+ if (!flags.has('--check') && isCodexTaskEnvironment()) {
152
+ throw new Error('拒绝从 Codex 任务内部启动或重启 Harness Mix:这会终止当前 Codex Desktop 和正在执行的任务。请在 Codex 外部终端运行 Start-Codex.cmd 或 npm start。');
153
+ }
154
+ if (flags.has('--update')) {
155
+ const outcome = await runUpdateFlow({
156
+ root, dataDir, mode: 'apply', completePendingBuild: true,
157
+ stopDesktop: async () => stopDesktopProcesses(await inspect()),
158
+ });
159
+ if (outcome.updated || outcome.repaired || outcome.rolledBack) {
160
+ console.log('[Harness Mix] 更新流程完成,重新运行 npm start 生效。');
161
+ }
162
+ return;
163
+ }
164
+ if (!flags.has('--check') && !flags.has('--restart')) {
165
+ const live = readLiveHostInstance(dataDir);
166
+ if (live) {
167
+ console.log(`[Harness Mix] 已在运行(Host PID ${live.pid});不会重启当前 Codex Desktop。需要显式重启时使用 npm start -- --restart。`);
168
+ return;
169
+ }
170
+ }
171
+ const installation = await inspect();
172
+ const compatibility = installation.version === 'unknown'
173
+ ? { state: 'unverified', desktopVersion: 'unknown', evidence: null }
174
+ : evaluateDesktopCompatibility(installation.version);
175
+ const paths = nativePaths();
176
+ for (const file of Object.values(paths)) if (!fs.existsSync(file)) throw new Error(`Missing ${file}; run npm run build:native`);
177
+ if (flags.has('--check')) {
178
+ console.log(`platform=${process.platform}\narchitecture=${process.arch}\ndesktop_version=${installation.version}\ndesktop_compatibility=${compatibility.state}\ndesktop_evidence=${compatibility.evidence?.level || 'none'}\nexecutable_codex_cli=${installation.stock}\ncodex_mode=official-passthrough\ncodex_managed_route=codex-harness\nlauncher=${paths.cli}\nshim=${paths.shim}\nruntime=${paths.runtime}\nrenderer=${paths.renderer}\ncore=src/main/protocol-core`);
179
+ return;
180
+ }
181
+ const skipUpdate = flags.has('--no-update') || process.env.HARNESS_MIX_AUTO_UPDATE === '0';
182
+ if (!skipUpdate) {
183
+ const outcome = await runUpdateFlow({
184
+ root, dataDir, mode: 'apply', completePendingBuild: true,
185
+ stopDesktop: async () => stopDesktopProcesses(installation),
186
+ });
187
+ if (outcome.restartRequired) {
188
+ if (process.env.HARNESS_MIX_UPDATED === '1') throw new Error('更新后重启循环,已停止:请手动运行 npm run check:native 检查安装');
189
+ console.log('[Harness Mix] 代码已更新,重新加载启动器…');
190
+ process.exitCode = await reexecLauncher(root, args);
191
+ return;
192
+ }
193
+ }
194
+ enforceDesktopCompatibility(compatibility);
195
+ if (compatibility.state !== 'verified') {
196
+ console.warn(`[Harness Mix] Codex Desktop ${installation.version} compatibility is ${compatibility.state}; protocol checks continue, but full restarted Desktop acceptance is not recorded.`);
197
+ }
198
+ const env = nativeEnvironment();
199
+ // Retire the previous runtime first: it still holds the data directory open
200
+ // and would otherwise write over live sessions and accounts.
201
+ console.log('[Harness Mix] Restarting Codex Desktop: official Codex passthrough + Harness Mix routes.');
202
+ stopDesktopProcesses(installation);
203
+ await new Promise(resolve => setTimeout(resolve, 1000));
204
+ sweepLeftovers(root, dataDir);
205
+ saveNativeSettings(env);
206
+ fs.writeFileSync(path.join(path.dirname(paths.shim), 'node-path.txt'), process.execPath);
207
+ fs.writeFileSync(path.join(path.dirname(paths.shim), 'stock-path.txt'), installation.stock);
208
+ const port = await freePort();
209
+ const attachmentPort = await freePort();
210
+ const nonce = crypto.randomBytes(16).toString('hex');
211
+ const overrides = { CODEX_CLI_PATH: paths.shim, HARNESSMIX_STOCK_CODEX_PATH: installation.stock,
212
+ HARNESSMIX_DATA_DIR: env.HARNESSMIX_DATA_DIR, HARNESS_MIX_NODE_PATH: process.execPath,
213
+ HARNESSMIX_DEFAULT_AGENT: 'codex' };
214
+ const block = Buffer.from(Object.entries(overrides).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le').toString('base64');
215
+ let desktop;
216
+ let pid;
217
+ if (process.platform === 'win32') {
218
+ pid = execFileSync(paths.activation, [installation.fullName, installation.appId, block, `--remote-debugging-port=${port}`], { encoding: 'utf8', windowsHide: true }).trim();
219
+ } else {
220
+ desktop = spawn(installation.executable, [`--remote-debugging-port=${port}`, '--remote-debugging-address=127.0.0.1'],
221
+ { env: { ...env, ...overrides }, stdio: 'ignore' });
222
+ await new Promise((resolve, reject) => { desktop.once('spawn', resolve); desktop.once('error', reject); });
223
+ pid = desktop.pid;
224
+ }
225
+ console.log(`[Harness Mix] Desktop PID ${pid}, CDP ${port}`);
226
+ const controller = spawn(process.execPath, [paths.controller, '--renderer-cdp-endpoint', `http://127.0.0.1:${port}`,
227
+ '--renderer', paths.renderer, '--default-agent', 'codex', '--attachment-port', String(attachmentPort), '--attachment-nonce', nonce],
228
+ { env, stdio: 'inherit', windowsHide: true });
229
+ controller.on('error', error => { console.error(error.message); process.exitCode = 1; });
230
+ controller.on('exit', code => { process.exitCode = code || 0; });
231
+ if (desktop) {
232
+ desktop.once('exit', () => controller.kill('SIGTERM'));
233
+ // The GUI may remain open after controller failure; do not hold the launcher alive.
234
+ controller.once('exit', () => desktop.unref());
235
+ const stop = () => { controller.kill('SIGTERM'); desktop.kill('SIGTERM'); };
236
+ process.once('SIGINT', stop);
237
+ process.once('SIGTERM', stop);
238
+ }
239
+ // A healthy boot = the controller still alive after 20s; then a freshly applied
240
+ // update is marked good and the crash-loop counter resets.
241
+ const bootTimer = setTimeout(() => { if (controller.exitCode === null) markBootOk(dataDir); }, 20000);
242
+ controller.on('exit', () => clearTimeout(bootTimer));
243
+ return controller;
244
+ }
245
+ module.exports = {
246
+ inspect,
247
+ launch,
248
+ cacheCodexRuntime,
249
+ isCodexTaskEnvironment,
250
+ readLiveHostInstance,
251
+ stopDesktopProcesses,
252
+ };