deskbuddy 0.2.0-beta.0 → 0.2.0-beta.2
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/README.md +32 -32
- package/dist/cli.js +3354 -424
- package/dist/gate.cjs +38 -0
- package/dist/hook.cjs +279 -6
- package/docs/INSTALL.md +95 -47
- package/hook/gate.cjs +38 -0
- package/hook/hook.cjs +279 -6
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/commands/daemon.ts
|
|
4
|
-
import { writeFileSync as
|
|
4
|
+
import { writeFileSync as writeFileSync13, rmSync as rmSync7 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/core/config.ts
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
9
9
|
import { homedir, hostname } from "os";
|
|
10
10
|
import { join } from "path";
|
|
11
|
-
function defaultRemoteCwd(home = homedir()) {
|
|
12
|
-
|
|
11
|
+
function defaultRemoteCwd(home = homedir(), platform = process.platform, exists = existsSync) {
|
|
12
|
+
const desktop = join(home, "Desktop");
|
|
13
|
+
if (platform === "win32" && !exists(desktop)) {
|
|
14
|
+
const od = join(home, "OneDrive", "Desktop");
|
|
15
|
+
if (exists(od)) return od;
|
|
16
|
+
}
|
|
17
|
+
return desktop;
|
|
13
18
|
}
|
|
14
19
|
var PROD_API_BASE = "https://api.shifengintelli.com";
|
|
15
20
|
var TEST_API_BASE = "https://test-api.shifengintelli.com";
|
|
@@ -83,7 +88,8 @@ function loadConfig() {
|
|
|
83
88
|
boundApiBase: parsed.boundApiBase,
|
|
84
89
|
remoteCwd: parsed.remoteCwd,
|
|
85
90
|
// 不认识的值一律回落默认,不能让一个手滑写错的配置变成"全放行"或"全拦"
|
|
86
|
-
approvalPolicy: parsed.approvalPolicy === "all" ? "all" : parsed.approvalPolicy === "commands" ? "commands" : void 0
|
|
91
|
+
approvalPolicy: parsed.approvalPolicy === "all" ? "all" : parsed.approvalPolicy === "commands" ? "commands" : void 0,
|
|
92
|
+
codexAppServer: typeof parsed.codexAppServer === "string" ? parsed.codexAppServer : void 0
|
|
87
93
|
};
|
|
88
94
|
} catch {
|
|
89
95
|
return fallback;
|
|
@@ -105,12 +111,124 @@ function ensureClientId() {
|
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
// src/daemon/autostart.ts
|
|
108
|
-
import { execFile } from "child_process";
|
|
109
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
114
|
+
import { execFile, spawn } from "child_process";
|
|
115
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
110
116
|
import { homedir as homedir2 } from "os";
|
|
111
117
|
import { fileURLToPath } from "url";
|
|
112
118
|
import { basename, dirname, join as join2 } from "path";
|
|
113
119
|
import { promisify } from "util";
|
|
120
|
+
|
|
121
|
+
// src/core/winproc.ts
|
|
122
|
+
import { execFileSync } from "child_process";
|
|
123
|
+
function winSplitArgs(cmdline) {
|
|
124
|
+
const out = [];
|
|
125
|
+
let cur = "";
|
|
126
|
+
let inQ = false;
|
|
127
|
+
let has = false;
|
|
128
|
+
const s = cmdline || "";
|
|
129
|
+
for (let i = 0; i < s.length; i++) {
|
|
130
|
+
const c = s[i];
|
|
131
|
+
if (c === "\\") {
|
|
132
|
+
let n = 0;
|
|
133
|
+
while (s[i + n] === "\\") n++;
|
|
134
|
+
if (s[i + n] === '"') {
|
|
135
|
+
cur += "\\".repeat(Math.floor(n / 2));
|
|
136
|
+
if (n % 2 === 1) cur += '"';
|
|
137
|
+
else inQ = !inQ;
|
|
138
|
+
i += n;
|
|
139
|
+
has = true;
|
|
140
|
+
} else {
|
|
141
|
+
cur += "\\".repeat(n);
|
|
142
|
+
i += n - 1;
|
|
143
|
+
has = true;
|
|
144
|
+
}
|
|
145
|
+
} else if (c === '"') {
|
|
146
|
+
inQ = !inQ;
|
|
147
|
+
has = true;
|
|
148
|
+
} else if (!inQ && (c === " " || c === " ")) {
|
|
149
|
+
if (has) out.push(cur);
|
|
150
|
+
cur = "";
|
|
151
|
+
has = false;
|
|
152
|
+
} else {
|
|
153
|
+
cur += c;
|
|
154
|
+
has = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (has) out.push(cur);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
var SCRIPT_HOSTS = /* @__PURE__ */ new Set(["node", "bun", "deno"]);
|
|
161
|
+
function stripExe(name) {
|
|
162
|
+
return name.replace(/\.exe$/i, "");
|
|
163
|
+
}
|
|
164
|
+
function lastTwo(p) {
|
|
165
|
+
return p.split(/[\\/]+/).filter(Boolean).slice(-2).join("/");
|
|
166
|
+
}
|
|
167
|
+
function hostedScript(argv) {
|
|
168
|
+
for (const a of argv.slice(1)) if (!a.startsWith("-")) return a;
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
function winEffectiveName(name, cmdline) {
|
|
172
|
+
if (!SCRIPT_HOSTS.has(stripExe(name).toLowerCase())) return name;
|
|
173
|
+
const script = hostedScript(winSplitArgs(cmdline));
|
|
174
|
+
return script ? lastTwo(script) : name;
|
|
175
|
+
}
|
|
176
|
+
function winNormalizedArgs(name, cmdline) {
|
|
177
|
+
let argv = winSplitArgs(cmdline);
|
|
178
|
+
if (argv.length === 0) return "";
|
|
179
|
+
if (SCRIPT_HOSTS.has(stripExe(name).toLowerCase())) {
|
|
180
|
+
const script = hostedScript(argv);
|
|
181
|
+
if (script) argv = argv.slice(argv.indexOf(script));
|
|
182
|
+
}
|
|
183
|
+
return argv.map((a) => a.replace(/\s+/g, "_").replace(/\\/g, "/")).join(" ");
|
|
184
|
+
}
|
|
185
|
+
var WIN_PS_SCRIPT = "[Console]::OutputEncoding=[Text.Encoding]::UTF8;Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
|
|
186
|
+
function parseWinProcJson(json) {
|
|
187
|
+
let d;
|
|
188
|
+
try {
|
|
189
|
+
d = JSON.parse(json.replace(/^\uFEFF/, ""));
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const list = Array.isArray(d) ? d : d ? [d] : [];
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const r of list) {
|
|
196
|
+
const pid = Number(r?.ProcessId);
|
|
197
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
198
|
+
const rawName = typeof r.Name === "string" ? r.Name : "";
|
|
199
|
+
const cmdline = typeof r.CommandLine === "string" ? r.CommandLine.replace(/[\r\n]+/g, " ") : "";
|
|
200
|
+
out.push({
|
|
201
|
+
pid,
|
|
202
|
+
ppid: Number(r.ParentProcessId) || 0,
|
|
203
|
+
name: winEffectiveName(rawName, cmdline),
|
|
204
|
+
args: winNormalizedArgs(rawName, cmdline),
|
|
205
|
+
cmdline
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
var CACHE_MS = 1500;
|
|
211
|
+
var FAIL_CACHE_MS = 1e4;
|
|
212
|
+
var cache = null;
|
|
213
|
+
function winProcs(now = Date.now()) {
|
|
214
|
+
if (cache && now - cache.at < (cache.procs ? CACHE_MS : FAIL_CACHE_MS)) return cache.procs;
|
|
215
|
+
try {
|
|
216
|
+
const out = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WIN_PS_SCRIPT], {
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
219
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
220
|
+
windowsHide: true,
|
|
221
|
+
timeout: 15e3
|
|
222
|
+
});
|
|
223
|
+
const procs = parseWinProcJson(out);
|
|
224
|
+
cache = { at: now, procs: procs.length > 0 ? procs : null };
|
|
225
|
+
} catch {
|
|
226
|
+
cache = { at: now, procs: null };
|
|
227
|
+
}
|
|
228
|
+
return cache.procs;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/daemon/autostart.ts
|
|
114
232
|
var run = promisify(execFile);
|
|
115
233
|
var LAUNCH_LABEL = "com.deskbuddy.daemon";
|
|
116
234
|
var SYSTEMD_UNIT = "deskbuddy.service";
|
|
@@ -157,6 +275,49 @@ RestartSec=10
|
|
|
157
275
|
WantedBy=default.target
|
|
158
276
|
`;
|
|
159
277
|
}
|
|
278
|
+
function winStartupVbsPath(appData = process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming")) {
|
|
279
|
+
return join2(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "DeskBuddyDaemon.vbs");
|
|
280
|
+
}
|
|
281
|
+
function vbsQuote(s) {
|
|
282
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
283
|
+
}
|
|
284
|
+
function winStartupVbs(t) {
|
|
285
|
+
const cmd = `"${t.execPath}" "${t.cliPath}" daemon`;
|
|
286
|
+
return [
|
|
287
|
+
"' DeskBuddy \u5B88\u62A4\u8FDB\u7A0B\u5F00\u673A\u81EA\u542F\u3002deskbuddy disable / logout \u4F1A\u5220\u6389\u5B83",
|
|
288
|
+
`CreateObject("WScript.Shell").Run ${vbsQuote(cmd)}, 0, False`,
|
|
289
|
+
""
|
|
290
|
+
].join("\r\n");
|
|
291
|
+
}
|
|
292
|
+
function isOurDaemonCmdline(cmdline) {
|
|
293
|
+
const argv = winSplitArgs(cmdline);
|
|
294
|
+
const i = argv.findIndex((a) => /(?:^|[\\/])deskbuddy[\\/]dist[\\/]cli\.js$/i.test(a));
|
|
295
|
+
return i >= 1 && argv[i + 1] === "daemon";
|
|
296
|
+
}
|
|
297
|
+
async function stopPidFileDaemon() {
|
|
298
|
+
let pid = 0;
|
|
299
|
+
try {
|
|
300
|
+
pid = Number(readFileSync2(pidFilePath(), "utf8").trim());
|
|
301
|
+
} catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return;
|
|
305
|
+
const p = winProcs()?.find((x) => x.pid === pid);
|
|
306
|
+
if (!p || !isOurDaemonCmdline(p.cmdline)) return;
|
|
307
|
+
try {
|
|
308
|
+
process.kill(pid);
|
|
309
|
+
} catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
for (let i = 0; i < 30; i++) {
|
|
313
|
+
try {
|
|
314
|
+
process.kill(pid, 0);
|
|
315
|
+
} catch {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
160
321
|
function launchAgentPath(home = homedir2()) {
|
|
161
322
|
return join2(home, "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
|
|
162
323
|
}
|
|
@@ -167,6 +328,8 @@ function write(path, content) {
|
|
|
167
328
|
mkdirSync2(dirname(path), { recursive: true });
|
|
168
329
|
writeFileSync2(path, content, "utf8");
|
|
169
330
|
}
|
|
331
|
+
var BOOTSTRAP_RETRIES = 10;
|
|
332
|
+
var BOOTSTRAP_RETRY_MS = 500;
|
|
170
333
|
async function installAutostart(t, platform = process.platform) {
|
|
171
334
|
try {
|
|
172
335
|
if (platform === "darwin") {
|
|
@@ -174,29 +337,33 @@ async function installAutostart(t, platform = process.platform) {
|
|
|
174
337
|
write(p, launchAgentPlist(t));
|
|
175
338
|
await run("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${LAUNCH_LABEL}`]).catch(() => {
|
|
176
339
|
});
|
|
177
|
-
|
|
340
|
+
for (let i = 0; ; i++) {
|
|
341
|
+
try {
|
|
342
|
+
await run("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, p]);
|
|
343
|
+
break;
|
|
344
|
+
} catch (e) {
|
|
345
|
+
if (i >= BOOTSTRAP_RETRIES) throw e;
|
|
346
|
+
await new Promise((r) => setTimeout(r, BOOTSTRAP_RETRY_MS));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
178
349
|
return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08launchd\uFF09" };
|
|
179
350
|
}
|
|
180
351
|
if (platform === "linux") {
|
|
181
352
|
write(systemdUnitPath(), systemdUnit(t));
|
|
182
353
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
183
354
|
await run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
|
|
355
|
+
await run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
|
|
184
356
|
return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08systemd --user\uFF09" };
|
|
185
357
|
}
|
|
186
358
|
if (platform === "win32") {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
SCHTASK_NAME,
|
|
196
|
-
"/tr",
|
|
197
|
-
`"${t.execPath}" "${t.cliPath}" daemon`
|
|
198
|
-
]);
|
|
199
|
-
return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08\u8BA1\u5212\u4EFB\u52A1\uFF09" };
|
|
359
|
+
const p = winStartupVbsPath();
|
|
360
|
+
mkdirSync2(dirname(p), { recursive: true });
|
|
361
|
+
writeFileSync2(p, "\uFEFF" + winStartupVbs(t), "utf16le");
|
|
362
|
+
await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
|
|
363
|
+
});
|
|
364
|
+
await stopPidFileDaemon();
|
|
365
|
+
spawn("wscript.exe", [p], { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
366
|
+
return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08\u300C\u542F\u52A8\u300D\u6587\u4EF6\u5939\uFF09" };
|
|
200
367
|
}
|
|
201
368
|
return { ok: false, message: `\u6682\u4E0D\u652F\u6301\u5728 ${platform} \u4E0A\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF0C\u8BF7\u81EA\u884C\u5E38\u9A7B\u8FD0\u884C deskbuddy daemon` };
|
|
202
369
|
} catch (e) {
|
|
@@ -222,8 +389,11 @@ async function uninstallAutostart(platform = process.platform) {
|
|
|
222
389
|
return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
|
|
223
390
|
}
|
|
224
391
|
if (platform === "win32") {
|
|
225
|
-
await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME]).catch(() => {
|
|
392
|
+
await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
|
|
226
393
|
});
|
|
394
|
+
const p = winStartupVbsPath();
|
|
395
|
+
if (existsSync2(p)) rmSync(p, { force: true });
|
|
396
|
+
await stopPidFileDaemon();
|
|
227
397
|
return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
|
|
228
398
|
}
|
|
229
399
|
return { ok: true, message: "" };
|
|
@@ -242,14 +412,16 @@ import { basename as basename6 } from "path";
|
|
|
242
412
|
import WebSocket2 from "ws";
|
|
243
413
|
|
|
244
414
|
// src/adapters/ocPlugin.ts
|
|
245
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
415
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
246
416
|
import { dirname as dirname2, join as join3 } from "path";
|
|
417
|
+
import { pathToFileURL } from "url";
|
|
247
418
|
var OC_MARKER = "// deskbuddy:opencode-plugin";
|
|
248
|
-
function ocPluginSource(pluginModule, nodePath) {
|
|
419
|
+
function ocPluginSource(pluginModule, nodePath, platform = process.platform) {
|
|
420
|
+
const spec = platform === "win32" ? pathToFileURL(pluginModule).href : pluginModule;
|
|
249
421
|
return [
|
|
250
422
|
OC_MARKER,
|
|
251
423
|
"// \u7531 deskbuddy enable opencode \u751F\u6210\uFF0Cdeskbuddy disable opencode \u4F1A\u5220\u6389\u5B83\u3002",
|
|
252
|
-
`import { make } from ${JSON.stringify(
|
|
424
|
+
`import { make } from ${JSON.stringify(spec)}`,
|
|
253
425
|
`export const DeskBuddy = make({ node: ${JSON.stringify(nodePath)} })`,
|
|
254
426
|
""
|
|
255
427
|
].join("\n");
|
|
@@ -258,7 +430,7 @@ function ocPluginModule(hookPath) {
|
|
|
258
430
|
return join3(dirname2(hookPath), "opencode.mjs");
|
|
259
431
|
}
|
|
260
432
|
function installOcPlugin(pluginPath, hookPath, nodePath) {
|
|
261
|
-
if (existsSync3(pluginPath) && !
|
|
433
|
+
if (existsSync3(pluginPath) && !readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) {
|
|
262
434
|
throw new Error(`${pluginPath} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F DeskBuddy \u751F\u6210\u7684\uFF0C\u8BF7\u5148\u632A\u8D70\u518D\u91CD\u8BD5`);
|
|
263
435
|
}
|
|
264
436
|
mkdirSync3(dirname2(pluginPath), { recursive: true });
|
|
@@ -268,13 +440,13 @@ function installOcPlugin(pluginPath, hookPath, nodePath) {
|
|
|
268
440
|
}
|
|
269
441
|
function uninstallOcPlugin(pluginPath) {
|
|
270
442
|
try {
|
|
271
|
-
if (
|
|
443
|
+
if (readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
|
|
272
444
|
} catch {
|
|
273
445
|
}
|
|
274
446
|
}
|
|
275
447
|
function isOcPluginInstalled(pluginPath) {
|
|
276
448
|
try {
|
|
277
|
-
return
|
|
449
|
+
return readFileSync3(pluginPath, "utf8").includes(OC_MARKER);
|
|
278
450
|
} catch {
|
|
279
451
|
return false;
|
|
280
452
|
}
|
|
@@ -282,14 +454,16 @@ function isOcPluginInstalled(pluginPath) {
|
|
|
282
454
|
var OC_NOTICE = " \u5DF2\u5F00\u7740\u7684 opencode \u7A97\u53E3\u91CD\u5F00\u540E\u751F\u6548";
|
|
283
455
|
|
|
284
456
|
// src/core/state.ts
|
|
285
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as
|
|
457
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
286
458
|
import { join as join4 } from "path";
|
|
287
459
|
|
|
288
460
|
// src/core/process.ts
|
|
289
|
-
import { execFileSync } from "child_process";
|
|
461
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
462
|
+
var WIN = process.platform === "win32";
|
|
290
463
|
function processName(pid) {
|
|
464
|
+
if (WIN) return winProcs()?.find((p) => p.pid === pid)?.name ?? null;
|
|
291
465
|
try {
|
|
292
|
-
const out =
|
|
466
|
+
const out = execFileSync2("ps", ["-p", String(pid), "-o", "comm="], {
|
|
293
467
|
encoding: "utf8",
|
|
294
468
|
stdio: ["ignore", "pipe", "ignore"]
|
|
295
469
|
});
|
|
@@ -307,6 +481,12 @@ function isAlive(pid, expectName) {
|
|
|
307
481
|
return false;
|
|
308
482
|
}
|
|
309
483
|
if (!expectName) return true;
|
|
484
|
+
if (WIN) {
|
|
485
|
+
const procs = winProcs();
|
|
486
|
+
if (!procs) return true;
|
|
487
|
+
const p = procs.find((x) => x.pid === pid);
|
|
488
|
+
return !!p && p.name.toLowerCase().includes(expectName.toLowerCase());
|
|
489
|
+
}
|
|
310
490
|
const name = processName(pid);
|
|
311
491
|
return name != null && name.toLowerCase().includes(expectName.toLowerCase());
|
|
312
492
|
}
|
|
@@ -321,7 +501,7 @@ function statePath(sessionId) {
|
|
|
321
501
|
}
|
|
322
502
|
function readState(sessionId) {
|
|
323
503
|
try {
|
|
324
|
-
return JSON.parse(
|
|
504
|
+
return JSON.parse(readFileSync4(statePath(sessionId), "utf8"));
|
|
325
505
|
} catch {
|
|
326
506
|
return null;
|
|
327
507
|
}
|
|
@@ -339,7 +519,7 @@ function readAliveStates(tool) {
|
|
|
339
519
|
if (!file.endsWith(".json")) continue;
|
|
340
520
|
let s;
|
|
341
521
|
try {
|
|
342
|
-
s = JSON.parse(
|
|
522
|
+
s = JSON.parse(readFileSync4(join4(stateDir(), file), "utf8"));
|
|
343
523
|
} catch {
|
|
344
524
|
rmSync2(join4(stateDir(), file), { force: true });
|
|
345
525
|
continue;
|
|
@@ -372,7 +552,7 @@ var ALIAS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
372
552
|
function resolveAlias(sessionId, now = Date.now()) {
|
|
373
553
|
let map = {};
|
|
374
554
|
try {
|
|
375
|
-
map = JSON.parse(
|
|
555
|
+
map = JSON.parse(readFileSync4(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
|
|
376
556
|
} catch {
|
|
377
557
|
return sessionId;
|
|
378
558
|
}
|
|
@@ -386,13 +566,13 @@ function resolveAlias(sessionId, now = Date.now()) {
|
|
|
386
566
|
}
|
|
387
567
|
|
|
388
568
|
// src/core/api.ts
|
|
389
|
-
import { execFileSync as
|
|
569
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
390
570
|
import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
391
|
-
import { readFileSync as
|
|
571
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
392
572
|
import { release } from "os";
|
|
393
573
|
|
|
394
574
|
// src/core/hookFile.ts
|
|
395
|
-
import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as
|
|
575
|
+
import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
396
576
|
import { createRequire } from "module";
|
|
397
577
|
import { dirname as dirname3, join as join5 } from "path";
|
|
398
578
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -427,7 +607,7 @@ function hookVersionPath(target = installedHookPath()) {
|
|
|
427
607
|
}
|
|
428
608
|
function installedHookVersion(target = installedHookPath()) {
|
|
429
609
|
try {
|
|
430
|
-
return
|
|
610
|
+
return readFileSync5(hookVersionPath(target), "utf8").trim() || null;
|
|
431
611
|
} catch {
|
|
432
612
|
return null;
|
|
433
613
|
}
|
|
@@ -471,6 +651,8 @@ var SERVICES = {
|
|
|
471
651
|
confirmAuth: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyconfirmauthapi"
|
|
472
652
|
};
|
|
473
653
|
var DIRS_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyclientdirsreportapi";
|
|
654
|
+
var INSTALL_REDEEM_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyinstallredeemapi";
|
|
655
|
+
var CMD_RESULT_SERVICE = "com.shifeng.api.service.api.v2.deskbuddy.deskbuddycommandresultapi";
|
|
474
656
|
var APPROVAL_SERVICES = {
|
|
475
657
|
create: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcreateapi",
|
|
476
658
|
cancel: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcancelapi",
|
|
@@ -537,12 +719,12 @@ function humanOs(platform = process.platform) {
|
|
|
537
719
|
return known[platform] ?? platform;
|
|
538
720
|
}
|
|
539
721
|
var defaultOsVersionDeps = {
|
|
540
|
-
runSwVers: () =>
|
|
722
|
+
runSwVers: () => execFileSync3("sw_vers", ["-productVersion"], {
|
|
541
723
|
encoding: "utf8",
|
|
542
724
|
stdio: ["ignore", "pipe", "ignore"],
|
|
543
725
|
timeout: 2e3
|
|
544
726
|
}),
|
|
545
|
-
readOsRelease: () =>
|
|
727
|
+
readOsRelease: () => readFileSync6("/etc/os-release", "utf8"),
|
|
546
728
|
kernelRelease: () => release()
|
|
547
729
|
};
|
|
548
730
|
function productOsVersion(platform = process.platform, deps = defaultOsVersionDeps) {
|
|
@@ -608,7 +790,10 @@ var api = {
|
|
|
608
790
|
approvalCreate: (payload) => call(APPROVAL_SERVICES.create, payload),
|
|
609
791
|
approvalCancel: (approvalId, reason) => call(APPROVAL_SERVICES.cancel, { approvalId, reason }),
|
|
610
792
|
approvalSync: (approvalIds) => call(APPROVAL_SERVICES.sync, { approvalIds }),
|
|
611
|
-
dirsReport: (payload) => call(DIRS_SERVICE, payload)
|
|
793
|
+
dirsReport: (payload) => call(DIRS_SERVICE, payload),
|
|
794
|
+
installRedeem: (installCode, clientId, hostName) => call(INSTALL_REDEEM_SERVICE, { installCode, clientId, hostName, ...machineInfo() }),
|
|
795
|
+
// 2 秒超时、失败不重试(spec §4.1):回报丢了只是设备少一句提示,任务状态仍由会话快照负责
|
|
796
|
+
commandResult: (payload) => call(CMD_RESULT_SERVICE, payload, 2e3)
|
|
612
797
|
};
|
|
613
798
|
|
|
614
799
|
// src/core/report.ts
|
|
@@ -623,7 +808,8 @@ async function postReport(payload) {
|
|
|
623
808
|
import { basename as basename2 } from "path";
|
|
624
809
|
var KEEPALIVE_MS = 2 * 60 * 1e3;
|
|
625
810
|
var MAX_SESSIONS = 20;
|
|
626
|
-
|
|
811
|
+
var CAPS = ["cmd_result", "window_inject"];
|
|
812
|
+
function buildSnapshot(states, tool, cfg, nowMs, remote) {
|
|
627
813
|
const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
|
|
628
814
|
sessionId: s.sessionId,
|
|
629
815
|
title: s.title,
|
|
@@ -632,14 +818,15 @@ function buildSnapshot(states, tool, cfg, nowMs) {
|
|
|
632
818
|
state: s.state,
|
|
633
819
|
startedAt: s.startedAt,
|
|
634
820
|
updatedAt: s.updatedAt,
|
|
635
|
-
interactive: s.interactive === true
|
|
821
|
+
interactive: s.interactive === true,
|
|
822
|
+
remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
|
|
636
823
|
}));
|
|
637
|
-
return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions };
|
|
824
|
+
return { clientId: cfg.clientId, hostName: cfg.hostName, tool, snapshotAt: nowMs, sessions, caps: [...CAPS] };
|
|
638
825
|
}
|
|
639
826
|
|
|
640
827
|
// src/core/dirs.ts
|
|
641
828
|
import { createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
642
|
-
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as
|
|
829
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
643
830
|
import { basename as basename3, join as join6 } from "path";
|
|
644
831
|
var DIRS_REPORT_MAX = 10;
|
|
645
832
|
function dirsPath() {
|
|
@@ -648,7 +835,7 @@ function dirsPath() {
|
|
|
648
835
|
function dirSecret() {
|
|
649
836
|
const file = join6(deskbuddyHome(), "dir-secret");
|
|
650
837
|
try {
|
|
651
|
-
const s2 =
|
|
838
|
+
const s2 = readFileSync7(file, "utf8").trim();
|
|
652
839
|
if (s2) return s2;
|
|
653
840
|
} catch {
|
|
654
841
|
}
|
|
@@ -666,7 +853,7 @@ function dirRef(path) {
|
|
|
666
853
|
}
|
|
667
854
|
function readMap() {
|
|
668
855
|
try {
|
|
669
|
-
const m = JSON.parse(
|
|
856
|
+
const m = JSON.parse(readFileSync7(dirsPath(), "utf8"));
|
|
670
857
|
const out = {};
|
|
671
858
|
for (const [k, v] of Object.entries(m ?? {})) if (typeof v === "number") out[k] = v;
|
|
672
859
|
return out;
|
|
@@ -762,7 +949,7 @@ function isExpired(cmd, now = Date.now()) {
|
|
|
762
949
|
}
|
|
763
950
|
|
|
764
951
|
// src/daemon/executor.ts
|
|
765
|
-
import { execFile as execFile2 } from "child_process";
|
|
952
|
+
import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
|
|
766
953
|
import { homedir as homedir4 } from "os";
|
|
767
954
|
import { join as join8 } from "path";
|
|
768
955
|
|
|
@@ -776,6 +963,19 @@ function waitModeFor(tool) {
|
|
|
776
963
|
function shQuote(s) {
|
|
777
964
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
778
965
|
}
|
|
966
|
+
function fwdSlash(p) {
|
|
967
|
+
return p.replace(/\\/g, "/");
|
|
968
|
+
}
|
|
969
|
+
function winBare(p) {
|
|
970
|
+
const f = fwdSlash(p);
|
|
971
|
+
return /^[A-Za-z0-9_.:/~+@\-\u0080-\uFFFF]+$/.test(f) ? f : null;
|
|
972
|
+
}
|
|
973
|
+
function winCmdPath(p) {
|
|
974
|
+
return winBare(p) ?? `"${fwdSlash(p)}"`;
|
|
975
|
+
}
|
|
976
|
+
function winPsPath(p) {
|
|
977
|
+
return winBare(p) ?? `'${fwdSlash(p).replace(/'/g, "''")}'`;
|
|
978
|
+
}
|
|
779
979
|
function defaultGateScript() {
|
|
780
980
|
const candidates = [
|
|
781
981
|
fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
|
|
@@ -783,17 +983,17 @@ function defaultGateScript() {
|
|
|
783
983
|
];
|
|
784
984
|
return candidates.find((p) => existsSync7(p)) ?? candidates[0];
|
|
785
985
|
}
|
|
786
|
-
function gateCommand(t) {
|
|
986
|
+
function gateCommand(t, platform = process.platform) {
|
|
987
|
+
if (platform === "win32") return winGateCommand(t);
|
|
787
988
|
return `${shQuote(t.nodePath)} ${shQuote(t.gateScript)} --run ${t.runId} || exit 2`;
|
|
788
989
|
}
|
|
789
|
-
function
|
|
790
|
-
return
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
});
|
|
990
|
+
function winGateCommand(t) {
|
|
991
|
+
return `node ${winPsPath(t.gateScript)} --run ${t.runId}; if (-not $? -or $LASTEXITCODE -ne 0) { exit 2 }`;
|
|
992
|
+
}
|
|
993
|
+
function ccSettingsJson(t, platform = process.platform) {
|
|
994
|
+
const entry = { type: "command", command: gateCommand(t, platform), timeout: GATE_HOOK_TIMEOUT_S };
|
|
995
|
+
if (platform === "win32") entry.shell = "powershell";
|
|
996
|
+
return JSON.stringify({ hooks: { PreToolUse: [{ matcher: "*", hooks: [entry] }] } });
|
|
797
997
|
}
|
|
798
998
|
function codexGateArgs(t) {
|
|
799
999
|
const hook = `hooks.PreToolUse=[{matcher="*",hooks=[{type="command",command=${JSON.stringify(gateCommand(t))},timeout=${GATE_HOOK_TIMEOUT_S}}]}]`;
|
|
@@ -809,7 +1009,7 @@ function codexGateArgs(t) {
|
|
|
809
1009
|
}
|
|
810
1010
|
|
|
811
1011
|
// src/daemon/locate.ts
|
|
812
|
-
import { accessSync, constants, existsSync as existsSync8, statSync } from "fs";
|
|
1012
|
+
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync8, statSync } from "fs";
|
|
813
1013
|
import { homedir as homedir3 } from "os";
|
|
814
1014
|
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
815
1015
|
var defaultLocateDeps = () => ({
|
|
@@ -825,9 +1025,33 @@ var defaultLocateDeps = () => ({
|
|
|
825
1025
|
return false;
|
|
826
1026
|
}
|
|
827
1027
|
},
|
|
828
|
-
exists: (p) => existsSync8(p)
|
|
1028
|
+
exists: (p) => existsSync8(p),
|
|
1029
|
+
platform: process.platform,
|
|
1030
|
+
appData: process.env.APPDATA ?? "",
|
|
1031
|
+
localAppData: process.env.LOCALAPPDATA ?? "",
|
|
1032
|
+
readText: (p) => {
|
|
1033
|
+
try {
|
|
1034
|
+
return readFileSync8(p, "utf8");
|
|
1035
|
+
} catch {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
// 🔴 daemon 自己也不能找到垫片:远程任务要的是真程序(窗口接管 spec 2026-09-30 §3.1)
|
|
1040
|
+
skipDirs: [join7(deskbuddyHome(), "bin")]
|
|
829
1041
|
});
|
|
830
|
-
|
|
1042
|
+
var isWin = (d) => (d.platform ?? process.platform) === "win32";
|
|
1043
|
+
var WIN_EXTS = [".exe", ".cmd", ".bat"];
|
|
1044
|
+
function extraBinDirs(home, nodePath = "", win) {
|
|
1045
|
+
if (win) {
|
|
1046
|
+
return [
|
|
1047
|
+
join7(home, ".local", "bin"),
|
|
1048
|
+
...win.appData ? [join7(win.appData, "npm")] : [],
|
|
1049
|
+
join7(home, ".bun", "bin"),
|
|
1050
|
+
...win.localAppData ? [join7(win.localAppData, "Volta", "bin")] : [],
|
|
1051
|
+
join7(home, "scoop", "shims"),
|
|
1052
|
+
...nodePath ? [dirname4(nodePath)] : []
|
|
1053
|
+
];
|
|
1054
|
+
}
|
|
831
1055
|
return [
|
|
832
1056
|
join7(home, ".local", "bin"),
|
|
833
1057
|
join7(home, ".claude", "local"),
|
|
@@ -840,19 +1064,110 @@ function extraBinDirs(home, nodePath = "") {
|
|
|
840
1064
|
];
|
|
841
1065
|
}
|
|
842
1066
|
function whichIn(name, d) {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1067
|
+
return whichAllIn(name, d)[0] ?? null;
|
|
1068
|
+
}
|
|
1069
|
+
function whichAllIn(name, d) {
|
|
1070
|
+
const exts = isWin(d) ? WIN_EXTS : [""];
|
|
1071
|
+
const out = [];
|
|
1072
|
+
for (const dir of binDirs(d)) {
|
|
1073
|
+
for (const ext of exts) {
|
|
1074
|
+
const p = join7(dir, name + ext);
|
|
1075
|
+
if (d.isExecutable(p)) out.push(p);
|
|
1076
|
+
}
|
|
847
1077
|
}
|
|
1078
|
+
return out;
|
|
1079
|
+
}
|
|
1080
|
+
function binDirs(d) {
|
|
1081
|
+
const win = isWin(d);
|
|
1082
|
+
const sep2 = win ? ";" : delimiter;
|
|
1083
|
+
const all = [
|
|
1084
|
+
...d.path.split(sep2).filter(Boolean),
|
|
1085
|
+
...extraBinDirs(d.home, d.nodePath, win ? { appData: d.appData, localAppData: d.localAppData } : void 0)
|
|
1086
|
+
];
|
|
1087
|
+
if (!d.skipDirs || d.skipDirs.length === 0) return all;
|
|
1088
|
+
const key = (p) => {
|
|
1089
|
+
const t = p.replace(/[\\/]+$/, "");
|
|
1090
|
+
return win ? t.replace(/\//g, "\\").toLowerCase() : t;
|
|
1091
|
+
};
|
|
1092
|
+
const skip = new Set(d.skipDirs.map(key));
|
|
1093
|
+
return all.filter((p) => !skip.has(key(p)));
|
|
1094
|
+
}
|
|
1095
|
+
function parseNpmCmdShim(shimPath, text) {
|
|
1096
|
+
const m = text.match(/"%~?dp0%?\\?([^"%]+)"\s+%\*/i);
|
|
1097
|
+
if (!m) return null;
|
|
1098
|
+
return join7(dirname4(shimPath), m[1].replace(/^\\/, ""));
|
|
1099
|
+
}
|
|
1100
|
+
function resolveWinLaunch(file, d) {
|
|
1101
|
+
const lower = file.toLowerCase();
|
|
1102
|
+
if (lower.endsWith(".exe") || lower.endsWith(".com")) return { file, prefixArgs: [] };
|
|
1103
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
1104
|
+
const text = d.readText?.(file);
|
|
1105
|
+
const target = text ? parseNpmCmdShim(file, text) : null;
|
|
1106
|
+
if (!target || !d.exists(target)) return null;
|
|
1107
|
+
if (/\.(?:cmd|bat)$/i.test(target)) return null;
|
|
1108
|
+
return resolveWinLaunch(target, d);
|
|
1109
|
+
}
|
|
1110
|
+
if (/\.(?:c|m)?js$/.test(lower)) return { file: d.nodePath, prefixArgs: [file] };
|
|
1111
|
+
const head = d.readText?.(file)?.slice(0, 200) ?? "";
|
|
1112
|
+
if (/^#!.*\bnode\b/.test(head)) return { file: d.nodePath, prefixArgs: [file] };
|
|
848
1113
|
return null;
|
|
849
1114
|
}
|
|
850
1115
|
function appDirs(home) {
|
|
851
1116
|
return ["/Applications", join7(home, "Applications")];
|
|
852
1117
|
}
|
|
1118
|
+
function winAppDirs(d, product) {
|
|
1119
|
+
return [
|
|
1120
|
+
...d.localAppData ? [join7(d.localAppData, "Programs", product)] : [],
|
|
1121
|
+
join7("C:\\Program Files", product)
|
|
1122
|
+
];
|
|
1123
|
+
}
|
|
1124
|
+
var WORKBUDDY_CLI_IN_WIN_APP = "resources/app.asar.unpacked/cli/bin/codebuddy";
|
|
1125
|
+
var QODER_CN_WORKER_IN_WIN_APP = "resources/app.asar.unpacked/node_modules/@qoder-ai/qoder-cn-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs";
|
|
1126
|
+
function locateWin(tool, d) {
|
|
1127
|
+
const plain = (name) => {
|
|
1128
|
+
for (const f of whichAllIn(name, d)) {
|
|
1129
|
+
const r = resolveWinLaunch(f, d);
|
|
1130
|
+
if (r) return r;
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
};
|
|
1134
|
+
switch (tool) {
|
|
1135
|
+
case "claude_code":
|
|
1136
|
+
return plain("claude");
|
|
1137
|
+
case "codex":
|
|
1138
|
+
return plain("codex");
|
|
1139
|
+
case "workbuddy": {
|
|
1140
|
+
for (const dir of winAppDirs(d, "WorkBuddy")) {
|
|
1141
|
+
for (const ext of [".exe", ".cmd", ""]) {
|
|
1142
|
+
const p = join7(dir, WORKBUDDY_CLI_IN_WIN_APP + ext);
|
|
1143
|
+
if (d.exists(p)) {
|
|
1144
|
+
const r = resolveWinLaunch(p, d);
|
|
1145
|
+
if (r) return r;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return plain("codebuddy");
|
|
1150
|
+
}
|
|
1151
|
+
case "qoder": {
|
|
1152
|
+
for (const dir of winAppDirs(d, "Qoder CN")) {
|
|
1153
|
+
const p = join7(dir, QODER_CN_WORKER_IN_WIN_APP);
|
|
1154
|
+
if (d.exists(p)) return { file: d.nodePath, prefixArgs: [p] };
|
|
1155
|
+
}
|
|
1156
|
+
return plain("qodercli");
|
|
1157
|
+
}
|
|
1158
|
+
case "opencode": {
|
|
1159
|
+
const p = join7(d.home, ".opencode", "bin", "opencode.exe");
|
|
1160
|
+
if (d.exists(p)) return { file: p, prefixArgs: [] };
|
|
1161
|
+
return plain("opencode");
|
|
1162
|
+
}
|
|
1163
|
+
default:
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
853
1167
|
var WORKBUDDY_CLI_IN_APP = "WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy";
|
|
854
1168
|
var QODER_CN_WORKER_IN_APP = "Qoder CN.app/Contents/Resources/app.asar.unpacked/node_modules/@qoder-ai/qoder-cn-agent-sdk/dist/_worker/qoder-worker-runtime.obf.mjs";
|
|
855
1169
|
function locate(tool, d = defaultLocateDeps()) {
|
|
1170
|
+
if (isWin(d)) return locateWin(tool, d);
|
|
856
1171
|
const plain = (name) => {
|
|
857
1172
|
const f = whichIn(name, d);
|
|
858
1173
|
return f ? { file: f, prefixArgs: [] } : null;
|
|
@@ -887,7 +1202,8 @@ function locate(tool, d = defaultLocateDeps()) {
|
|
|
887
1202
|
}
|
|
888
1203
|
function childPath(d = defaultLocateDeps()) {
|
|
889
1204
|
const seen = /* @__PURE__ */ new Set();
|
|
890
|
-
|
|
1205
|
+
const key = (p) => isWin(d) ? p.toLowerCase() : p;
|
|
1206
|
+
return binDirs(d).filter((p) => p && !seen.has(key(p)) && (seen.add(key(p)), true)).join(isWin(d) ? ";" : delimiter);
|
|
891
1207
|
}
|
|
892
1208
|
|
|
893
1209
|
// src/daemon/executor.ts
|
|
@@ -992,21 +1308,21 @@ function materialize(plan, tool, d = defaultLocateDeps()) {
|
|
|
992
1308
|
env: { ...plan.env, PATH: childPath(d) }
|
|
993
1309
|
};
|
|
994
1310
|
}
|
|
995
|
-
function isDeferred(
|
|
1311
|
+
function isDeferred(stdout7) {
|
|
996
1312
|
try {
|
|
997
|
-
return JSON.parse(
|
|
1313
|
+
return JSON.parse(stdout7).stop_reason === "tool_deferred";
|
|
998
1314
|
} catch {
|
|
999
1315
|
return false;
|
|
1000
1316
|
}
|
|
1001
1317
|
}
|
|
1002
|
-
function parseClaudeSessionId(
|
|
1318
|
+
function parseClaudeSessionId(stdout7) {
|
|
1003
1319
|
const pick = (o) => {
|
|
1004
1320
|
const d = o ?? {};
|
|
1005
1321
|
const v = d.session_id ?? d.sessionId;
|
|
1006
1322
|
return typeof v === "string" && v ? v : null;
|
|
1007
1323
|
};
|
|
1008
1324
|
try {
|
|
1009
|
-
const d = JSON.parse(
|
|
1325
|
+
const d = JSON.parse(stdout7);
|
|
1010
1326
|
if (Array.isArray(d)) {
|
|
1011
1327
|
for (const m of d) {
|
|
1012
1328
|
const id = pick(m);
|
|
@@ -1019,8 +1335,8 @@ function parseClaudeSessionId(stdout6) {
|
|
|
1019
1335
|
return null;
|
|
1020
1336
|
}
|
|
1021
1337
|
}
|
|
1022
|
-
function parseCodexSessionId(
|
|
1023
|
-
for (const line of
|
|
1338
|
+
function parseCodexSessionId(stdout7) {
|
|
1339
|
+
for (const line of stdout7.split("\n")) {
|
|
1024
1340
|
const t = line.trim();
|
|
1025
1341
|
if (!t) continue;
|
|
1026
1342
|
try {
|
|
@@ -1034,8 +1350,8 @@ function parseCodexSessionId(stdout6) {
|
|
|
1034
1350
|
}
|
|
1035
1351
|
return null;
|
|
1036
1352
|
}
|
|
1037
|
-
function parseOpencodeSessionId(
|
|
1038
|
-
for (const line of
|
|
1353
|
+
function parseOpencodeSessionId(stdout7) {
|
|
1354
|
+
for (const line of stdout7.split("\n")) {
|
|
1039
1355
|
const t = line.trim();
|
|
1040
1356
|
if (!t) continue;
|
|
1041
1357
|
try {
|
|
@@ -1048,10 +1364,10 @@ function parseOpencodeSessionId(stdout6) {
|
|
|
1048
1364
|
}
|
|
1049
1365
|
return null;
|
|
1050
1366
|
}
|
|
1051
|
-
function parseSessionId(tool,
|
|
1052
|
-
if (tool === "codex") return parseCodexSessionId(
|
|
1053
|
-
if (tool === "opencode") return parseOpencodeSessionId(
|
|
1054
|
-
return parseClaudeSessionId(
|
|
1367
|
+
function parseSessionId(tool, stdout7) {
|
|
1368
|
+
if (tool === "codex") return parseCodexSessionId(stdout7);
|
|
1369
|
+
if (tool === "opencode") return parseOpencodeSessionId(stdout7);
|
|
1370
|
+
return parseClaudeSessionId(stdout7);
|
|
1055
1371
|
}
|
|
1056
1372
|
function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
1057
1373
|
let remaining = limitMs;
|
|
@@ -1064,11 +1380,11 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1064
1380
|
startedAt = Date.now();
|
|
1065
1381
|
timer = setTimeout(() => {
|
|
1066
1382
|
timedOut = true;
|
|
1067
|
-
child
|
|
1383
|
+
killTree(child);
|
|
1068
1384
|
}, Math.max(0, remaining));
|
|
1069
1385
|
};
|
|
1070
1386
|
let timedOut = false;
|
|
1071
|
-
const done = new Promise((
|
|
1387
|
+
const done = new Promise((resolve5) => {
|
|
1072
1388
|
child = execFile2(
|
|
1073
1389
|
plan.file,
|
|
1074
1390
|
plan.args,
|
|
@@ -1076,18 +1392,20 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1076
1392
|
cwd: plan.cwd,
|
|
1077
1393
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
1078
1394
|
// agent 自己要读 PATH 里的东西,环境原样继承
|
|
1079
|
-
env:
|
|
1395
|
+
env: mergeEnv(process.env, plan.env),
|
|
1396
|
+
// Windows:别给子进程弹黑框
|
|
1397
|
+
windowsHide: true
|
|
1080
1398
|
},
|
|
1081
|
-
(err,
|
|
1399
|
+
(err, stdout7) => {
|
|
1082
1400
|
finished = true;
|
|
1083
1401
|
if (timer) clearTimeout(timer);
|
|
1084
|
-
const out = String(
|
|
1402
|
+
const out = String(stdout7 ?? "");
|
|
1085
1403
|
if (err || timedOut) {
|
|
1086
1404
|
const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
|
|
1087
|
-
|
|
1405
|
+
resolve5({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
|
|
1088
1406
|
return;
|
|
1089
1407
|
}
|
|
1090
|
-
|
|
1408
|
+
resolve5({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
|
|
1091
1409
|
}
|
|
1092
1410
|
);
|
|
1093
1411
|
child?.stdin?.end();
|
|
@@ -1109,10 +1427,31 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1109
1427
|
}
|
|
1110
1428
|
};
|
|
1111
1429
|
}
|
|
1430
|
+
function mergeEnv(base, extra, platform = process.platform) {
|
|
1431
|
+
const env = { ...base };
|
|
1432
|
+
for (const [k, v] of Object.entries(extra ?? {})) {
|
|
1433
|
+
if (platform === "win32") {
|
|
1434
|
+
for (const old of Object.keys(env)) if (old !== k && old.toUpperCase() === k.toUpperCase()) delete env[old];
|
|
1435
|
+
}
|
|
1436
|
+
env[k] = v;
|
|
1437
|
+
}
|
|
1438
|
+
return env;
|
|
1439
|
+
}
|
|
1440
|
+
function killTree(child) {
|
|
1441
|
+
if (!child) return;
|
|
1442
|
+
if (process.platform === "win32" && child.pid) {
|
|
1443
|
+
try {
|
|
1444
|
+
execFileSync4("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
1445
|
+
return;
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
child.kill("SIGTERM");
|
|
1450
|
+
}
|
|
1112
1451
|
|
|
1113
1452
|
// src/daemon/gate/approvals.ts
|
|
1114
1453
|
import { createHash as createHash2 } from "crypto";
|
|
1115
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
1454
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
1116
1455
|
import { join as join9 } from "path";
|
|
1117
1456
|
var RETAIN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1118
1457
|
function fingerprint(toolName, toolInput) {
|
|
@@ -1134,7 +1473,7 @@ var ApprovalStore = class _ApprovalStore {
|
|
|
1134
1473
|
}
|
|
1135
1474
|
static load() {
|
|
1136
1475
|
try {
|
|
1137
|
-
const raw = JSON.parse(
|
|
1476
|
+
const raw = JSON.parse(readFileSync9(storePath(), "utf8"));
|
|
1138
1477
|
return new _ApprovalStore(Array.isArray(raw) ? raw : []);
|
|
1139
1478
|
} catch {
|
|
1140
1479
|
return new _ApprovalStore();
|
|
@@ -1330,12 +1669,12 @@ function isReadOnlyCommand(command) {
|
|
|
1330
1669
|
}
|
|
1331
1670
|
function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
|
|
1332
1671
|
const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter(Boolean);
|
|
1333
|
-
const
|
|
1672
|
+
const real2 = opts.real;
|
|
1334
1673
|
const inside = (p) => {
|
|
1335
1674
|
if (!roots.some((r) => isInside(r, p))) return false;
|
|
1336
|
-
if (!
|
|
1337
|
-
const rp =
|
|
1338
|
-
return roots.some((r) => isInside(
|
|
1675
|
+
if (!real2) return true;
|
|
1676
|
+
const rp = real2(resolve(roots[0] ?? "/", p));
|
|
1677
|
+
return roots.some((r) => isInside(real2(r), rp));
|
|
1339
1678
|
};
|
|
1340
1679
|
const rel = (p) => {
|
|
1341
1680
|
const r = roots.find((x) => isInside(x, p)) ?? roots[0] ?? "";
|
|
@@ -1352,7 +1691,7 @@ function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
|
|
|
1352
1691
|
if (paths) {
|
|
1353
1692
|
const allInside = paths.length > 0 && paths.every(inside);
|
|
1354
1693
|
const touchesProtected = paths.some(
|
|
1355
|
-
(p) => isProtectedPath(roots[0] ?? "/", p) || (
|
|
1694
|
+
(p) => isProtectedPath(roots[0] ?? "/", p) || (real2 ? isProtectedPath("/", real2(resolve(roots[0] ?? "/", p))) : false)
|
|
1356
1695
|
);
|
|
1357
1696
|
if (policy === "commands" && allInside && !touchesProtected) return { action: "auto" };
|
|
1358
1697
|
const shown = paths.map((p) => inside(p) ? rel(p) : p);
|
|
@@ -1375,11 +1714,14 @@ function describe(toolName, toolInput, cwd) {
|
|
|
1375
1714
|
}
|
|
1376
1715
|
|
|
1377
1716
|
// src/daemon/gate/procs.ts
|
|
1378
|
-
import { execFileSync as
|
|
1717
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1379
1718
|
function listDescendants(rootPid) {
|
|
1719
|
+
if (process.platform === "win32") {
|
|
1720
|
+
return descendantsOf(rootPid, (winProcs() ?? []).map((p) => ({ pid: p.pid, ppid: p.ppid, args: p.cmdline })));
|
|
1721
|
+
}
|
|
1380
1722
|
let out = "";
|
|
1381
1723
|
try {
|
|
1382
|
-
out =
|
|
1724
|
+
out = execFileSync5("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
|
|
1383
1725
|
encoding: "utf8",
|
|
1384
1726
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1385
1727
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -1387,19 +1729,28 @@ function listDescendants(rootPid) {
|
|
|
1387
1729
|
} catch {
|
|
1388
1730
|
return [];
|
|
1389
1731
|
}
|
|
1390
|
-
const
|
|
1732
|
+
const rows = [];
|
|
1391
1733
|
for (const line of out.split("\n")) {
|
|
1392
1734
|
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
1393
|
-
if (
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1735
|
+
if (m) rows.push({ pid: Number(m[1]), ppid: Number(m[2]), args: m[3] });
|
|
1736
|
+
}
|
|
1737
|
+
return descendantsOf(rootPid, rows);
|
|
1738
|
+
}
|
|
1739
|
+
function descendantsOf(rootPid, rows) {
|
|
1740
|
+
const kids = /* @__PURE__ */ new Map();
|
|
1741
|
+
for (const r of rows) {
|
|
1742
|
+
if (r.pid === r.ppid) continue;
|
|
1743
|
+
const list = kids.get(r.ppid) ?? [];
|
|
1744
|
+
list.push({ pid: r.pid, args: r.args });
|
|
1745
|
+
kids.set(r.ppid, list);
|
|
1398
1746
|
}
|
|
1747
|
+
const seen = /* @__PURE__ */ new Set([rootPid]);
|
|
1399
1748
|
const res = [];
|
|
1400
1749
|
const stack = [rootPid];
|
|
1401
1750
|
while (stack.length > 0) {
|
|
1402
1751
|
for (const c of kids.get(stack.pop()) ?? []) {
|
|
1752
|
+
if (seen.has(c.pid)) continue;
|
|
1753
|
+
seen.add(c.pid);
|
|
1403
1754
|
res.push(c);
|
|
1404
1755
|
stack.push(c.pid);
|
|
1405
1756
|
}
|
|
@@ -1428,9 +1779,9 @@ function pickRememberSuggestion(suggestions) {
|
|
|
1428
1779
|
return null;
|
|
1429
1780
|
}
|
|
1430
1781
|
var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
|
|
1431
|
-
function windowNotice(decision,
|
|
1782
|
+
function windowNotice(decision, summary2, deviceName = "") {
|
|
1432
1783
|
const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
|
|
1433
|
-
const what =
|
|
1784
|
+
const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
|
|
1434
1785
|
return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
|
|
1435
1786
|
}
|
|
1436
1787
|
var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
@@ -1463,17 +1814,17 @@ var ApprovalCoordinator = class {
|
|
|
1463
1814
|
sessionOf(runId) {
|
|
1464
1815
|
return this.#runs.get(runId)?.sessionId ?? "";
|
|
1465
1816
|
}
|
|
1466
|
-
async onGate(
|
|
1467
|
-
if (
|
|
1468
|
-
const run2 = this.#runs.get(
|
|
1817
|
+
async onGate(req2, signal) {
|
|
1818
|
+
if (req2.window) return this.#onWindow(req2.hook, signal, req2.tool || "claude_code");
|
|
1819
|
+
const run2 = this.#runs.get(req2.runId ?? "");
|
|
1469
1820
|
if (!run2) return { decision: "deny", reason: REASON_UNKNOWN_RUN };
|
|
1470
1821
|
if (run2.superseded) return { decision: "deny", reason: REASON_SUPERSEDED };
|
|
1471
|
-
const toolName = typeof
|
|
1472
|
-
const sessionId = typeof
|
|
1822
|
+
const toolName = typeof req2.hook.tool_name === "string" ? req2.hook.tool_name : "";
|
|
1823
|
+
const sessionId = typeof req2.hook.session_id === "string" && req2.hook.session_id || run2.sessionId || "";
|
|
1473
1824
|
if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
|
|
1474
|
-
const v = classify(toolName,
|
|
1825
|
+
const v = classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
|
|
1475
1826
|
if (v.action === "auto") return { decision: "allow" };
|
|
1476
|
-
const fp = fingerprint(toolName,
|
|
1827
|
+
const fp = fingerprint(toolName, req2.hook.tool_input);
|
|
1477
1828
|
const store = this.#d.store;
|
|
1478
1829
|
if (run2.mode === "defer" && sessionId) {
|
|
1479
1830
|
const prev = store.latestFor(sessionId, fp);
|
|
@@ -1502,7 +1853,7 @@ var ApprovalCoordinator = class {
|
|
|
1502
1853
|
store.save();
|
|
1503
1854
|
let early = null;
|
|
1504
1855
|
let settle;
|
|
1505
|
-
const decided = new Promise((
|
|
1856
|
+
const decided = new Promise((resolve5) => settle = resolve5);
|
|
1506
1857
|
this.#waiters.set(rec.approvalId, (r) => {
|
|
1507
1858
|
early = r;
|
|
1508
1859
|
settle(r);
|
|
@@ -1526,8 +1877,8 @@ var ApprovalCoordinator = class {
|
|
|
1526
1877
|
}
|
|
1527
1878
|
run2.onWaitStart?.();
|
|
1528
1879
|
try {
|
|
1529
|
-
return await new Promise((
|
|
1530
|
-
void decided.then(
|
|
1880
|
+
return await new Promise((resolve5) => {
|
|
1881
|
+
void decided.then(resolve5);
|
|
1531
1882
|
const onAbort = () => {
|
|
1532
1883
|
if (!this.#waiters.delete(rec.approvalId)) return;
|
|
1533
1884
|
if (rec.status === "pending") {
|
|
@@ -1536,7 +1887,7 @@ var ApprovalCoordinator = class {
|
|
|
1536
1887
|
store.save();
|
|
1537
1888
|
void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
|
|
1538
1889
|
}
|
|
1539
|
-
|
|
1890
|
+
resolve5({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
|
|
1540
1891
|
};
|
|
1541
1892
|
if (signal.aborted) onAbort();
|
|
1542
1893
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1559,7 +1910,7 @@ var ApprovalCoordinator = class {
|
|
|
1559
1910
|
if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
|
|
1560
1911
|
for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
|
|
1561
1912
|
const store = this.#d.store;
|
|
1562
|
-
const { kind, summary } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
1913
|
+
const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
1563
1914
|
const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : null;
|
|
1564
1915
|
const rec = {
|
|
1565
1916
|
approvalId: randomUUID3(),
|
|
@@ -1569,7 +1920,7 @@ var ApprovalCoordinator = class {
|
|
|
1569
1920
|
cwd,
|
|
1570
1921
|
fingerprint: fingerprint(toolName, hook.tool_input),
|
|
1571
1922
|
kind,
|
|
1572
|
-
summary,
|
|
1923
|
+
summary: summary2,
|
|
1573
1924
|
mode: "window",
|
|
1574
1925
|
status: "pending",
|
|
1575
1926
|
createdAt: this.#now(),
|
|
@@ -1588,7 +1939,7 @@ var ApprovalCoordinator = class {
|
|
|
1588
1939
|
...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
|
|
1589
1940
|
});
|
|
1590
1941
|
let settle;
|
|
1591
|
-
const decided = new Promise((
|
|
1942
|
+
const decided = new Promise((resolve5) => settle = resolve5);
|
|
1592
1943
|
this.#waiters.set(rec.approvalId, settle);
|
|
1593
1944
|
try {
|
|
1594
1945
|
await this.#d.backend.create(rec);
|
|
@@ -1604,11 +1955,11 @@ var ApprovalCoordinator = class {
|
|
|
1604
1955
|
}
|
|
1605
1956
|
this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
|
|
1606
1957
|
if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
|
|
1607
|
-
return new Promise((
|
|
1608
|
-
void decided.then(
|
|
1958
|
+
return new Promise((resolve5) => {
|
|
1959
|
+
void decided.then(resolve5);
|
|
1609
1960
|
const onAbort = () => {
|
|
1610
1961
|
if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
|
|
1611
|
-
|
|
1962
|
+
resolve5(none);
|
|
1612
1963
|
};
|
|
1613
1964
|
if (signal.aborted) onAbort();
|
|
1614
1965
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1882,8 +2233,8 @@ function realpathNearest(p) {
|
|
|
1882
2233
|
}
|
|
1883
2234
|
function cwdForms(cwd) {
|
|
1884
2235
|
try {
|
|
1885
|
-
const
|
|
1886
|
-
return
|
|
2236
|
+
const real2 = realpathSync(cwd);
|
|
2237
|
+
return real2 === cwd ? [cwd] : [cwd, real2];
|
|
1887
2238
|
} catch {
|
|
1888
2239
|
return [cwd];
|
|
1889
2240
|
}
|
|
@@ -1915,10 +2266,10 @@ var GateServer = class {
|
|
|
1915
2266
|
}
|
|
1916
2267
|
/** 起监听并写 gate.json。返回实际端口 */
|
|
1917
2268
|
async start() {
|
|
1918
|
-
const server = createServer((
|
|
1919
|
-
await new Promise((
|
|
2269
|
+
const server = createServer((req2, res) => void this.#serve(req2, res));
|
|
2270
|
+
await new Promise((resolve5, reject) => {
|
|
1920
2271
|
server.once("error", reject);
|
|
1921
|
-
server.listen(0, "127.0.0.1", () =>
|
|
2272
|
+
server.listen(0, "127.0.0.1", () => resolve5());
|
|
1922
2273
|
});
|
|
1923
2274
|
this.#server = server;
|
|
1924
2275
|
const addr = server.address();
|
|
@@ -1939,21 +2290,21 @@ var GateServer = class {
|
|
|
1939
2290
|
}
|
|
1940
2291
|
const s = this.#server;
|
|
1941
2292
|
this.#server = null;
|
|
1942
|
-
if (s) await new Promise((
|
|
2293
|
+
if (s) await new Promise((resolve5) => s.close(() => resolve5()));
|
|
1943
2294
|
}
|
|
1944
|
-
async #serve(
|
|
2295
|
+
async #serve(req2, res) {
|
|
1945
2296
|
const reply = (status, body2) => {
|
|
1946
2297
|
if (res.writableEnded || res.destroyed) return;
|
|
1947
2298
|
res.writeHead(status, { "content-type": "application/json" });
|
|
1948
2299
|
res.end(body2 ? JSON.stringify(body2) : "");
|
|
1949
2300
|
};
|
|
1950
|
-
if (
|
|
1951
|
-
const token =
|
|
2301
|
+
if (req2.method !== "POST" || req2.url !== "/gate") return reply(404);
|
|
2302
|
+
const token = req2.headers["x-gate-token"];
|
|
1952
2303
|
if (typeof token !== "string" || !tokenEquals(token, this.#token)) return reply(403);
|
|
1953
2304
|
let body = "";
|
|
1954
2305
|
let size = 0;
|
|
1955
2306
|
try {
|
|
1956
|
-
for await (const chunk of
|
|
2307
|
+
for await (const chunk of req2) {
|
|
1957
2308
|
size += chunk.length;
|
|
1958
2309
|
if (size > MAX_BODY_BYTES) return reply(413);
|
|
1959
2310
|
body += String(chunk);
|
|
@@ -1975,12 +2326,12 @@ var GateServer = class {
|
|
|
1975
2326
|
if (!res.writableEnded) ac.abort();
|
|
1976
2327
|
});
|
|
1977
2328
|
try {
|
|
1978
|
-
const
|
|
2329
|
+
const req3 = window ? {
|
|
1979
2330
|
window: true,
|
|
1980
2331
|
tool: typeof parsed.tool === "string" && /^[a-z_]{1,32}$/.test(parsed.tool) ? parsed.tool : "claude_code",
|
|
1981
2332
|
hook: parsed.hook
|
|
1982
2333
|
} : { runId: parsed.runId, hook: parsed.hook };
|
|
1983
|
-
const out = await this.#handler(
|
|
2334
|
+
const out = await this.#handler(req3, ac.signal);
|
|
1984
2335
|
reply(200, out);
|
|
1985
2336
|
} catch {
|
|
1986
2337
|
reply(500);
|
|
@@ -2097,6 +2448,10 @@ var SessionQueue = class {
|
|
|
2097
2448
|
});
|
|
2098
2449
|
return next;
|
|
2099
2450
|
}
|
|
2451
|
+
/** 这个会话有没有在跑 / 在排队的任务 */
|
|
2452
|
+
busy(sessionId) {
|
|
2453
|
+
return this.#tails.has(sessionId || "");
|
|
2454
|
+
}
|
|
2100
2455
|
/** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
|
|
2101
2456
|
get activeSessions() {
|
|
2102
2457
|
return this.#tails.size;
|
|
@@ -2110,7 +2465,7 @@ var SessionQueue = class {
|
|
|
2110
2465
|
};
|
|
2111
2466
|
|
|
2112
2467
|
// src/daemon/remote.ts
|
|
2113
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
2468
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
2114
2469
|
import { join as join13 } from "path";
|
|
2115
2470
|
function handleDir() {
|
|
2116
2471
|
return join13(deskbuddyHome(), "remote");
|
|
@@ -2121,7 +2476,7 @@ function handlePath(tool) {
|
|
|
2121
2476
|
}
|
|
2122
2477
|
function readHandle(tool) {
|
|
2123
2478
|
try {
|
|
2124
|
-
const h = JSON.parse(
|
|
2479
|
+
const h = JSON.parse(readFileSync10(handlePath(tool), "utf8"));
|
|
2125
2480
|
if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
|
|
2126
2481
|
if (typeof h?.cwd !== "string" || !h.cwd) return null;
|
|
2127
2482
|
return h;
|
|
@@ -2187,7 +2542,7 @@ function failureMessage(reason) {
|
|
|
2187
2542
|
}
|
|
2188
2543
|
|
|
2189
2544
|
// src/daemon/seen.ts
|
|
2190
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
2545
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync11, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
2191
2546
|
import { join as join14 } from "path";
|
|
2192
2547
|
var SEEN_CAPACITY = 200;
|
|
2193
2548
|
function seenPath() {
|
|
@@ -2222,7 +2577,7 @@ var SeenIds = class _SeenIds {
|
|
|
2222
2577
|
}
|
|
2223
2578
|
static load() {
|
|
2224
2579
|
try {
|
|
2225
|
-
const raw = JSON.parse(
|
|
2580
|
+
const raw = JSON.parse(readFileSync11(seenPath(), "utf8"));
|
|
2226
2581
|
return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
|
|
2227
2582
|
} catch {
|
|
2228
2583
|
return new _SeenIds();
|
|
@@ -2240,11 +2595,25 @@ var SeenIds = class _SeenIds {
|
|
|
2240
2595
|
};
|
|
2241
2596
|
|
|
2242
2597
|
// src/daemon/codexWatch.ts
|
|
2243
|
-
import { execFileSync as
|
|
2598
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2244
2599
|
import WebSocket from "ws";
|
|
2600
|
+
var TURN_LIMIT_MS = 10 * 60 * 1e3;
|
|
2601
|
+
var QUEUE_MAX_MS = 10 * 60 * 1e3;
|
|
2602
|
+
var IDLE_GRACE_MS = 5 * 1e3;
|
|
2245
2603
|
var RESUME_RETRY_MS = 1e3;
|
|
2246
2604
|
var RESUME_MAX_TRIES = 120;
|
|
2247
2605
|
var REQUEST_TIMEOUT_MS = 30 * 1e3;
|
|
2606
|
+
function finalReply(items) {
|
|
2607
|
+
let last = "";
|
|
2608
|
+
let final = "";
|
|
2609
|
+
for (const it of items) {
|
|
2610
|
+
const i = it ?? {};
|
|
2611
|
+
if (i.type !== "agentMessage" || typeof i.text !== "string") continue;
|
|
2612
|
+
last = i.text;
|
|
2613
|
+
if (i.phase === "final_answer") final = i.text;
|
|
2614
|
+
}
|
|
2615
|
+
return final || last;
|
|
2616
|
+
}
|
|
2248
2617
|
function isLoopbackWs(url) {
|
|
2249
2618
|
try {
|
|
2250
2619
|
const u = new URL(url);
|
|
@@ -2263,17 +2632,29 @@ function parseAppServerUrls(psOutput) {
|
|
|
2263
2632
|
return urls;
|
|
2264
2633
|
}
|
|
2265
2634
|
function discoverAppServer(configured) {
|
|
2266
|
-
|
|
2635
|
+
return discoverAppServers(configured)[0] ?? null;
|
|
2636
|
+
}
|
|
2637
|
+
function discoverAppServers(configured) {
|
|
2638
|
+
if (configured) return isLoopbackWs(configured) ? [configured] : [];
|
|
2639
|
+
if (process.platform === "win32") {
|
|
2640
|
+
return parseAppServerUrls((winProcs() ?? []).map((p) => p.cmdline).join("\n"));
|
|
2641
|
+
}
|
|
2267
2642
|
try {
|
|
2268
|
-
const out =
|
|
2269
|
-
return parseAppServerUrls(out)
|
|
2643
|
+
const out = execFileSync6("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
2644
|
+
return parseAppServerUrls(out);
|
|
2270
2645
|
} catch {
|
|
2271
|
-
return
|
|
2646
|
+
return [];
|
|
2272
2647
|
}
|
|
2273
2648
|
}
|
|
2274
2649
|
function unwrapShell(command) {
|
|
2275
2650
|
const m = command.match(/^\/bin\/(?:ba|z)?sh -lc '([\s\S]*)'$/);
|
|
2276
|
-
|
|
2651
|
+
if (m) return m[1].replace(/'\\''/g, "'");
|
|
2652
|
+
const w = command.match(/^"?(?:[^"]*[\\/])?(?:powershell|pwsh)(?:\.exe)?"?(?: -NoLogo| -NoProfile| -NonInteractive)* -Command ([\s\S]+)$/i);
|
|
2653
|
+
if (!w) return command;
|
|
2654
|
+
const body = w[1].trim();
|
|
2655
|
+
if (body.length >= 2 && body.startsWith('"') && body.endsWith('"')) return body.slice(1, -1).replace(/\\"/g, '"');
|
|
2656
|
+
if (body.length >= 2 && body.startsWith("'") && body.endsWith("'")) return body.slice(1, -1).replace(/''/g, "'");
|
|
2657
|
+
return body;
|
|
2277
2658
|
}
|
|
2278
2659
|
var CodexWatch = class {
|
|
2279
2660
|
#d;
|
|
@@ -2295,17 +2676,48 @@ var CodexWatch = class {
|
|
|
2295
2676
|
#idle = /* @__PURE__ */ new Set();
|
|
2296
2677
|
/** 连接代数:断线重连后,上一条连接遗留的订阅重试不再跑 */
|
|
2297
2678
|
#gen = 0;
|
|
2679
|
+
/** initialize 已成功 */
|
|
2680
|
+
#ready = false;
|
|
2681
|
+
/** app-server 里已加载的会话(thread/loaded/list + 通知维护)。窗口开着 = 能往里发,据此上报 remoteWindow */
|
|
2682
|
+
#loaded = /* @__PURE__ */ new Set();
|
|
2683
|
+
/** 会话状态(thread/status/changed 是广播,没订阅也收得到) */
|
|
2684
|
+
#status = /* @__PURE__ */ new Map();
|
|
2685
|
+
/** 会话里正在跑的那一轮(turn/started 起、turn/completed 止;只有订阅了才收得到) */
|
|
2686
|
+
#activeTurn = /* @__PURE__ */ new Map();
|
|
2687
|
+
/** 会话 → 我们自己在排队 / 在跑的活数。🔴 有活时不退订(否则收不到 turn/completed) */
|
|
2688
|
+
#own = /* @__PURE__ */ new Map();
|
|
2689
|
+
/** 会话 → 我们发的轮次串行链:同一会话的设备指令一条跑完再发下一条 */
|
|
2690
|
+
#chain = /* @__PURE__ */ new Map();
|
|
2691
|
+
/** turnId → 我们发出、正在跟踪的那一轮 */
|
|
2692
|
+
#turns = /* @__PURE__ */ new Map();
|
|
2693
|
+
/** 会话 → 在途的 thread/resume(授权推送那条路径发的) */
|
|
2694
|
+
#inflight = /* @__PURE__ */ new Map();
|
|
2695
|
+
/** 回话还没到就先到了的 turn/completed(按 turnId 缓冲,最多几条) */
|
|
2696
|
+
#early = /* @__PURE__ */ new Map();
|
|
2697
|
+
/** 会话 → 等状态变化的回调(排队等空闲用) */
|
|
2698
|
+
#wake = /* @__PURE__ */ new Map();
|
|
2298
2699
|
constructor(deps) {
|
|
2299
2700
|
this.#d = { connect: (url) => new WebSocket(url), ...deps };
|
|
2300
2701
|
}
|
|
2301
2702
|
get connected() {
|
|
2302
2703
|
return this.#ws !== null;
|
|
2303
2704
|
}
|
|
2705
|
+
get url() {
|
|
2706
|
+
return this.#url;
|
|
2707
|
+
}
|
|
2708
|
+
/** 这个会话此刻是否加载在 app-server 里(能直接往里发一轮) */
|
|
2709
|
+
hasLoaded(threadId) {
|
|
2710
|
+
return this.#ready && this.#loaded.has(threadId);
|
|
2711
|
+
}
|
|
2712
|
+
loadedThreads() {
|
|
2713
|
+
return this.#ready ? [...this.#loaded] : [];
|
|
2714
|
+
}
|
|
2304
2715
|
/** 定时调:没连上就找一次 app-server 连上去;连着时 codex 被 disable 了就断开 */
|
|
2305
2716
|
tick() {
|
|
2306
2717
|
if (this.#stopped) return;
|
|
2307
2718
|
if (this.#ws) {
|
|
2308
2719
|
if (this.#d.enabled && !this.#d.enabled()) this.#teardown();
|
|
2720
|
+
else if (this.#ready) void this.#syncLoaded();
|
|
2309
2721
|
return;
|
|
2310
2722
|
}
|
|
2311
2723
|
const url = (this.#d.discover ?? (() => discoverAppServer()))();
|
|
@@ -2328,10 +2740,10 @@ var CodexWatch = class {
|
|
|
2328
2740
|
this.#gen++;
|
|
2329
2741
|
ws.on("open", () => void this.#onOpen());
|
|
2330
2742
|
ws.on("message", (buf) => this.#onMessage(String(buf)));
|
|
2331
|
-
ws.on("close", () => this.#teardown());
|
|
2332
|
-
ws.on("error", () => this.#teardown());
|
|
2743
|
+
ws.on("close", (code) => this.#teardown(`\u5173\u95ED ${code}`));
|
|
2744
|
+
ws.on("error", (e) => this.#teardown(`\u51FA\u9519 ${e?.message ?? ""}`));
|
|
2333
2745
|
}
|
|
2334
|
-
#teardown() {
|
|
2746
|
+
#teardown(why = "") {
|
|
2335
2747
|
const ws = this.#ws;
|
|
2336
2748
|
if (!ws) return;
|
|
2337
2749
|
this.#ws = null;
|
|
@@ -2341,7 +2753,7 @@ var CodexWatch = class {
|
|
|
2341
2753
|
}
|
|
2342
2754
|
for (const c of this.#calls.values()) {
|
|
2343
2755
|
clearTimeout(c.timer);
|
|
2344
|
-
c.resolve({ error: { message: "disconnected" } });
|
|
2756
|
+
c.resolve({ error: { message: "disconnected" }, local: true });
|
|
2345
2757
|
}
|
|
2346
2758
|
this.#calls.clear();
|
|
2347
2759
|
for (const { ac } of this.#pending.values()) ac.abort();
|
|
@@ -2351,7 +2763,17 @@ var CodexWatch = class {
|
|
|
2351
2763
|
this.#cwd.clear();
|
|
2352
2764
|
this.#changes.clear();
|
|
2353
2765
|
this.#idle.clear();
|
|
2354
|
-
this.#
|
|
2766
|
+
this.#ready = false;
|
|
2767
|
+
this.#status.clear();
|
|
2768
|
+
this.#activeTurn.clear();
|
|
2769
|
+
this.#inflight.clear();
|
|
2770
|
+
this.#early.clear();
|
|
2771
|
+
for (const t of [...this.#turns.values()]) this.#finishTurn(t, "disconnected");
|
|
2772
|
+
this.#wakeAll();
|
|
2773
|
+
const hadLoaded = this.#loaded.size > 0;
|
|
2774
|
+
this.#loaded.clear();
|
|
2775
|
+
if (hadLoaded) this.#notifyLoaded();
|
|
2776
|
+
this.#d.log(`\u5DF2\u65AD\u5F00 Codex app-server ${this.#url}${why ? `\uFF08${why}\uFF09` : ""}`);
|
|
2355
2777
|
}
|
|
2356
2778
|
#send(obj) {
|
|
2357
2779
|
try {
|
|
@@ -2361,13 +2783,13 @@ var CodexWatch = class {
|
|
|
2361
2783
|
}
|
|
2362
2784
|
#request(method, params) {
|
|
2363
2785
|
const id = this.#nextId++;
|
|
2364
|
-
return new Promise((
|
|
2786
|
+
return new Promise((resolve5) => {
|
|
2365
2787
|
const timer = setTimeout(() => {
|
|
2366
2788
|
this.#calls.delete(id);
|
|
2367
|
-
|
|
2789
|
+
resolve5({ error: { message: "timeout" }, local: true });
|
|
2368
2790
|
}, REQUEST_TIMEOUT_MS);
|
|
2369
2791
|
timer.unref?.();
|
|
2370
|
-
this.#calls.set(id, { resolve:
|
|
2792
|
+
this.#calls.set(id, { resolve: resolve5, timer });
|
|
2371
2793
|
this.#send({ id, method, params });
|
|
2372
2794
|
});
|
|
2373
2795
|
}
|
|
@@ -2379,7 +2801,64 @@ var CodexWatch = class {
|
|
|
2379
2801
|
return;
|
|
2380
2802
|
}
|
|
2381
2803
|
this.#send({ method: "initialized" });
|
|
2804
|
+
this.#ready = true;
|
|
2382
2805
|
this.#d.log(`\u5DF2\u8FDE\u4E0A Codex app-server ${this.#url}\uFF0C\u7A97\u53E3\u6388\u6743\u53EF\u63A8\u5230\u8BBE\u5907`);
|
|
2806
|
+
await this.#syncLoaded();
|
|
2807
|
+
}
|
|
2808
|
+
#notifyLoaded() {
|
|
2809
|
+
try {
|
|
2810
|
+
this.#d.onLoadedChange?.();
|
|
2811
|
+
} catch {
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
#setLoaded(threadId, on) {
|
|
2815
|
+
if (on === this.#loaded.has(threadId)) return;
|
|
2816
|
+
if (on) this.#loaded.add(threadId);
|
|
2817
|
+
else this.#loaded.delete(threadId);
|
|
2818
|
+
this.#notifyLoaded();
|
|
2819
|
+
}
|
|
2820
|
+
/** 拉一遍已加载的会话(分页)。拉不到返回 null,不动现有集合 */
|
|
2821
|
+
async #fetchLoaded() {
|
|
2822
|
+
const gen = this.#gen;
|
|
2823
|
+
const all = /* @__PURE__ */ new Set();
|
|
2824
|
+
let cursor = null;
|
|
2825
|
+
for (let page = 0; page < 20; page++) {
|
|
2826
|
+
const r = await this.#request("thread/loaded/list", cursor ? { cursor } : {});
|
|
2827
|
+
if (r.error || gen !== this.#gen) return null;
|
|
2828
|
+
const res = r.result ?? {};
|
|
2829
|
+
if (Array.isArray(res.data)) {
|
|
2830
|
+
for (const id of res.data) if (typeof id === "string") all.add(id);
|
|
2831
|
+
}
|
|
2832
|
+
cursor = typeof res.nextCursor === "string" && res.nextCursor ? res.nextCursor : null;
|
|
2833
|
+
if (!cursor) break;
|
|
2834
|
+
}
|
|
2835
|
+
return all;
|
|
2836
|
+
}
|
|
2837
|
+
async #syncLoaded() {
|
|
2838
|
+
const gen = this.#gen;
|
|
2839
|
+
const fresh = await this.#fetchLoaded();
|
|
2840
|
+
if (!fresh || gen !== this.#gen) return null;
|
|
2841
|
+
let changed = fresh.size !== this.#loaded.size;
|
|
2842
|
+
if (!changed) {
|
|
2843
|
+
for (const id of fresh) if (!this.#loaded.has(id)) changed = true;
|
|
2844
|
+
}
|
|
2845
|
+
if (changed) {
|
|
2846
|
+
this.#loaded.clear();
|
|
2847
|
+
for (const id of fresh) this.#loaded.add(id);
|
|
2848
|
+
this.#notifyLoaded();
|
|
2849
|
+
}
|
|
2850
|
+
return fresh;
|
|
2851
|
+
}
|
|
2852
|
+
/** resume 的回话:记下工作目录与会话状态 */
|
|
2853
|
+
#applyThread(threadId, r) {
|
|
2854
|
+
const th = (r.result ?? {}).thread;
|
|
2855
|
+
if (typeof th?.cwd === "string") this.#cwd.set(threadId, th.cwd);
|
|
2856
|
+
const st = th?.status?.type;
|
|
2857
|
+
if (typeof st === "string") {
|
|
2858
|
+
this.#status.set(threadId, st);
|
|
2859
|
+
if (st === "active") this.#idle.delete(threadId);
|
|
2860
|
+
else this.#idle.add(threadId);
|
|
2861
|
+
}
|
|
2383
2862
|
}
|
|
2384
2863
|
#subscribe(threadId) {
|
|
2385
2864
|
if (this.#subscribed.has(threadId) || this.#retrying.has(threadId)) return;
|
|
@@ -2387,16 +2866,26 @@ var CodexWatch = class {
|
|
|
2387
2866
|
const gen = this.#gen;
|
|
2388
2867
|
const attempt = async () => {
|
|
2389
2868
|
if (!this.#ws || gen !== this.#gen || !this.#retrying.has(threadId)) return;
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2869
|
+
const req2 = this.#request("thread/resume", { threadId, excludeTurns: true });
|
|
2870
|
+
this.#inflight.set(threadId, req2);
|
|
2871
|
+
const r = await req2;
|
|
2872
|
+
if (this.#inflight.get(threadId) === req2) this.#inflight.delete(threadId);
|
|
2873
|
+
if (gen !== this.#gen) return;
|
|
2874
|
+
if (!this.#retrying.has(threadId)) {
|
|
2875
|
+
if (!r.error) {
|
|
2876
|
+
if ((this.#own.get(threadId) ?? 0) > 0 || this.#subscribed.has(threadId)) {
|
|
2877
|
+
this.#subscribed.add(threadId);
|
|
2878
|
+
this.#applyThread(threadId, r);
|
|
2879
|
+
} else {
|
|
2880
|
+
void this.#request("thread/unsubscribe", { threadId });
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2393
2883
|
return;
|
|
2394
2884
|
}
|
|
2395
2885
|
if (!r.error) {
|
|
2396
2886
|
this.#retrying.delete(threadId);
|
|
2397
2887
|
this.#subscribed.add(threadId);
|
|
2398
|
-
|
|
2399
|
-
if (typeof cwd === "string") this.#cwd.set(threadId, cwd);
|
|
2888
|
+
this.#applyThread(threadId, r);
|
|
2400
2889
|
return;
|
|
2401
2890
|
}
|
|
2402
2891
|
const n = (this.#retrying.get(threadId) ?? 0) + 1;
|
|
@@ -2414,9 +2903,194 @@ var CodexWatch = class {
|
|
|
2414
2903
|
this.#retrying.delete(threadId);
|
|
2415
2904
|
if (!this.#subscribed.has(threadId)) return;
|
|
2416
2905
|
for (const p of this.#pending.values()) if (p.threadId === threadId) return;
|
|
2906
|
+
if ((this.#own.get(threadId) ?? 0) > 0) return;
|
|
2417
2907
|
this.#subscribed.delete(threadId);
|
|
2418
2908
|
void this.#request("thread/unsubscribe", { threadId });
|
|
2419
2909
|
}
|
|
2910
|
+
/**
|
|
2911
|
+
* 往一个已加载的会话里发一轮(切片 B)。同一会话的多条串行;会话正忙时排队到空闲再发,
|
|
2912
|
+
* 🔴 绝不在人正在跑的那一轮里插话(turn/start 打到 active 的会话上会并进当前那一轮,0.157.1 实测)。
|
|
2913
|
+
*
|
|
2914
|
+
* 返回 sent=false = 确定没发出去,调用方可以退回后台 exec;sent=true 之后无论结局如何都不能再重跑
|
|
2915
|
+
*/
|
|
2916
|
+
async runTurn(req2, hooks = {}) {
|
|
2917
|
+
const tid = req2.threadId;
|
|
2918
|
+
let queued = false;
|
|
2919
|
+
const h = {
|
|
2920
|
+
onQueued: () => {
|
|
2921
|
+
if (queued) return;
|
|
2922
|
+
queued = true;
|
|
2923
|
+
hooks.onQueued?.();
|
|
2924
|
+
},
|
|
2925
|
+
onStarted: () => hooks.onStarted?.()
|
|
2926
|
+
};
|
|
2927
|
+
this.#own.set(tid, (this.#own.get(tid) ?? 0) + 1);
|
|
2928
|
+
const prev = this.#chain.get(tid);
|
|
2929
|
+
let release2;
|
|
2930
|
+
const mine = new Promise((r) => release2 = r);
|
|
2931
|
+
const tail = (prev ?? Promise.resolve()).then(() => mine);
|
|
2932
|
+
this.#chain.set(tid, tail);
|
|
2933
|
+
try {
|
|
2934
|
+
if (prev) {
|
|
2935
|
+
h.onQueued?.();
|
|
2936
|
+
await prev;
|
|
2937
|
+
}
|
|
2938
|
+
return await this.#runTurnNow(req2, h);
|
|
2939
|
+
} catch (e) {
|
|
2940
|
+
this.#d.log(`Codex \u53D1\u4E00\u8F6E\u51FA\u9519\uFF1A${e.message}`);
|
|
2941
|
+
return { sent: false, reason: "start_failed" };
|
|
2942
|
+
} finally {
|
|
2943
|
+
release2();
|
|
2944
|
+
if (this.#chain.get(tid) === tail) this.#chain.delete(tid);
|
|
2945
|
+
const n = (this.#own.get(tid) ?? 1) - 1;
|
|
2946
|
+
if (n > 0) this.#own.set(tid, n);
|
|
2947
|
+
else {
|
|
2948
|
+
this.#own.delete(tid);
|
|
2949
|
+
if (this.#idle.has(tid)) this.#unsubscribe(tid);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
#expired(req2, since) {
|
|
2954
|
+
const now = Date.now();
|
|
2955
|
+
return req2.expireAt > 0 ? now > req2.expireAt : now - since > QUEUE_MAX_MS;
|
|
2956
|
+
}
|
|
2957
|
+
async #runTurnNow(req2, h) {
|
|
2958
|
+
const tid = req2.threadId;
|
|
2959
|
+
const since = Date.now();
|
|
2960
|
+
const gen = this.#gen;
|
|
2961
|
+
const lost = () => !this.#ws || gen !== this.#gen;
|
|
2962
|
+
if (!this.#ws || !this.#ready) return { sent: false, reason: "not_connected" };
|
|
2963
|
+
const loaded = await this.#syncLoaded();
|
|
2964
|
+
if (lost()) return { sent: false, reason: "disconnected" };
|
|
2965
|
+
if (!loaded || !loaded.has(tid)) return { sent: false, reason: "not_loaded" };
|
|
2966
|
+
const inflight = this.#inflight.get(tid);
|
|
2967
|
+
if (inflight) {
|
|
2968
|
+
this.#retrying.delete(tid);
|
|
2969
|
+
await inflight;
|
|
2970
|
+
if (lost()) return { sent: false, reason: "disconnected" };
|
|
2971
|
+
}
|
|
2972
|
+
if (!this.#subscribed.has(tid)) {
|
|
2973
|
+
this.#retrying.delete(tid);
|
|
2974
|
+
const r2 = await this.#request("thread/resume", { threadId: tid, excludeTurns: true });
|
|
2975
|
+
if (lost()) return { sent: false, reason: "disconnected" };
|
|
2976
|
+
if (r2.error) {
|
|
2977
|
+
this.#d.log(`\u8BA2\u9605 Codex \u4F1A\u8BDD\u5931\u8D25\uFF1A${JSON.stringify(r2.error).slice(0, 120)}`);
|
|
2978
|
+
return { sent: false, reason: "resume_failed" };
|
|
2979
|
+
}
|
|
2980
|
+
this.#subscribed.add(tid);
|
|
2981
|
+
this.#applyThread(tid, r2);
|
|
2982
|
+
}
|
|
2983
|
+
for (; ; ) {
|
|
2984
|
+
if (lost()) return { sent: false, reason: "disconnected" };
|
|
2985
|
+
const st = this.#status.get(tid);
|
|
2986
|
+
if (st === "notLoaded" || !this.#loaded.has(tid)) return { sent: false, reason: "closed" };
|
|
2987
|
+
if (this.#expired(req2, since)) return { sent: false, reason: "expired" };
|
|
2988
|
+
if (st !== "active") break;
|
|
2989
|
+
h.onQueued?.();
|
|
2990
|
+
const left = req2.expireAt > 0 ? req2.expireAt - Date.now() : QUEUE_MAX_MS - (Date.now() - since);
|
|
2991
|
+
await this.#waitThread(tid, Math.max(1, Math.min(left + 5, 60 * 1e3)));
|
|
2992
|
+
}
|
|
2993
|
+
const humanTurn = this.#activeTurn.get(tid);
|
|
2994
|
+
const r = await this.#request("turn/start", {
|
|
2995
|
+
threadId: tid,
|
|
2996
|
+
input: [{ type: "text", text: req2.text, text_elements: [] }]
|
|
2997
|
+
});
|
|
2998
|
+
if (r.error && !r.local) {
|
|
2999
|
+
this.#d.log(`Codex turn/start \u88AB\u62D2\uFF1A${JSON.stringify(r.error).slice(0, 120)}`);
|
|
3000
|
+
return { sent: false, reason: "start_failed" };
|
|
3001
|
+
}
|
|
3002
|
+
if (r.error) {
|
|
3003
|
+
return { sent: true, end: "disconnected", turnId: "", text: "", error: String(r.error.message ?? "") };
|
|
3004
|
+
}
|
|
3005
|
+
const turnId = String((r.result ?? {}).turn?.id ?? "");
|
|
3006
|
+
const merged = !!turnId && (turnId === humanTurn || turnId === this.#activeTurn.get(tid));
|
|
3007
|
+
if (merged) this.#d.log("\u8BBE\u5907\u6307\u4EE4\u5E76\u8FDB\u4E86\u7A97\u53E3\u91CC\u6B63\u5728\u8DD1\u7684\u90A3\u4E00\u8F6E");
|
|
3008
|
+
h.onStarted?.();
|
|
3009
|
+
if (!turnId) return { sent: true, end: "failed", turnId: "", text: "", error: "no turn id" };
|
|
3010
|
+
if (lost()) return { sent: true, end: "disconnected", turnId, text: "" };
|
|
3011
|
+
const out = await this.#trackTurn(tid, turnId);
|
|
3012
|
+
return { ...out, ...merged ? { merged } : {} };
|
|
3013
|
+
}
|
|
3014
|
+
#trackTurn(threadId, turnId) {
|
|
3015
|
+
return new Promise((resolve5) => {
|
|
3016
|
+
const limit = this.#d.turnLimitMs ?? TURN_LIMIT_MS;
|
|
3017
|
+
const step = this.#d.turnTickMs ?? 1e3;
|
|
3018
|
+
const t = {
|
|
3019
|
+
threadId,
|
|
3020
|
+
turnId,
|
|
3021
|
+
items: [],
|
|
3022
|
+
used: 0,
|
|
3023
|
+
timer: null,
|
|
3024
|
+
grace: null,
|
|
3025
|
+
done: (end, text, error) => resolve5({ sent: true, end, turnId, text, ...error ? { error } : {} })
|
|
3026
|
+
};
|
|
3027
|
+
this.#turns.set(turnId, t);
|
|
3028
|
+
const early = this.#early.get(turnId);
|
|
3029
|
+
if (early) {
|
|
3030
|
+
this.#early.delete(turnId);
|
|
3031
|
+
const items = Array.isArray(early.items) ? early.items : [];
|
|
3032
|
+
const st = early.status;
|
|
3033
|
+
const err = early.error?.message;
|
|
3034
|
+
this.#finishTurn(t, st === "interrupted" ? "interrupted" : st === "failed" ? "failed" : "completed", finalReply(items), typeof err === "string" ? err : void 0);
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
t.timer = setInterval(() => {
|
|
3038
|
+
if (this.#waitingApproval(threadId)) return;
|
|
3039
|
+
t.used += step;
|
|
3040
|
+
if (t.used < limit) return;
|
|
3041
|
+
this.#d.log(`Codex \u4F1A\u8BDD\u8FD9\u4E00\u8F6E\u8D85\u8FC7 ${Math.round(limit / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u6253\u65AD`);
|
|
3042
|
+
void this.#request("turn/interrupt", { threadId, turnId });
|
|
3043
|
+
this.#finishTurn(t, "timeout");
|
|
3044
|
+
}, step);
|
|
3045
|
+
t.timer.unref?.();
|
|
3046
|
+
if (this.#status.get(threadId) !== "active") this.#armIdleGrace(t);
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
#waitingApproval(threadId) {
|
|
3050
|
+
for (const p of this.#pending.values()) if (p.threadId === threadId) return true;
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
/** 会话已空闲却等不到 turn/completed(丢了一条通知):过一会儿按已完成收尾 */
|
|
3054
|
+
#armIdleGrace(t) {
|
|
3055
|
+
if (t.grace) return;
|
|
3056
|
+
t.grace = setTimeout(() => {
|
|
3057
|
+
t.grace = null;
|
|
3058
|
+
if (!this.#turns.has(t.turnId) || this.#status.get(t.threadId) === "active") return;
|
|
3059
|
+
this.#d.log("Codex \u4F1A\u8BDD\u5DF2\u7A7A\u95F2\u4F46\u6CA1\u6536\u5230 turn/completed\uFF0C\u6309\u5DF2\u5B8C\u6210\u6536\u5C3E");
|
|
3060
|
+
this.#finishTurn(t, "completed", finalReply(t.items));
|
|
3061
|
+
}, IDLE_GRACE_MS);
|
|
3062
|
+
t.grace.unref?.();
|
|
3063
|
+
}
|
|
3064
|
+
#finishTurn(t, end, text = "", error) {
|
|
3065
|
+
if (this.#turns.get(t.turnId) !== t) return;
|
|
3066
|
+
this.#turns.delete(t.turnId);
|
|
3067
|
+
if (t.timer) clearInterval(t.timer);
|
|
3068
|
+
if (t.grace) clearTimeout(t.grace);
|
|
3069
|
+
t.done(end, text || (end === "completed" ? "" : finalReply(t.items)), error);
|
|
3070
|
+
}
|
|
3071
|
+
#waitThread(threadId, ms) {
|
|
3072
|
+
return new Promise((resolve5) => {
|
|
3073
|
+
let set = this.#wake.get(threadId);
|
|
3074
|
+
if (!set) this.#wake.set(threadId, set = /* @__PURE__ */ new Set());
|
|
3075
|
+
const s = set;
|
|
3076
|
+
const fire = () => {
|
|
3077
|
+
clearTimeout(timer);
|
|
3078
|
+
s.delete(fire);
|
|
3079
|
+
if (s.size === 0 && this.#wake.get(threadId) === s) this.#wake.delete(threadId);
|
|
3080
|
+
resolve5();
|
|
3081
|
+
};
|
|
3082
|
+
const timer = setTimeout(fire, ms);
|
|
3083
|
+
timer.unref?.();
|
|
3084
|
+
s.add(fire);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
#wakeThread(threadId) {
|
|
3088
|
+
const s = this.#wake.get(threadId);
|
|
3089
|
+
if (s) for (const f of [...s]) f();
|
|
3090
|
+
}
|
|
3091
|
+
#wakeAll() {
|
|
3092
|
+
for (const id of [...this.#wake.keys()]) this.#wakeThread(id);
|
|
3093
|
+
}
|
|
2420
3094
|
#onMessage(raw) {
|
|
2421
3095
|
let m;
|
|
2422
3096
|
try {
|
|
@@ -2447,29 +3121,64 @@ var CodexWatch = class {
|
|
|
2447
3121
|
case "thread/started": {
|
|
2448
3122
|
const t = p.thread;
|
|
2449
3123
|
if (t && typeof t.id === "string" && typeof t.cwd === "string") this.#cwd.set(t.id, t.cwd);
|
|
3124
|
+
if (t && typeof t.id === "string") this.#setLoaded(t.id, true);
|
|
2450
3125
|
return;
|
|
2451
3126
|
}
|
|
2452
3127
|
case "thread/status/changed": {
|
|
2453
3128
|
const id = typeof p.threadId === "string" ? p.threadId : "";
|
|
2454
3129
|
const type = p.status?.type;
|
|
2455
3130
|
if (!id) return;
|
|
3131
|
+
if (typeof type === "string") this.#status.set(id, type);
|
|
3132
|
+
if (type === "notLoaded") this.#setLoaded(id, false);
|
|
3133
|
+
else if (typeof type === "string") this.#setLoaded(id, true);
|
|
2456
3134
|
if (type === "active") {
|
|
2457
3135
|
this.#idle.delete(id);
|
|
2458
3136
|
this.#subscribe(id);
|
|
2459
3137
|
} else {
|
|
2460
3138
|
this.#idle.add(id);
|
|
2461
3139
|
this.#unsubscribe(id);
|
|
3140
|
+
for (const t of this.#turns.values()) if (t.threadId === id) this.#armIdleGrace(t);
|
|
2462
3141
|
}
|
|
3142
|
+
this.#wakeThread(id);
|
|
2463
3143
|
return;
|
|
2464
3144
|
}
|
|
2465
3145
|
case "thread/closed":
|
|
2466
3146
|
if (typeof p.threadId === "string") {
|
|
2467
|
-
|
|
2468
|
-
this.#
|
|
2469
|
-
this.#
|
|
2470
|
-
this.#
|
|
3147
|
+
const id = p.threadId;
|
|
3148
|
+
this.#idle.delete(id);
|
|
3149
|
+
this.#subscribed.delete(id);
|
|
3150
|
+
this.#retrying.delete(id);
|
|
3151
|
+
this.#cwd.delete(id);
|
|
3152
|
+
this.#status.set(id, "notLoaded");
|
|
3153
|
+
this.#activeTurn.delete(id);
|
|
3154
|
+
this.#setLoaded(id, false);
|
|
3155
|
+
for (const t of [...this.#turns.values()]) if (t.threadId === id) this.#finishTurn(t, "closed");
|
|
3156
|
+
this.#wakeThread(id);
|
|
3157
|
+
}
|
|
3158
|
+
return;
|
|
3159
|
+
case "turn/started": {
|
|
3160
|
+
const turn = p.turn;
|
|
3161
|
+
if (typeof p.threadId === "string" && turn && typeof turn.id === "string") this.#activeTurn.set(p.threadId, turn.id);
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
case "turn/completed": {
|
|
3165
|
+
const turn = p.turn ?? {};
|
|
3166
|
+
const tid = typeof p.threadId === "string" ? p.threadId : "";
|
|
3167
|
+
if (tid && this.#activeTurn.get(tid) === turn.id) this.#activeTurn.delete(tid);
|
|
3168
|
+
const own = typeof turn.id === "string" ? this.#turns.get(turn.id) : void 0;
|
|
3169
|
+
if (own) {
|
|
3170
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
3171
|
+
const text = finalReply(items) || finalReply(own.items);
|
|
3172
|
+
const st = turn.status;
|
|
3173
|
+
const end = st === "interrupted" ? "interrupted" : st === "failed" ? "failed" : "completed";
|
|
3174
|
+
const err = turn.error?.message;
|
|
3175
|
+
this.#finishTurn(own, end, text, typeof err === "string" ? err : void 0);
|
|
3176
|
+
} else if (typeof turn.id === "string") {
|
|
3177
|
+
this.#early.set(turn.id, turn);
|
|
3178
|
+
while (this.#early.size > 16) this.#early.delete(this.#early.keys().next().value);
|
|
2471
3179
|
}
|
|
2472
3180
|
return;
|
|
3181
|
+
}
|
|
2473
3182
|
case "item/started": {
|
|
2474
3183
|
const item = p.item;
|
|
2475
3184
|
if (item && item.type === "fileChange" && typeof item.id === "string" && Array.isArray(item.changes)) {
|
|
@@ -2481,6 +3190,8 @@ var CodexWatch = class {
|
|
|
2481
3190
|
case "item/completed": {
|
|
2482
3191
|
const item = p.item;
|
|
2483
3192
|
if (item && typeof item.id === "string") this.#changes.delete(item.id);
|
|
3193
|
+
const own = typeof p.turnId === "string" ? this.#turns.get(p.turnId) : void 0;
|
|
3194
|
+
if (own && item && item.type === "agentMessage") own.items.push(item);
|
|
2484
3195
|
return;
|
|
2485
3196
|
}
|
|
2486
3197
|
case "serverRequest/resolved": {
|
|
@@ -2517,7 +3228,7 @@ var CodexWatch = class {
|
|
|
2517
3228
|
const ac = new AbortController();
|
|
2518
3229
|
const entry = { ac, threadId };
|
|
2519
3230
|
this.#pending.set(key, entry);
|
|
2520
|
-
const
|
|
3231
|
+
const req2 = {
|
|
2521
3232
|
window: true,
|
|
2522
3233
|
tool: "codex",
|
|
2523
3234
|
hook: { hook_event_name: "PermissionRequest", session_id: threadId, ...hook }
|
|
@@ -2525,7 +3236,7 @@ var CodexWatch = class {
|
|
|
2525
3236
|
const settled = () => {
|
|
2526
3237
|
if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
|
|
2527
3238
|
};
|
|
2528
|
-
void this.#d.gate(
|
|
3239
|
+
void this.#d.gate(req2, ac.signal).then((reply) => {
|
|
2529
3240
|
if (this.#pending.get(key) !== entry) return;
|
|
2530
3241
|
this.#pending.delete(key);
|
|
2531
3242
|
if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
|
|
@@ -2538,64 +3249,376 @@ var CodexWatch = class {
|
|
|
2538
3249
|
}
|
|
2539
3250
|
};
|
|
2540
3251
|
|
|
2541
|
-
// src/
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
var CODEX_WATCH_MS = 5 * 1e3;
|
|
2547
|
-
function defaultCodexAppServer() {
|
|
2548
|
-
const cfg = loadConfig();
|
|
2549
|
-
return cfg.enabledTools.includes("codex") ? discoverAppServer(cfg.codexAppServer) : null;
|
|
2550
|
-
}
|
|
2551
|
-
function opencodeReady() {
|
|
2552
|
-
return isOcPluginInstalled(opencodePluginPath()) && existsSync10(ocPluginModule(installedHookPath()));
|
|
3252
|
+
// src/core/codexRemote.ts
|
|
3253
|
+
import { mkdirSync as mkdirSync12, renameSync as renameSync9, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
|
|
3254
|
+
import { join as join15 } from "path";
|
|
3255
|
+
function codexRemotePath() {
|
|
3256
|
+
return join15(deskbuddyHome(), "codex-remote.json");
|
|
2553
3257
|
}
|
|
2554
|
-
function
|
|
2555
|
-
|
|
3258
|
+
function writeCodexRemote(threads, pid = process.pid, now = Date.now()) {
|
|
3259
|
+
try {
|
|
3260
|
+
mkdirSync12(deskbuddyHome(), { recursive: true });
|
|
3261
|
+
const p = codexRemotePath();
|
|
3262
|
+
writeFileSync11(p + ".tmp", JSON.stringify({ pid, threads: [...threads], at: now }), "utf8");
|
|
3263
|
+
renameSync9(p + ".tmp", p);
|
|
3264
|
+
} catch {
|
|
3265
|
+
}
|
|
2556
3266
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
3267
|
+
function removeCodexRemote() {
|
|
3268
|
+
try {
|
|
3269
|
+
rmSync4(codexRemotePath(), { force: true });
|
|
3270
|
+
} catch {
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// src/wrap/channel.ts
|
|
3275
|
+
import { chmodSync as chmodSync2, rmSync as rmSync5 } from "fs";
|
|
3276
|
+
import { connect, createServer as createServer2 } from "net";
|
|
3277
|
+
var MAX_LINE_BYTES = 64 * 1024;
|
|
3278
|
+
function lines(sock, onLine) {
|
|
3279
|
+
let buf = "";
|
|
3280
|
+
sock.setEncoding("utf8");
|
|
3281
|
+
sock.on("data", (d) => {
|
|
3282
|
+
buf += d;
|
|
3283
|
+
if (buf.length > MAX_LINE_BYTES) {
|
|
3284
|
+
sock.destroy();
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
let i;
|
|
3288
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
3289
|
+
const line = buf.slice(0, i);
|
|
3290
|
+
buf = buf.slice(i + 1);
|
|
3291
|
+
if (line.trim()) onLine(line);
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
function parseRequest(line) {
|
|
3296
|
+
let m;
|
|
3297
|
+
try {
|
|
3298
|
+
m = JSON.parse(line);
|
|
3299
|
+
} catch {
|
|
3300
|
+
return null;
|
|
3301
|
+
}
|
|
3302
|
+
if (!m || typeof m !== "object") return null;
|
|
3303
|
+
const instructionId = typeof m.instructionId === "string" ? m.instructionId : "";
|
|
3304
|
+
const sessionId = typeof m.sessionId === "string" ? m.sessionId : "";
|
|
3305
|
+
if (!isValidId(instructionId) || !isValidId(sessionId)) return null;
|
|
3306
|
+
const text = sanitizeText(typeof m.text === "string" ? m.text : "");
|
|
3307
|
+
if (!text || text.length > MAX_TEXT_LENGTH) return null;
|
|
3308
|
+
const expireAt = typeof m.expireAt === "number" && Number.isFinite(m.expireAt) ? m.expireAt : 0;
|
|
3309
|
+
return { instructionId, sessionId, text, expireAt };
|
|
3310
|
+
}
|
|
3311
|
+
function startChannelServer(path, onRequest, platform = process.platform) {
|
|
3312
|
+
return new Promise((resolve5) => {
|
|
3313
|
+
const server = createServer2((sock) => {
|
|
3314
|
+
let open = true;
|
|
3315
|
+
sock.on("error", () => {
|
|
3316
|
+
});
|
|
3317
|
+
sock.on("close", () => open = false);
|
|
3318
|
+
const reply = (r) => {
|
|
3319
|
+
if (sock.destroyed || !sock.writable) return;
|
|
3320
|
+
try {
|
|
3321
|
+
sock.write(JSON.stringify(r) + "\n");
|
|
3322
|
+
} catch {
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
lines(sock, (line) => {
|
|
3326
|
+
const req2 = parseRequest(line);
|
|
3327
|
+
if (!req2) {
|
|
3328
|
+
reply({ instructionId: "", status: "rejected", reason: "bad_request" });
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
onRequest(req2, reply, () => open);
|
|
3332
|
+
});
|
|
3333
|
+
});
|
|
3334
|
+
if (platform !== "win32") {
|
|
3335
|
+
try {
|
|
3336
|
+
rmSync5(path, { force: true });
|
|
3337
|
+
} catch {
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
const oldMask = platform === "win32" ? null : process.umask(127);
|
|
3341
|
+
const restore = () => {
|
|
3342
|
+
if (oldMask !== null) process.umask(oldMask);
|
|
3343
|
+
};
|
|
3344
|
+
server.once("error", () => {
|
|
3345
|
+
restore();
|
|
3346
|
+
resolve5(null);
|
|
3347
|
+
});
|
|
3348
|
+
server.listen(path, () => {
|
|
3349
|
+
restore();
|
|
3350
|
+
if (platform !== "win32") {
|
|
3351
|
+
try {
|
|
3352
|
+
chmodSync2(path, 384);
|
|
3353
|
+
} catch {
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
server.on("error", () => {
|
|
3357
|
+
});
|
|
3358
|
+
resolve5(server);
|
|
3359
|
+
});
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
function closeChannelServer(server, path, platform = process.platform) {
|
|
3363
|
+
try {
|
|
3364
|
+
server?.close();
|
|
3365
|
+
} catch {
|
|
3366
|
+
}
|
|
3367
|
+
if (platform !== "win32") {
|
|
3368
|
+
try {
|
|
3369
|
+
rmSync5(path, { force: true });
|
|
3370
|
+
} catch {
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
var FINAL = /* @__PURE__ */ new Set(["started", "rejected", "failed", "not_here"]);
|
|
3375
|
+
function sendToWindow(path, req2, onReply, opts = {}) {
|
|
3376
|
+
return new Promise((resolve5) => {
|
|
3377
|
+
let settled = false;
|
|
3378
|
+
let handled = false;
|
|
3379
|
+
let done = false;
|
|
3380
|
+
const settle = (v) => {
|
|
3381
|
+
if (settled) return;
|
|
3382
|
+
settled = true;
|
|
3383
|
+
handled = v === "handled";
|
|
3384
|
+
clearTimeout(timer);
|
|
3385
|
+
resolve5(v);
|
|
3386
|
+
};
|
|
3387
|
+
let sock;
|
|
3388
|
+
try {
|
|
3389
|
+
sock = connect(path);
|
|
3390
|
+
} catch {
|
|
3391
|
+
resolve5("fallback");
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const timer = setTimeout(() => {
|
|
3395
|
+
settle("fallback");
|
|
3396
|
+
sock.destroy();
|
|
3397
|
+
}, opts.ackTimeoutMs ?? 3e3);
|
|
3398
|
+
sock.on("error", () => {
|
|
3399
|
+
if (!settled) settle("fallback");
|
|
3400
|
+
});
|
|
3401
|
+
sock.on("close", () => {
|
|
3402
|
+
if (!settled) {
|
|
3403
|
+
settle("fallback");
|
|
3404
|
+
return;
|
|
3405
|
+
}
|
|
3406
|
+
if (handled && !done) {
|
|
3407
|
+
done = true;
|
|
3408
|
+
onReply({ instructionId: req2.instructionId, status: "failed", mode: "window", reason: "window_failed" });
|
|
3409
|
+
}
|
|
3410
|
+
});
|
|
3411
|
+
sock.on("connect", () => {
|
|
3412
|
+
sock.write(JSON.stringify(req2) + "\n");
|
|
3413
|
+
});
|
|
3414
|
+
lines(sock, (line) => {
|
|
3415
|
+
let r;
|
|
3416
|
+
try {
|
|
3417
|
+
r = JSON.parse(line);
|
|
3418
|
+
} catch {
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
if (r.instructionId !== req2.instructionId) return;
|
|
3422
|
+
if (!settled) {
|
|
3423
|
+
if (r.status === "ack") {
|
|
3424
|
+
settle("handled");
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
settle("fallback");
|
|
3428
|
+
sock.destroy();
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
if (done) return;
|
|
3432
|
+
if (FINAL.has(r.status)) {
|
|
3433
|
+
done = true;
|
|
3434
|
+
onReply(r);
|
|
3435
|
+
sock.end();
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
onReply(r);
|
|
3439
|
+
});
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
// src/core/wrapWindows.ts
|
|
3444
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync13, readdirSync as readdirSync2, readFileSync as readFileSync12, renameSync as renameSync10, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync12 } from "fs";
|
|
3445
|
+
import { tmpdir } from "os";
|
|
3446
|
+
import { join as join16 } from "path";
|
|
3447
|
+
var WINDOW_ID_RE = /^[a-f0-9]{32}$/;
|
|
3448
|
+
function windowsDir() {
|
|
3449
|
+
return join16(deskbuddyHome(), "windows");
|
|
3450
|
+
}
|
|
3451
|
+
function bySessionDir() {
|
|
3452
|
+
return join16(windowsDir(), "by-session");
|
|
3453
|
+
}
|
|
3454
|
+
function safeSid(sessionId) {
|
|
3455
|
+
return String(sessionId).replace(/[^A-Za-z0-9_-]/g, "");
|
|
3456
|
+
}
|
|
3457
|
+
function ensureDir(dir) {
|
|
3458
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
3459
|
+
}
|
|
3460
|
+
function atomicWrite(file, text) {
|
|
3461
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
3462
|
+
writeFileSync12(tmp, text, { encoding: "utf8", mode: 384 });
|
|
3463
|
+
renameSync10(tmp, file);
|
|
3464
|
+
}
|
|
3465
|
+
function windowFile(windowId) {
|
|
3466
|
+
return join16(windowsDir(), `${windowId}.json`);
|
|
3467
|
+
}
|
|
3468
|
+
function writeWindow(windowId, rec) {
|
|
3469
|
+
if (!WINDOW_ID_RE.test(windowId)) return;
|
|
3470
|
+
ensureDir(windowsDir());
|
|
3471
|
+
atomicWrite(windowFile(windowId), JSON.stringify(rec));
|
|
3472
|
+
}
|
|
3473
|
+
function readWindow(windowId) {
|
|
3474
|
+
if (!WINDOW_ID_RE.test(windowId)) return null;
|
|
3475
|
+
try {
|
|
3476
|
+
const r = JSON.parse(readFileSync12(windowFile(windowId), "utf8"));
|
|
3477
|
+
return r && typeof r.pid === "number" ? r : null;
|
|
3478
|
+
} catch {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
function removeWindow(windowId) {
|
|
3483
|
+
if (!WINDOW_ID_RE.test(windowId)) return;
|
|
3484
|
+
for (const f of [windowFile(windowId), lastEventFile(windowId)]) {
|
|
3485
|
+
try {
|
|
3486
|
+
rmSync6(f, { force: true });
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
let names = [];
|
|
3491
|
+
try {
|
|
3492
|
+
names = readdirSync2(bySessionDir());
|
|
3493
|
+
} catch {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
for (const n of names) {
|
|
3497
|
+
const f = join16(bySessionDir(), n);
|
|
3498
|
+
try {
|
|
3499
|
+
if (readFileSync12(f, "utf8").trim() === windowId) rmSync6(f, { force: true });
|
|
3500
|
+
} catch {
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
function readBySession(sessionId) {
|
|
3505
|
+
const safe = safeSid(sessionId);
|
|
3506
|
+
if (!safe) return null;
|
|
3507
|
+
try {
|
|
3508
|
+
const id = readFileSync12(join16(bySessionDir(), safe), "utf8").trim();
|
|
3509
|
+
return WINDOW_ID_RE.test(id) ? id : null;
|
|
3510
|
+
} catch {
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function lastEventFile(windowId) {
|
|
3515
|
+
return join16(windowsDir(), `${windowId}.last`);
|
|
3516
|
+
}
|
|
3517
|
+
function readLastEvent(windowId) {
|
|
3518
|
+
if (!WINDOW_ID_RE.test(windowId)) return null;
|
|
3519
|
+
try {
|
|
3520
|
+
const e = JSON.parse(readFileSync12(lastEventFile(windowId), "utf8"));
|
|
3521
|
+
return e && typeof e.ev === "string" && typeof e.at === "number" ? { ev: e.ev, sid: String(e.sid ?? ""), at: e.at } : null;
|
|
3522
|
+
} catch {
|
|
3523
|
+
return null;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function channelPath(windowId, platform = process.platform, home = deskbuddyHome()) {
|
|
3527
|
+
if (platform === "win32") return `\\\\.\\pipe\\deskbuddy-${windowId}`;
|
|
3528
|
+
const name = `${windowId.slice(0, 16)}.sock`;
|
|
3529
|
+
const primary = join16(home, "win", name);
|
|
3530
|
+
if (Buffer.byteLength(primary) <= 103) return primary;
|
|
3531
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
3532
|
+
return join16(tmpdir(), `deskbuddy-${uid}`, name);
|
|
3533
|
+
}
|
|
3534
|
+
function ensureChannelDir(sockPath) {
|
|
3535
|
+
const dir = join16(sockPath, "..");
|
|
3536
|
+
try {
|
|
3537
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
3538
|
+
const st = statSync3(dir);
|
|
3539
|
+
if (typeof process.getuid === "function" && st.uid !== process.getuid()) return false;
|
|
3540
|
+
chmodSync3(dir, 448);
|
|
3541
|
+
return true;
|
|
3542
|
+
} catch {
|
|
3543
|
+
return false;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
// src/daemon/window.ts
|
|
3548
|
+
function windowForSession(sessionId) {
|
|
3549
|
+
const id = readBySession(sessionId);
|
|
3550
|
+
if (!id) return null;
|
|
3551
|
+
const rec = readWindow(id);
|
|
3552
|
+
if (!rec || !isAlive(rec.pid)) return null;
|
|
3553
|
+
return id;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/daemon/index.ts
|
|
3557
|
+
var IDLE_TIMEOUT_MS = 150 * 1e3;
|
|
3558
|
+
var POLICY_VIOLATION = 1008;
|
|
3559
|
+
var PING_FRAME_SERVICE = "ping";
|
|
3560
|
+
var PONG_FRAME = JSON.stringify({ service: "pong", version: "1.0" });
|
|
3561
|
+
var CODEX_REMOTE_REFRESH_MS = 30 * 1e3;
|
|
3562
|
+
var CODEX_WATCH_MS = 5 * 1e3;
|
|
3563
|
+
function defaultCodexAppServers() {
|
|
3564
|
+
const cfg = loadConfig();
|
|
3565
|
+
return cfg.enabledTools.includes("codex") ? discoverAppServers(cfg.codexAppServer) : [];
|
|
3566
|
+
}
|
|
3567
|
+
function opencodeReady() {
|
|
3568
|
+
return isOcPluginInstalled(opencodePluginPath()) && existsSync10(ocPluginModule(installedHookPath()));
|
|
3569
|
+
}
|
|
3570
|
+
function orphanResumeText(summary2) {
|
|
3571
|
+
return `\uFF08\u4F60\u5728\u8BBE\u5907\u4E0A\u5DF2\u6279\u51C6\uFF1A${summary2}\uFF09\u8BF7\u7EE7\u7EED\u521A\u624D\u88AB\u4E2D\u65AD\u7684\u64CD\u4F5C\u3002`;
|
|
3572
|
+
}
|
|
3573
|
+
var ALIVE_REPORT_MS = 60 * 1e3;
|
|
3574
|
+
var Daemon = class {
|
|
3575
|
+
#queue = new SessionQueue();
|
|
3576
|
+
#seen = SeenIds.load();
|
|
3577
|
+
#deps;
|
|
3578
|
+
#store = ApprovalStore.load();
|
|
3579
|
+
#coord;
|
|
3580
|
+
#gate;
|
|
3581
|
+
/** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
|
|
3582
|
+
#runsDone = /* @__PURE__ */ new Map();
|
|
3583
|
+
#attempt = 0;
|
|
3584
|
+
#stopped = false;
|
|
3585
|
+
#ws = null;
|
|
3586
|
+
#idleTimer = null;
|
|
3587
|
+
#windowScan = null;
|
|
3588
|
+
#aliveReport = null;
|
|
3589
|
+
#codexTimer = null;
|
|
3590
|
+
/** url → 连着它的 CodexWatch。插入顺序 = 发现顺序(DeskBuddy 管的在前) */
|
|
3591
|
+
#codex = /* @__PURE__ */ new Map();
|
|
3592
|
+
#codexStarted = false;
|
|
3593
|
+
#codexReportTimer = null;
|
|
3594
|
+
constructor(deps = {}) {
|
|
3595
|
+
this.#deps = {
|
|
3596
|
+
connect: deps.connect ?? ((url) => new WebSocket2(url)),
|
|
3597
|
+
now: deps.now ?? (() => Date.now()),
|
|
3598
|
+
backend: deps.backend ?? serverBackend(),
|
|
3599
|
+
gateScript: deps.gateScript ?? defaultGateScript(),
|
|
3600
|
+
// 🔴 优先用 PATH 上那个稳定的 node(如 /opt/homebrew/bin/node 这种链接),不用 process.execPath:
|
|
3601
|
+
// 后者是 Cellar 里的真实路径,`brew upgrade node` 之后就被删了,而 daemon 还活着、
|
|
3602
|
+
// hook 命令里写的还是旧路径 —— hook 起不来,Qoder 在 bypass 下就是全放行(评审 R3)
|
|
3603
|
+
nodePath: deps.nodePath ?? whichIn("node", defaultLocateDeps()) ?? process.execPath,
|
|
3604
|
+
materialize: deps.materialize ?? ((plan, tool) => materialize(plan, tool)),
|
|
3605
|
+
opencodeReady: deps.opencodeReady ?? opencodeReady,
|
|
3606
|
+
codexAppServers: deps.codexAppServers ?? defaultCodexAppServers,
|
|
3607
|
+
codexRoute: deps.codexRoute ?? ((tid) => this.#codexRouteFor(tid)),
|
|
3608
|
+
report: deps.report ?? ((p) => api.commandResult(p)),
|
|
3609
|
+
windowFor: deps.windowFor ?? windowForSession,
|
|
3610
|
+
sendToWindow: deps.sendToWindow ?? ((id, req2, onReply) => sendToWindow(channelPath(id), req2, onReply))
|
|
3611
|
+
};
|
|
3612
|
+
this.#store.prune(this.#deps.now());
|
|
3613
|
+
this.#coord = new ApprovalCoordinator({
|
|
3614
|
+
store: this.#store,
|
|
3615
|
+
backend: this.#deps.backend,
|
|
3616
|
+
policy: () => loadConfig().approvalPolicy ?? "commands",
|
|
3617
|
+
resume: (r, kind) => this.#scheduleResume(r, kind),
|
|
3618
|
+
log,
|
|
3619
|
+
now: this.#deps.now
|
|
3620
|
+
});
|
|
3621
|
+
this.#gate = new GateServer((req2, signal) => this.#coord.onGate(req2, signal));
|
|
2599
3622
|
}
|
|
2600
3623
|
/** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
|
|
2601
3624
|
async startGate() {
|
|
@@ -2605,17 +3628,82 @@ var Daemon = class {
|
|
|
2605
3628
|
this.#windowScan.unref();
|
|
2606
3629
|
this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
|
|
2607
3630
|
this.#aliveReport.unref();
|
|
2608
|
-
this.#
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
discover: () => this.#deps.codexAppServer(),
|
|
2612
|
-
enabled: () => loadConfig().enabledTools.includes("codex")
|
|
2613
|
-
});
|
|
2614
|
-
this.#codex.tick();
|
|
2615
|
-
this.#codexTimer = setInterval(() => this.#codex?.tick(), CODEX_WATCH_MS);
|
|
3631
|
+
this.#codexStarted = true;
|
|
3632
|
+
this.#codexTick();
|
|
3633
|
+
this.#codexTimer = setInterval(() => this.#codexTick(), CODEX_WATCH_MS);
|
|
2616
3634
|
this.#codexTimer.unref();
|
|
2617
3635
|
return port;
|
|
2618
3636
|
}
|
|
3637
|
+
/** 找 app-server:新出现的连上,不在了且已断开的丢掉;连着的各自 tick */
|
|
3638
|
+
#codexTick() {
|
|
3639
|
+
let urls = [];
|
|
3640
|
+
try {
|
|
3641
|
+
urls = this.#deps.codexAppServers();
|
|
3642
|
+
} catch {
|
|
3643
|
+
urls = [];
|
|
3644
|
+
}
|
|
3645
|
+
for (const url of urls) {
|
|
3646
|
+
if (this.#codex.has(url)) continue;
|
|
3647
|
+
this.#codex.set(
|
|
3648
|
+
url,
|
|
3649
|
+
new CodexWatch({
|
|
3650
|
+
gate: (req2, signal) => this.#coord.onGate(req2, signal),
|
|
3651
|
+
log,
|
|
3652
|
+
discover: () => url,
|
|
3653
|
+
enabled: () => loadConfig().enabledTools.includes("codex"),
|
|
3654
|
+
onLoadedChange: () => this.#onCodexLoaded()
|
|
3655
|
+
})
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
for (const [url, w] of [...this.#codex]) {
|
|
3659
|
+
if (!urls.includes(url) && !w.connected) {
|
|
3660
|
+
w.stop();
|
|
3661
|
+
this.#codex.delete(url);
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
w.tick();
|
|
3665
|
+
}
|
|
3666
|
+
if (this.#deps.now() - this.#codexRemoteAt >= CODEX_REMOTE_REFRESH_MS) this.#writeCodexRemote();
|
|
3667
|
+
}
|
|
3668
|
+
#codexRemoteAt = 0;
|
|
3669
|
+
#writeCodexRemote() {
|
|
3670
|
+
this.#codexRemoteAt = this.#deps.now();
|
|
3671
|
+
writeCodexRemote(this.#codexLoaded(), process.pid, this.#codexRemoteAt);
|
|
3672
|
+
}
|
|
3673
|
+
/** 哪个 app-server 里加载着这个会话;不止一个 → 'ambiguous'(评审 #6) */
|
|
3674
|
+
#codexRouteFor(threadId) {
|
|
3675
|
+
const hits = [...this.#codex.values()].filter((w) => w.hasLoaded(threadId));
|
|
3676
|
+
if (hits.length > 1) return "ambiguous";
|
|
3677
|
+
return hits[0] ?? null;
|
|
3678
|
+
}
|
|
3679
|
+
#codexLoaded() {
|
|
3680
|
+
const all = /* @__PURE__ */ new Set();
|
|
3681
|
+
for (const w of this.#codex.values()) for (const id of w.loadedThreads()) all.add(id);
|
|
3682
|
+
return all;
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* 已加载的会话变了(切片 B):落清单给 hook 用,并立刻补报一次 codex 快照(remoteWindow 进 / 出),
|
|
3686
|
+
* 否则设备要等下一次 hook 事件或 60 秒保活才知道
|
|
3687
|
+
*/
|
|
3688
|
+
#onCodexLoaded() {
|
|
3689
|
+
if (!this.#codexStarted) return;
|
|
3690
|
+
this.#writeCodexRemote();
|
|
3691
|
+
if (this.#codexReportTimer) return;
|
|
3692
|
+
this.#codexReportTimer = setTimeout(() => {
|
|
3693
|
+
this.#codexReportTimer = null;
|
|
3694
|
+
void this.#reportCodex();
|
|
3695
|
+
}, 300);
|
|
3696
|
+
this.#codexReportTimer.unref?.();
|
|
3697
|
+
}
|
|
3698
|
+
async #reportCodex() {
|
|
3699
|
+
const cfg = loadConfig();
|
|
3700
|
+
if (!cfg.clientId || !cfg.boundAt || !cfg.enabledTools.includes("codex")) return;
|
|
3701
|
+
try {
|
|
3702
|
+
await postReport(buildSnapshot(readAliveStates("codex"), "codex", cfg, this.#deps.now(), this.#codexLoaded()));
|
|
3703
|
+
} catch (e) {
|
|
3704
|
+
log(`\u4E0A\u62A5 codex \u5FEB\u7167\u5931\u8D25\uFF1A${e.message}`);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
2619
3707
|
#lastDirs = "";
|
|
2620
3708
|
#dirsTick = 0;
|
|
2621
3709
|
/**
|
|
@@ -2640,7 +3728,7 @@ var Daemon = class {
|
|
|
2640
3728
|
const cfg = loadConfig();
|
|
2641
3729
|
if (!cfg.clientId || !cfg.boundAt) return;
|
|
2642
3730
|
for (const tool of cfg.enabledTools) {
|
|
2643
|
-
await postReport(buildSnapshot(readAliveStates(tool), tool, cfg, this.#deps.now()));
|
|
3731
|
+
await postReport(buildSnapshot(readAliveStates(tool), tool, cfg, this.#deps.now(), tool === "codex" ? this.#codexLoaded() : null));
|
|
2644
3732
|
}
|
|
2645
3733
|
if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
|
|
2646
3734
|
}
|
|
@@ -2653,7 +3741,10 @@ var Daemon = class {
|
|
|
2653
3741
|
if (this.#windowScan) clearInterval(this.#windowScan);
|
|
2654
3742
|
if (this.#aliveReport) clearInterval(this.#aliveReport);
|
|
2655
3743
|
if (this.#codexTimer) clearInterval(this.#codexTimer);
|
|
2656
|
-
this.#
|
|
3744
|
+
if (this.#codexReportTimer) clearTimeout(this.#codexReportTimer);
|
|
3745
|
+
for (const w of this.#codex.values()) w.stop();
|
|
3746
|
+
this.#codex.clear();
|
|
3747
|
+
if (this.#codexStarted) removeCodexRemote();
|
|
2657
3748
|
this.#ws?.close();
|
|
2658
3749
|
void this.#gate.stop();
|
|
2659
3750
|
}
|
|
@@ -2757,8 +3848,139 @@ var Daemon = class {
|
|
|
2757
3848
|
return;
|
|
2758
3849
|
}
|
|
2759
3850
|
this.#seen.save();
|
|
3851
|
+
if (cmd.sessionId && !isExpired(cmd, this.#deps.now())) {
|
|
3852
|
+
const sid = resolveAlias(cmd.sessionId);
|
|
3853
|
+
let win = null;
|
|
3854
|
+
try {
|
|
3855
|
+
win = this.#deps.windowFor(sid);
|
|
3856
|
+
} catch {
|
|
3857
|
+
win = null;
|
|
3858
|
+
}
|
|
3859
|
+
if (win) {
|
|
3860
|
+
void this.#toWindow(cmd, sid, win);
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3863
|
+
if (cmd.tool === "codex" && !this.#queue.busy(sid)) {
|
|
3864
|
+
let route = null;
|
|
3865
|
+
try {
|
|
3866
|
+
route = this.#deps.codexRoute(sid);
|
|
3867
|
+
} catch {
|
|
3868
|
+
route = null;
|
|
3869
|
+
}
|
|
3870
|
+
if (route === "ambiguous") {
|
|
3871
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u4F1A\u8BDD\u540C\u65F6\u5F00\u5728\u591A\u4E2A\u7A97\u53E3\u91CC\uFF0C\u4E0D\u77E5\u9053\u53D1\u7ED9\u54EA\u4E2A`);
|
|
3872
|
+
this.#report(cmd, sid, "failed", "window", "window_failed");
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
if (route) {
|
|
3876
|
+
void this.#toCodexWindow(cmd, sid, route);
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
this.#dispatch(cmd);
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* 经 app-server 往 Codex 会话里发一轮(切片 B)。回报语义与 Claude 窗口一致(mode=window):
|
|
3885
|
+
* 排队 queued → 发出 started;排队中过期 rejected/expired。
|
|
3886
|
+
* 🔴 只有确定窗口里没有这个会话(未连上 / 未加载 / 会话已关)才退回后台 exec;
|
|
3887
|
+
* 订阅失败 / turn/start 被拒 / 发之前断线 → failed/window_failed(会话还开在窗口里,后台跑会两个进程抢一个会话);
|
|
3888
|
+
* 发出去之后任何失败 → failed/exec_failed,绝不再后台重跑(会执行两遍)
|
|
3889
|
+
*/
|
|
3890
|
+
async #toCodexWindow(cmd, sessionId, route) {
|
|
3891
|
+
let out;
|
|
3892
|
+
try {
|
|
3893
|
+
out = await route.runTurn(
|
|
3894
|
+
{ threadId: sessionId, text: cmd.text, expireAt: cmd.expireAt },
|
|
3895
|
+
{
|
|
3896
|
+
onQueued: () => {
|
|
3897
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u6392\u961F`);
|
|
3898
|
+
this.#report(cmd, sessionId, "queued", "window");
|
|
3899
|
+
},
|
|
3900
|
+
onStarted: () => {
|
|
3901
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u5728 Codex \u7A97\u53E3\u4F1A\u8BDD\u91CC\u53D1\u51FA len=${cmd.text.length}`);
|
|
3902
|
+
this.#report(cmd, sessionId, "started", "window");
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
);
|
|
3906
|
+
} catch (e) {
|
|
3907
|
+
out = { sent: false, reason: "start_failed" };
|
|
3908
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u7ECF app-server \u53D1\u51FA\u51FA\u9519\uFF1A${e.message}`);
|
|
3909
|
+
}
|
|
3910
|
+
if (!out.sent) {
|
|
3911
|
+
if (out.reason === "expired") {
|
|
3912
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u6392\u961F\u7B49 Codex \u4F1A\u8BDD\u7A7A\u95F2\u65F6\u8FC7\u671F`);
|
|
3913
|
+
this.#report(cmd, sessionId, "rejected", "window", "expired");
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
if (out.reason === "not_connected" || out.reason === "not_loaded" || out.reason === "closed") {
|
|
3917
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u91CC\u5DF2\u6CA1\u6709\u8FD9\u4E2A Codex \u4F1A\u8BDD\uFF08${out.reason}\uFF09\uFF0C\u6539\u8D70\u540E\u53F0`);
|
|
3918
|
+
this.#dispatch(cmd);
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u6CA1\u80FD\u7ECF app-server \u53D1\u51FA\uFF08${out.reason}\uFF09`);
|
|
3922
|
+
this.#report(cmd, sessionId, "failed", "window", "window_failed");
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
if (out.end === "completed") {
|
|
3926
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u7A97\u53E3\u8FD9\u4E00\u8F6E\u5B8C\u6210\uFF0C\u56DE\u590D ${out.text.length} \u5B57`);
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} Codex \u7A97\u53E3\u8FD9\u4E00\u8F6E\u6CA1\u5B8C\u6210\uFF1A${out.end}${out.error ? " " + out.error.slice(0, 120) : ""}`);
|
|
3930
|
+
this.#report(cmd, sessionId, "failed", "window", "exec_failed");
|
|
3931
|
+
}
|
|
3932
|
+
/** 回报一条指令的处理结果。2 秒超时、失败不重试,只记日志(seen 去重命中的不会走到这里) */
|
|
3933
|
+
#report(cmd, sessionId, status, mode, reason = "") {
|
|
3934
|
+
const payload = {
|
|
3935
|
+
instructionId: cmd.instructionId,
|
|
3936
|
+
sessionId,
|
|
3937
|
+
tool: cmd.tool,
|
|
3938
|
+
status,
|
|
3939
|
+
mode,
|
|
3940
|
+
reason: status === "started" || status === "queued" ? "" : reason,
|
|
3941
|
+
at: String(this.#deps.now())
|
|
3942
|
+
};
|
|
3943
|
+
let p;
|
|
3944
|
+
try {
|
|
3945
|
+
p = this.#deps.report(payload);
|
|
3946
|
+
} catch (e) {
|
|
3947
|
+
log(`\u56DE\u62A5\u6307\u4EE4 ${cmd.instructionId} \u7ED3\u679C\u5931\u8D25\uFF1A${e.message}`);
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
p.catch((e) => log(`\u56DE\u62A5\u6307\u4EE4 ${cmd.instructionId} \u7ED3\u679C\u5931\u8D25\uFF1A${e.message}`));
|
|
3951
|
+
}
|
|
3952
|
+
async #toWindow(cmd, sessionId, windowId) {
|
|
3953
|
+
let fellBack = false;
|
|
3954
|
+
const fallback = () => {
|
|
3955
|
+
if (fellBack) return;
|
|
3956
|
+
fellBack = true;
|
|
3957
|
+
this.#dispatch(cmd);
|
|
3958
|
+
};
|
|
3959
|
+
const req2 = { instructionId: cmd.instructionId, sessionId, text: cmd.text, expireAt: cmd.expireAt };
|
|
3960
|
+
let r;
|
|
3961
|
+
try {
|
|
3962
|
+
r = await this.#deps.sendToWindow(windowId, req2, (reply) => {
|
|
3963
|
+
if (reply.status === "not_here") {
|
|
3964
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3\u91CC\u5DF2\u4E0D\u662F\u8FD9\u4E2A\u4F1A\u8BDD\uFF0C\u6539\u8D70\u540E\u53F0`);
|
|
3965
|
+
fallback();
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
if (reply.status === "ack") return;
|
|
3969
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u7A97\u53E3 ${reply.status}${reply.reason ? " " + reply.reason : ""}`);
|
|
3970
|
+
this.#report(cmd, sessionId, reply.status, "window", reply.reason ?? "");
|
|
3971
|
+
});
|
|
3972
|
+
} catch (e) {
|
|
3973
|
+
log(`\u6307\u4EE4 ${cmd.instructionId} \u4EA4\u7ED9\u7A97\u53E3\u51FA\u9519\uFF1A${e.message}`);
|
|
3974
|
+
r = "fallback";
|
|
3975
|
+
}
|
|
3976
|
+
if (r === "fallback") fallback();
|
|
3977
|
+
else log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u4EA4\u7ED9\u7A97\u53E3 len=${cmd.text.length}`);
|
|
3978
|
+
}
|
|
3979
|
+
/** 原来的后台路径:校验 → 定位会话与目录 → 排队执行 */
|
|
3980
|
+
#dispatch(cmd) {
|
|
2760
3981
|
if (isExpired(cmd, this.#deps.now())) {
|
|
2761
3982
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8FC7\u671F\uFF0C\u4E22\u5F03`);
|
|
3983
|
+
this.#report(cmd, cmd.sessionId, "rejected", "", "expired");
|
|
2762
3984
|
return;
|
|
2763
3985
|
}
|
|
2764
3986
|
const target = resolveTarget(cmd, {
|
|
@@ -2772,6 +3994,7 @@ var Daemon = class {
|
|
|
2772
3994
|
});
|
|
2773
3995
|
if (!target.ok) {
|
|
2774
3996
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u65E0\u6CD5\u6267\u884C\uFF1A${failureMessage(target.reason)}`);
|
|
3997
|
+
this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
|
|
2775
3998
|
return;
|
|
2776
3999
|
}
|
|
2777
4000
|
if (target.sessionId) this.#coord.cancelSession(target.sessionId);
|
|
@@ -2782,6 +4005,7 @@ var Daemon = class {
|
|
|
2782
4005
|
const fresh = readState(sessionId);
|
|
2783
4006
|
if (!fresh) {
|
|
2784
4007
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u4F1A\u8BDD ${sessionId} \u5728\u6267\u884C\u524D\u5DF2\u6D88\u5931`);
|
|
4008
|
+
this.#report(cmd, sessionId, "rejected", "", "session_gone");
|
|
2785
4009
|
return;
|
|
2786
4010
|
}
|
|
2787
4011
|
}
|
|
@@ -2789,17 +4013,22 @@ var Daemon = class {
|
|
|
2789
4013
|
const plan = buildSpawnPlan({ ...cmd, sessionId }, cwd, this.#gateTarget(runId));
|
|
2790
4014
|
if (!plan) {
|
|
2791
4015
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u4E0D\u652F\u6301\u8FDC\u7A0B\u6267\u884C ${cmd.tool}`);
|
|
4016
|
+
this.#report(cmd, sessionId, "rejected", "", "tool_unsupported");
|
|
2792
4017
|
return;
|
|
2793
4018
|
}
|
|
2794
4019
|
log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename6(cwd)} len=${cmd.text.length}`);
|
|
2795
|
-
const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd);
|
|
2796
|
-
if (
|
|
2797
|
-
log(
|
|
4020
|
+
const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd, () => this.#report(cmd, sessionId, "started", "headless"));
|
|
4021
|
+
if ("fail" in result) {
|
|
4022
|
+
log(
|
|
4023
|
+
result.fail === "gate_unavailable" ? `\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528` : `\u6307\u4EE4 ${cmd.instructionId} \u653E\u5F03\uFF1A\u672C\u673A\u627E\u4E0D\u5230 ${cmd.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`
|
|
4024
|
+
);
|
|
4025
|
+
this.#report(cmd, sessionId, "failed", "", result.fail);
|
|
2798
4026
|
return;
|
|
2799
4027
|
}
|
|
2800
4028
|
if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
|
|
2801
4029
|
if (!result.ok) {
|
|
2802
4030
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
|
|
4031
|
+
this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
|
|
2803
4032
|
}
|
|
2804
4033
|
if (!sessionId) {
|
|
2805
4034
|
const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
|
|
@@ -2845,28 +4074,29 @@ var Daemon = class {
|
|
|
2845
4074
|
r.consumed = true;
|
|
2846
4075
|
this.#store.save();
|
|
2847
4076
|
}
|
|
2848
|
-
if (
|
|
4077
|
+
if ("fail" in result) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u653E\u5F03\uFF1A${result.fail === "gate_unavailable" ? "\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528" : `\u672C\u673A\u627E\u4E0D\u5230 ${r.tool} \u7684\u53EF\u6267\u884C\u6587\u4EF6`}`);
|
|
2849
4078
|
else if (isDeferred(result.stdout)) log(`\u4F1A\u8BDD ${r.sessionId} \u53C8\u505C\u5728\u4E86\u4E0B\u4E00\u4E2A\u7B49\u6388\u6743\u7684\u64CD\u4F5C\u4E0A`);
|
|
2850
4079
|
else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
|
|
2851
4080
|
else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
|
|
2852
4081
|
}
|
|
2853
4082
|
/**
|
|
2854
4083
|
* 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
|
|
2855
|
-
*
|
|
4084
|
+
* 起不了(关卡不可用 / 找不到可执行文件)返回 {fail};进程起来那一刻调 onStarted
|
|
2856
4085
|
*/
|
|
2857
|
-
async #run(plan, tool, runId, sessionId, cwd) {
|
|
4086
|
+
async #run(plan, tool, runId, sessionId, cwd, onStarted) {
|
|
2858
4087
|
if (!existsSync10(this.#deps.nodePath) || !existsSync10(this.#deps.gateScript)) {
|
|
2859
4088
|
log(`\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528\uFF08node \u6216 gate.cjs \u627E\u4E0D\u5230\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C ${tool}`);
|
|
2860
|
-
return
|
|
4089
|
+
return { fail: "gate_unavailable" };
|
|
2861
4090
|
}
|
|
2862
4091
|
if (tool === "opencode" && !this.#deps.opencodeReady()) {
|
|
2863
4092
|
log("opencode \u63D2\u4EF6\u4E0D\u5728\uFF08deskbuddy enable opencode \u6CA1\u88C5\u6216\u5DF2\u5378\u8F7D\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C opencode");
|
|
2864
|
-
return
|
|
4093
|
+
return { fail: "gate_unavailable" };
|
|
2865
4094
|
}
|
|
2866
|
-
const
|
|
2867
|
-
if (!
|
|
4095
|
+
const real2 = this.#deps.materialize(plan, tool);
|
|
4096
|
+
if (!real2) return { fail: "binary_not_found" };
|
|
2868
4097
|
const startedAt = this.#deps.now();
|
|
2869
|
-
const rc = startCommand(
|
|
4098
|
+
const rc = startCommand(real2, tool);
|
|
4099
|
+
onStarted?.();
|
|
2870
4100
|
this.#coord.registerRun({
|
|
2871
4101
|
runId,
|
|
2872
4102
|
tool,
|
|
@@ -2907,7 +4137,7 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2907
4137
|
}
|
|
2908
4138
|
if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
|
|
2909
4139
|
try {
|
|
2910
|
-
|
|
4140
|
+
writeFileSync13(pidFilePath(), String(process.pid), "utf8");
|
|
2911
4141
|
} catch {
|
|
2912
4142
|
}
|
|
2913
4143
|
let daemon;
|
|
@@ -2923,7 +4153,7 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2923
4153
|
log(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`);
|
|
2924
4154
|
daemon.stop();
|
|
2925
4155
|
try {
|
|
2926
|
-
|
|
4156
|
+
rmSync7(pidFilePath(), { force: true });
|
|
2927
4157
|
} catch {
|
|
2928
4158
|
}
|
|
2929
4159
|
process.exit(0);
|
|
@@ -2940,8 +4170,8 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2940
4170
|
}
|
|
2941
4171
|
|
|
2942
4172
|
// src/core/registry.ts
|
|
2943
|
-
import { mkdirSync as
|
|
2944
|
-
import { join as
|
|
4173
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "fs";
|
|
4174
|
+
import { join as join17 } from "path";
|
|
2945
4175
|
var FALLBACK = {
|
|
2946
4176
|
version: "builtin",
|
|
2947
4177
|
tools: {
|
|
@@ -3019,11 +4249,11 @@ var FALLBACK = {
|
|
|
3019
4249
|
}
|
|
3020
4250
|
};
|
|
3021
4251
|
function registryCachePath() {
|
|
3022
|
-
return
|
|
4252
|
+
return join17(deskbuddyHome(), "tool-registry.json");
|
|
3023
4253
|
}
|
|
3024
4254
|
function loadRegistry() {
|
|
3025
4255
|
try {
|
|
3026
|
-
return JSON.parse(
|
|
4256
|
+
return JSON.parse(readFileSync13(registryCachePath(), "utf8"));
|
|
3027
4257
|
} catch {
|
|
3028
4258
|
return FALLBACK;
|
|
3029
4259
|
}
|
|
@@ -3031,15 +4261,15 @@ function loadRegistry() {
|
|
|
3031
4261
|
async function refreshRegistry() {
|
|
3032
4262
|
try {
|
|
3033
4263
|
const registry = await api.toolRegistry();
|
|
3034
|
-
|
|
3035
|
-
|
|
4264
|
+
mkdirSync14(deskbuddyHome(), { recursive: true });
|
|
4265
|
+
writeFileSync14(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
|
|
3036
4266
|
} catch {
|
|
3037
4267
|
}
|
|
3038
4268
|
}
|
|
3039
4269
|
|
|
3040
4270
|
// src/adapters/ccHook.ts
|
|
3041
|
-
import { chmodSync as
|
|
3042
|
-
import { dirname as dirname6, join as
|
|
4271
|
+
import { chmodSync as chmodSync4, existsSync as existsSync11, mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync11, statSync as statSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
4272
|
+
import { dirname as dirname6, join as join18 } from "path";
|
|
3043
4273
|
var CC_EVENTS = [
|
|
3044
4274
|
"UserPromptSubmit",
|
|
3045
4275
|
"PreToolUse",
|
|
@@ -3050,7 +4280,7 @@ var CC_EVENTS = [
|
|
|
3050
4280
|
];
|
|
3051
4281
|
function readSettings(path) {
|
|
3052
4282
|
if (!existsSync11(path)) return {};
|
|
3053
|
-
const raw =
|
|
4283
|
+
const raw = readFileSync14(path, "utf8");
|
|
3054
4284
|
if (raw.trim() === "") return {};
|
|
3055
4285
|
try {
|
|
3056
4286
|
return JSON.parse(raw);
|
|
@@ -3059,26 +4289,32 @@ function readSettings(path) {
|
|
|
3059
4289
|
}
|
|
3060
4290
|
}
|
|
3061
4291
|
function writeSettings(path, settings) {
|
|
3062
|
-
|
|
4292
|
+
mkdirSync15(dirname6(path), { recursive: true });
|
|
3063
4293
|
let mode;
|
|
3064
4294
|
try {
|
|
3065
|
-
mode =
|
|
4295
|
+
mode = statSync4(path).mode & 511;
|
|
3066
4296
|
} catch {
|
|
3067
4297
|
mode = void 0;
|
|
3068
4298
|
}
|
|
3069
4299
|
const tmp = path + ".deskbuddy.tmp";
|
|
3070
|
-
|
|
3071
|
-
if (mode !== void 0)
|
|
3072
|
-
|
|
4300
|
+
writeFileSync15(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
4301
|
+
if (mode !== void 0) chmodSync4(tmp, mode);
|
|
4302
|
+
renameSync11(tmp, path);
|
|
3073
4303
|
}
|
|
3074
|
-
function
|
|
4304
|
+
function mentions(command, script) {
|
|
4305
|
+
return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
|
|
4306
|
+
}
|
|
4307
|
+
function scriptArg(script, platform, quote = (s) => s) {
|
|
4308
|
+
return platform === "win32" ? winCmdPath(script) : quote(script);
|
|
4309
|
+
}
|
|
4310
|
+
function installCcHook(settingsPath, hookPath, events = CC_EVENTS, tool = "claude_code", platform = process.platform) {
|
|
3075
4311
|
const settings = readSettings(settingsPath);
|
|
3076
4312
|
settings.hooks ??= {};
|
|
3077
4313
|
for (const event of events) {
|
|
3078
4314
|
const groups = settings.hooks[event] ??= [];
|
|
3079
|
-
const already = groups.some((g) => g.hooks?.some((h) => h.command
|
|
4315
|
+
const already = groups.some((g) => g.hooks?.some((h) => mentions(h.command, hookPath)));
|
|
3080
4316
|
if (already) continue;
|
|
3081
|
-
groups.push({ hooks: [{ type: "command", command: `node ${hookPath} ${tool}` }] });
|
|
4317
|
+
groups.push({ hooks: [{ type: "command", command: `node ${scriptArg(hookPath, platform)} ${tool}` }] });
|
|
3082
4318
|
}
|
|
3083
4319
|
writeSettings(settingsPath, settings);
|
|
3084
4320
|
}
|
|
@@ -3086,26 +4322,26 @@ var WINDOW_GATE_TIMEOUT_S = 7 * 24 * 60 * 60;
|
|
|
3086
4322
|
function shQuote2(s) {
|
|
3087
4323
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3088
4324
|
}
|
|
3089
|
-
function installCcWindowGate(settingsPath, gatePath, tool = "claude_code") {
|
|
4325
|
+
function installCcWindowGate(settingsPath, gatePath, tool = "claude_code", platform = process.platform) {
|
|
3090
4326
|
const settings = readSettings(settingsPath);
|
|
3091
4327
|
settings.hooks ??= {};
|
|
3092
4328
|
const groups = settings.hooks.PermissionRequest ??= [];
|
|
3093
|
-
if (groups.some((g) => g.hooks?.some((h) => h.command
|
|
4329
|
+
if (groups.some((g) => g.hooks?.some((h) => mentions(h.command, gatePath)))) return;
|
|
3094
4330
|
const toolArg = tool === "claude_code" ? "" : ` --tool ${tool}`;
|
|
3095
4331
|
groups.push({
|
|
3096
|
-
hooks: [{ type: "command", command: `node ${
|
|
4332
|
+
hooks: [{ type: "command", command: `node ${scriptArg(gatePath, platform, shQuote2)} --window${toolArg}`, timeout: WINDOW_GATE_TIMEOUT_S }]
|
|
3097
4333
|
});
|
|
3098
4334
|
writeSettings(settingsPath, settings);
|
|
3099
4335
|
}
|
|
3100
4336
|
function uninstallCcHook(settingsPath, hookPath) {
|
|
3101
4337
|
const settings = readSettings(settingsPath);
|
|
3102
4338
|
if (!settings.hooks) return;
|
|
3103
|
-
const gatePath =
|
|
4339
|
+
const gatePath = join18(dirname6(hookPath), "gate.cjs");
|
|
3104
4340
|
for (const event of Object.keys(settings.hooks)) {
|
|
3105
4341
|
const groups = settings.hooks[event] ?? [];
|
|
3106
4342
|
const kept = [];
|
|
3107
4343
|
for (const g of groups) {
|
|
3108
|
-
const hooks = (g.hooks ?? []).filter((h) => !h.command
|
|
4344
|
+
const hooks = (g.hooks ?? []).filter((h) => !mentions(h.command, hookPath) && !mentions(h.command, gatePath));
|
|
3109
4345
|
if (hooks.length > 0) kept.push({ ...g, hooks });
|
|
3110
4346
|
}
|
|
3111
4347
|
settings.hooks[event] = kept;
|
|
@@ -3114,8 +4350,8 @@ function uninstallCcHook(settingsPath, hookPath) {
|
|
|
3114
4350
|
}
|
|
3115
4351
|
|
|
3116
4352
|
// src/adapters/codexHook.ts
|
|
3117
|
-
import { chmodSync as
|
|
3118
|
-
import { dirname as dirname7, join as
|
|
4353
|
+
import { chmodSync as chmodSync5, existsSync as existsSync12, mkdirSync as mkdirSync16, readFileSync as readFileSync15, renameSync as renameSync12, statSync as statSync5, writeFileSync as writeFileSync16 } from "fs";
|
|
4354
|
+
import { dirname as dirname7, join as join19 } from "path";
|
|
3119
4355
|
var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
|
|
3120
4356
|
var CODEX_MARKER_END = "# <<< deskbuddy <<<";
|
|
3121
4357
|
var CODEX_EVENTS = [
|
|
@@ -3128,13 +4364,13 @@ var CODEX_EVENTS = [
|
|
|
3128
4364
|
];
|
|
3129
4365
|
var CODEX_TRUST_NOTICE = "\u26A0\uFE0F \u4E0B\u6B21\u542F\u52A8 Codex \u65F6\u4F1A\u8981\u6C42\u4F60\u786E\u8BA4\u4FE1\u4EFB\u8FD9\u4E2A hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F\u2014\u2014\u4E0D\u786E\u8BA4\u7684\u8BDD Codex \u4E0D\u4F1A\u6267\u884C\u5B83\u3002\n Codex \u7ECF `--remote` \u8FDE\u5E38\u9A7B app-server \u4F7F\u7528\u65F6\uFF0C\u7A97\u53E3\u91CC\u7684\u6388\u6743\u6846\u4E5F\u80FD\u5728\u8BBE\u5907\u4E0A\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09";
|
|
3130
4366
|
function codexConfigPath() {
|
|
3131
|
-
return
|
|
4367
|
+
return join19(codexHome(), "config.toml");
|
|
3132
4368
|
}
|
|
3133
|
-
function renderBlock(hookPath, events, tool) {
|
|
3134
|
-
const command = JSON.stringify(`node ${hookPath} ${tool}`);
|
|
3135
|
-
const
|
|
4369
|
+
function renderBlock(hookPath, events, tool, platform) {
|
|
4370
|
+
const command = JSON.stringify(`node ${platform === "win32" ? winPsPath(hookPath) : hookPath} ${tool}`);
|
|
4371
|
+
const lines2 = [CODEX_MARKER_BEGIN];
|
|
3136
4372
|
for (const event of events) {
|
|
3137
|
-
|
|
4373
|
+
lines2.push(
|
|
3138
4374
|
`[[hooks.${event}]]`,
|
|
3139
4375
|
'matcher = ""',
|
|
3140
4376
|
"hooks = [",
|
|
@@ -3143,21 +4379,21 @@ function renderBlock(hookPath, events, tool) {
|
|
|
3143
4379
|
""
|
|
3144
4380
|
);
|
|
3145
4381
|
}
|
|
3146
|
-
|
|
3147
|
-
return
|
|
4382
|
+
lines2.push(CODEX_MARKER_END);
|
|
4383
|
+
return lines2.join("\n");
|
|
3148
4384
|
}
|
|
3149
4385
|
function writeAtomic(path, content) {
|
|
3150
|
-
|
|
4386
|
+
mkdirSync16(dirname7(path), { recursive: true });
|
|
3151
4387
|
let mode;
|
|
3152
4388
|
try {
|
|
3153
|
-
mode =
|
|
4389
|
+
mode = statSync5(path).mode & 511;
|
|
3154
4390
|
} catch {
|
|
3155
4391
|
mode = void 0;
|
|
3156
4392
|
}
|
|
3157
4393
|
const tmp = path + ".deskbuddy.tmp";
|
|
3158
|
-
|
|
3159
|
-
if (mode !== void 0)
|
|
3160
|
-
|
|
4394
|
+
writeFileSync16(tmp, content, "utf8");
|
|
4395
|
+
if (mode !== void 0) chmodSync5(tmp, mode);
|
|
4396
|
+
renameSync12(tmp, path);
|
|
3161
4397
|
}
|
|
3162
4398
|
function assertSafeToAppend(raw, path) {
|
|
3163
4399
|
for (const quote of ['"""', "'''"]) {
|
|
@@ -3166,35 +4402,35 @@ function assertSafeToAppend(raw, path) {
|
|
|
3166
4402
|
}
|
|
3167
4403
|
}
|
|
3168
4404
|
}
|
|
3169
|
-
function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex") {
|
|
3170
|
-
const raw = existsSync12(configPath2) ?
|
|
4405
|
+
function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex", platform = process.platform) {
|
|
4406
|
+
const raw = existsSync12(configPath2) ? readFileSync15(configPath2, "utf8") : "";
|
|
3171
4407
|
if (raw.includes(CODEX_MARKER_BEGIN)) return;
|
|
3172
4408
|
assertSafeToAppend(raw, configPath2);
|
|
3173
|
-
const block = renderBlock(hookPath, events, tool);
|
|
4409
|
+
const block = renderBlock(hookPath, events, tool, platform);
|
|
3174
4410
|
const head = raw === "" ? "" : (raw.endsWith("\n") ? raw : raw + "\n") + "\n";
|
|
3175
4411
|
writeAtomic(configPath2, head + block + "\n");
|
|
3176
4412
|
}
|
|
3177
4413
|
function uninstallCodexHook(configPath2) {
|
|
3178
4414
|
if (!existsSync12(configPath2)) return;
|
|
3179
|
-
const raw =
|
|
3180
|
-
const
|
|
3181
|
-
const begin =
|
|
4415
|
+
const raw = readFileSync15(configPath2, "utf8");
|
|
4416
|
+
const lines2 = raw.split("\n");
|
|
4417
|
+
const begin = lines2.findIndex((l) => l.trim() === CODEX_MARKER_BEGIN);
|
|
3182
4418
|
if (begin === -1) return;
|
|
3183
|
-
const end =
|
|
4419
|
+
const end = lines2.findIndex((l, i) => i > begin && l.trim() === CODEX_MARKER_END);
|
|
3184
4420
|
if (end === -1) {
|
|
3185
4421
|
throw new Error(`${configPath2} \u91CC\u53EA\u627E\u5230\u5F00\u59CB\u6807\u8BB0\u3001\u6CA1\u6709\u7ED3\u675F\u6807\u8BB0\uFF0C\u4E3A\u514D\u8BEF\u5220\u5DF2\u4E2D\u6B62\uFF0C\u8BF7\u624B\u5DE5\u68C0\u67E5`);
|
|
3186
4422
|
}
|
|
3187
|
-
const start = begin > 0 &&
|
|
3188
|
-
|
|
3189
|
-
writeAtomic(configPath2,
|
|
4423
|
+
const start = begin > 0 && lines2[begin - 1].trim() === "" ? begin - 1 : begin;
|
|
4424
|
+
lines2.splice(start, end - start + 1);
|
|
4425
|
+
writeAtomic(configPath2, lines2.join("\n"));
|
|
3190
4426
|
}
|
|
3191
4427
|
|
|
3192
4428
|
// src/commands/tools.ts
|
|
3193
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4429
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
|
|
3194
4430
|
function fileContains(path, needle) {
|
|
3195
4431
|
try {
|
|
3196
4432
|
if (!existsSync13(path)) return false;
|
|
3197
|
-
return
|
|
4433
|
+
return readFileSync16(path, "utf8").includes(needle);
|
|
3198
4434
|
} catch {
|
|
3199
4435
|
return false;
|
|
3200
4436
|
}
|
|
@@ -3209,7 +4445,8 @@ var ADAPTERS = {
|
|
|
3209
4445
|
configPath: (tool) => CC_CONFIG_PATHS[tool]?.() ?? null,
|
|
3210
4446
|
install: (p, hook, events, tool) => installCcHook(p, hook, events, tool),
|
|
3211
4447
|
uninstall: (p, hook) => uninstallCcHook(p, hook),
|
|
3212
|
-
|
|
4448
|
+
// Windows 上装出去的是正斜杠形式;JSON 里的反斜杠还会被转义成两个,原样搜不到
|
|
4449
|
+
isInstalled: (p, hook) => fileContains(p, hook) || fileContains(p, fwdSlash(hook)),
|
|
3213
4450
|
defaultEvents: CC_EVENTS,
|
|
3214
4451
|
// 实测 PermissionRequest 与窗口授权框并行的:Claude Code(S10 §2)、Qoder CN 桌面端(二期 V8)。
|
|
3215
4452
|
// WorkBuddy 桌面端弹授权框不触发 PermissionRequest;Codex 的 hook 会阻塞授权框——都不装
|
|
@@ -3260,91 +4497,427 @@ function subscribedEvents(registry, tool, fallback) {
|
|
|
3260
4497
|
return picked.length > 0 ? picked : fallback;
|
|
3261
4498
|
}
|
|
3262
4499
|
|
|
3263
|
-
// src/commands/
|
|
3264
|
-
|
|
3265
|
-
|
|
4500
|
+
// src/commands/enable.ts
|
|
4501
|
+
import { dirname as dirname9, join as join22 } from "path";
|
|
4502
|
+
|
|
4503
|
+
// src/wrap/shim.ts
|
|
4504
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
4505
|
+
import { chmodSync as chmodSync6, existsSync as existsSync14, mkdirSync as mkdirSync17, readdirSync as readdirSync3, readFileSync as readFileSync17, rmdirSync, rmSync as rmSync8, writeFileSync as writeFileSync17 } from "fs";
|
|
4506
|
+
import { homedir as homedir5 } from "os";
|
|
4507
|
+
import { delimiter as delimiter2, join as join20, resolve as resolve2 } from "path";
|
|
4508
|
+
var SHIM_FALLBACK_EXIT = 213;
|
|
4509
|
+
var WIN_SHIM_ENABLED = false;
|
|
4510
|
+
var SHIM_COMMANDS = { claude_code: "claude", codex: "codex" };
|
|
4511
|
+
var SHIM_MARKER = "DeskBuddy window-wrap shim";
|
|
4512
|
+
var RC_BEGIN = "# >>> deskbuddy >>>";
|
|
4513
|
+
var RC_END = "# <<< deskbuddy <<<";
|
|
4514
|
+
var RC_FILES = [".zshrc", ".bashrc"];
|
|
4515
|
+
var WIN_PENDING = "Windows \u7A97\u53E3\u63A5\u7BA1\u5F85\u9A8C\u8BC1\uFF0C\u6682\u672A\u5F00\u542F";
|
|
4516
|
+
function shimDir() {
|
|
4517
|
+
return join20(deskbuddyHome(), "bin");
|
|
4518
|
+
}
|
|
4519
|
+
function shimFile(binDir, command, platform = process.platform) {
|
|
4520
|
+
return join20(binDir, platform === "win32" ? `${command}.cmd` : command);
|
|
4521
|
+
}
|
|
4522
|
+
function shQuote3(s) {
|
|
4523
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3266
4524
|
}
|
|
3267
|
-
function
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
4525
|
+
function shStripPath(command) {
|
|
4526
|
+
return [
|
|
4527
|
+
// 🔴 防递归:exec 不换 pid,同一个 pid 第二次走到这里 = 垫片 exec 到了自己(PATH 里还有别的写法指向垫片)
|
|
4528
|
+
'if [ "${DESKBUDDY_SHIM_HOPS:-}" = "$$" ]; then',
|
|
4529
|
+
` echo "deskbuddy: \u627E\u4E0D\u5230\u771F\u6B63\u7684 ${command}\uFF08\u57AB\u7247\u5728\u8C03\u7528\u81EA\u5DF1\uFF09\u3002\u53EF\u8BBE DESKBUDDY_WRAP_OFF=1 \u6216\u68C0\u67E5 PATH" >&2`,
|
|
4530
|
+
" exit 127",
|
|
4531
|
+
"fi",
|
|
4532
|
+
"DESKBUDDY_SHIM_HOPS=$$",
|
|
4533
|
+
"export DESKBUDDY_SHIM_HOPS",
|
|
4534
|
+
"set -f",
|
|
4535
|
+
"DB_P=''",
|
|
4536
|
+
"DB_IFS=$IFS",
|
|
4537
|
+
"IFS=:",
|
|
4538
|
+
"for d in $PATH; do",
|
|
4539
|
+
' case "$d" in "$DB_BIN"|"$DB_BIN/") ;; *) DB_P="${DB_P:+$DB_P:}$d" ;; esac',
|
|
4540
|
+
"done",
|
|
4541
|
+
"IFS=$DB_IFS",
|
|
4542
|
+
"set +f",
|
|
4543
|
+
"PATH=$DB_P",
|
|
4544
|
+
"export PATH"
|
|
4545
|
+
];
|
|
3271
4546
|
}
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
4547
|
+
function shScript(tool, command, nodePath, cliPath, binDir) {
|
|
4548
|
+
return [
|
|
4549
|
+
"#!/bin/sh",
|
|
4550
|
+
`# ${SHIM_MARKER} (${tool}). Generated by deskbuddy; \`deskbuddy disable ${tool}\` removes it.`,
|
|
4551
|
+
"# DeskBuddy \u7A97\u53E3\u63A5\u7BA1\u57AB\u7247\uFF1A\u51FA\u4EFB\u4F55\u95EE\u9898\u53EF\u7528 DESKBUDDY_WRAP_OFF=1 \u4E34\u65F6\u7ED5\u5F00",
|
|
4552
|
+
`DB_NODE=${shQuote3(nodePath)}`,
|
|
4553
|
+
`DB_CLI=${shQuote3(cliPath)}`,
|
|
4554
|
+
`DB_BIN=${shQuote3(binDir)}`,
|
|
4555
|
+
'if [ -z "${DESKBUDDY_WRAPPED:-}" ] && [ "${DESKBUDDY_WRAP_OFF:-}" != "1" ] && [ -x "$DB_NODE" ] && [ -f "$DB_CLI" ]; then',
|
|
4556
|
+
// 不 exec:wrap 找不到真程序时以 213 退出,垫片接着走下面的放行分支(约定见 run.ts SHIM_FALLBACK_EXIT)
|
|
4557
|
+
` DESKBUDDY_SHIM=1 "$DB_NODE" "$DB_CLI" wrap ${tool} "$@"`,
|
|
4558
|
+
" DB_RC=$?",
|
|
4559
|
+
` [ "$DB_RC" -eq ${SHIM_FALLBACK_EXIT} ] || exit "$DB_RC"`,
|
|
4560
|
+
"fi",
|
|
4561
|
+
"# \u653E\u884C\uFF1A\u628A\u672C\u76EE\u5F55\u4ECE PATH \u91CC\u62FF\u6389\uFF0C\u76F4\u63A5\u8DD1\u771F\u7684\u7A0B\u5E8F",
|
|
4562
|
+
...shStripPath(command),
|
|
4563
|
+
`exec ${command} "$@"`,
|
|
4564
|
+
""
|
|
4565
|
+
].join("\n");
|
|
4566
|
+
}
|
|
4567
|
+
function cmdPath(p, env) {
|
|
4568
|
+
let out = p.replace(/%/g, "%%");
|
|
4569
|
+
for (const name of ["LOCALAPPDATA", "APPDATA", "USERPROFILE"]) {
|
|
4570
|
+
const v = env[name];
|
|
4571
|
+
if (v && p.toLowerCase().startsWith(v.toLowerCase() + "\\")) {
|
|
4572
|
+
out = `%${name}%` + p.slice(v.length).replace(/%/g, "%%");
|
|
4573
|
+
break;
|
|
3284
4574
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
4575
|
+
}
|
|
4576
|
+
return /^[\x20-\x7e]*$/.test(out) && !out.includes('"') ? out : null;
|
|
4577
|
+
}
|
|
4578
|
+
function cmdScript(tool, command, node, cli, bin) {
|
|
4579
|
+
return [
|
|
4580
|
+
"@echo off",
|
|
4581
|
+
`rem ${SHIM_MARKER} (${tool}). Generated by deskbuddy; "deskbuddy disable ${tool}" removes it.`,
|
|
4582
|
+
"rem Set DESKBUDDY_WRAP_OFF=1 to bypass.",
|
|
4583
|
+
"setlocal",
|
|
4584
|
+
"if defined DESKBUDDY_WRAPPED goto direct",
|
|
4585
|
+
'if "%DESKBUDDY_WRAP_OFF%"=="1" goto direct',
|
|
4586
|
+
`if not exist "${node}" goto direct`,
|
|
4587
|
+
`if not exist "${cli}" goto direct`,
|
|
4588
|
+
'set "DESKBUDDY_SHIM=1"',
|
|
4589
|
+
`"${node}" "${cli}" wrap ${tool} %*`,
|
|
4590
|
+
`if not "%ERRORLEVEL%"=="${SHIM_FALLBACK_EXIT}" exit /b %ERRORLEVEL%`,
|
|
4591
|
+
'set "DESKBUDDY_SHIM="',
|
|
4592
|
+
":direct",
|
|
4593
|
+
// 从 PATH 里去掉本目录(cmd 的替换不分大小写);相邻重复项一次替换删不净,循环到不再变化。
|
|
4594
|
+
// 延迟展开只开在这一小段:它会吃掉 %* 里的 !
|
|
4595
|
+
"setlocal EnableDelayedExpansion",
|
|
4596
|
+
`set "DB_BIN=${bin}"`,
|
|
4597
|
+
'set "DB_P=;!PATH!;"',
|
|
4598
|
+
":dbstrip",
|
|
4599
|
+
'set "DB_Q=!DB_P:;%DB_BIN%;=;!"',
|
|
4600
|
+
'if not "!DB_Q!"=="!DB_P!" (set "DB_P=!DB_Q!" & goto dbstrip)',
|
|
4601
|
+
'endlocal & set "PATH=%DB_P:~1,-1%"',
|
|
4602
|
+
`${command} %*`,
|
|
4603
|
+
"exit /b %ERRORLEVEL%",
|
|
4604
|
+
""
|
|
4605
|
+
].join("\r\n");
|
|
4606
|
+
}
|
|
4607
|
+
function rcBlock(binDir, rcHome) {
|
|
4608
|
+
const std = resolve2(rcHome, ".deskbuddy", "bin") === resolve2(binDir);
|
|
4609
|
+
const dir = std ? "$HOME/.deskbuddy/bin" : binDir.replace(/(["\\$`])/g, "\\$1");
|
|
4610
|
+
return [
|
|
4611
|
+
RC_BEGIN,
|
|
4612
|
+
"# DeskBuddy \u7A97\u53E3\u63A5\u7BA1\uFF1A\u7EC8\u7AEF\u91CC\u7684 claude \u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u4E0A\u7684\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\u3002",
|
|
4613
|
+
"# `deskbuddy disable claude_code` \u4F1A\u5220\u6389\u8FD9\u4E00\u6BB5\u3002",
|
|
4614
|
+
`export PATH="${dir}:$PATH"`,
|
|
4615
|
+
RC_END
|
|
4616
|
+
].join("\n");
|
|
4617
|
+
}
|
|
4618
|
+
var RC_NL_FLAG = " (+nl)";
|
|
4619
|
+
function locateBlock(content) {
|
|
4620
|
+
const i = content.indexOf(RC_BEGIN);
|
|
4621
|
+
if (i < 0) return null;
|
|
4622
|
+
if (i > 0 && content[i - 1] !== "\n") return null;
|
|
4623
|
+
const k = content.indexOf(RC_END, i);
|
|
4624
|
+
if (k < 0) return null;
|
|
4625
|
+
const lineEnd = content.indexOf("\n", i);
|
|
4626
|
+
const beginLine = content.slice(i, lineEnd < 0 ? void 0 : lineEnd);
|
|
4627
|
+
return { i, j: k + RC_END.length, addedNl: beginLine.endsWith(RC_NL_FLAG) };
|
|
4628
|
+
}
|
|
4629
|
+
function flagged(block) {
|
|
4630
|
+
return block.replace(RC_BEGIN, RC_BEGIN + RC_NL_FLAG);
|
|
4631
|
+
}
|
|
4632
|
+
function addRcBlock(content, block) {
|
|
4633
|
+
const loc = locateBlock(content);
|
|
4634
|
+
if (loc) {
|
|
4635
|
+
const next = content.slice(0, loc.i) + (loc.addedNl ? flagged(block) : block) + content.slice(loc.j);
|
|
4636
|
+
return next === content ? null : next;
|
|
4637
|
+
}
|
|
4638
|
+
if (content === "" || content.endsWith("\n")) return content + block + "\n";
|
|
4639
|
+
return content + "\n" + flagged(block) + "\n";
|
|
4640
|
+
}
|
|
4641
|
+
function removeRcBlock(content) {
|
|
4642
|
+
const loc = locateBlock(content);
|
|
4643
|
+
if (!loc) return null;
|
|
4644
|
+
const start = loc.addedNl && loc.i > 0 && content[loc.i - 1] === "\n" ? loc.i - 1 : loc.i;
|
|
4645
|
+
const end = content[loc.j] === "\n" ? loc.j + 1 : loc.j;
|
|
4646
|
+
return content.slice(0, start) + content.slice(end);
|
|
4647
|
+
}
|
|
4648
|
+
function readRc(p) {
|
|
4649
|
+
return readFileSync17(p).toString("latin1");
|
|
4650
|
+
}
|
|
4651
|
+
function writeRc(p, latin1) {
|
|
4652
|
+
writeFileSync17(p, Buffer.from(latin1, "latin1"));
|
|
4653
|
+
}
|
|
4654
|
+
function toLatin1(utf8) {
|
|
4655
|
+
return Buffer.from(utf8, "utf8").toString("latin1");
|
|
4656
|
+
}
|
|
4657
|
+
function winKey(p) {
|
|
4658
|
+
return p.replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
|
|
4659
|
+
}
|
|
4660
|
+
function addPathEntry(value, dir) {
|
|
4661
|
+
const parts = value.split(";");
|
|
4662
|
+
if (parts.some((p) => winKey(p) === winKey(dir))) return null;
|
|
4663
|
+
return value === "" ? dir : `${dir};${value}`;
|
|
4664
|
+
}
|
|
4665
|
+
function removePathEntry(value, dir) {
|
|
4666
|
+
const parts = value.split(";");
|
|
4667
|
+
const kept = parts.filter((p) => winKey(p) !== winKey(dir));
|
|
4668
|
+
if (kept.length === parts.length) return null;
|
|
4669
|
+
return kept.join(";");
|
|
4670
|
+
}
|
|
4671
|
+
var PS_READ = "$k=[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment');$v='';if($k){$v=$k.GetValue('Path','',[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)};[Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$v)))";
|
|
4672
|
+
var PS_WRITE = "$k=[Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment');$kind=[Microsoft.Win32.RegistryValueKind]::ExpandString;try{$kind=$k.GetValueKind('Path')}catch{};if([string]::IsNullOrEmpty($env:DESKBUDDY_NEW_PATH)){$k.DeleteValue('Path',$false)}else{$k.SetValue('Path',$env:DESKBUDDY_NEW_PATH,$kind)};[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH','1','User');[Environment]::SetEnvironmentVariable('DESKBUDDY_PATH_REFRESH',$null,'User')";
|
|
4673
|
+
function psWinPath() {
|
|
4674
|
+
const ps = (script, env) => execFileSync7("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
|
4675
|
+
encoding: "utf8",
|
|
4676
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4677
|
+
windowsHide: true,
|
|
4678
|
+
timeout: 15e3,
|
|
4679
|
+
env: env ?? process.env
|
|
4680
|
+
});
|
|
4681
|
+
return {
|
|
4682
|
+
read: () => {
|
|
4683
|
+
try {
|
|
4684
|
+
return Buffer.from(ps(PS_READ).trim(), "base64").toString("utf8");
|
|
4685
|
+
} catch {
|
|
4686
|
+
return null;
|
|
4687
|
+
}
|
|
4688
|
+
},
|
|
4689
|
+
write: (v) => {
|
|
4690
|
+
ps(PS_WRITE, { ...process.env, DESKBUDDY_NEW_PATH: v });
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
function defaultShimEnv(nodePath, cliPath) {
|
|
4695
|
+
const env = process.env;
|
|
4696
|
+
const isolated = !!env.VITEST || !!env.DESKBUDDY_HOME;
|
|
4697
|
+
const rcHome = env.DESKBUDDY_RC_HOME || (isolated ? null : homedir5());
|
|
4698
|
+
return {
|
|
4699
|
+
platform: process.platform,
|
|
4700
|
+
binDir: shimDir(),
|
|
4701
|
+
nodePath,
|
|
4702
|
+
cliPath,
|
|
4703
|
+
rcHome,
|
|
4704
|
+
winPath: process.platform === "win32" && !isolated ? psWinPath() : null,
|
|
4705
|
+
env
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
function installShim(tool, e) {
|
|
4709
|
+
const command = SHIM_COMMANDS[tool];
|
|
4710
|
+
if (!command) return { ok: false, message: "" };
|
|
4711
|
+
if (e.platform === "win32" && !WIN_SHIM_ENABLED) return { ok: false, message: WIN_PENDING };
|
|
4712
|
+
if (e.platform === "win32" && tool === "codex") return { ok: false, message: WIN_PENDING };
|
|
4713
|
+
const file = shimFile(e.binDir, command, e.platform);
|
|
4714
|
+
if (e.platform === "win32") {
|
|
4715
|
+
const node = cmdPath(e.nodePath, e.env);
|
|
4716
|
+
const cli = cmdPath(e.cliPath, e.env);
|
|
4717
|
+
const bin = cmdPath(e.binDir, e.env);
|
|
4718
|
+
if (!node || !cli || !bin) {
|
|
4719
|
+
return { ok: false, message: "\u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\uFF1A\u5B89\u88C5\u8DEF\u5F84\u91CC\u6709\u975E ASCII \u5B57\u7B26\uFF0CWindows \u7684 .cmd \u8BFB\u4E0D\u4E86\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09" };
|
|
4720
|
+
}
|
|
4721
|
+
mkdirSync17(e.binDir, { recursive: true });
|
|
4722
|
+
writeFileSync17(file, cmdScript(tool, command, node, cli, bin), "utf8");
|
|
4723
|
+
} else {
|
|
4724
|
+
mkdirSync17(e.binDir, { recursive: true, mode: 448 });
|
|
4725
|
+
writeFileSync17(file, shScript(tool, command, e.nodePath, e.cliPath, e.binDir), { encoding: "utf8", mode: 493 });
|
|
4726
|
+
chmodSync6(file, 493);
|
|
4727
|
+
}
|
|
4728
|
+
const touched = [];
|
|
4729
|
+
if (e.platform === "win32") {
|
|
4730
|
+
if (e.winPath) {
|
|
4731
|
+
const cur = e.winPath.read();
|
|
4732
|
+
if (cur === null) return { ok: false, message: "\u7A97\u53E3\u63A5\u7BA1\uFF1A\u8BFB\u4E0D\u5230\u7528\u6237 PATH\uFF0C\u8BF7\u624B\u52A8\u628A " + e.binDir + " \u52A0\u5230\u7528\u6237 PATH \u6700\u524D\u9762" };
|
|
4733
|
+
const next = addPathEntry(cur, e.binDir);
|
|
4734
|
+
if (next !== null) e.winPath.write(next);
|
|
4735
|
+
touched.push("\u7528\u6237 PATH");
|
|
4736
|
+
}
|
|
4737
|
+
} else if (e.rcHome) {
|
|
4738
|
+
const block = toLatin1(rcBlock(e.binDir, e.rcHome));
|
|
4739
|
+
for (const name of RC_FILES) {
|
|
4740
|
+
const rc = join20(e.rcHome, name);
|
|
4741
|
+
if (!existsSync14(rc)) continue;
|
|
4742
|
+
const next = addRcBlock(readRc(rc), block);
|
|
4743
|
+
if (next !== null) writeRc(rc, next);
|
|
4744
|
+
touched.push(`~/${name}`);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
if (touched.length === 0) {
|
|
4748
|
+
return {
|
|
4749
|
+
ok: true,
|
|
4750
|
+
message: `\u7A97\u53E3\u63A5\u7BA1\uFF1A\u57AB\u7247\u5DF2\u88C5\u5728 ${e.binDir}\uFF0C\u4F46\u6CA1\u6539 shell \u914D\u7F6E\u2014\u2014\u8981\u751F\u6548\u8BF7\u628A\u5B83\u52A0\u5230 PATH \u6700\u524D\u9762`
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
return {
|
|
4754
|
+
ok: true,
|
|
4755
|
+
message: `\u7A97\u53E3\u63A5\u7BA1\u5DF2\u5F00\u542F\uFF08\u5DF2\u6539 ${touched.join("\u3001")}\uFF09\uFF1A\u65B0\u5F00\u7684\u7EC8\u7AEF\u91CC\u6572 ${command} \u5C31\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u5DF2\u5F00\u7740\u7684\u7EC8\u7AEF\u8981\u91CD\u5F00\u624D\u751F\u6548`
|
|
4756
|
+
};
|
|
4757
|
+
}
|
|
4758
|
+
function ourShims(binDir) {
|
|
4759
|
+
let names;
|
|
4760
|
+
try {
|
|
4761
|
+
names = readdirSync3(binDir);
|
|
4762
|
+
} catch {
|
|
4763
|
+
return [];
|
|
4764
|
+
}
|
|
4765
|
+
return names.filter((n) => {
|
|
3287
4766
|
try {
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
out(`\u2717 ${tool}\uFF1A${e.message}`);
|
|
3292
|
-
failed += 1;
|
|
3293
|
-
continue;
|
|
4767
|
+
return readFileSync17(join20(binDir, n), "utf8").includes(SHIM_MARKER);
|
|
4768
|
+
} catch {
|
|
4769
|
+
return false;
|
|
3294
4770
|
}
|
|
3295
|
-
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
function uninstallShim(tool, e) {
|
|
4774
|
+
const command = SHIM_COMMANDS[tool];
|
|
4775
|
+
if (!command) return { ok: true, message: "" };
|
|
4776
|
+
let removed = false;
|
|
4777
|
+
const file = shimFile(e.binDir, command, e.platform);
|
|
4778
|
+
try {
|
|
4779
|
+
if (existsSync14(file) && readFileSync17(file, "utf8").includes(SHIM_MARKER)) {
|
|
4780
|
+
rmSync8(file, { force: true });
|
|
4781
|
+
removed = true;
|
|
4782
|
+
}
|
|
4783
|
+
} catch {
|
|
3296
4784
|
}
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
if (
|
|
3300
|
-
|
|
3301
|
-
|
|
4785
|
+
if (ourShims(e.binDir).length > 0) return { ok: true, message: removed ? `\u5DF2\u5220\u9664\u7A97\u53E3\u63A5\u7BA1\u57AB\u7247 ${file}` : "" };
|
|
4786
|
+
const touched = [];
|
|
4787
|
+
if (e.platform === "win32") {
|
|
4788
|
+
if (e.winPath) {
|
|
4789
|
+
const cur = e.winPath.read();
|
|
4790
|
+
const next = cur === null ? null : removePathEntry(cur, e.binDir);
|
|
4791
|
+
if (next !== null) {
|
|
4792
|
+
e.winPath.write(next);
|
|
4793
|
+
touched.push("\u7528\u6237 PATH");
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
} else if (e.rcHome) {
|
|
4797
|
+
for (const name of RC_FILES) {
|
|
4798
|
+
const rc = join20(e.rcHome, name);
|
|
4799
|
+
if (!existsSync14(rc)) continue;
|
|
4800
|
+
const next = removeRcBlock(readRc(rc));
|
|
4801
|
+
if (next !== null) {
|
|
4802
|
+
writeRc(rc, next);
|
|
4803
|
+
touched.push(`~/${name}`);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
3302
4806
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
out("\u8981\u5F7B\u5E95\u6E05\u9664\u672C\u673A\u6570\u636E\u8BF7\u8FD0\u884C\uFF1Adeskbuddy logout");
|
|
4807
|
+
try {
|
|
4808
|
+
if (existsSync14(e.binDir) && readdirSync3(e.binDir).length === 0) rmdirSync(e.binDir);
|
|
4809
|
+
} catch {
|
|
3307
4810
|
}
|
|
3308
|
-
|
|
4811
|
+
if (!removed && touched.length === 0) return { ok: true, message: "" };
|
|
4812
|
+
return { ok: true, message: `\u7A97\u53E3\u63A5\u7BA1\u5DF2\u5173\u95ED${touched.length ? `\uFF08\u5DF2\u4ECE ${touched.join("\u3001")} \u79FB\u9664\uFF09` : ""}\uFF0C\u65B0\u5F00\u7684\u7EC8\u7AEF\u751F\u6548` };
|
|
4813
|
+
}
|
|
4814
|
+
function shimState(tool, binDir, pathEnv, platform = process.platform) {
|
|
4815
|
+
const command = SHIM_COMMANDS[tool];
|
|
4816
|
+
if (!command) return { installed: false, inPath: false };
|
|
4817
|
+
const file = shimFile(binDir, command, platform);
|
|
4818
|
+
let installed = false;
|
|
4819
|
+
try {
|
|
4820
|
+
installed = readFileSync17(file, "utf8").includes(SHIM_MARKER);
|
|
4821
|
+
} catch {
|
|
4822
|
+
installed = false;
|
|
4823
|
+
}
|
|
4824
|
+
const win = platform === "win32";
|
|
4825
|
+
const key = (p) => win ? winKey(p) : p.replace(/\/+$/, "");
|
|
4826
|
+
const inPath = pathEnv.split(win ? ";" : delimiter2).some((p) => p && key(p) === key(binDir));
|
|
4827
|
+
return { installed, inPath };
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// src/wrap/pty.ts
|
|
4831
|
+
import { chmodSync as chmodSync7, existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
4832
|
+
import { createRequire as createRequire2 } from "module";
|
|
4833
|
+
import { dirname as dirname8, join as join21 } from "path";
|
|
4834
|
+
var req = createRequire2(import.meta.url);
|
|
4835
|
+
function fixSpawnHelper() {
|
|
4836
|
+
if (process.platform !== "darwin") return;
|
|
4837
|
+
try {
|
|
4838
|
+
const dir = join21(dirname8(req.resolve("node-pty/package.json")), "prebuilds", `darwin-${process.arch}`);
|
|
4839
|
+
const helper = join21(dir, "spawn-helper");
|
|
4840
|
+
if (existsSync15(helper) && (statSync6(helper).mode & 73) === 0) chmodSync7(helper, 493);
|
|
4841
|
+
} catch {
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
function loadPty() {
|
|
4845
|
+
try {
|
|
4846
|
+
fixSpawnHelper();
|
|
4847
|
+
const m = req("node-pty");
|
|
4848
|
+
return typeof m?.spawn === "function" ? m : null;
|
|
4849
|
+
} catch {
|
|
4850
|
+
return null;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function loadHeadless() {
|
|
4854
|
+
try {
|
|
4855
|
+
const m = req("@xterm/headless");
|
|
4856
|
+
return typeof m?.Terminal === "function" ? m.Terminal : null;
|
|
4857
|
+
} catch {
|
|
4858
|
+
return null;
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
function screenLines(term) {
|
|
4862
|
+
const b = term.buffer.active;
|
|
4863
|
+
const out = [];
|
|
4864
|
+
for (let i = b.viewportY; i < b.viewportY + term.rows; i++) {
|
|
4865
|
+
const l = b.getLine(i);
|
|
4866
|
+
out.push(l ? l.translateToString(true) : "");
|
|
4867
|
+
}
|
|
4868
|
+
return out;
|
|
3309
4869
|
}
|
|
3310
4870
|
|
|
3311
4871
|
// src/commands/enable.ts
|
|
3312
|
-
|
|
3313
|
-
function stdout2(line) {
|
|
4872
|
+
function stdout(line) {
|
|
3314
4873
|
process.stdout.write(line + "\n");
|
|
3315
4874
|
}
|
|
3316
4875
|
function parseTools(arg) {
|
|
3317
4876
|
return (arg ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
3318
4877
|
}
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
4878
|
+
function defaultShimEnvForInstall() {
|
|
4879
|
+
let node = null;
|
|
4880
|
+
try {
|
|
4881
|
+
node = whichIn("node", defaultLocateDeps());
|
|
4882
|
+
} catch {
|
|
4883
|
+
node = null;
|
|
3324
4884
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
4885
|
+
return defaultShimEnv(node ?? process.execPath, defaultCliPath());
|
|
4886
|
+
}
|
|
4887
|
+
function installShims(tools, out, shim) {
|
|
4888
|
+
if (!shim) return;
|
|
4889
|
+
for (const tool of tools) {
|
|
4890
|
+
if (!SHIM_COMMANDS[tool]) continue;
|
|
4891
|
+
try {
|
|
4892
|
+
const r = installShim(tool, shim);
|
|
4893
|
+
if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
|
|
4894
|
+
if (r.ok && tool === "claude_code" && (!loadPty() || !loadHeadless())) {
|
|
4895
|
+
out("\u26A0\uFE0F node-pty \u6CA1\u88C5\u4E0A\uFF1A\u7A97\u53E3\u63A5\u7BA1\u5148\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C\uFF08claude \u7167\u5E38\u80FD\u7528\uFF0C\u53EA\u662F\u8BBE\u5907\u56DE\u590D\u5199\u4E0D\u8FDB\u7A97\u53E3\uFF09");
|
|
4896
|
+
}
|
|
4897
|
+
} catch (e) {
|
|
4898
|
+
out(`\u26A0\uFE0F \u7A97\u53E3\u63A5\u7BA1\u6CA1\u88C5\u4E0A\uFF1A${e.message}\uFF08\u4E0D\u5F71\u54CD\u5176\u5B83\u529F\u80FD\uFF09`);
|
|
4899
|
+
}
|
|
3328
4900
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
4901
|
+
}
|
|
4902
|
+
function installTools(tools, out, shim = defaultShimEnvForInstall()) {
|
|
3331
4903
|
const registry = loadRegistry();
|
|
3332
4904
|
const hookPath = installedHookPath();
|
|
3333
4905
|
syncHookFile();
|
|
3334
4906
|
const cfg = loadConfig();
|
|
3335
4907
|
const enabled = new Set(cfg.enabledTools);
|
|
3336
|
-
|
|
4908
|
+
const installed = [];
|
|
4909
|
+
const failed = [];
|
|
3337
4910
|
for (const tool of tools) {
|
|
3338
4911
|
const adapter = adapterFor(registry, tool);
|
|
3339
4912
|
if (!adapter) {
|
|
3340
4913
|
out(`\u2717 ${tool}\uFF1A\u6620\u5C04\u8868\u91CC\u6CA1\u6709\u8FD9\u4E2A\u5DE5\u5177\uFF08\u6216\u5B83\u7684 format \u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301\uFF09\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
3341
|
-
failed
|
|
4914
|
+
failed.push({ tool, reason: "\u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301" });
|
|
3342
4915
|
continue;
|
|
3343
4916
|
}
|
|
3344
4917
|
const configPath2 = adapter.configPath(tool);
|
|
3345
4918
|
if (!configPath2) {
|
|
3346
4919
|
out(`\u2717 ${tool}\uFF1A\u672C\u5BA2\u6237\u7AEF\u4E0D\u77E5\u9053\u5B83\u7684\u914D\u7F6E\u6587\u4EF6\u5728\u54EA\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
3347
|
-
failed
|
|
4920
|
+
failed.push({ tool, reason: "\u627E\u4E0D\u5230\u914D\u7F6E\u6587\u4EF6" });
|
|
3348
4921
|
continue;
|
|
3349
4922
|
}
|
|
3350
4923
|
const events = subscribedEvents(registry, tool, adapter.defaultEvents);
|
|
@@ -3352,13 +4925,14 @@ async function runEnable(tools, opts = {}) {
|
|
|
3352
4925
|
adapter.install(configPath2, hookPath, events, tool);
|
|
3353
4926
|
} catch (e) {
|
|
3354
4927
|
out(`\u2717 ${tool}\uFF1A${e.message}`);
|
|
3355
|
-
failed
|
|
4928
|
+
failed.push({ tool, reason: e.message });
|
|
3356
4929
|
continue;
|
|
3357
4930
|
}
|
|
3358
4931
|
enabled.add(tool);
|
|
4932
|
+
installed.push(tool);
|
|
3359
4933
|
out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
|
|
3360
4934
|
try {
|
|
3361
|
-
if (adapter.installWindowGate?.(configPath2,
|
|
4935
|
+
if (adapter.installWindowGate?.(configPath2, join22(dirname9(hookPath), "gate.cjs"), tool)) {
|
|
3362
4936
|
out(` \u7A97\u53E3\u91CC\u5F39\u6388\u6743\u6846\u65F6\uFF0C\u8BBE\u5907\u4E0A\u4E5F\u80FD\u786E\u8BA4\uFF08\u5B88\u62A4\u8FDB\u7A0B\u5728\u8DD1\u65F6\u751F\u6548\uFF09`);
|
|
3363
4937
|
}
|
|
3364
4938
|
} catch (e) {
|
|
@@ -3368,6 +4942,22 @@ async function runEnable(tools, opts = {}) {
|
|
|
3368
4942
|
}
|
|
3369
4943
|
cfg.enabledTools = [...enabled];
|
|
3370
4944
|
saveConfig(cfg);
|
|
4945
|
+
installShims(installed, out, shim);
|
|
4946
|
+
return { installed, failed };
|
|
4947
|
+
}
|
|
4948
|
+
async function runEnable(tools, opts = {}) {
|
|
4949
|
+
const out = opts.out ?? stdout;
|
|
4950
|
+
if (tools.length === 0) {
|
|
4951
|
+
out("\u7528\u6CD5\uFF1Adeskbuddy enable <tool[,tool]>\uFF0C\u4F8B\u5982 deskbuddy enable claude_code,codex");
|
|
4952
|
+
return 1;
|
|
4953
|
+
}
|
|
4954
|
+
if (!loadConfig().boundAt) {
|
|
4955
|
+
out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
|
|
4956
|
+
return 1;
|
|
4957
|
+
}
|
|
4958
|
+
ensureClientId();
|
|
4959
|
+
if (!opts.skipRefresh) await refreshRegistry();
|
|
4960
|
+
const { failed } = installTools(tools, out, opts.shim === void 0 ? defaultShimEnvForInstall() : opts.shim);
|
|
3371
4961
|
if (!opts.skipAutostart) {
|
|
3372
4962
|
const r = await installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() });
|
|
3373
4963
|
out("");
|
|
@@ -3377,69 +4967,132 @@ async function runEnable(tools, opts = {}) {
|
|
|
3377
4967
|
out("\u88C5\u5B8C\u4E86\u3002\u72B6\u6001\u91C7\u96C6\u9760 hook\uFF0C\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF1B");
|
|
3378
4968
|
out("\u5B88\u62A4\u8FDB\u7A0B\u53EA\u5728\u300C\u786C\u4EF6\u4E0A\u7ED9\u8FD9\u53F0\u7535\u8111\u4E0B\u6307\u4EE4\u300D\u65F6\u624D\u7528\u5F97\u7740\uFF0C\u5DF2\u968F\u4E0A\u9762\u4E00\u6B65\u8BBE\u597D\u5F00\u673A\u81EA\u542F\u3002");
|
|
3379
4969
|
out("\u968F\u65F6\u53EF\u4EE5 deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001deskbuddy disable \u5E72\u51C0\u79FB\u9664\u3002");
|
|
3380
|
-
return failed > 0 ? 1 : 0;
|
|
4970
|
+
return failed.length > 0 ? 1 : 0;
|
|
3381
4971
|
}
|
|
3382
4972
|
|
|
3383
|
-
// src/commands/
|
|
3384
|
-
|
|
3385
|
-
import { existsSync as existsSync14, rmSync as rmSync5 } from "fs";
|
|
3386
|
-
function stdout3(line) {
|
|
4973
|
+
// src/commands/disable.ts
|
|
4974
|
+
function stdout2(line) {
|
|
3387
4975
|
process.stdout.write(line + "\n");
|
|
3388
4976
|
}
|
|
3389
|
-
function
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
rl.close();
|
|
3394
|
-
resolve3(/^(y|yes)$/i.test(answer.trim()));
|
|
3395
|
-
});
|
|
3396
|
-
});
|
|
4977
|
+
function toolsToDisable(explicit) {
|
|
4978
|
+
if (explicit.length > 0) return explicit;
|
|
4979
|
+
const known = Object.keys(loadRegistry()?.tools ?? {});
|
|
4980
|
+
return [.../* @__PURE__ */ new Set([...loadConfig().enabledTools, ...known])];
|
|
3397
4981
|
}
|
|
3398
|
-
async function
|
|
3399
|
-
const out = opts.out ??
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
4982
|
+
async function runDisable(tools, opts = {}) {
|
|
4983
|
+
const out = opts.out ?? stdout2;
|
|
4984
|
+
const registry = loadRegistry();
|
|
4985
|
+
const hookPath = installedHookPath();
|
|
4986
|
+
const targets = toolsToDisable(tools);
|
|
4987
|
+
const cfg = loadConfig();
|
|
4988
|
+
const enabled = new Set(cfg.enabledTools);
|
|
4989
|
+
let failed = 0;
|
|
4990
|
+
for (const tool of targets) {
|
|
4991
|
+
const adapter = adapterFor(registry, tool);
|
|
4992
|
+
if (!adapter) {
|
|
4993
|
+
continue;
|
|
4994
|
+
}
|
|
4995
|
+
const configPath2 = adapter.configPath(tool);
|
|
4996
|
+
if (!configPath2) continue;
|
|
4997
|
+
try {
|
|
4998
|
+
adapter.uninstall(configPath2, hookPath);
|
|
4999
|
+
out(`\u2705 ${tool}\uFF1A\u5DF2\u4ECE ${configPath2} \u79FB\u9664`);
|
|
5000
|
+
} catch (e) {
|
|
5001
|
+
out(`\u2717 ${tool}\uFF1A${e.message}`);
|
|
5002
|
+
failed += 1;
|
|
5003
|
+
continue;
|
|
3413
5004
|
}
|
|
5005
|
+
enabled.delete(tool);
|
|
3414
5006
|
}
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
5007
|
+
cfg.enabledTools = [...enabled];
|
|
5008
|
+
saveConfig(cfg);
|
|
5009
|
+
const shim = opts.shim === void 0 ? defaultShimEnvForInstall() : opts.shim;
|
|
5010
|
+
if (shim) {
|
|
5011
|
+
for (const tool of targets) {
|
|
5012
|
+
if (!SHIM_COMMANDS[tool]) continue;
|
|
5013
|
+
try {
|
|
5014
|
+
const r = uninstallShim(tool, shim);
|
|
5015
|
+
if (r.message) out(`\u2705 ${r.message}`);
|
|
5016
|
+
} catch (e) {
|
|
5017
|
+
out(`\u2717 \u7A97\u53E3\u63A5\u7BA1\u57AB\u7247\u6CA1\u5220\u5E72\u51C0\uFF1A${e.message}`);
|
|
5018
|
+
failed += 1;
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
3418
5021
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
5022
|
+
if (enabled.size === 0 && !opts.keepAutostart) {
|
|
5023
|
+
const r = await uninstallAutostart();
|
|
5024
|
+
if (r.message) out(r.ok ? `\u2705 ${r.message}` : `\u26A0\uFE0F ${r.message}`);
|
|
5025
|
+
}
|
|
5026
|
+
if (!opts.brief) {
|
|
5027
|
+
out("");
|
|
5028
|
+
out("\u5DF2\u505C\u6B62\u91C7\u96C6\u3002\u6388\u6743\u4E0E clientId \u4ECD\u4FDD\u7559\uFF0C\u91CD\u65B0 deskbuddy enable \u5373\u53EF\u6062\u590D\u3002");
|
|
5029
|
+
out("\u8981\u5F7B\u5E95\u6E05\u9664\u672C\u673A\u6570\u636E\u8BF7\u8FD0\u884C\uFF1Adeskbuddy logout");
|
|
5030
|
+
}
|
|
5031
|
+
return failed > 0 ? 1 : 0;
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
// src/commands/install.ts
|
|
5035
|
+
import { mkdirSync as mkdirSync18, rmSync as rmSync9, writeFileSync as writeFileSync18 } from "fs";
|
|
5036
|
+
import { join as join23 } from "path";
|
|
5037
|
+
|
|
5038
|
+
// src/core/detect.ts
|
|
5039
|
+
import { existsSync as existsSync16 } from "fs";
|
|
5040
|
+
import { dirname as dirname10 } from "path";
|
|
5041
|
+
var ALL_TOOLS = ["claude_code", "codex", "workbuddy", "qoder", "opencode"];
|
|
5042
|
+
function toolConfigDir(tool) {
|
|
5043
|
+
switch (tool) {
|
|
5044
|
+
case "claude_code":
|
|
5045
|
+
return claudeHome();
|
|
5046
|
+
case "codex":
|
|
5047
|
+
return codexHome();
|
|
5048
|
+
case "workbuddy":
|
|
5049
|
+
return dirname10(workbuddySettingsPath());
|
|
5050
|
+
case "qoder":
|
|
5051
|
+
return dirname10(qoderSettingsPath());
|
|
5052
|
+
case "opencode":
|
|
5053
|
+
return dirname10(dirname10(opencodePluginPath()));
|
|
5054
|
+
default:
|
|
5055
|
+
return null;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
var defaultDetectDeps = () => ({
|
|
5059
|
+
located: (tool) => locate(tool) != null,
|
|
5060
|
+
exists: existsSync16,
|
|
5061
|
+
env: process.env
|
|
5062
|
+
});
|
|
5063
|
+
function callerTool(env) {
|
|
5064
|
+
if (env.CLAUDECODE === "1") return "claude_code";
|
|
5065
|
+
return null;
|
|
5066
|
+
}
|
|
5067
|
+
function detectTools(d = defaultDetectDeps()) {
|
|
5068
|
+
const caller = callerTool(d.env);
|
|
5069
|
+
return ALL_TOOLS.filter((tool) => {
|
|
5070
|
+
if (tool === caller) return true;
|
|
5071
|
+
if (d.located(tool)) return true;
|
|
5072
|
+
const dir = toolConfigDir(tool);
|
|
5073
|
+
return dir != null && d.exists(dir);
|
|
5074
|
+
});
|
|
3422
5075
|
}
|
|
3423
5076
|
|
|
3424
5077
|
// src/commands/setup.ts
|
|
3425
5078
|
import qrcode from "qrcode-terminal";
|
|
3426
5079
|
|
|
3427
5080
|
// src/commands/remoteCwd.ts
|
|
3428
|
-
import { existsSync as
|
|
3429
|
-
import { createInterface
|
|
3430
|
-
import { homedir as
|
|
3431
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
3432
|
-
function normalizePath(raw, home =
|
|
5081
|
+
import { existsSync as existsSync17, statSync as statSync7 } from "fs";
|
|
5082
|
+
import { createInterface } from "readline";
|
|
5083
|
+
import { homedir as homedir6 } from "os";
|
|
5084
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
5085
|
+
function normalizePath(raw, home = homedir6()) {
|
|
3433
5086
|
const s = (raw || "").trim();
|
|
3434
5087
|
if (!s) return null;
|
|
3435
|
-
const expanded = s === "~" ? home : s.startsWith("~/") ?
|
|
3436
|
-
const abs = isAbsolute2(expanded) ? expanded :
|
|
5088
|
+
const expanded = s === "~" ? home : s.startsWith("~/") || process.platform === "win32" && s.startsWith("~\\") ? resolve3(home, s.slice(2)) : s;
|
|
5089
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve3(process.cwd(), expanded);
|
|
3437
5090
|
return abs;
|
|
3438
5091
|
}
|
|
3439
5092
|
function checkPath(abs) {
|
|
3440
|
-
if (!
|
|
5093
|
+
if (!existsSync17(abs)) return "missing";
|
|
3441
5094
|
try {
|
|
3442
|
-
return
|
|
5095
|
+
return statSync7(abs).isDirectory() ? "ok" : "not_a_dir";
|
|
3443
5096
|
} catch {
|
|
3444
5097
|
return "missing";
|
|
3445
5098
|
}
|
|
@@ -3470,7 +5123,7 @@ async function askRemoteCwd(deps) {
|
|
|
3470
5123
|
}
|
|
3471
5124
|
function terminalAsk(prompt) {
|
|
3472
5125
|
if (!process.stdin.isTTY) return Promise.resolve(null);
|
|
3473
|
-
const rl =
|
|
5126
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3474
5127
|
return new Promise((res) => {
|
|
3475
5128
|
rl.question(prompt, (answer) => {
|
|
3476
5129
|
rl.close();
|
|
@@ -3551,11 +5204,11 @@ function parseApiBase(argv) {
|
|
|
3551
5204
|
function sleep(ms) {
|
|
3552
5205
|
return new Promise((r) => setTimeout(r, ms));
|
|
3553
5206
|
}
|
|
3554
|
-
function
|
|
5207
|
+
function stdout3(line) {
|
|
3555
5208
|
process.stdout.write(line + "\n");
|
|
3556
5209
|
}
|
|
3557
5210
|
async function runSetup(opts = {}) {
|
|
3558
|
-
const out = opts.out ??
|
|
5211
|
+
const out = opts.out ?? stdout3;
|
|
3559
5212
|
const clientId = ensureClientId();
|
|
3560
5213
|
if (opts.apiBase) applyApiBase(opts.apiBase);
|
|
3561
5214
|
const cfg = loadConfig();
|
|
@@ -3588,25 +5241,250 @@ async function runSetup(opts = {}) {
|
|
|
3588
5241
|
return 0;
|
|
3589
5242
|
}
|
|
3590
5243
|
|
|
3591
|
-
// src/commands/
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
var STATE_LABELS = {
|
|
3601
|
-
watch: "\u8FDB\u884C\u4E2D",
|
|
3602
|
-
ask_approval: "\u5F85\u786E\u8BA4",
|
|
3603
|
-
done: "\u5DF2\u5B8C\u6210",
|
|
3604
|
-
failed: "\u5931\u8D25"
|
|
5244
|
+
// src/commands/install.ts
|
|
5245
|
+
var EXIT = {
|
|
5246
|
+
ok: 0,
|
|
5247
|
+
error: 1,
|
|
5248
|
+
codeInvalid: 2,
|
|
5249
|
+
boundOther: 3,
|
|
5250
|
+
network: 4,
|
|
5251
|
+
permission: 5,
|
|
5252
|
+
partial: 6
|
|
3605
5253
|
};
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
5254
|
+
var CODE_INSTALL_INVALID = "8257";
|
|
5255
|
+
var CODE_BOUND_OTHER = "8254";
|
|
5256
|
+
var CODE_RATE_LIMITED = "5020";
|
|
5257
|
+
var TOOL_LABELS = {
|
|
5258
|
+
claude_code: "Claude Code",
|
|
5259
|
+
codex: "Codex",
|
|
5260
|
+
workbuddy: "WorkBuddy",
|
|
5261
|
+
qoder: "Qoder CN",
|
|
5262
|
+
opencode: "opencode"
|
|
5263
|
+
};
|
|
5264
|
+
function checkApiBase(raw, env = process.env) {
|
|
5265
|
+
const norm2 = raw.replace(/\/+$/, "");
|
|
5266
|
+
return norm2 === PROD_API_BASE || norm2 === TEST_API_BASE || env.DESKBUDDY_ALLOW_CUSTOM_API === "1";
|
|
5267
|
+
}
|
|
5268
|
+
function parseCode(argv) {
|
|
5269
|
+
const eq = argv.find((a) => a.startsWith("--code="));
|
|
5270
|
+
if (eq) return eq.slice("--code=".length).trim();
|
|
5271
|
+
const i = argv.indexOf("--code");
|
|
5272
|
+
const v = i >= 0 ? argv[i + 1] : void 0;
|
|
5273
|
+
return v && !v.startsWith("-") ? v.trim() : "";
|
|
5274
|
+
}
|
|
5275
|
+
function npxPrefix(version = packageVersion()) {
|
|
5276
|
+
return `npx -y deskbuddy@${/-beta/.test(version) ? "beta" : "latest"}`;
|
|
5277
|
+
}
|
|
5278
|
+
var PERM_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
|
|
5279
|
+
var NET_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "EHOSTUNREACH"]);
|
|
5280
|
+
function errCode(e) {
|
|
5281
|
+
const x = e;
|
|
5282
|
+
return String(x?.code ?? x?.cause?.code ?? "");
|
|
5283
|
+
}
|
|
5284
|
+
function isPermissionError(e) {
|
|
5285
|
+
return PERM_CODES.has(errCode(e));
|
|
5286
|
+
}
|
|
5287
|
+
function isNetworkError(e) {
|
|
5288
|
+
if (e instanceof ApiError) return false;
|
|
5289
|
+
const name = e?.name;
|
|
5290
|
+
return name === "AbortError" || name === "TimeoutError" || NET_CODES.has(errCode(e)) || e?.message === "fetch failed";
|
|
5291
|
+
}
|
|
5292
|
+
function inSandbox(env) {
|
|
5293
|
+
return !!env.CODEX_SANDBOX || env.CODEX_SANDBOX_NETWORK_DISABLED === "1";
|
|
5294
|
+
}
|
|
5295
|
+
function precheckWritable(dirs) {
|
|
5296
|
+
for (const dir of dirs) {
|
|
5297
|
+
const probe = join23(dir, `.deskbuddy-probe-${process.pid}`);
|
|
5298
|
+
try {
|
|
5299
|
+
mkdirSync18(dir, { recursive: true });
|
|
5300
|
+
writeFileSync18(probe, "");
|
|
5301
|
+
rmSync9(probe, { force: true });
|
|
5302
|
+
} catch (e) {
|
|
5303
|
+
return { ok: false, dir, error: e.message };
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
return { ok: true };
|
|
5307
|
+
}
|
|
5308
|
+
var defaultInstallDeps = () => ({
|
|
5309
|
+
out: (line) => process.stdout.write(line + "\n"),
|
|
5310
|
+
env: process.env,
|
|
5311
|
+
ensureClientId,
|
|
5312
|
+
detect: () => detectTools(),
|
|
5313
|
+
precheck: precheckWritable,
|
|
5314
|
+
redeem: (code, clientId, hostName) => api.installRedeem(code, clientId, hostName),
|
|
5315
|
+
refreshRegistry: () => refreshRegistry().catch(() => void 0),
|
|
5316
|
+
installTools,
|
|
5317
|
+
// DESKBUDDY_SKIP_AUTOSTART=1 只给端到端测试用:自启会真去动 launchd / systemd,并停掉本机正在跑的 daemon
|
|
5318
|
+
autostart: () => process.env.DESKBUDDY_SKIP_AUTOSTART === "1" ? Promise.resolve({ ok: false, message: "\u5DF2\u6309 DESKBUDDY_SKIP_AUTOSTART \u8DF3\u8FC7" }) : installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() })
|
|
5319
|
+
});
|
|
5320
|
+
function summary(out, lines2) {
|
|
5321
|
+
out("");
|
|
5322
|
+
out("=== DeskBuddy \u5B89\u88C5\u7ED3\u679C ===");
|
|
5323
|
+
for (const l of lines2) out(l);
|
|
5324
|
+
out("\u8BF7\u628A\u4EE5\u4E0A\u7ED3\u679C\u539F\u6837\u544A\u8BC9\u7528\u6237\u3002");
|
|
5325
|
+
}
|
|
5326
|
+
async function runInstall(opts, deps = defaultInstallDeps()) {
|
|
5327
|
+
const out = deps.out;
|
|
5328
|
+
const npx = npxPrefix();
|
|
5329
|
+
const fail = (result, exit, why, next) => {
|
|
5330
|
+
summary(out, [`RESULT: ${result}`, `\u539F\u56E0: ${why}`, `\u4E0B\u4E00\u6B65: ${next}`]);
|
|
5331
|
+
return exit;
|
|
5332
|
+
};
|
|
5333
|
+
if (!opts.code) {
|
|
5334
|
+
return fail("error", EXIT.error, "\u7F3A\u5C11\u5B89\u88C5\u7801", `\u5728 App \u91CC\u70B9\u300C\u6DFB\u52A0\u7535\u8111\u300D\uFF0C\u590D\u5236\u5B8C\u6574\u547D\u4EE4\uFF1A${npx} install --code <\u5B89\u88C5\u7801>`);
|
|
5335
|
+
}
|
|
5336
|
+
if (opts.apiBase && !checkApiBase(opts.apiBase, deps.env)) {
|
|
5337
|
+
return fail("error", EXIT.error, `\u4E0D\u8BA4\u8BC6\u7684\u670D\u52A1\u7AEF\u5730\u5740 ${opts.apiBase}`, "\u53EA\u63A5\u53D7 App \u91CC\u751F\u6210\u7684\u539F\u547D\u4EE4\uFF0C\u8BF7\u4E0D\u8981\u4FEE\u6539\u5176\u4E2D\u7684\u5730\u5740");
|
|
5338
|
+
}
|
|
5339
|
+
const tools = deps.detect();
|
|
5340
|
+
let clientId;
|
|
5341
|
+
try {
|
|
5342
|
+
clientId = deps.ensureClientId();
|
|
5343
|
+
applyApiBase(opts.apiBase ?? PROD_API_BASE);
|
|
5344
|
+
} catch (e) {
|
|
5345
|
+
if (isPermissionError(e)) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ~/.deskbuddy\uFF08${e.message}\uFF09`);
|
|
5346
|
+
throw e;
|
|
5347
|
+
}
|
|
5348
|
+
const dirs = [deskbuddyHome(), ...tools.map(toolConfigDir).filter((d) => !!d)];
|
|
5349
|
+
const pre = deps.precheck(dirs);
|
|
5350
|
+
if (!pre.ok) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ${pre.dir}\uFF08${pre.error}\uFF09`);
|
|
5351
|
+
function permissionFail(why) {
|
|
5352
|
+
return fail(
|
|
5353
|
+
"permission",
|
|
5354
|
+
EXIT.permission,
|
|
5355
|
+
why,
|
|
5356
|
+
"\u5F53\u524D\u53EF\u80FD\u8FD0\u884C\u5728\u6C99\u7BB1\u91CC\uFF0C\u6CA1\u6709\u5199\u914D\u7F6E\u76EE\u5F55\u6216\u8054\u7F51\u7684\u6743\u9650\u3002\u8BF7\u7533\u8BF7\u63D0\u5347\u6743\u9650\uFF08\u5728\u6C99\u7BB1\u5916\u6267\u884C\uFF09\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4\uFF0C\u5B89\u88C5\u7801\u4ECD\u7136\u6709\u6548"
|
|
5357
|
+
);
|
|
5358
|
+
}
|
|
5359
|
+
const cfg = loadConfig();
|
|
5360
|
+
out(`\u670D\u52A1\u7AEF\uFF1A${cfg.apiBase}`);
|
|
5361
|
+
const envBase = deps.env.DESKBUDDY_API_BASE;
|
|
5362
|
+
if (envBase && !checkApiBase(envBase, {})) out(`\u26A0\uFE0F \u6CE8\u610F\uFF1A\u73AF\u5883\u53D8\u91CF DESKBUDDY_API_BASE \u628A\u670D\u52A1\u7AEF\u6307\u5411\u4E86 ${envBase}\uFF0C\u8FD9\u4E0D\u662F\u5B98\u65B9\u5730\u5740`);
|
|
5363
|
+
let redeemed;
|
|
5364
|
+
try {
|
|
5365
|
+
redeemed = await deps.redeem(opts.code, clientId, cfg.hostName);
|
|
5366
|
+
} catch (e) {
|
|
5367
|
+
if (e instanceof ApiError) {
|
|
5368
|
+
if (e.code === CODE_INSTALL_INVALID) {
|
|
5369
|
+
return fail("code_invalid", EXIT.codeInvalid, "\u5B89\u88C5\u7801\u65E0\u6548\u3001\u5DF2\u8FC7\u671F\u6216\u5DF2\u88AB\u4F7F\u7528", "\u8BF7\u5728 App \u91CC\u91CD\u65B0\u70B9\u300C\u6DFB\u52A0\u7535\u8111\u300D\u751F\u6210\u65B0\u7801\u3002\u4E0D\u8981\u91CD\u8BD5\u540C\u4E00\u4E2A\u7801");
|
|
5370
|
+
}
|
|
5371
|
+
if (e.code === CODE_BOUND_OTHER) {
|
|
5372
|
+
return fail(
|
|
5373
|
+
"bound_other",
|
|
5374
|
+
EXIT.boundOther,
|
|
5375
|
+
"\u8FD9\u53F0\u7535\u8111\u5DF2\u7ED1\u5B9A\u5176\u4ED6\u8D26\u53F7",
|
|
5376
|
+
`\u5148\u95EE\u7528\u6237\u662F\u5426\u8981\u6362\u7ED1\uFF0C**\u4E0D\u8981\u81EA\u52A8\u6267\u884C**\u3002\u7528\u6237\u786E\u8BA4\u540E\u8FD0\u884C ${npx} logout --yes\uFF0C\u518D\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u5B89\u88C5\u547D\u4EE4\uFF08\u5B89\u88C5\u7801 10 \u5206\u949F\u5185\u6709\u6548\uFF09`
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
if (e.code === CODE_RATE_LIMITED) {
|
|
5380
|
+
return fail("error", EXIT.error, "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41", "\u7B49\u4E00\u5206\u949F\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
|
|
5381
|
+
}
|
|
5382
|
+
return fail("error", EXIT.error, `\u670D\u52A1\u7AEF\u8FD4\u56DE ${e.message}`, "\u7A0D\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4\uFF1B\u53CD\u590D\u5931\u8D25\u8BF7\u5728 App \u91CC\u91CD\u65B0\u751F\u6210\u5B89\u88C5\u7801");
|
|
5383
|
+
}
|
|
5384
|
+
if (isNetworkError(e) && inSandbox(deps.env)) return permissionFail(`\u8FDE\u4E0D\u4E0A\u670D\u52A1\u7AEF\uFF08${e.message}\uFF09\uFF0C\u6C99\u7BB1\u53EF\u80FD\u7981\u6B62\u4E86\u8054\u7F51`);
|
|
5385
|
+
if (isNetworkError(e)) {
|
|
5386
|
+
return fail("network", EXIT.network, `\u8FDE\u4E0D\u4E0A\u670D\u52A1\u7AEF\uFF08${e.message}\uFF09`, "\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
|
|
5387
|
+
}
|
|
5388
|
+
return fail("error", EXIT.error, `\u670D\u52A1\u7AEF\u5F02\u5E38\uFF08${e.message}\uFF09`, "\u7A0D\u540E\u91CD\u65B0\u8FD0\u884C\u540C\u4E00\u6761\u547D\u4EE4");
|
|
5389
|
+
}
|
|
5390
|
+
try {
|
|
5391
|
+
markBound();
|
|
5392
|
+
} catch (e) {
|
|
5393
|
+
if (isPermissionError(e)) return permissionFail(`\u65E0\u6CD5\u5199\u5165 ~/.deskbuddy\uFF08${e.message}\uFF09`);
|
|
5394
|
+
throw e;
|
|
5395
|
+
}
|
|
5396
|
+
await deps.refreshRegistry();
|
|
5397
|
+
out("");
|
|
5398
|
+
const r = tools.length > 0 ? deps.installTools(tools, out) : { installed: [], failed: [] };
|
|
5399
|
+
const bound = loadConfig();
|
|
5400
|
+
if (!bound.remoteCwd) {
|
|
5401
|
+
bound.remoteCwd = defaultRemoteCwd();
|
|
5402
|
+
saveConfig(bound);
|
|
5403
|
+
}
|
|
5404
|
+
const auto = await deps.autostart();
|
|
5405
|
+
const label = (t) => TOOL_LABELS[t] ?? t;
|
|
5406
|
+
const ok = r.installed.length > 0;
|
|
5407
|
+
const lines2 = [
|
|
5408
|
+
`RESULT: ${ok ? "ok" : "partial"}`,
|
|
5409
|
+
`\u8D26\u53F7: ${redeemed.accountHint || "\uFF08\u672A\u77E5\uFF09"}${redeemed.reused ? "\uFF08\u8FD9\u53F0\u7535\u8111\u4E4B\u524D\u5DF2\u7ED1\u5B9A\u8BE5\u8D26\u53F7\uFF09" : ""}\u2014\u2014\u5982\u679C\u8FD9\u4E0D\u662F\u4F60\u7684\u8D26\u53F7\uFF0C\u8BF7\u7ACB\u5373\u8FD0\u884C ${npx} logout --yes`,
|
|
5410
|
+
`\u5DF2\u63A5\u5165: ${r.installed.map(label).join("\u3001") || "\u65E0"}` + (r.failed.length > 0 ? ` \u672A\u63A5\u5165: ${r.failed.map((f) => `${label(f.tool)}\uFF08${f.reason}\uFF09`).join("\u3001")}` : ""),
|
|
5411
|
+
`\u5F00\u673A\u81EA\u542F/\u5B88\u62A4\u8FDB\u7A0B: ${auto.ok ? "\u5DF2\u542F\u52A8" : `\u672A\u8BBE\u7F6E\uFF08${auto.message}\uFF09`}`,
|
|
5412
|
+
`\u8FDC\u7A0B\u65B0\u5EFA\u4EFB\u52A1\u76EE\u5F55: ${loadConfig().remoteCwd}`
|
|
5413
|
+
];
|
|
5414
|
+
if (ok) {
|
|
5415
|
+
const restart = r.installed.map(label).join(" / ");
|
|
5416
|
+
lines2.push(
|
|
5417
|
+
`\u4E0B\u4E00\u6B65: \u91CD\u542F ${restart} \u540E\u5F00\u59CB\u540C\u6B65\u72B6\u6001\uFF08\u6B63\u5728\u8FD0\u884C\u7684\u4F1A\u8BDD\u4E0D\u4F1A\u91CD\u65B0\u52A0\u8F7D hook\uFF09` + (r.installed.includes("codex") ? "\uFF1BCodex \u4E0B\u6B21\u542F\u52A8\u4F1A\u8981\u6C42\u4FE1\u4EFB DeskBuddy \u7684 hook\uFF0C\u8BF7\u9009\u62E9\u540C\u610F" : "")
|
|
5418
|
+
);
|
|
5419
|
+
} else {
|
|
5420
|
+
lines2.push(
|
|
5421
|
+
tools.length === 0 ? `\u4E0B\u4E00\u6B65: \u8FD9\u53F0\u7535\u8111\u4E0A\u6CA1\u68C0\u6D4B\u5230\u652F\u6301\u7684\u667A\u80FD\u4F53\uFF08Claude Code / Codex / WorkBuddy / Qoder CN / opencode\uFF09\u3002\u88C5\u597D\u540E\u8FD0\u884C ${npx} enable <\u5DE5\u5177\u540D>\uFF0C\u5982 claude_code` : `\u4E0B\u4E00\u6B65: \u624B\u52A8\u8FD0\u884C ${npx} enable ${tools.join(",")} \u91CD\u8BD5\uFF0C\u5E76\u628A\u62A5\u9519\u544A\u8BC9\u7528\u6237`
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
summary(out, lines2);
|
|
5425
|
+
return ok ? EXIT.ok : EXIT.partial;
|
|
5426
|
+
}
|
|
5427
|
+
|
|
5428
|
+
// src/commands/logout.ts
|
|
5429
|
+
import { createInterface as createInterface2 } from "readline";
|
|
5430
|
+
import { existsSync as existsSync18, rmSync as rmSync10 } from "fs";
|
|
5431
|
+
function stdout4(line) {
|
|
5432
|
+
process.stdout.write(line + "\n");
|
|
5433
|
+
}
|
|
5434
|
+
function askYesNo(question) {
|
|
5435
|
+
return new Promise((resolve5) => {
|
|
5436
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
5437
|
+
rl.question(question, (answer) => {
|
|
5438
|
+
rl.close();
|
|
5439
|
+
resolve5(/^(y|yes)$/i.test(answer.trim()));
|
|
5440
|
+
});
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
async function runLogout(opts = {}) {
|
|
5444
|
+
const out = opts.out ?? stdout4;
|
|
5445
|
+
const home = deskbuddyHome();
|
|
5446
|
+
out("deskbuddy logout \u4F1A\u505A\u8FD9\u4E24\u4EF6\u4E8B\uFF1A");
|
|
5447
|
+
out(" 1. \u4ECE\u5404\u667A\u80FD\u4F53\u7684\u914D\u7F6E\u91CC\u79FB\u9664 DeskBuddy \u7684 hook\uFF08\u4F60\u81EA\u5DF1\u7684 hook \u4E0D\u52A8\uFF09");
|
|
5448
|
+
out(` 2. \u5220\u9664\u6574\u4E2A ${home}\uFF08clientId\u3001\u6388\u6743\u72B6\u6001\u3001\u4F1A\u8BDD\u72B6\u6001\u3001\u6620\u5C04\u8868\u7F13\u5B58\uFF09`);
|
|
5449
|
+
out("");
|
|
5450
|
+
out("\u5220\u6389\u4E4B\u540E\u518D\u7528\u5C31\u8981\u91CD\u65B0 deskbuddy setup \u626B\u7801\u3002");
|
|
5451
|
+
out("\u6CE8\u610F\uFF1A\u8FD9\u53EA\u6E05\u9664\u672C\u673A\u6570\u636E\uFF0C\u670D\u52A1\u7AEF\u7684\u7ED1\u5B9A\u5173\u7CFB\u9700\u8981\u5728 App \u91CC\u64A4\u9500\uFF08\u6682\u672A\u652F\u6301\uFF09\uFF1B");
|
|
5452
|
+
out("\u4F46\u91CD\u65B0 setup \u4F1A\u6362\u4E00\u4E2A\u65B0\u7684 clientId\uFF0C\u65E7\u7684\u4E0D\u4F1A\u518D\u4E0A\u62A5\u3002");
|
|
5453
|
+
if (!opts.yes) {
|
|
5454
|
+
const confirm = opts.confirm ?? (() => askYesNo("\u786E\u8BA4\u6267\u884C\uFF1F(y/N) "));
|
|
5455
|
+
if (!await confirm()) {
|
|
5456
|
+
out("\u5DF2\u53D6\u6D88\uFF0C\u4EC0\u4E48\u90FD\u6CA1\u52A8\u3002");
|
|
5457
|
+
return 1;
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
await runDisable([], { out, brief: true, keepAutostart: opts.keepAutostart });
|
|
5461
|
+
if (existsSync18(home)) {
|
|
5462
|
+
rmSync10(home, { recursive: true, force: true });
|
|
5463
|
+
}
|
|
5464
|
+
out("");
|
|
5465
|
+
out("\u2705 \u5DF2\u6E05\u9664\u672C\u673A\u7684 DeskBuddy \u6570\u636E\u3002");
|
|
5466
|
+
return 0;
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
// src/commands/status.ts
|
|
5470
|
+
function fmtTime(ms) {
|
|
5471
|
+
const d = new Date(ms);
|
|
5472
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
5473
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
5474
|
+
}
|
|
5475
|
+
function stdout5(line) {
|
|
5476
|
+
process.stdout.write(line + "\n");
|
|
5477
|
+
}
|
|
5478
|
+
var STATE_LABELS = {
|
|
5479
|
+
watch: "\u8FDB\u884C\u4E2D",
|
|
5480
|
+
ask_approval: "\u5F85\u786E\u8BA4",
|
|
5481
|
+
done: "\u5DF2\u5B8C\u6210",
|
|
5482
|
+
failed: "\u5931\u8D25"
|
|
5483
|
+
};
|
|
5484
|
+
function relativeTime(from, now) {
|
|
5485
|
+
const sec = Math.max(0, Math.round((now - from) / 1e3));
|
|
5486
|
+
if (sec < 60) return `${sec} \u79D2\u524D`;
|
|
5487
|
+
const min = Math.round(sec / 60);
|
|
3610
5488
|
if (min < 60) return `${min} \u5206\u949F\u524D`;
|
|
3611
5489
|
const hour = Math.round(min / 60);
|
|
3612
5490
|
if (hour < 24) return `${hour} \u5C0F\u65F6\u524D`;
|
|
@@ -3617,48 +5495,58 @@ function pad(s, width) {
|
|
|
3617
5495
|
return s + " ".repeat(Math.max(1, width - w));
|
|
3618
5496
|
}
|
|
3619
5497
|
function formatStatus(input) {
|
|
3620
|
-
const
|
|
5498
|
+
const lines2 = [];
|
|
3621
5499
|
const cfg = input.config;
|
|
3622
|
-
|
|
5500
|
+
lines2.push("DeskBuddy \u72B6\u6001");
|
|
3623
5501
|
const account = cfg.boundAt ? `\u5DF2\u6388\u6743\uFF08clientId ${cfg.clientId.slice(0, 4)}\u2026\uFF0C\u7ED1\u5B9A\u4E8E ${fmtTime(cfg.boundAt)}\uFF09` : "\u672A\u6388\u6743 \u2014\u2014 \u8FD0\u884C deskbuddy setup \u626B\u7801\u6388\u6743";
|
|
3624
|
-
|
|
3625
|
-
|
|
5502
|
+
lines2.push(` ${pad("\u8D26\u53F7", 10)}${account}`);
|
|
5503
|
+
lines2.push(` ${pad("\u670D\u52A1\u7AEF", 10)}${cfg.apiBase}\uFF08${environmentLabel(cfg.apiBase)}\uFF09`);
|
|
3626
5504
|
if (cfg.boundAt && cfg.boundApiBase && cfg.boundApiBase !== cfg.apiBase) {
|
|
3627
|
-
|
|
5505
|
+
lines2.push(
|
|
3628
5506
|
` \u26A0\uFE0F \u6388\u6743\u65F6\u8FDE\u7684\u662F ${cfg.boundApiBase}\uFF08${environmentLabel(cfg.boundApiBase)}\uFF09\uFF0C\u4E0E\u5F53\u524D\u4E0D\u4E00\u81F4 \u2014\u2014 \u5F53\u524D\u73AF\u5883\u5E76\u4E0D\u8BA4\u8BC6\u8FD9\u53F0\u673A\u5668\uFF0C\u9700\u8981\u91CD\u65B0 deskbuddy setup`
|
|
3629
5507
|
);
|
|
3630
5508
|
}
|
|
3631
5509
|
if (input.envOverride?.active) {
|
|
3632
|
-
|
|
5510
|
+
lines2.push(
|
|
3633
5511
|
` \u26A0\uFE0F \u73AF\u5883\u53D8\u91CF DESKBUDDY_API_BASE=${input.envOverride.envValue} \u6B63\u5728\u8986\u76D6 config.json \u91CC\u7684 ${input.envOverride.fileValue}\uFF08\u591A\u534A\u662F\u5199\u8FDB\u4E86 .zshrc \u4E4B\u7C7B\uFF09`
|
|
3634
5512
|
);
|
|
3635
5513
|
}
|
|
3636
5514
|
if (input.tools.length === 0) {
|
|
3637
|
-
|
|
5515
|
+
lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}\u65E0 \u2014\u2014 \u8FD0\u884C deskbuddy enable claude_code,codex`);
|
|
3638
5516
|
} else {
|
|
3639
5517
|
const desc = input.tools.map((t) => `${t.tool}\uFF08${t.installed ? "hook \u5DF2\u88C5" : "\u26A0\uFE0F hook \u672A\u88C5\uFF0C\u91CD\u8DD1 deskbuddy enable"}\uFF09`).join("\u3001");
|
|
3640
|
-
|
|
5518
|
+
lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
|
|
3641
5519
|
}
|
|
3642
5520
|
const versionNote = input.hookVersion === null ? "\u672A\u5B89\u88C5" : input.hookVersion === input.currentVersion ? `${input.hookVersion}\uFF08\u4E0E\u5F53\u524D\u5305\u4E00\u81F4\uFF09` : `${input.hookVersion} \u26A0\uFE0F \u5F53\u524D\u5305\u662F ${input.currentVersion}\uFF0C\u4E0B\u6B21\u8FD0\u884C\u4EFB\u610F deskbuddy \u547D\u4EE4\u4F1A\u81EA\u52A8\u6821\u51C6`;
|
|
3643
|
-
|
|
5521
|
+
lines2.push(` ${pad("hook \u7248\u672C", 10)}${versionNote}`);
|
|
5522
|
+
if (input.windowWrap) {
|
|
5523
|
+
const w = input.windowWrap;
|
|
5524
|
+
const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable claude_code" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : !w.ptyOk ? "\u5DF2\u5F00\u542F\uFF0C\u4F46 node-pty \u6CA1\u88C5\u4E0A\uFF0C\u6682\u65F6\u964D\u7EA7\u4E3A\u76F4\u63A5\u8FD0\u884C" : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 claude \u4F1A\u7ECF DeskBuddy \u6253\u5F00\uFF0C\u8BBE\u5907\u56DE\u590D\u80FD\u5199\u8FDB\u7A97\u53E3\uFF09";
|
|
5525
|
+
lines2.push(` ${pad("\u7A97\u53E3\u63A5\u7BA1", 10)}${text}`);
|
|
5526
|
+
}
|
|
5527
|
+
if (input.codexWrap) {
|
|
5528
|
+
const w = input.codexWrap;
|
|
5529
|
+
const text = w.winPending ? WIN_PENDING : !w.installed ? "\u672A\u5B89\u88C5 \u2014\u2014 \u91CD\u8DD1 deskbuddy enable codex" : !w.inPath ? `\u672A\u751F\u6548\uFF08\u5F53\u524D shell \u7684 PATH \u91CC\u6CA1\u6709 ${w.binDir}\uFF0C\u65B0\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u518D\u770B\uFF09` : "\u5DF2\u5F00\u542F\uFF08\u7EC8\u7AEF\u91CC\u6572 codex \u4F1A\u7ED9\u8FD9\u4E2A\u7A97\u53E3\u8D77\u4E00\u4E2A app-server \u518D\u8FDE\u4E0A\u53BB\uFF1Bdaemon \u5728\u8DD1\u65F6\u8BBE\u5907\u6307\u4EE4\u80FD\u53D1\u8FDB\u7A97\u53E3\uFF09";
|
|
5530
|
+
lines2.push(` ${pad("Codex\u7A97\u53E3", 10)}${text}`);
|
|
5531
|
+
}
|
|
3644
5532
|
for (const t of input.tools) {
|
|
3645
|
-
if (t.notice)
|
|
5533
|
+
if (t.notice) lines2.push(` ${t.notice}`);
|
|
3646
5534
|
}
|
|
3647
|
-
|
|
5535
|
+
lines2.push("");
|
|
3648
5536
|
const sorted = [...input.sessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3649
|
-
|
|
5537
|
+
lines2.push(`\u5F53\u524D\u672C\u5730\u4F1A\u8BDD\uFF08${sorted.length}\uFF09`);
|
|
3650
5538
|
if (sorted.length === 0) {
|
|
3651
|
-
|
|
5539
|
+
lines2.push(" \u6682\u65E0 \u2014\u2014 \u667A\u80FD\u4F53\u8DD1\u8D77\u6765\u4E4B\u540E\u8FD9\u91CC\u5C31\u4F1A\u6709\u5185\u5BB9");
|
|
3652
5540
|
}
|
|
3653
5541
|
for (const s of sorted) {
|
|
3654
|
-
|
|
5542
|
+
lines2.push(
|
|
3655
5543
|
` ${pad(s.tool, 14)}${pad(s.title || s.project || "-", 24)}${pad(STATE_LABELS[s.state] ?? s.state, 8)}${relativeTime(s.updatedAt, input.now)}`
|
|
3656
5544
|
);
|
|
3657
5545
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
return
|
|
5546
|
+
lines2.push("");
|
|
5547
|
+
lines2.push("\u91C7\u96C6\u8303\u56F4\uFF1A\u53EA\u4E0A\u62A5 \u5DE5\u5177\u540D/\u72B6\u6001/\u4F1A\u8BDD\u6807\u9898/\u9879\u76EE\u76EE\u5F55\u540D/\u65F6\u95F4\u6233");
|
|
5548
|
+
lines2.push("\u4E0D\u4E0A\u62A5\uFF1Aprompt \u539F\u6587\u3001\u547D\u4EE4\u3001\u6587\u4EF6\u8DEF\u5F84\u3001\u4EE3\u7801\u5185\u5BB9");
|
|
5549
|
+
return lines2.join("\n");
|
|
3662
5550
|
}
|
|
3663
5551
|
function collectStatus(now = Date.now()) {
|
|
3664
5552
|
const config = loadConfig();
|
|
@@ -3680,7 +5568,21 @@ function collectStatus(now = Date.now()) {
|
|
|
3680
5568
|
});
|
|
3681
5569
|
sessions.push(...readAliveStates(tool));
|
|
3682
5570
|
}
|
|
5571
|
+
const wrapTool = config.enabledTools.find((t) => t === "claude_code" && SHIM_COMMANDS[t]);
|
|
5572
|
+
let windowWrap;
|
|
5573
|
+
let codexWrap;
|
|
5574
|
+
if (config.enabledTools.includes("codex")) {
|
|
5575
|
+
const binDir = shimDir();
|
|
5576
|
+
codexWrap = { ...shimState("codex", binDir, process.env.PATH ?? ""), binDir, winPending: process.platform === "win32" };
|
|
5577
|
+
}
|
|
5578
|
+
if (wrapTool) {
|
|
5579
|
+
const binDir = shimDir();
|
|
5580
|
+
const st = shimState(wrapTool, binDir, process.env.PATH ?? "");
|
|
5581
|
+
windowWrap = { ...st, binDir, ptyOk: !!loadPty() && !!loadHeadless(), winPending: process.platform === "win32" && !WIN_SHIM_ENABLED };
|
|
5582
|
+
}
|
|
3683
5583
|
return {
|
|
5584
|
+
windowWrap,
|
|
5585
|
+
codexWrap,
|
|
3684
5586
|
config,
|
|
3685
5587
|
hookVersion: installedHookVersion(),
|
|
3686
5588
|
currentVersion: packageVersion(),
|
|
@@ -3696,16 +5598,1038 @@ function runStatus(opts = {}) {
|
|
|
3696
5598
|
return 0;
|
|
3697
5599
|
}
|
|
3698
5600
|
|
|
5601
|
+
// src/commands/update.ts
|
|
5602
|
+
function stdout6(line) {
|
|
5603
|
+
process.stdout.write(line + "\n");
|
|
5604
|
+
}
|
|
5605
|
+
async function runUpdate(opts = {}) {
|
|
5606
|
+
const out = opts.out ?? stdout6;
|
|
5607
|
+
const cfg = loadConfig();
|
|
5608
|
+
if (!cfg.boundAt) {
|
|
5609
|
+
out("\u8FD8\u6CA1\u6709\u6388\u6743\u3002\u8BF7\u5148\u8FD0\u884C\uFF1Adeskbuddy setup");
|
|
5610
|
+
return 1;
|
|
5611
|
+
}
|
|
5612
|
+
if (cfg.enabledTools.length === 0) {
|
|
5613
|
+
out("\u672C\u673A\u8FD8\u6CA1\u6709\u542F\u7528\u4EFB\u4F55\u5DE5\u5177\uFF0C\u6CA1\u4EC0\u4E48\u8981\u5347\u7EA7\u7684\u3002\u7528 deskbuddy enable <tool> \u542F\u7528");
|
|
5614
|
+
return 1;
|
|
5615
|
+
}
|
|
5616
|
+
out(`\u5347\u7EA7\u5230 DeskBuddy ${packageVersion()}\uFF0C\u91CD\u88C5\u5DF2\u542F\u7528\u7684\uFF1A${cfg.enabledTools.join(", ")}`);
|
|
5617
|
+
out("");
|
|
5618
|
+
return runEnable([...cfg.enabledTools], opts);
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// src/wrap/run.ts
|
|
5622
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
5623
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync20, statSync as statSync9, truncateSync as truncateSync3 } from "fs";
|
|
5624
|
+
import { join as join25 } from "path";
|
|
5625
|
+
import { StringDecoder } from "string_decoder";
|
|
5626
|
+
|
|
5627
|
+
// src/wrap/screen.ts
|
|
5628
|
+
var ESC = "\x1B";
|
|
5629
|
+
var PASTE_BEGIN = ESC + "[200~";
|
|
5630
|
+
var PASTE_END = ESC + "[201~";
|
|
5631
|
+
function sanitize(text) {
|
|
5632
|
+
return String(text).replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
5633
|
+
}
|
|
5634
|
+
function prepare(text) {
|
|
5635
|
+
return " " + sanitize(text).trim();
|
|
5636
|
+
}
|
|
5637
|
+
var PERMISSION_RE = /Do you want to (proceed|make|create|run|allow)|[❯>]\s*1\. Yes/;
|
|
5638
|
+
function inspectLines(lines2) {
|
|
5639
|
+
const text = lines2.join("\n");
|
|
5640
|
+
const permission = PERMISSION_RE.test(text);
|
|
5641
|
+
const rules = [];
|
|
5642
|
+
lines2.forEach((l, i) => {
|
|
5643
|
+
if (/^─{20,}/.test(l.trim())) rules.push(i);
|
|
5644
|
+
});
|
|
5645
|
+
let input = null;
|
|
5646
|
+
if (rules.length >= 2) {
|
|
5647
|
+
const a = rules[rules.length - 2];
|
|
5648
|
+
const b = rules[rules.length - 1];
|
|
5649
|
+
input = lines2.slice(a + 1, b).join("\n").replace(/^\s*[❯>]\s?/, "").trimEnd();
|
|
5650
|
+
}
|
|
5651
|
+
return { permission, input };
|
|
5652
|
+
}
|
|
5653
|
+
function boxMatches(view, body) {
|
|
5654
|
+
if (view.input === null || view.permission) return false;
|
|
5655
|
+
const flat = (x) => x.replace(/\s+/g, "");
|
|
5656
|
+
return flat(view.input) === flat(body) || /\[Pasted text #\d+/.test(view.input);
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// src/wrap/injector.ts
|
|
5660
|
+
var QUIET_MS = 1500;
|
|
5661
|
+
var KEY_QUIET_MS = 3e3;
|
|
5662
|
+
var PASTE_SETTLE_MS = 400;
|
|
5663
|
+
var SUBMIT_WAIT_MS = 1e4;
|
|
5664
|
+
var IDLE_EVENTS = /* @__PURE__ */ new Set(["Stop", "SessionStart"]);
|
|
5665
|
+
var Injector = class {
|
|
5666
|
+
#d;
|
|
5667
|
+
#queue = [];
|
|
5668
|
+
#lastOut;
|
|
5669
|
+
#lastKey = 0;
|
|
5670
|
+
#busy = false;
|
|
5671
|
+
#closed = false;
|
|
5672
|
+
constructor(deps) {
|
|
5673
|
+
this.#d = deps;
|
|
5674
|
+
this.#lastOut = deps.now();
|
|
5675
|
+
}
|
|
5676
|
+
noteOutput() {
|
|
5677
|
+
this.#lastOut = this.#d.now();
|
|
5678
|
+
}
|
|
5679
|
+
noteKey() {
|
|
5680
|
+
this.#lastKey = this.#d.now();
|
|
5681
|
+
}
|
|
5682
|
+
get pending() {
|
|
5683
|
+
return this.#queue.length;
|
|
5684
|
+
}
|
|
5685
|
+
/** 回终态(只回一次),并移出队列 */
|
|
5686
|
+
#final(job, msg) {
|
|
5687
|
+
const i = this.#queue.indexOf(job);
|
|
5688
|
+
if (i >= 0) this.#queue.splice(i, 1);
|
|
5689
|
+
if (job.done) return;
|
|
5690
|
+
job.done = true;
|
|
5691
|
+
job.reply({ instructionId: job.instructionId, ...msg });
|
|
5692
|
+
}
|
|
5693
|
+
/**
|
|
5694
|
+
* 这个会话还是不是本窗口当前的会话。
|
|
5695
|
+
* 🔴 不是(/clear 换了会话号、会话 SessionEnd)时回 rejected/session_gone,**不回 not_here**:
|
|
5696
|
+
* claude 还在这个窗口里活着,交回 daemon 走后台也只会被 window_open / session_gone 拒掉,
|
|
5697
|
+
* 而且旧会话的上下文已经不在窗口里了,写进新会话等于答非所问。只有窗口真正关闭(drain)才 not_here
|
|
5698
|
+
*/
|
|
5699
|
+
#owns(sessionId) {
|
|
5700
|
+
const last = this.#d.lastEvent();
|
|
5701
|
+
if (!last) return true;
|
|
5702
|
+
return last.sid === sessionId && last.ev !== "SessionEnd";
|
|
5703
|
+
}
|
|
5704
|
+
/** 通道收到一条指令 */
|
|
5705
|
+
submit(job) {
|
|
5706
|
+
if (this.#closed) {
|
|
5707
|
+
job.reply({ instructionId: job.instructionId, status: "not_here" });
|
|
5708
|
+
return;
|
|
5709
|
+
}
|
|
5710
|
+
if (!this.#owns(job.sessionId)) {
|
|
5711
|
+
job.done = true;
|
|
5712
|
+
job.reply({ instructionId: job.instructionId, status: "rejected", mode: "window", reason: "session_gone" });
|
|
5713
|
+
return;
|
|
5714
|
+
}
|
|
5715
|
+
job.phase = "queued";
|
|
5716
|
+
job.reply({ instructionId: job.instructionId, status: "ack" });
|
|
5717
|
+
this.#queue.push(job);
|
|
5718
|
+
if (this.#queue.length > 1 || this.#busy) this.#markQueued(job);
|
|
5719
|
+
void this.tick();
|
|
5720
|
+
}
|
|
5721
|
+
#markQueued(job) {
|
|
5722
|
+
if (job.queuedSent) return;
|
|
5723
|
+
job.queuedSent = true;
|
|
5724
|
+
job.reply({ instructionId: job.instructionId, status: "queued", mode: "window", reason: "" });
|
|
5725
|
+
}
|
|
5726
|
+
/** 空闲判断。返回不满足的原因,空数组 = 可以写 */
|
|
5727
|
+
precheck() {
|
|
5728
|
+
const why = [];
|
|
5729
|
+
const v = this.#d.view();
|
|
5730
|
+
if (!v) return ["no-screen"];
|
|
5731
|
+
if (v.permission) why.push("permission");
|
|
5732
|
+
if (v.input === null) why.push("no-input-box");
|
|
5733
|
+
else if (v.input !== "") why.push("user-typing");
|
|
5734
|
+
const last = this.#d.lastEvent();
|
|
5735
|
+
if (!last) why.push("hook:none");
|
|
5736
|
+
else if (!IDLE_EVENTS.has(last.ev)) why.push("hook:" + last.ev);
|
|
5737
|
+
const now = this.#d.now();
|
|
5738
|
+
if (now - this.#lastOut < QUIET_MS) why.push("screen-busy");
|
|
5739
|
+
if (now - this.#lastKey < KEY_QUIET_MS) why.push("recent-key");
|
|
5740
|
+
return why;
|
|
5741
|
+
}
|
|
5742
|
+
/** 定时调(包装层每 300ms 一次)。一次最多处理队首一条 */
|
|
5743
|
+
async tick() {
|
|
5744
|
+
if (this.#busy || this.#closed || this.#queue.length === 0) return;
|
|
5745
|
+
const job = this.#queue[0];
|
|
5746
|
+
if (job.alive && !job.alive()) {
|
|
5747
|
+
this.#queue.shift();
|
|
5748
|
+
job.done = true;
|
|
5749
|
+
this.#d.log?.("daemon \u8FDE\u63A5\u5DF2\u65AD\uFF0C\u4E22\u5F03\u672A\u5199\u5165\u7684\u4E00\u6761");
|
|
5750
|
+
return;
|
|
5751
|
+
}
|
|
5752
|
+
if (job.expireAt > 0 && this.#d.now() > job.expireAt) {
|
|
5753
|
+
this.#final(job, { status: "rejected", mode: "window", reason: "expired" });
|
|
5754
|
+
return;
|
|
5755
|
+
}
|
|
5756
|
+
if (!this.#owns(job.sessionId)) {
|
|
5757
|
+
this.#final(job, { status: "rejected", mode: "window", reason: "session_gone" });
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5760
|
+
let why;
|
|
5761
|
+
try {
|
|
5762
|
+
why = this.precheck();
|
|
5763
|
+
} catch (e) {
|
|
5764
|
+
why = ["precheck-error"];
|
|
5765
|
+
this.#d.log?.(`\u7A7A\u95F2\u5224\u65AD\u51FA\u9519\uFF1A${e.message}`);
|
|
5766
|
+
}
|
|
5767
|
+
if (why.length > 0) {
|
|
5768
|
+
this.#markQueued(job);
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
this.#busy = true;
|
|
5772
|
+
try {
|
|
5773
|
+
await this.#inject(job);
|
|
5774
|
+
} catch (e) {
|
|
5775
|
+
this.#d.log?.(`\u5199\u5165\u51FA\u9519\uFF1A${e.message}`);
|
|
5776
|
+
this.#final(job, job.phase === "entered" ? { status: "started", mode: "window", reason: "" } : { status: "failed", mode: "window", reason: "window_failed" });
|
|
5777
|
+
} finally {
|
|
5778
|
+
this.#busy = false;
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
async #inject(job) {
|
|
5782
|
+
const body = prepare(job.text);
|
|
5783
|
+
let enterAt = 0;
|
|
5784
|
+
this.#d.holdKeys(true);
|
|
5785
|
+
try {
|
|
5786
|
+
job.phase = "pasted";
|
|
5787
|
+
this.#d.write(PASTE_BEGIN + body + PASTE_END);
|
|
5788
|
+
await this.#d.sleep(PASTE_SETTLE_MS);
|
|
5789
|
+
if (job.done) return;
|
|
5790
|
+
const v = this.#d.view();
|
|
5791
|
+
if (!v || !boxMatches(v, body)) {
|
|
5792
|
+
this.#d.log?.("\u7C98\u8D34\u540E\u8F93\u5165\u6846\u5BF9\u4E0D\u4E0A\uFF0C\u653E\u5F03\u8FD9\u6761");
|
|
5793
|
+
this.#final(job, { status: "failed", mode: "window", reason: "window_failed" });
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
enterAt = this.#d.now();
|
|
5797
|
+
job.phase = "entered";
|
|
5798
|
+
this.#d.write("\r");
|
|
5799
|
+
} finally {
|
|
5800
|
+
this.#d.holdKeys(false);
|
|
5801
|
+
}
|
|
5802
|
+
let ok = false;
|
|
5803
|
+
const deadline = this.#d.now() + SUBMIT_WAIT_MS;
|
|
5804
|
+
while (this.#d.now() < deadline && !job.done) {
|
|
5805
|
+
const e = this.#d.lastEvent();
|
|
5806
|
+
if (e && e.at >= enterAt && e.ev !== "SessionEnd") {
|
|
5807
|
+
ok = true;
|
|
5808
|
+
break;
|
|
5809
|
+
}
|
|
5810
|
+
await this.#d.sleep(200);
|
|
5811
|
+
}
|
|
5812
|
+
if (!ok && !job.done) {
|
|
5813
|
+
const v = this.#d.view();
|
|
5814
|
+
ok = !!v && v.input === "";
|
|
5815
|
+
}
|
|
5816
|
+
this.#final(job, ok ? { status: "started", mode: "window", reason: "" } : { status: "failed", mode: "window", reason: "window_failed" });
|
|
5817
|
+
}
|
|
5818
|
+
/**
|
|
5819
|
+
* 窗口要关了(或包装层降级、关掉写入能力):
|
|
5820
|
+
* - 还没碰窗口的 → not_here,交回 daemon 走后台(窗口一关,会话就能 --resume 了)
|
|
5821
|
+
* - 已回车的 → started(大概率已经提交);已粘贴未回车的 → failed/window_failed
|
|
5822
|
+
* 🔴 后两种绝不能 not_here:daemon 会在后台再执行一遍
|
|
5823
|
+
*/
|
|
5824
|
+
drain() {
|
|
5825
|
+
this.#closed = true;
|
|
5826
|
+
for (const job of [...this.#queue]) {
|
|
5827
|
+
if (job.phase === "entered") this.#final(job, { status: "started", mode: "window", reason: "" });
|
|
5828
|
+
else if (job.phase === "pasted") this.#final(job, { status: "failed", mode: "window", reason: "window_failed" });
|
|
5829
|
+
else this.#final(job, { status: "not_here" });
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
};
|
|
5833
|
+
|
|
5834
|
+
// src/wrap/passthrough.ts
|
|
5835
|
+
import { spawn as spawn2 } from "child_process";
|
|
5836
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
5837
|
+
import { constants as constants2 } from "os";
|
|
5838
|
+
import { dirname as dirname11, resolve as resolve4 } from "path";
|
|
5839
|
+
var CLAUDE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
5840
|
+
"agents",
|
|
5841
|
+
"attach",
|
|
5842
|
+
"auth",
|
|
5843
|
+
"auto-mode",
|
|
5844
|
+
"config",
|
|
5845
|
+
"doctor",
|
|
5846
|
+
"gateway",
|
|
5847
|
+
"import",
|
|
5848
|
+
"install",
|
|
5849
|
+
"kill",
|
|
5850
|
+
"logs",
|
|
5851
|
+
"mcp",
|
|
5852
|
+
"migrate-installer",
|
|
5853
|
+
"plugin",
|
|
5854
|
+
"plugins",
|
|
5855
|
+
"project",
|
|
5856
|
+
"respawn",
|
|
5857
|
+
"rm",
|
|
5858
|
+
"setup-token",
|
|
5859
|
+
"stop",
|
|
5860
|
+
"ultrareview",
|
|
5861
|
+
"update",
|
|
5862
|
+
"upgrade"
|
|
5863
|
+
]);
|
|
5864
|
+
var WRAPPABLE_TOOLS = /* @__PURE__ */ new Set(["claude_code"]);
|
|
5865
|
+
var LONG_PASS = /* @__PURE__ */ new Set(["--print", "--output-format", "--version", "--help", "--input-format"]);
|
|
5866
|
+
function passthroughReason(i) {
|
|
5867
|
+
if (!WRAPPABLE_TOOLS.has(i.tool)) return "tool";
|
|
5868
|
+
if (i.env.DESKBUDDY_WRAP_OFF === "1") return "wrap_off";
|
|
5869
|
+
if (i.env.DESKBUDDY_WRAPPED) return "wrapped";
|
|
5870
|
+
if (i.env.DESKBUDDY_RUN_ID) return "remote_run";
|
|
5871
|
+
if (!i.stdinTTY || !i.stdoutTTY) return "not_tty";
|
|
5872
|
+
let firstPositional = null;
|
|
5873
|
+
for (const a of i.args) {
|
|
5874
|
+
if (a === "--") break;
|
|
5875
|
+
if (a.startsWith("--")) {
|
|
5876
|
+
if (LONG_PASS.has(a.split("=")[0])) return "flag";
|
|
5877
|
+
continue;
|
|
5878
|
+
}
|
|
5879
|
+
if (/^-[a-zA-Z]+$/.test(a)) {
|
|
5880
|
+
if (/[pvh]/.test(a.slice(1))) return "flag";
|
|
5881
|
+
continue;
|
|
5882
|
+
}
|
|
5883
|
+
if (firstPositional === null) firstPositional = a;
|
|
5884
|
+
}
|
|
5885
|
+
if (firstPositional !== null && CLAUDE_SUBCOMMANDS.has(firstPositional)) return "subcommand";
|
|
5886
|
+
return null;
|
|
5887
|
+
}
|
|
5888
|
+
function norm(p, win) {
|
|
5889
|
+
const r = resolve4(p).replace(/[\\/]+$/, "");
|
|
5890
|
+
return win ? r.replace(/\//g, "\\").toLowerCase() : r;
|
|
5891
|
+
}
|
|
5892
|
+
function real(p) {
|
|
5893
|
+
try {
|
|
5894
|
+
return realpathSync2(p);
|
|
5895
|
+
} catch {
|
|
5896
|
+
return p;
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
function findReal(tool, d = defaultLocateDeps(), skip = shimDir()) {
|
|
5900
|
+
const win = (d.platform ?? process.platform) === "win32";
|
|
5901
|
+
const skipKeys = /* @__PURE__ */ new Set([norm(skip, win), norm(real(skip), win)]);
|
|
5902
|
+
const inSkip = (file) => skipKeys.has(norm(dirname11(file), win)) || skipKeys.has(norm(dirname11(real(file)), win));
|
|
5903
|
+
const dd = { ...d, skipDirs: [...d.skipDirs ?? [], skip] };
|
|
5904
|
+
const name = SHIM_COMMANDS[tool];
|
|
5905
|
+
if (!name) {
|
|
5906
|
+
const loc = locate(tool, dd);
|
|
5907
|
+
return loc && !inSkip(loc.prefixArgs[0] && /\.(?:c|m)?js$/i.test(loc.prefixArgs[0]) ? loc.prefixArgs[0] : loc.file) ? loc : null;
|
|
5908
|
+
}
|
|
5909
|
+
for (const f of whichAllIn(name, dd)) {
|
|
5910
|
+
if (inSkip(f)) continue;
|
|
5911
|
+
const loc = win ? resolveWinLaunch(f, dd) : { file: f, prefixArgs: [] };
|
|
5912
|
+
if (!loc) continue;
|
|
5913
|
+
const target = loc.prefixArgs[0] && /\.(?:c|m)?js$/i.test(loc.prefixArgs[0]) ? loc.prefixArgs[0] : loc.file;
|
|
5914
|
+
if (inSkip(target)) continue;
|
|
5915
|
+
return loc;
|
|
5916
|
+
}
|
|
5917
|
+
return null;
|
|
5918
|
+
}
|
|
5919
|
+
function withoutShimFlag(env) {
|
|
5920
|
+
const out = { ...env };
|
|
5921
|
+
delete out.DESKBUDDY_SHIM;
|
|
5922
|
+
return out;
|
|
5923
|
+
}
|
|
5924
|
+
function runDirect(real2, args, env = process.env) {
|
|
5925
|
+
return new Promise((done) => {
|
|
5926
|
+
let child;
|
|
5927
|
+
try {
|
|
5928
|
+
child = spawn2(real2.file, [...real2.prefixArgs, ...args], {
|
|
5929
|
+
stdio: "inherit",
|
|
5930
|
+
// 🔴 再经过垫片也直接放行,不会绕回来
|
|
5931
|
+
env: withoutShimFlag({ ...env, DESKBUDDY_WRAPPED: "1" }),
|
|
5932
|
+
windowsHide: false
|
|
5933
|
+
});
|
|
5934
|
+
} catch (e) {
|
|
5935
|
+
process.stderr.write(`deskbuddy: \u542F\u52A8 ${real2.file} \u5931\u8D25\uFF1A${e.message}
|
|
5936
|
+
`);
|
|
5937
|
+
done(127);
|
|
5938
|
+
return;
|
|
5939
|
+
}
|
|
5940
|
+
const ignore = () => {
|
|
5941
|
+
};
|
|
5942
|
+
const forward = (sig) => () => {
|
|
5943
|
+
try {
|
|
5944
|
+
child.kill(sig);
|
|
5945
|
+
} catch {
|
|
5946
|
+
}
|
|
5947
|
+
};
|
|
5948
|
+
const handlers = [
|
|
5949
|
+
["SIGINT", ignore],
|
|
5950
|
+
["SIGQUIT", ignore],
|
|
5951
|
+
["SIGTERM", forward("SIGTERM")],
|
|
5952
|
+
["SIGHUP", forward("SIGHUP")]
|
|
5953
|
+
];
|
|
5954
|
+
for (const [s, h] of handlers) {
|
|
5955
|
+
try {
|
|
5956
|
+
process.on(s, h);
|
|
5957
|
+
} catch {
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
const cleanup = () => {
|
|
5961
|
+
for (const [s, h] of handlers) process.removeListener(s, h);
|
|
5962
|
+
};
|
|
5963
|
+
child.on("error", (e) => {
|
|
5964
|
+
cleanup();
|
|
5965
|
+
process.stderr.write(`deskbuddy: \u542F\u52A8 ${real2.file} \u5931\u8D25\uFF1A${e.message}
|
|
5966
|
+
`);
|
|
5967
|
+
done(127);
|
|
5968
|
+
});
|
|
5969
|
+
child.on("exit", (code, signal) => {
|
|
5970
|
+
cleanup();
|
|
5971
|
+
if (code !== null) done(code);
|
|
5972
|
+
else done(128 + (signal && constants2.signals[signal] || 1));
|
|
5973
|
+
});
|
|
5974
|
+
});
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5977
|
+
// src/wrap/codexLaunch.ts
|
|
5978
|
+
import { spawn as spawn3 } from "child_process";
|
|
5979
|
+
import { closeSync, mkdirSync as mkdirSync19, openSync, readdirSync as readdirSync4, readFileSync as readFileSync18, rmSync as rmSync11, statSync as statSync8, truncateSync as truncateSync2, writeFileSync as writeFileSync19, renameSync as renameSync13 } from "fs";
|
|
5980
|
+
import { request } from "http";
|
|
5981
|
+
import { createServer as createServer3 } from "net";
|
|
5982
|
+
import { join as join24 } from "path";
|
|
5983
|
+
|
|
5984
|
+
// src/core/codexServer.ts
|
|
5985
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
5986
|
+
function isAppServerArgs(args, url) {
|
|
5987
|
+
return /\bapp-server\b/.test(args) && new RegExp(`--listen[= ]${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`).test(args);
|
|
5988
|
+
}
|
|
5989
|
+
function processArgs(pid) {
|
|
5990
|
+
if (process.platform === "win32") return null;
|
|
5991
|
+
try {
|
|
5992
|
+
const out = execFileSync8("ps", ["-ww", "-o", "args=", "-p", String(pid)], {
|
|
5993
|
+
encoding: "utf8",
|
|
5994
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5995
|
+
});
|
|
5996
|
+
return out.trim() || null;
|
|
5997
|
+
} catch {
|
|
5998
|
+
return null;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
|
|
6002
|
+
// src/wrap/codexLaunch.ts
|
|
6003
|
+
var TAKEOVER_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "--config", "-m", "--model", "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--enable", "--disable"]);
|
|
6004
|
+
var SHORT_VALUE_OPTS = /* @__PURE__ */ new Set(["-c", "-m", "-s", "-a", "-C"]);
|
|
6005
|
+
var TAKEOVER_FLAGS = /* @__PURE__ */ new Set(["--search", "--no-alt-screen", "--dangerously-bypass-approvals-and-sandbox"]);
|
|
6006
|
+
var RESUME_FLAGS = /* @__PURE__ */ new Set(["--last", "--all", "--include-non-interactive"]);
|
|
6007
|
+
var CODEX_TUI_SUBCOMMANDS = /* @__PURE__ */ new Set(["resume", "fork"]);
|
|
6008
|
+
var CODEX_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
6009
|
+
"agents",
|
|
6010
|
+
"exec",
|
|
6011
|
+
"e",
|
|
6012
|
+
"review",
|
|
6013
|
+
"login",
|
|
6014
|
+
"logout",
|
|
6015
|
+
"mcp",
|
|
6016
|
+
"plugin",
|
|
6017
|
+
"app-server",
|
|
6018
|
+
"remote-control",
|
|
6019
|
+
"app",
|
|
6020
|
+
"completion",
|
|
6021
|
+
"update",
|
|
6022
|
+
"doctor",
|
|
6023
|
+
"sandbox",
|
|
6024
|
+
"debug",
|
|
6025
|
+
"apply",
|
|
6026
|
+
"a",
|
|
6027
|
+
"resume",
|
|
6028
|
+
"queue",
|
|
6029
|
+
"archive",
|
|
6030
|
+
"delete",
|
|
6031
|
+
"migrate-rollouts",
|
|
6032
|
+
"unarchive",
|
|
6033
|
+
"fork",
|
|
6034
|
+
"cloud",
|
|
6035
|
+
"exec-server",
|
|
6036
|
+
"features",
|
|
6037
|
+
"help",
|
|
6038
|
+
"mcp-server"
|
|
6039
|
+
]);
|
|
6040
|
+
function codexTakeoverBlock(args) {
|
|
6041
|
+
let sub = null;
|
|
6042
|
+
let prompt = false;
|
|
6043
|
+
for (let i = 0; i < args.length; i++) {
|
|
6044
|
+
const a = args[i];
|
|
6045
|
+
if (a === "--") return null;
|
|
6046
|
+
if (a.startsWith("-") && a !== "-") {
|
|
6047
|
+
let name = a;
|
|
6048
|
+
let inline = false;
|
|
6049
|
+
if (a.startsWith("--")) {
|
|
6050
|
+
name = a.split("=")[0];
|
|
6051
|
+
inline = a.includes("=");
|
|
6052
|
+
} else if (a.length > 2 && SHORT_VALUE_OPTS.has(a.slice(0, 2))) {
|
|
6053
|
+
name = a.slice(0, 2);
|
|
6054
|
+
inline = true;
|
|
6055
|
+
}
|
|
6056
|
+
if (name === "--remote") return "remote";
|
|
6057
|
+
if (TAKEOVER_VALUE_OPTS.has(name)) {
|
|
6058
|
+
if (!inline) i++;
|
|
6059
|
+
continue;
|
|
6060
|
+
}
|
|
6061
|
+
if (TAKEOVER_FLAGS.has(name)) continue;
|
|
6062
|
+
if (sub && RESUME_FLAGS.has(name)) continue;
|
|
6063
|
+
return `option:${name}`;
|
|
6064
|
+
}
|
|
6065
|
+
if (sub || prompt) continue;
|
|
6066
|
+
if (CODEX_TUI_SUBCOMMANDS.has(a)) {
|
|
6067
|
+
sub = a;
|
|
6068
|
+
continue;
|
|
6069
|
+
}
|
|
6070
|
+
if (CODEX_SUBCOMMANDS.has(a)) return `subcommand:${a}`;
|
|
6071
|
+
prompt = true;
|
|
6072
|
+
}
|
|
6073
|
+
return null;
|
|
6074
|
+
}
|
|
6075
|
+
function hasCdFlag(args) {
|
|
6076
|
+
const end = args.indexOf("--");
|
|
6077
|
+
return (end < 0 ? args : args.slice(0, end)).some((a) => a === "-C" || a === "--cd" || a.startsWith("--cd=") || a.startsWith("-C") && a.length > 2);
|
|
6078
|
+
}
|
|
6079
|
+
var READY_WAIT_MS = 10 * 1e3;
|
|
6080
|
+
var READY_POLL_MS = 50;
|
|
6081
|
+
function codexPassthroughReason(args, d) {
|
|
6082
|
+
if (d.platform === "win32") return "windows";
|
|
6083
|
+
if (d.env.DESKBUDDY_WRAP_OFF === "1") return "wrap_off";
|
|
6084
|
+
if (d.env.DESKBUDDY_WRAPPED) return "wrapped";
|
|
6085
|
+
if (d.env.DESKBUDDY_RUN_ID) return "remote_run";
|
|
6086
|
+
if (!d.stdinTTY || !d.stdoutTTY) return "not_tty";
|
|
6087
|
+
return codexTakeoverBlock(args);
|
|
6088
|
+
}
|
|
6089
|
+
async function runCodexWindow(real2, args, d) {
|
|
6090
|
+
let rec = null;
|
|
6091
|
+
let tuiArgs = null;
|
|
6092
|
+
try {
|
|
6093
|
+
const why = codexPassthroughReason(args, d) ?? (d.daemonAlive() ? null : "no_daemon");
|
|
6094
|
+
if (why) {
|
|
6095
|
+
d.log(`codex \u76F4\u63A5\u8FD0\u884C\uFF08${why}\uFF09`);
|
|
6096
|
+
} else {
|
|
6097
|
+
try {
|
|
6098
|
+
d.reap();
|
|
6099
|
+
} catch {
|
|
6100
|
+
}
|
|
6101
|
+
const t0 = d.now();
|
|
6102
|
+
const url = `ws://127.0.0.1:${await d.freePort()}`;
|
|
6103
|
+
const pid = d.startServer(real2, url);
|
|
6104
|
+
if (!pid) {
|
|
6105
|
+
d.log("app-server \u8D77\u4E0D\u6765\uFF0Ccodex \u76F4\u63A5\u8FD0\u884C");
|
|
6106
|
+
} else {
|
|
6107
|
+
rec = { supervisor: process.pid, server: pid, url };
|
|
6108
|
+
d.record(rec);
|
|
6109
|
+
let ok = false;
|
|
6110
|
+
for (; ; ) {
|
|
6111
|
+
if (await d.ready(url)) {
|
|
6112
|
+
ok = true;
|
|
6113
|
+
break;
|
|
6114
|
+
}
|
|
6115
|
+
if (d.now() - t0 >= READY_WAIT_MS) break;
|
|
6116
|
+
await d.sleep(READY_POLL_MS);
|
|
6117
|
+
}
|
|
6118
|
+
if (ok) {
|
|
6119
|
+
d.log(`app-server ${url} \u5C31\u7EEA ${d.now() - t0}ms`);
|
|
6120
|
+
tuiArgs = ["--remote", url, ...hasCdFlag(args) ? [] : ["-C", d.cwd()], ...args];
|
|
6121
|
+
} else {
|
|
6122
|
+
d.log("app-server \u6CA1\u80FD\u6309\u65F6\u5C31\u7EEA\uFF0Ccodex \u76F4\u63A5\u8FD0\u884C");
|
|
6123
|
+
d.killServer(pid, url);
|
|
6124
|
+
d.unrecord(rec);
|
|
6125
|
+
rec = null;
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
} catch (e) {
|
|
6130
|
+
d.log(`codex \u7A97\u53E3\u51C6\u5907\u51FA\u9519\uFF0C\u76F4\u63A5\u8FD0\u884C\uFF1A${e.message}`);
|
|
6131
|
+
if (rec) {
|
|
6132
|
+
d.killServer(rec.server, rec.url);
|
|
6133
|
+
d.unrecord(rec);
|
|
6134
|
+
rec = null;
|
|
6135
|
+
}
|
|
6136
|
+
tuiArgs = null;
|
|
6137
|
+
}
|
|
6138
|
+
if (!tuiArgs) return d.runDirect(real2, args);
|
|
6139
|
+
try {
|
|
6140
|
+
return await d.runDirect(real2, tuiArgs);
|
|
6141
|
+
} finally {
|
|
6142
|
+
if (rec) {
|
|
6143
|
+
d.killServer(rec.server, rec.url);
|
|
6144
|
+
d.unrecord(rec);
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
var MAX_LOG_BYTES2 = 1024 * 1024;
|
|
6149
|
+
function appServerLogPath() {
|
|
6150
|
+
return join24(deskbuddyHome(), "codex-app-server.log");
|
|
6151
|
+
}
|
|
6152
|
+
function windowsDir2() {
|
|
6153
|
+
return join24(deskbuddyHome(), "codex-windows");
|
|
6154
|
+
}
|
|
6155
|
+
function pidAlive(pid) {
|
|
6156
|
+
try {
|
|
6157
|
+
process.kill(pid, 0);
|
|
6158
|
+
return true;
|
|
6159
|
+
} catch {
|
|
6160
|
+
return false;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
function daemonAlive(pidFile = pidFilePath(), args = processArgs) {
|
|
6164
|
+
let pid = 0;
|
|
6165
|
+
try {
|
|
6166
|
+
pid = Number(readFileSync18(pidFile, "utf8").trim());
|
|
6167
|
+
} catch {
|
|
6168
|
+
return false;
|
|
6169
|
+
}
|
|
6170
|
+
if (!Number.isInteger(pid) || pid <= 0 || !pidAlive(pid)) return false;
|
|
6171
|
+
const a = args(pid);
|
|
6172
|
+
return !!a && /cli\.js daemon(?:\s|$)/.test(a);
|
|
6173
|
+
}
|
|
6174
|
+
function probeReady(url) {
|
|
6175
|
+
const port = Number(url.slice(url.lastIndexOf(":") + 1));
|
|
6176
|
+
return new Promise((resolve5) => {
|
|
6177
|
+
const req2 = request({ host: "127.0.0.1", port, path: "/readyz", method: "GET", timeout: 800 }, (res) => {
|
|
6178
|
+
res.resume();
|
|
6179
|
+
resolve5((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
|
|
6180
|
+
});
|
|
6181
|
+
req2.on("timeout", () => req2.destroy());
|
|
6182
|
+
req2.on("error", () => resolve5(false));
|
|
6183
|
+
req2.end();
|
|
6184
|
+
});
|
|
6185
|
+
}
|
|
6186
|
+
function freePort() {
|
|
6187
|
+
return new Promise((resolve5, reject) => {
|
|
6188
|
+
const srv = createServer3();
|
|
6189
|
+
srv.unref();
|
|
6190
|
+
srv.on("error", reject);
|
|
6191
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
6192
|
+
const addr = srv.address();
|
|
6193
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
6194
|
+
srv.close(() => port ? resolve5(port) : reject(new Error("no port")));
|
|
6195
|
+
});
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
function startServer(real2, url, env, cwd) {
|
|
6199
|
+
const logPath2 = appServerLogPath();
|
|
6200
|
+
mkdirSync19(deskbuddyHome(), { recursive: true });
|
|
6201
|
+
try {
|
|
6202
|
+
if (statSync8(logPath2).size > MAX_LOG_BYTES2) truncateSync2(logPath2, 0);
|
|
6203
|
+
} catch {
|
|
6204
|
+
}
|
|
6205
|
+
const fd = openSync(logPath2, "a");
|
|
6206
|
+
try {
|
|
6207
|
+
const childEnv2 = { ...env, DESKBUDDY_WRAPPED: "1" };
|
|
6208
|
+
delete childEnv2.DESKBUDDY_SHIM;
|
|
6209
|
+
const child = spawn3(real2.file, [...real2.prefixArgs, "app-server", "--listen", url], {
|
|
6210
|
+
detached: true,
|
|
6211
|
+
stdio: ["ignore", fd, fd],
|
|
6212
|
+
cwd,
|
|
6213
|
+
env: childEnv2
|
|
6214
|
+
});
|
|
6215
|
+
child.on("error", () => {
|
|
6216
|
+
});
|
|
6217
|
+
child.unref();
|
|
6218
|
+
return child.pid ?? null;
|
|
6219
|
+
} finally {
|
|
6220
|
+
closeSync(fd);
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
function killServer(pid) {
|
|
6224
|
+
try {
|
|
6225
|
+
process.kill(-pid, "SIGTERM");
|
|
6226
|
+
} catch {
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
function killOwnServer(pid, url) {
|
|
6230
|
+
const a = processArgs(pid);
|
|
6231
|
+
if (a && isAppServerArgs(a, url)) killServer(pid);
|
|
6232
|
+
}
|
|
6233
|
+
function recordPath(rec) {
|
|
6234
|
+
return join24(windowsDir2(), `${rec.server}.json`);
|
|
6235
|
+
}
|
|
6236
|
+
function writeRecord(rec) {
|
|
6237
|
+
try {
|
|
6238
|
+
mkdirSync19(windowsDir2(), { recursive: true, mode: 448 });
|
|
6239
|
+
const p = recordPath(rec);
|
|
6240
|
+
writeFileSync19(p + ".tmp", JSON.stringify(rec), { encoding: "utf8", mode: 384 });
|
|
6241
|
+
renameSync13(p + ".tmp", p);
|
|
6242
|
+
} catch {
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
function removeRecord(rec) {
|
|
6246
|
+
try {
|
|
6247
|
+
rmSync11(recordPath(rec), { force: true });
|
|
6248
|
+
} catch {
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
function reapWindows(dir = windowsDir2(), alive = pidAlive, args = processArgs, kill = killServer) {
|
|
6252
|
+
let names = [];
|
|
6253
|
+
try {
|
|
6254
|
+
names = readdirSync4(dir).filter((n) => n.endsWith(".json"));
|
|
6255
|
+
} catch {
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
for (const n of names) {
|
|
6259
|
+
const p = join24(dir, n);
|
|
6260
|
+
let rec;
|
|
6261
|
+
try {
|
|
6262
|
+
rec = JSON.parse(readFileSync18(p, "utf8"));
|
|
6263
|
+
} catch {
|
|
6264
|
+
rmSync11(p, { force: true });
|
|
6265
|
+
continue;
|
|
6266
|
+
}
|
|
6267
|
+
if (typeof rec.supervisor !== "number" || typeof rec.server !== "number" || typeof rec.url !== "string") {
|
|
6268
|
+
rmSync11(p, { force: true });
|
|
6269
|
+
continue;
|
|
6270
|
+
}
|
|
6271
|
+
if (alive(rec.supervisor)) continue;
|
|
6272
|
+
if (alive(rec.server)) {
|
|
6273
|
+
const a = args(rec.server);
|
|
6274
|
+
if (a && isAppServerArgs(a, rec.url)) kill(rec.server);
|
|
6275
|
+
}
|
|
6276
|
+
rmSync11(p, { force: true });
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
function launchLog(line) {
|
|
6280
|
+
try {
|
|
6281
|
+
mkdirSync19(deskbuddyHome(), { recursive: true });
|
|
6282
|
+
writeFileSync19(join24(deskbuddyHome(), "wrap.log"), `${(/* @__PURE__ */ new Date()).toISOString()} [codex ${process.pid}] ${line}
|
|
6283
|
+
`, { flag: "a" });
|
|
6284
|
+
} catch {
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
function defaultCodexWindowDeps(base) {
|
|
6288
|
+
return {
|
|
6289
|
+
...base,
|
|
6290
|
+
now: () => Date.now(),
|
|
6291
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
6292
|
+
daemonAlive: () => daemonAlive(),
|
|
6293
|
+
freePort,
|
|
6294
|
+
startServer: (real2, url) => startServer(real2, url, base.env, base.cwd()),
|
|
6295
|
+
ready: probeReady,
|
|
6296
|
+
killServer: killOwnServer,
|
|
6297
|
+
record: writeRecord,
|
|
6298
|
+
unrecord: removeRecord,
|
|
6299
|
+
reap: () => reapWindows(),
|
|
6300
|
+
log: launchLog
|
|
6301
|
+
};
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
// src/wrap/run.ts
|
|
6305
|
+
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
6306
|
+
var TICK_MS = 300;
|
|
6307
|
+
var MAX_LOG_BYTES3 = 512 * 1024;
|
|
6308
|
+
var WRAP_HINT = "DeskBuddy \u5305\u88C5\u51FA\u9519\uFF0C\u8BF7\u8BBE DESKBUDDY_WRAP_OFF=1 \u4E34\u65F6\u7ED5\u5F00\uFF08\u4F8B\u5982 DESKBUDDY_WRAP_OFF=1 claude\uFF09";
|
|
6309
|
+
var SESSION_ENV = ["DESKBUDDY_SHIM", "CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION", "CLAUDE_CODE_SESSION_ID", "CLAUDE_PID", "CLAUDE_CODE_ENTRYPOINT"];
|
|
6310
|
+
function childEnv(src, windowId) {
|
|
6311
|
+
const env = {};
|
|
6312
|
+
for (const [k, v] of Object.entries(src)) {
|
|
6313
|
+
if (SESSION_ENV.includes(k)) continue;
|
|
6314
|
+
env[k] = v;
|
|
6315
|
+
}
|
|
6316
|
+
env.DESKBUDDY_WINDOW = windowId;
|
|
6317
|
+
env.DESKBUDDY_WRAPPED = "1";
|
|
6318
|
+
return env;
|
|
6319
|
+
}
|
|
6320
|
+
function wrapLog(line) {
|
|
6321
|
+
try {
|
|
6322
|
+
const p = join25(deskbuddyHome(), "wrap.log");
|
|
6323
|
+
mkdirSync20(deskbuddyHome(), { recursive: true });
|
|
6324
|
+
try {
|
|
6325
|
+
if (statSync9(p).size > MAX_LOG_BYTES3) truncateSync3(p, 0);
|
|
6326
|
+
} catch {
|
|
6327
|
+
}
|
|
6328
|
+
appendFileSync2(p, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${line}
|
|
6329
|
+
`, "utf8");
|
|
6330
|
+
} catch {
|
|
6331
|
+
}
|
|
6332
|
+
}
|
|
6333
|
+
var defaultWrapDeps = () => ({
|
|
6334
|
+
env: process.env,
|
|
6335
|
+
stdin: process.stdin,
|
|
6336
|
+
stdout: process.stdout,
|
|
6337
|
+
stderr: process.stderr,
|
|
6338
|
+
platform: process.platform,
|
|
6339
|
+
cwd: () => process.cwd(),
|
|
6340
|
+
findReal: (tool) => findReal(tool),
|
|
6341
|
+
runDirect: (real2, args) => runDirect(real2, args),
|
|
6342
|
+
loadPty,
|
|
6343
|
+
loadHeadless,
|
|
6344
|
+
log: wrapLog
|
|
6345
|
+
});
|
|
6346
|
+
async function runWrap(argv, deps = defaultWrapDeps()) {
|
|
6347
|
+
const viaShim = deps.env.DESKBUDDY_SHIM === "1";
|
|
6348
|
+
const r = await runWrapInner(argv, deps);
|
|
6349
|
+
if (r === "not_found") {
|
|
6350
|
+
if (viaShim) return SHIM_FALLBACK_EXIT;
|
|
6351
|
+
deps.stderr.write(`deskbuddy: \u627E\u4E0D\u5230 ${SHIM_COMMANDS[argv[0] ?? ""] ?? argv[0]}\uFF08\u5DF2\u8DF3\u8FC7 DeskBuddy \u81EA\u5DF1\u7684\u57AB\u7247\u76EE\u5F55\uFF09
|
|
6352
|
+
`);
|
|
6353
|
+
return 127;
|
|
6354
|
+
}
|
|
6355
|
+
return viaShim && r === SHIM_FALLBACK_EXIT ? 1 : r;
|
|
6356
|
+
}
|
|
6357
|
+
async function runWrapInner(argv, deps) {
|
|
6358
|
+
const tool = argv[0] ?? "";
|
|
6359
|
+
const args = argv.slice(1);
|
|
6360
|
+
let real2 = null;
|
|
6361
|
+
try {
|
|
6362
|
+
real2 = deps.findReal(tool);
|
|
6363
|
+
} catch {
|
|
6364
|
+
real2 = null;
|
|
6365
|
+
}
|
|
6366
|
+
if (!real2) return "not_found";
|
|
6367
|
+
if (tool === "codex") {
|
|
6368
|
+
return runCodexWindow(
|
|
6369
|
+
real2,
|
|
6370
|
+
args,
|
|
6371
|
+
defaultCodexWindowDeps({
|
|
6372
|
+
env: deps.env,
|
|
6373
|
+
platform: deps.platform,
|
|
6374
|
+
stdinTTY: !!deps.stdin.isTTY,
|
|
6375
|
+
stdoutTTY: !!deps.stdout.isTTY,
|
|
6376
|
+
cwd: deps.cwd,
|
|
6377
|
+
runDirect: deps.runDirect
|
|
6378
|
+
})
|
|
6379
|
+
);
|
|
6380
|
+
}
|
|
6381
|
+
let reason;
|
|
6382
|
+
try {
|
|
6383
|
+
reason = passthroughReason({
|
|
6384
|
+
tool,
|
|
6385
|
+
args,
|
|
6386
|
+
env: deps.env,
|
|
6387
|
+
stdinTTY: !!deps.stdin.isTTY,
|
|
6388
|
+
stdoutTTY: !!deps.stdout.isTTY
|
|
6389
|
+
});
|
|
6390
|
+
} catch {
|
|
6391
|
+
reason = "error";
|
|
6392
|
+
}
|
|
6393
|
+
if (reason) return deps.runDirect(real2, args);
|
|
6394
|
+
let r = null;
|
|
6395
|
+
try {
|
|
6396
|
+
r = await wrapInPty(tool, real2, args, deps);
|
|
6397
|
+
} catch (e) {
|
|
6398
|
+
deps.log(`\u5305\u88C5\u5931\u8D25\uFF0C\u653E\u884C\u76F4\u8DD1\uFF1A${e.message}`);
|
|
6399
|
+
r = null;
|
|
6400
|
+
}
|
|
6401
|
+
if (r !== null) return r;
|
|
6402
|
+
return deps.runDirect(real2, args);
|
|
6403
|
+
}
|
|
6404
|
+
async function wrapInPty(tool, real2, args, deps) {
|
|
6405
|
+
const pty = deps.loadPty();
|
|
6406
|
+
const Headless = deps.loadHeadless();
|
|
6407
|
+
if (!pty || !Headless) {
|
|
6408
|
+
deps.log("node-pty / @xterm/headless \u52A0\u8F7D\u4E0D\u4E86\uFF0C\u653E\u884C\u76F4\u8DD1");
|
|
6409
|
+
return null;
|
|
6410
|
+
}
|
|
6411
|
+
const { stdout: stdout7 } = deps;
|
|
6412
|
+
const windowId = randomBytes6(16).toString("hex");
|
|
6413
|
+
const cols = stdout7.columns || 80;
|
|
6414
|
+
const rows = stdout7.rows || 24;
|
|
6415
|
+
const term = new Headless({ cols, rows, allowProposedApi: true });
|
|
6416
|
+
let p;
|
|
6417
|
+
try {
|
|
6418
|
+
p = pty.spawn(real2.file, [...real2.prefixArgs, ...args], {
|
|
6419
|
+
name: deps.env.TERM || "xterm-256color",
|
|
6420
|
+
cols,
|
|
6421
|
+
rows,
|
|
6422
|
+
cwd: deps.cwd(),
|
|
6423
|
+
env: childEnv(deps.env, windowId)
|
|
6424
|
+
});
|
|
6425
|
+
} catch (e) {
|
|
6426
|
+
deps.log(`pty \u8D77\u4E0D\u6765\uFF0C\u653E\u884C\u76F4\u8DD1\uFF1A${e.message}`);
|
|
6427
|
+
return null;
|
|
6428
|
+
}
|
|
6429
|
+
return runAttached(tool, p, term, windowId, deps);
|
|
6430
|
+
}
|
|
6431
|
+
function runAttached(tool, p, term0, windowId, deps) {
|
|
6432
|
+
const { stdin, stdout: stdout7 } = deps;
|
|
6433
|
+
let term = term0;
|
|
6434
|
+
let finished = false;
|
|
6435
|
+
let held = null;
|
|
6436
|
+
let server = null;
|
|
6437
|
+
let timer = null;
|
|
6438
|
+
const sock = channelPath(windowId, deps.platform);
|
|
6439
|
+
const decoder = new StringDecoder("utf8");
|
|
6440
|
+
const toPty = (s) => {
|
|
6441
|
+
try {
|
|
6442
|
+
p.write(s);
|
|
6443
|
+
} catch {
|
|
6444
|
+
}
|
|
6445
|
+
};
|
|
6446
|
+
const injector = new Injector({
|
|
6447
|
+
now: () => Date.now(),
|
|
6448
|
+
view: () => term ? inspectLines(screenLines(term)) : null,
|
|
6449
|
+
lastEvent: () => readLastEvent(windowId),
|
|
6450
|
+
write: toPty,
|
|
6451
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
6452
|
+
holdKeys: (on) => {
|
|
6453
|
+
if (on) {
|
|
6454
|
+
held = held ?? [];
|
|
6455
|
+
return;
|
|
6456
|
+
}
|
|
6457
|
+
const h = held ?? [];
|
|
6458
|
+
held = null;
|
|
6459
|
+
for (const s of h) toPty(s);
|
|
6460
|
+
},
|
|
6461
|
+
log: (m) => deps.log(`[${windowId.slice(0, 8)}] ${m}`)
|
|
6462
|
+
});
|
|
6463
|
+
p.onData((d) => {
|
|
6464
|
+
try {
|
|
6465
|
+
stdout7.write(d);
|
|
6466
|
+
} catch {
|
|
6467
|
+
}
|
|
6468
|
+
if (term) {
|
|
6469
|
+
try {
|
|
6470
|
+
term.write(d);
|
|
6471
|
+
} catch {
|
|
6472
|
+
term = null;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
injector.noteOutput();
|
|
6476
|
+
});
|
|
6477
|
+
const onStdin = (b) => {
|
|
6478
|
+
injector.noteKey();
|
|
6479
|
+
const s = typeof b === "string" ? b : decoder.write(b);
|
|
6480
|
+
if (!s) return;
|
|
6481
|
+
if (held) held.push(s);
|
|
6482
|
+
else toPty(s);
|
|
6483
|
+
};
|
|
6484
|
+
const onResize = () => {
|
|
6485
|
+
const c = stdout7.columns;
|
|
6486
|
+
const r = stdout7.rows;
|
|
6487
|
+
if (!c || !r) return;
|
|
6488
|
+
try {
|
|
6489
|
+
p.resize(c, r);
|
|
6490
|
+
} catch {
|
|
6491
|
+
}
|
|
6492
|
+
try {
|
|
6493
|
+
term?.resize(c, r);
|
|
6494
|
+
} catch {
|
|
6495
|
+
term = null;
|
|
6496
|
+
}
|
|
6497
|
+
};
|
|
6498
|
+
const restoreTerminal = () => {
|
|
6499
|
+
try {
|
|
6500
|
+
stdin.off("data", onStdin);
|
|
6501
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
6502
|
+
stdin.pause();
|
|
6503
|
+
} catch {
|
|
6504
|
+
}
|
|
6505
|
+
try {
|
|
6506
|
+
stdout7.off("resize", onResize);
|
|
6507
|
+
} catch {
|
|
6508
|
+
}
|
|
6509
|
+
};
|
|
6510
|
+
try {
|
|
6511
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
6512
|
+
stdin.on("data", onStdin);
|
|
6513
|
+
stdin.resume();
|
|
6514
|
+
stdout7.on("resize", onResize);
|
|
6515
|
+
} catch (e) {
|
|
6516
|
+
deps.log(`\u63A5\u7BA1\u7EC8\u7AEF\u51FA\u9519\uFF1A${e.message}`);
|
|
6517
|
+
}
|
|
6518
|
+
try {
|
|
6519
|
+
writeWindow(windowId, { pid: process.pid, childPid: p.pid, sock, tool, cwd: deps.cwd(), at: Date.now() });
|
|
6520
|
+
} catch (e) {
|
|
6521
|
+
deps.log(`\u767B\u8BB0\u7A97\u53E3\u5931\u8D25\uFF1A${e.message}`);
|
|
6522
|
+
}
|
|
6523
|
+
const channelOk = deps.platform === "win32" || ensureChannelDir(sock);
|
|
6524
|
+
if (channelOk) {
|
|
6525
|
+
void startChannelServer(
|
|
6526
|
+
sock,
|
|
6527
|
+
(req2, reply, alive) => {
|
|
6528
|
+
try {
|
|
6529
|
+
injector.submit({ ...req2, expireAt: req2.expireAt || Date.now() + DEFAULT_TTL_MS, reply, alive });
|
|
6530
|
+
} catch (e) {
|
|
6531
|
+
deps.log(`\u6536\u6307\u4EE4\u51FA\u9519\uFF1A${e.message}`);
|
|
6532
|
+
}
|
|
6533
|
+
},
|
|
6534
|
+
deps.platform
|
|
6535
|
+
).then((s) => {
|
|
6536
|
+
if (finished) {
|
|
6537
|
+
closeChannelServer(s, sock, deps.platform);
|
|
6538
|
+
return;
|
|
6539
|
+
}
|
|
6540
|
+
server = s;
|
|
6541
|
+
if (!s) deps.log("\u901A\u9053\u8D77\u4E0D\u6765\uFF0C\u8FD9\u4E2A\u7A97\u53E3\u6536\u4E0D\u5230\u8BBE\u5907\u6307\u4EE4");
|
|
6542
|
+
});
|
|
6543
|
+
}
|
|
6544
|
+
timer = setInterval(() => {
|
|
6545
|
+
injector.tick().catch(() => {
|
|
6546
|
+
});
|
|
6547
|
+
}, TICK_MS);
|
|
6548
|
+
return new Promise((resolve5) => {
|
|
6549
|
+
let degraded = false;
|
|
6550
|
+
const onFatal = (e) => {
|
|
6551
|
+
deps.log(`\u5305\u88C5\u5C42\u5F02\u5E38\uFF1A${e?.stack ?? String(e)}`);
|
|
6552
|
+
if (degraded) return;
|
|
6553
|
+
degraded = true;
|
|
6554
|
+
if (timer) clearInterval(timer);
|
|
6555
|
+
term = null;
|
|
6556
|
+
try {
|
|
6557
|
+
injector.drain();
|
|
6558
|
+
} catch {
|
|
6559
|
+
}
|
|
6560
|
+
closeChannelServer(server, sock, deps.platform);
|
|
6561
|
+
try {
|
|
6562
|
+
removeWindow(windowId);
|
|
6563
|
+
} catch {
|
|
6564
|
+
}
|
|
6565
|
+
try {
|
|
6566
|
+
deps.stderr.write(`\r
|
|
6567
|
+
${WRAP_HINT}\uFF08\u8BBE\u5907\u56DE\u590D\u6682\u65F6\u5199\u4E0D\u8FDB\u8FD9\u4E2A\u7A97\u53E3\uFF09\r
|
|
6568
|
+
`);
|
|
6569
|
+
} catch {
|
|
6570
|
+
}
|
|
6571
|
+
};
|
|
6572
|
+
const sigs = ["SIGTERM", "SIGHUP", "SIGINT"];
|
|
6573
|
+
const onSignal = (sig) => {
|
|
6574
|
+
try {
|
|
6575
|
+
if (deps.platform === "win32") p.kill();
|
|
6576
|
+
else p.kill(sig);
|
|
6577
|
+
} catch {
|
|
6578
|
+
}
|
|
6579
|
+
setTimeout(() => finish(128 + (sig === "SIGHUP" ? 1 : sig === "SIGINT" ? 2 : 15)), 3e3).unref();
|
|
6580
|
+
};
|
|
6581
|
+
const sigHandlers = sigs.map((s) => [s, () => onSignal(s)]);
|
|
6582
|
+
function finish(code) {
|
|
6583
|
+
if (finished) return;
|
|
6584
|
+
finished = true;
|
|
6585
|
+
if (timer) clearInterval(timer);
|
|
6586
|
+
const pending = injector.pending;
|
|
6587
|
+
try {
|
|
6588
|
+
injector.drain();
|
|
6589
|
+
} catch {
|
|
6590
|
+
}
|
|
6591
|
+
closeChannelServer(server, sock, deps.platform);
|
|
6592
|
+
try {
|
|
6593
|
+
removeWindow(windowId);
|
|
6594
|
+
} catch {
|
|
6595
|
+
}
|
|
6596
|
+
restoreTerminal();
|
|
6597
|
+
process.removeListener("uncaughtException", onFatal);
|
|
6598
|
+
for (const [s, h] of sigHandlers) process.removeListener(s, h);
|
|
6599
|
+
if (pending > 0) setTimeout(() => resolve5(code), 150);
|
|
6600
|
+
else resolve5(code);
|
|
6601
|
+
}
|
|
6602
|
+
process.on("uncaughtException", onFatal);
|
|
6603
|
+
for (const [s, h] of sigHandlers) {
|
|
6604
|
+
try {
|
|
6605
|
+
process.on(s, h);
|
|
6606
|
+
} catch {
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
p.onExit(({ exitCode, signal }) => {
|
|
6610
|
+
const flushed = decoder.end();
|
|
6611
|
+
if (flushed) toPty(flushed);
|
|
6612
|
+
finish(signal ? 128 + signal : exitCode);
|
|
6613
|
+
});
|
|
6614
|
+
});
|
|
6615
|
+
}
|
|
6616
|
+
|
|
3699
6617
|
// src/cli.ts
|
|
3700
6618
|
var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6BCF\u4E2A\u4F1A\u8BDD\u7A97\u53E3\u7684\u72B6\u6001\u540C\u6B65\u5230\u684C\u9762\u786C\u4EF6\u5C4F\u5E55
|
|
3701
6619
|
|
|
3702
6620
|
\u7528\u6CD5:
|
|
6621
|
+
deskbuddy install --code <\u5B89\u88C5\u7801> \u4E00\u952E\u63A5\u5165\uFF1A\u7528 App\u300C\u6DFB\u52A0\u7535\u8111\u300D\u7ED9\u7684\u5B89\u88C5\u7801\u7ED1\u5B9A\u8D26\u53F7\uFF0C
|
|
6622
|
+
\u5E76\u63A5\u5165\u672C\u673A\u6240\u6709\u667A\u80FD\u4F53\u3001\u8BBE\u597D\u5F00\u673A\u81EA\u542F\uFF08\u4E0D\u9700\u8981\u4EFB\u4F55\u4EA4\u4E92\uFF09
|
|
3703
6623
|
deskbuddy setup [--api-base <url>] \u626B\u7801\u6388\u6743\uFF08--api-base \u6307\u5B9A\u7F51\u5173\u5E76\u8BB0\u4F4F\uFF0C\u9ED8\u8BA4\u751F\u4EA7\uFF09
|
|
3704
6624
|
deskbuddy enable <tool[,tool]> \u5B89\u88C5\u91C7\u96C6 hook\uFF08\u5982 claude_code,codex\uFF09
|
|
3705
6625
|
deskbuddy disable [tool[,tool]] \u79FB\u9664\u91C7\u96C6 hook\uFF08\u4E0D\u5E26\u53C2\u6570=\u5168\u90E8\uFF09
|
|
6626
|
+
deskbuddy update \u5347\u7EA7\u5230\u5F53\u524D\u8FD9\u7248\uFF1A\u91CD\u88C5\u5DF2\u542F\u7528\u5DE5\u5177\u7684 hook\u3001\u91CD\u542F daemon
|
|
6627
|
+
\uFF08npx deskbuddy@<\u7248\u672C> update\uFF09
|
|
3706
6628
|
deskbuddy daemon [--foreground] \u5E38\u9A7B\u6536\u53D6\u8BBE\u5907\u4E0B\u53D1\u7684\u6307\u4EE4\uFF08\u5F00\u673A\u81EA\u542F\u4F1A\u81EA\u52A8\u62C9\u8D77\uFF09
|
|
3707
6629
|
deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
|
|
3708
6630
|
deskbuddy logout [--yes] \u79FB\u9664 hook \u5E76\u6E05\u9664\u672C\u673A\u6570\u636E
|
|
6631
|
+
deskbuddy wrap <tool> [\u53C2\u6570\u2026] \uFF08\u57AB\u7247\u5185\u90E8\u7528\uFF09\u7ECF DeskBuddy \u6253\u5F00\u667A\u80FD\u4F53\u7A97\u53E3\uFF0C
|
|
6632
|
+
\u8BBE\u5907\u4E0A\u7684\u56DE\u590D\u80FD\u5199\u8FDB\u8FD9\u4E2A\u7A97\u53E3\uFF1BDESKBUDDY_WRAP_OFF=1 \u53EF\u7ED5\u5F00
|
|
3709
6633
|
|
|
3710
6634
|
\u72B6\u6001\u91C7\u96C6\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF08\u53EA\u662F\u5F80\u5404\u667A\u80FD\u4F53\u7684 hook \u914D\u7F6E\u91CC\u52A0\u4E86\u4E00\u6761\u8BB0\u5F55\uFF09\uFF1B
|
|
3711
6635
|
\u8981\u5728\u786C\u4EF6\u4E0A\u7ED9\u7535\u8111\u4E0B\u6307\u4EE4\uFF0C\u624D\u9700\u8981 daemon \u5E38\u9A7B\u2014\u2014setup \u65F6\u4F1A\u5E2E\u4F60\u8BBE\u597D\u5F00\u673A\u81EA\u542F\u3002
|
|
@@ -3714,7 +6638,7 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
|
|
|
3714
6638
|
async function main(argv) {
|
|
3715
6639
|
const cmd = argv[2];
|
|
3716
6640
|
const arg = argv[3];
|
|
3717
|
-
if (cmd && !["-h", "--help", "help", "-v", "--version"].includes(cmd)) {
|
|
6641
|
+
if (cmd && !["-h", "--help", "help", "-v", "--version", "wrap"].includes(cmd)) {
|
|
3718
6642
|
try {
|
|
3719
6643
|
syncHookFile();
|
|
3720
6644
|
} catch {
|
|
@@ -3731,18 +6655,24 @@ async function main(argv) {
|
|
|
3731
6655
|
case "--version":
|
|
3732
6656
|
process.stdout.write(packageVersion() + "\n");
|
|
3733
6657
|
return 0;
|
|
6658
|
+
case "install":
|
|
6659
|
+
return runInstall({ code: parseCode(argv), apiBase: parseApiBase(argv) });
|
|
3734
6660
|
case "setup":
|
|
3735
6661
|
return runSetup({ apiBase: parseApiBase(argv) });
|
|
3736
6662
|
case "enable":
|
|
3737
6663
|
return runEnable(parseTools(arg));
|
|
3738
6664
|
case "disable":
|
|
3739
6665
|
return runDisable(parseTools(arg));
|
|
6666
|
+
case "update":
|
|
6667
|
+
return runUpdate();
|
|
3740
6668
|
case "daemon":
|
|
3741
6669
|
return runDaemonCommand({ foreground: argv.includes("--foreground") || argv.includes("-f") });
|
|
3742
6670
|
case "status":
|
|
3743
6671
|
return runStatus();
|
|
3744
6672
|
case "logout":
|
|
3745
6673
|
return runLogout({ yes: argv.includes("--yes") || argv.includes("-y") });
|
|
6674
|
+
case "wrap":
|
|
6675
|
+
return runWrap(argv.slice(3));
|
|
3746
6676
|
default:
|
|
3747
6677
|
process.stderr.write(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
|
|
3748
6678
|
|