@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,120 +1,130 @@
1
- const { spawn } = require("node:child_process");
2
- const { StringDecoder } = require("node:string_decoder");
3
- const { terminateTree } = require("../native/process-utils");
4
-
5
- /**
6
- * JSONL 进程传输层。
7
- * - 严格 JSONL 分帧:仅以 \n 切分,剥离行尾 \r(Node readline 会把 U+2028/U+2029
8
- * 当作换行,不符合 Pi RPC 协议要求,这里按规范自行实现)。
9
- * - 同时支持两种报文形态:
10
- * 1. Pi 命令式:{ id, type, ... },响应为 { type: "response", id, success, data|error }
11
- * 2. JSON-RPC 式:{ jsonrpc, id, method, params }(DSH/ACP),包括 Agent → Client 的请求
12
- */
13
- class JsonlProcess {
14
- constructor(command, args, options, hooks) {
15
- this.pending = new Map();
16
- this.nextId = 1;
17
- this.hooks = hooks;
18
- this.child = spawn(command, args, { windowsHide: true, ...options, stdio: ["pipe", "pipe", "pipe"] });
19
- // 子进程异常退出/管道破裂时,迟到的 stdin.write 会在流上异步抛 EPIPE;
20
- // Writable error 监听会被 Node 当作未捕获异常直接 crash 宿主进程。
21
- // 真实失败由 exit/error 路径统一结算,这里仅吞掉管道噪声。
22
- this.child.stdin.on("error", (error) => this.hooks.onDiagnostic?.(`stdin: ${error.message}`));
23
- this.#attachReader(this.child.stdout, (line) => this.#dispatch(line));
24
- this.#attachReader(this.child.stderr, (line) => hooks.onDiagnostic?.(line));
25
- this.child.on("error", (error) => this.#failAll(error));
26
- this.child.on("exit", (code, signal) => this.#failAll(new Error(`Harness 进程已退出 (${code ?? signal ?? "unknown"})`)));
27
- }
28
-
29
- #attachReader(stream, onLine) {
30
- const decoder = new StringDecoder("utf8");
31
- let buffer = "";
32
- stream.on("data", (chunk) => {
33
- buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
34
- let index;
35
- while ((index = buffer.indexOf("\n")) !== -1) {
36
- let line = buffer.slice(0, index);
37
- buffer = buffer.slice(index + 1);
38
- if (line.endsWith("\r")) line = line.slice(0, -1);
39
- if (line.trim()) onLine(line);
40
- }
41
- });
42
- stream.on("end", () => {
43
- buffer += decoder.end();
44
- if (buffer.trim()) onLine(buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer);
45
- });
46
- }
47
-
48
- #dispatch(line) {
49
- let value;
50
- try { value = JSON.parse(line); }
51
- catch { this.hooks.onDiagnostic?.(`Non-JSON stdout: ${line.slice(0, 500)}`); return; }
52
- // Agent Client 请求(JSON-RPC,带 method 和 id),需要回复
53
- if (value.method !== undefined && value.id !== undefined) {
54
- Promise.resolve()
55
- .then(() => {
56
- if (!this.hooks.onRequest) throw new Error(`Unsupported native client request: ${value.method}`);
57
- return this.hooks.onRequest(value);
58
- })
59
- .then((result) => this.#write({ jsonrpc: "2.0", id: value.id, result: result ?? {} }))
60
- .catch((error) => this.#write({ jsonrpc: "2.0", id: value.id, error: { code: -32603, message: error.message } }));
61
- return;
62
- }
63
- // 命令响应(id 匹配 pending)
64
- if (value.id !== undefined && this.pending.has(value.id)) {
65
- const { resolve, reject } = this.pending.get(value.id);
66
- this.pending.delete(value.id);
67
- if (value.error || value.success === false) {
68
- reject(new Error(typeof value.error === "string" ? value.error : value.error?.message || "Harness 请求失败"));
69
- } else {
70
- resolve(value.result ?? value.data ?? value);
71
- }
72
- return;
73
- }
74
- // 普通事件 / 通知
75
- this.hooks.onEvent?.(value);
76
- }
77
-
78
- #write(payload) {
79
- if (!this.child.stdin.destroyed) this.child.stdin.write(`${JSON.stringify(payload)}\n`);
80
- }
81
-
82
- /** JSON-RPC 请求(DSH/ACP) */
83
- request(method, params) {
84
- const id = this.nextId++;
85
- this.#write({ jsonrpc: "2.0", id, method, params });
86
- return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
87
- }
88
-
89
- /** JSON-RPC 通知(无响应) */
90
- notify(method, params) { this.#write({ jsonrpc: "2.0", method, params }); }
91
-
92
- /** Pi 命令式请求 */
93
- command(payload) {
94
- const id = String(this.nextId++);
95
- this.#write({ id, ...payload });
96
- return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
97
- }
98
-
99
- /** 回复 Agent → Client 请求之外的自由格式报文(如 Pi extension_ui_response) */
100
- send(payload) { this.#write(payload); }
101
-
102
- stop() { void terminateTree(this.child.pid); }
103
-
104
- #failAll(error) {
105
- for (const { reject } of this.pending.values()) reject(error);
106
- this.pending.clear();
107
- this.hooks.onExit?.(error);
108
- }
109
- }
110
-
111
- /** 跨平台 CLI 启动:Windows 上 .cmd shim 需要经 cmd.exe 执行 */
112
- function cliSpawn(bin, args) {
113
- if (process.platform === "win32") {
114
- const safe = [`${bin}.cmd`, ...args.map(String)].map((a) => (/[&|<>^%"]/.test(a) ? `"${a.replace(/["&|<>^%]/g, "")}"` : a.includes(" ") ? `"${a}"` : a));
115
- return { command: "cmd.exe", args: ["/d", "/s", "/c", safe.join(" ")] };
116
- }
117
- return { command: bin, args: args.map(String) };
118
- }
119
-
120
- module.exports = { JsonlProcess, cliSpawn };
1
+ const { spawn } = require("node:child_process");
2
+ const { StringDecoder } = require("node:string_decoder");
3
+ const { terminateTree } = require("../native/process-utils");
4
+
5
+ /**
6
+ * JSONL 进程传输层。
7
+ * - 严格 JSONL 分帧:仅以 \n 切分,剥离行尾 \r(Node readline 会把 U+2028/U+2029
8
+ * 当作换行,不符合 Pi RPC 协议要求,这里按规范自行实现)。
9
+ * - 同时支持两种报文形态:
10
+ * 1. Pi 命令式:{ id, type, ... },响应为 { type: "response", id, success, data|error }
11
+ * 2. JSON-RPC 式:{ jsonrpc, id, method, params }(DSH/ACP),包括 Agent → Client 的请求
12
+ */
13
+ class JsonlProcess {
14
+ constructor(command, args, options = {}, hooks = {}) {
15
+ this.pending = new Map();
16
+ this.nextId = 1;
17
+ this.hooks = hooks;
18
+ // jsonrpc: false = {id, method, params} 帧(省略 "jsonrpc" 字段)。
19
+ // ZCode app-server zod 校验把 "jsonrpc" 当 unrecognized key 拒收。
20
+ this.jsonrpc = options.jsonrpc !== false;
21
+ const spawnOptions = { ...options };
22
+ delete spawnOptions.jsonrpc;
23
+ this.child = spawn(command, args, { windowsHide: true, ...spawnOptions, stdio: ["pipe", "pipe", "pipe"] });
24
+ // 子进程异常退出/管道破裂时,迟到的 stdin.write 会在流上异步抛 EPIPE;
25
+ // Writable 无 error 监听会被 Node 当作未捕获异常直接 crash 宿主进程。
26
+ // 真实失败由 exit/error 路径统一结算,这里仅吞掉管道噪声。
27
+ this.child.stdin.on("error", (error) => this.hooks.onDiagnostic?.(`stdin: ${error.message}`));
28
+ this.#attachReader(this.child.stdout, (line) => this.#dispatch(line));
29
+ this.#attachReader(this.child.stderr, (line) => hooks.onDiagnostic?.(line));
30
+ this.child.on("error", (error) => this.#failAll(error));
31
+ this.child.on("exit", (code, signal) => {
32
+ const error = new Error(`Harness 进程已退出 (${code ?? signal ?? "unknown"})`);
33
+ // 确定性失败标记:harness 二进制在应答挂起请求前就退出。调用方用它抑制重试循环。
34
+ error.harnessExited = true;
35
+ this.#failAll(error);
36
+ });
37
+ }
38
+
39
+ #attachReader(stream, onLine) {
40
+ const decoder = new StringDecoder("utf8");
41
+ let buffer = "";
42
+ stream.on("data", (chunk) => {
43
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
44
+ let index;
45
+ while ((index = buffer.indexOf("\n")) !== -1) {
46
+ let line = buffer.slice(0, index);
47
+ buffer = buffer.slice(index + 1);
48
+ if (line.endsWith("\r")) line = line.slice(0, -1);
49
+ if (line.trim()) onLine(line);
50
+ }
51
+ });
52
+ stream.on("end", () => {
53
+ buffer += decoder.end();
54
+ if (buffer.trim()) onLine(buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer);
55
+ });
56
+ }
57
+
58
+ #dispatch(line) {
59
+ let value;
60
+ try { value = JSON.parse(line); }
61
+ catch { this.hooks.onDiagnostic?.(`Non-JSON stdout: ${line.slice(0, 500)}`); return; }
62
+ // Agent → Client 请求(JSON-RPC,带 method 和 id),需要回复
63
+ if (value.method !== undefined && value.id !== undefined) {
64
+ Promise.resolve()
65
+ .then(() => {
66
+ if (!this.hooks.onRequest) throw new Error(`Unsupported native client request: ${value.method}`);
67
+ return this.hooks.onRequest(value);
68
+ })
69
+ .then((result) => this.#write(this.jsonrpc ? { jsonrpc: "2.0", id: value.id, result: result ?? {} } : { id: value.id, result: result ?? {} }))
70
+ .catch((error) => this.#write(this.jsonrpc ? { jsonrpc: "2.0", id: value.id, error: { code: -32603, message: error.message } } : { id: value.id, error: { code: -32603, message: error.message } }));
71
+ return;
72
+ }
73
+ // 命令响应(id 匹配 pending)
74
+ if (value.id !== undefined && this.pending.has(value.id)) {
75
+ const { resolve, reject } = this.pending.get(value.id);
76
+ this.pending.delete(value.id);
77
+ if (value.error || value.success === false) {
78
+ reject(new Error(typeof value.error === "string" ? value.error : value.error?.message || "Harness 请求失败"));
79
+ } else {
80
+ resolve(value.result ?? value.data ?? value);
81
+ }
82
+ return;
83
+ }
84
+ // 普通事件 / 通知
85
+ this.hooks.onEvent?.(value);
86
+ }
87
+
88
+ #write(payload) {
89
+ if (!this.child.stdin.destroyed) this.child.stdin.write(`${JSON.stringify(payload)}\n`);
90
+ }
91
+
92
+ /** JSON-RPC 请求(DSH/ACP);纯帧模式下省略 jsonrpc 字段(ZCode) */
93
+ request(method, params) {
94
+ const id = this.nextId++;
95
+ this.#write(this.jsonrpc ? { jsonrpc: "2.0", id, method, params } : { id, method, params });
96
+ return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
97
+ }
98
+
99
+ /** JSON-RPC 通知(无响应) */
100
+ notify(method, params) { this.#write(this.jsonrpc ? { jsonrpc: "2.0", method, params } : { method, params }); }
101
+
102
+ /** Pi 命令式请求 */
103
+ command(payload) {
104
+ const id = String(this.nextId++);
105
+ this.#write({ id, ...payload });
106
+ return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
107
+ }
108
+
109
+ /** 回复 Agent → Client 请求之外的自由格式报文(如 Pi extension_ui_response) */
110
+ send(payload) { this.#write(payload); }
111
+
112
+ stop() { void terminateTree(this.child.pid); }
113
+
114
+ #failAll(error) {
115
+ for (const { reject } of this.pending.values()) reject(error);
116
+ this.pending.clear();
117
+ this.hooks.onExit?.(error);
118
+ }
119
+ }
120
+
121
+ /** 跨平台 CLI 启动:Windows 上 .cmd shim 需要经 cmd.exe 执行 */
122
+ function cliSpawn(bin, args) {
123
+ if (process.platform === "win32") {
124
+ const safe = [`${bin}.cmd`, ...args.map(String)].map((a) => (/[&|<>^%"]/.test(a) ? `"${a.replace(/["&|<>^%]/g, "")}"` : a.includes(" ") ? `"${a}"` : a));
125
+ return { command: "cmd.exe", args: ["/d", "/s", "/c", safe.join(" ")] };
126
+ }
127
+ return { command: bin, args: args.map(String) };
128
+ }
129
+
130
+ module.exports = { JsonlProcess, cliSpawn };
@@ -1,18 +1,18 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
- const { dataDirectory, executableName } = require('./platform');
3
+ const { dataDirectory, executableName } = require('./platform');
4
4
  const root = path.resolve(__dirname, '../../..');
5
- const settingKeys = ['HARNESS_MIX_DSH_ROOT', 'HARNESSMIX_PI_COMMAND', 'HARNESSMIX_CLAUDE_COMMAND', 'HARNESSMIX_DEEPSEEK_HARNESS_COMMAND', 'HARNESSMIX_ANTIGRAVITY_COMMAND', 'HARNESS_MIX_CODEBUDDY_EXECUTABLE', 'HARNESS_MIX_WORKBUDDY_EXECUTABLE', 'HARNESS_MIX_KIRO_EXECUTABLE', 'HARNESS_MIX_CURSOR_EXECUTABLE', 'HARNESS_MIX_QODER_EXECUTABLE', 'HARNESS_MIX_ZCODE_ACP_EXECUTABLE', 'HARNESS_MIX_TRAE_EXECUTABLE'];
5
+ const settingKeys = ['HARNESS_MIX_DSH_ROOT', 'HARNESSMIX_PI_COMMAND', 'HARNESSMIX_CLAUDE_COMMAND', 'HARNESSMIX_DEEPSEEK_HARNESS_COMMAND', 'HARNESSMIX_ANTIGRAVITY_COMMAND', 'HARNESS_MIX_CODEBUDDY_EXECUTABLE', 'HARNESS_MIX_WORKBUDDY_EXECUTABLE', 'HARNESS_MIX_KIRO_EXECUTABLE', 'HARNESS_MIX_CURSOR_EXECUTABLE', 'HARNESS_MIX_QODER_EXECUTABLE', 'HARNESS_MIX_ZCODE_EXECUTABLE', 'HARNESS_MIX_TRAE_EXECUTABLE'];
6
6
 
7
- function nativePaths(platform = process.platform) {
7
+ function nativePaths(platform = process.platform) {
8
8
  const build = path.join(root, 'output/native-build');
9
9
  return {
10
10
  cli: path.join(root, 'scripts/launch-codex.cjs'),
11
- shim: path.join(build, executableName('harness-mix-shim', platform)),
12
- ...(platform === 'win32' ? {
13
- activation: path.join(build, 'harness-mix-appx.exe'),
14
- secret: path.join(build, 'harness-mix-secret.exe'),
15
- } : {}),
11
+ shim: path.join(build, executableName('harness-mix-shim', platform)),
12
+ ...(platform === 'win32' ? {
13
+ activation: path.join(build, 'harness-mix-appx.exe'),
14
+ secret: path.join(build, 'harness-mix-secret.exe'),
15
+ } : {}),
16
16
  runtime: path.join(root, 'src/main/native/host.js'),
17
17
  controller: path.join(build, 'desktop-controller.mjs'),
18
18
  renderer: path.join(build, 'renderer-extension.js'),
@@ -22,7 +22,7 @@ function nativePaths(platform = process.platform) {
22
22
 
23
23
  function nativeEnvironment(environment = process.env) {
24
24
  const env = { ...environment };
25
- env.HARNESSMIX_DATA_DIR = dataDirectory(env);
25
+ env.HARNESSMIX_DATA_DIR = dataDirectory(env);
26
26
  const settingsPath = path.join(env.HARNESSMIX_DATA_DIR, 'harness-mix-settings.json');
27
27
  if (fs.existsSync(settingsPath)) {
28
28
  const saved = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));