@harness-mix/cli 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +469 -467
  3. package/docs/harness-management.md +1 -1
  4. package/docs/multi-agent-collaboration.md +3 -1
  5. package/docs/native-acp.md +138 -127
  6. package/output/native-build/desktop-controller.mjs +1 -1
  7. package/output/native-build/renderer-extension.js +172 -23
  8. package/package.json +13 -9
  9. package/scripts/acp-image-test.cjs +69 -0
  10. package/scripts/adapters-test.cjs +25 -0
  11. package/scripts/antigravity-adapter-test.cjs +647 -626
  12. package/scripts/codex-accounts-test.cjs +34 -0
  13. package/scripts/codex-adapter-test.cjs +238 -127
  14. package/scripts/collaboration-test.cjs +577 -262
  15. package/scripts/collaboration-ui-smoke.cjs +25 -3
  16. package/scripts/e2e-delegate.cjs +1 -1
  17. package/scripts/e2e-hermes-image.cjs +60 -0
  18. package/scripts/jsonl-stdin-test.cjs +40 -31
  19. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  20. package/scripts/native-acp-depth-test.cjs +30 -5
  21. package/scripts/native-update-apply-test.cjs +269 -215
  22. package/scripts/native-update.cjs +78 -0
  23. package/scripts/native-vendor-adapters-test.cjs +196 -154
  24. package/scripts/openclaw-adapter-test.cjs +121 -3
  25. package/scripts/openclaw-mcp-probe.cjs +100 -0
  26. package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
  27. package/scripts/openclaw-thinking-probe.cjs +73 -0
  28. package/scripts/salvage-rollout-writes.cjs +72 -0
  29. package/scripts/storage-verification-test.cjs +11 -0
  30. package/scripts/zcode-adapter-test.cjs +329 -0
  31. package/scripts/zcode-live-probe.cjs +66 -0
  32. package/src/main/adapters/antigravity.js +1428 -1418
  33. package/src/main/adapters/claude.js +15 -7
  34. package/src/main/adapters/codex-app-server.js +29 -11
  35. package/src/main/adapters/codex.js +690 -649
  36. package/src/main/adapters/native-acp-command.js +51 -48
  37. package/src/main/adapters/native-acp.js +47 -12
  38. package/src/main/adapters/omp.js +7 -2
  39. package/src/main/adapters/openclaw.js +534 -343
  40. package/src/main/adapters/pi-family.js +35 -8
  41. package/src/main/adapters/qoder.js +12 -8
  42. package/src/main/adapters/zcode.js +925 -10
  43. package/src/main/host/collaboration-tools.js +1 -1
  44. package/src/main/host/collaboration.js +971 -714
  45. package/src/main/host/jsonl.js +130 -120
  46. package/src/main/host/runtime.js +9 -3
  47. package/src/main/host/verification-gates.js +14 -2
  48. package/src/main/native/codex-accounts.js +15 -4
  49. package/src/main/native/config.js +9 -9
  50. package/src/main/native/launcher.js +252 -237
  51. package/src/main/native/process-utils.js +157 -57
  52. package/src/main/native/protocol.js +1227 -1187
  53. package/src/main/native/update-state.js +123 -110
  54. package/src/main/native/updater.js +460 -394
  55. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
  56. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
  57. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  58. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
  65. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3224 -3181
  68. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
  69. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
  70. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
  71. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
  72. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
