@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.
- package/CHANGELOG.md +24 -0
- package/README.md +469 -467
- package/docs/harness-management.md +1 -1
- package/docs/multi-agent-collaboration.md +3 -1
- package/docs/native-acp.md +138 -127
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +172 -23
- package/package.json +13 -9
- package/scripts/acp-image-test.cjs +69 -0
- package/scripts/adapters-test.cjs +25 -0
- package/scripts/antigravity-adapter-test.cjs +647 -626
- package/scripts/codex-accounts-test.cjs +34 -0
- package/scripts/codex-adapter-test.cjs +238 -127
- package/scripts/collaboration-test.cjs +577 -262
- package/scripts/collaboration-ui-smoke.cjs +25 -3
- package/scripts/e2e-delegate.cjs +1 -1
- package/scripts/e2e-hermes-image.cjs +60 -0
- package/scripts/jsonl-stdin-test.cjs +40 -31
- package/scripts/kiro-cursor-adapters-test.cjs +124 -100
- package/scripts/native-acp-depth-test.cjs +30 -5
- package/scripts/native-update-apply-test.cjs +269 -215
- package/scripts/native-update.cjs +78 -0
- package/scripts/native-vendor-adapters-test.cjs +196 -154
- package/scripts/openclaw-adapter-test.cjs +121 -3
- package/scripts/openclaw-mcp-probe.cjs +100 -0
- package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
- package/scripts/openclaw-thinking-probe.cjs +73 -0
- package/scripts/salvage-rollout-writes.cjs +72 -0
- package/scripts/storage-verification-test.cjs +11 -0
- package/scripts/zcode-adapter-test.cjs +329 -0
- package/scripts/zcode-live-probe.cjs +66 -0
- package/src/main/adapters/antigravity.js +1428 -1418
- package/src/main/adapters/claude.js +15 -7
- package/src/main/adapters/codex-app-server.js +29 -11
- package/src/main/adapters/codex.js +690 -649
- package/src/main/adapters/native-acp-command.js +51 -48
- package/src/main/adapters/native-acp.js +47 -12
- package/src/main/adapters/omp.js +7 -2
- package/src/main/adapters/openclaw.js +534 -343
- package/src/main/adapters/pi-family.js +35 -8
- package/src/main/adapters/qoder.js +12 -8
- package/src/main/adapters/zcode.js +925 -10
- package/src/main/host/collaboration-tools.js +1 -1
- package/src/main/host/collaboration.js +971 -714
- package/src/main/host/jsonl.js +130 -120
- package/src/main/host/runtime.js +9 -3
- package/src/main/host/verification-gates.js +14 -2
- package/src/main/native/codex-accounts.js +15 -4
- package/src/main/native/config.js +9 -9
- package/src/main/native/launcher.js +252 -237
- package/src/main/native/process-utils.js +157 -57
- package/src/main/native/protocol.js +1227 -1187
- package/src/main/native/update-state.js +123 -110
- package/src/main/native/updater.js +460 -394
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3224 -3181
- package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
- package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
package/src/main/host/jsonl.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
stream.on("
|
|
43
|
-
buffer += decoder.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
const id =
|
|
95
|
-
this.#write({ id,
|
|
96
|
-
return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.pending.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
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 };
|
package/src/main/host/runtime.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
fs.rmSync(
|
|
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', '
|
|
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'));
|