@@ -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 };
@@ -295,6 +295,9 @@ class HostRuntime {
295
295
  this.verificationGates.assertSatisfied(thread, '合并隔离分支');
296
296
  const review = await reviewWorkspace(thread.workspace);
297
297
  const result = await applyWorkspace(thread.workspace, digest || review.digest);
298
+ // off 策略下的零配置安全网:结果附一次 advisory 验证(不阻断、不改门禁语义)
299
+ const advisory = await this.verificationGates.advisory(thread).catch(() => null);
300
+ if (advisory) result.verification = { mode: 'advisory', status: advisory.status, checks: advisory.checks };
298
301
  this.#notify('status', '已成功将隔离分支改动应用到主项目', thread.id);
299
302
  await this.#save();
300
303
  this.#broadcast();
@@ -666,11 +669,13 @@ class HostRuntime {
666
669
  const thread = this.threads.find(t => t.id === threadId);
667
670
  // send 进行中(会话恢复 / prompt 尚未投递)时 abort 可能落空:按票据登记取消请求,由 #send 在投递前结算
668
671
  if (this.sending.has(threadId)) this.cancelRequests.set(threadId, this.sendTickets.get(threadId));
669
- // If this thread is a collaboration child task, mark the collaboration job as cancelled immediately
672
+ // If this thread is a collaboration child task, stop its job through the shared
673
+ // settle path so the team graph (task/member status) is unwedged too — a bare
674
+ // status write here previously left the team task in_progress forever.
670
675
  const childJob = [...this.collaboration.jobs.values()].find(j => j.childId === threadId && j.status === 'running');
671
676
  if (childJob) {
672
- childJob.status = 'cancelled';
673
- void this.collaboration.save();
677
+ childJob.status = this.collaboration.closing ? 'interrupted' : 'cancelled';
678
+ void this.collaboration.settleStoppedJob(childJob).catch(() => {});
674
679
  }
675
680
  // Record the user's cancellation immediately so UI and Core become idle without waiting on subtasks
676
681
  if (thread && this.execution.isRunning(thread.id)) this.#applyEvent({ threadId, event: { kind: 'completed', stopReason: 'cancelled' } });
@@ -1093,6 +1098,7 @@ class HostRuntime {
1093
1098
  await removeWorkspace(thread.workspace).catch(() => {});
1094
1099
  }
1095
1100
  this.threads = this.threads.filter((t) => t.id !== threadId);
1101
+ await this.collaboration.forgetThread(threadId).catch(() => {});
1096
1102
  await this.#save();
1097
1103
  await this.store.remove(threadId);
1098
1104
  this.#broadcast();
@@ -82,8 +82,19 @@ class VerificationGates {
82
82
  this.running.set(thread.id, task);
83
83
  return task;
84
84
  }
85
- async #run(thread, turnId) {
86
- const policy = this.policy(thread);
85
+ // Apply 路径的零配置安全网:线程策略为 off 时仍跑一遍内建本地检查(advisory)。
86
+ // 不跑用户命令、不写 latestReport、不发 verification.updated——显式策略
87
+ // (advisory/required)的语义完全不变;返回的报告只附在 apply 结果里。
88
+ advisory(thread) {
89
+ if (this.policy(thread).mode !== 'off') return null;
90
+ if (this.running.has(thread.id)) return this.running.get(thread.id);
91
+ const task = this.#run(thread, undefined, { schemaVersion: 1, mode: 'advisory', autoRun: false, checks: { ...DEFAULT_CHECKS }, commands: [] }, false)
92
+ .finally(() => this.running.delete(thread.id));
93
+ this.running.set(thread.id, task);
94
+ return task;
95
+ }
96
+ async #run(thread, turnId, policyOverride, persist = true) {
97
+ const policy = policyOverride ?? this.policy(thread);
87
98
  const startedAt = Date.now();
88
99
  const turn = turnId ? this.runtime.core.getTurn(turnId) : this.runtime.execution.lastTurn(thread.id);
89
100
  const items = turn ? this.runtime.core.getItemsForTurn(turn.id) : [];
@@ -106,6 +117,7 @@ class VerificationGates {
106
117
  startedAt, completedAt: Date.now(), checks, commands,
107
118
  };
108
119
  report.contentDigest = createHash('sha256').update(JSON.stringify(report)).digest('hex');
120
+ if (!persist) return report;
109
121
  thread.verificationGate = { policy, latestReport: report };
110
122
  if (turn) {
111
123
  this.runtime.core.dispatch({ threadId: thread.id, turnId: turn.id, type: 'verification.updated', payload: { report }, timestamp: report.completedAt });
@@ -80,6 +80,8 @@ class CodexAccountManager {
80
80
  if (!entry || !ACCOUNT_ID.test(entry.accountId) || typeof entry.label !== 'string' || !entry.label.trim() || entry.accountId === OFFICIAL_ACCOUNT_ID || seen.has(entry.accountId)) return false;
81
81
  seen.add(entry.accountId);
82
82
  entry.label = entry.label.trim().slice(0, 256);
83
+ // 旧结构条目才带 codexHome;非字符串的残留一律丢弃,回落到托管 profile 路径
84
+ if (typeof entry.codexHome !== 'string' || !entry.codexHome.trim()) delete entry.codexHome;
83
85
  return true;
84
86
  }).slice(0, 127);
85
87
  const activeAccountId = value.activeAccountId === OFFICIAL_ACCOUNT_ID || accounts.some(entry => entry.accountId === value.activeAccountId)
@@ -102,8 +104,17 @@ class CodexAccountManager {
102
104
  if (accountId === OFFICIAL_ACCOUNT_ID) return { accountId, label: 'Codex 官方账号', codexHome: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), native: true };
103
105
  const entry = this.registry.accounts.find(candidate => candidate.accountId === accountId);
104
106
  if (!entry) throw new Error('Unknown Codex account');
107
+ // 旧结构条目带显式绝对 codexHome(如 default → ~/.codex):尊重原路径。
108
+ // codex 拒绝在不存在的 CODEX_HOME 下启动(进程退出码 1),改写路径会让这类账号
109
+ // 的每次会话都死在拉起阶段,且用户无从看到原因。
110
+ if (typeof entry.codexHome === 'string' && path.isAbsolute(entry.codexHome)) {
111
+ return { ...entry, codexHome: path.resolve(entry.codexHome), native: false };
112
+ }
105
113
  const codexHome = path.resolve(this.profilesRoot, entry.accountId);
106
114
  if (!codexHome.startsWith(`${path.resolve(this.profilesRoot)}${path.sep}`)) throw new Error('Invalid Codex account directory');
115
+ // 托管 profile 目录缺失时自愈:目录被删/迁移未带上时补建空目录(未登录态),
116
+ // 而不是让 codex app-server 直接拒绝启动
117
+ if (!fs.existsSync(codexHome)) fs.mkdirSync(codexHome, { recursive: true });
107
118
  return { ...entry, codexHome, native: false };
108
119
  }
109
120
 
@@ -161,10 +172,10 @@ class CodexAccountManager {
161
172
  const entry = this.#entry(accountId);
162
173
  if (entry.native) throw new Error('The official Codex account cannot be deleted');
163
174
  if ([...this.logins.values()].some(login => login.accountId === accountId)) throw new Error('Cancel account sign-in before deleting it');
164
- const resolved = path.resolve(entry.codexHome);
165
- const root = path.resolve(this.profilesRoot);
166
- if (!resolved.startsWith(`${root}${path.sep}`)) throw new Error('Refusing to delete an account outside the managed profile directory');
167
- fs.rmSync(resolved, { recursive: true, force: true });
175
+ // 只删除托管 profile 目录(profiles/<accountId>);旧条目指向的外部目录
176
+ // (如 ~/.codex)绝不能随账号删除,但账号槽位本身要能移除
177
+ const managed = path.join(path.resolve(this.profilesRoot), entry.accountId);
178
+ if (path.resolve(entry.codexHome) === managed) fs.rmSync(managed, { recursive: true, force: true });
168
179
  this.registry.accounts = this.registry.accounts.filter(candidate => candidate.accountId !== accountId);
169
180
  if (this.registry.activeAccountId === accountId) this.registry.activeAccountId = OFFICIAL_ACCOUNT_ID;
170
181
  this.#save();
@@ -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'));