deskbuddy 0.2.0-beta.1 → 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 +3304 -409
- 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
|
}
|
|
@@ -195,19 +356,14 @@ async function installAutostart(t, platform = process.platform) {
|
|
|
195
356
|
return { ok: true, message: "\u5DF2\u8BBE\u7F6E\u5F00\u673A\u81EA\u542F\uFF08systemd --user\uFF09" };
|
|
196
357
|
}
|
|
197
358
|
if (platform === "win32") {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
SCHTASK_NAME,
|
|
207
|
-
"/tr",
|
|
208
|
-
`"${t.execPath}" "${t.cliPath}" daemon`
|
|
209
|
-
]);
|
|
210
|
-
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" };
|
|
211
367
|
}
|
|
212
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` };
|
|
213
369
|
} catch (e) {
|
|
@@ -233,8 +389,11 @@ async function uninstallAutostart(platform = process.platform) {
|
|
|
233
389
|
return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
|
|
234
390
|
}
|
|
235
391
|
if (platform === "win32") {
|
|
236
|
-
await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME]).catch(() => {
|
|
392
|
+
await run("schtasks", ["/delete", "/f", "/tn", SCHTASK_NAME], { windowsHide: true }).catch(() => {
|
|
237
393
|
});
|
|
394
|
+
const p = winStartupVbsPath();
|
|
395
|
+
if (existsSync2(p)) rmSync(p, { force: true });
|
|
396
|
+
await stopPidFileDaemon();
|
|
238
397
|
return { ok: true, message: "\u5DF2\u53D6\u6D88\u5F00\u673A\u81EA\u542F" };
|
|
239
398
|
}
|
|
240
399
|
return { ok: true, message: "" };
|
|
@@ -253,14 +412,16 @@ import { basename as basename6 } from "path";
|
|
|
253
412
|
import WebSocket2 from "ws";
|
|
254
413
|
|
|
255
414
|
// src/adapters/ocPlugin.ts
|
|
256
|
-
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";
|
|
257
416
|
import { dirname as dirname2, join as join3 } from "path";
|
|
417
|
+
import { pathToFileURL } from "url";
|
|
258
418
|
var OC_MARKER = "// deskbuddy:opencode-plugin";
|
|
259
|
-
function ocPluginSource(pluginModule, nodePath) {
|
|
419
|
+
function ocPluginSource(pluginModule, nodePath, platform = process.platform) {
|
|
420
|
+
const spec = platform === "win32" ? pathToFileURL(pluginModule).href : pluginModule;
|
|
260
421
|
return [
|
|
261
422
|
OC_MARKER,
|
|
262
423
|
"// \u7531 deskbuddy enable opencode \u751F\u6210\uFF0Cdeskbuddy disable opencode \u4F1A\u5220\u6389\u5B83\u3002",
|
|
263
|
-
`import { make } from ${JSON.stringify(
|
|
424
|
+
`import { make } from ${JSON.stringify(spec)}`,
|
|
264
425
|
`export const DeskBuddy = make({ node: ${JSON.stringify(nodePath)} })`,
|
|
265
426
|
""
|
|
266
427
|
].join("\n");
|
|
@@ -269,7 +430,7 @@ function ocPluginModule(hookPath) {
|
|
|
269
430
|
return join3(dirname2(hookPath), "opencode.mjs");
|
|
270
431
|
}
|
|
271
432
|
function installOcPlugin(pluginPath, hookPath, nodePath) {
|
|
272
|
-
if (existsSync3(pluginPath) && !
|
|
433
|
+
if (existsSync3(pluginPath) && !readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) {
|
|
273
434
|
throw new Error(`${pluginPath} \u5DF2\u5B58\u5728\u4E14\u4E0D\u662F DeskBuddy \u751F\u6210\u7684\uFF0C\u8BF7\u5148\u632A\u8D70\u518D\u91CD\u8BD5`);
|
|
274
435
|
}
|
|
275
436
|
mkdirSync3(dirname2(pluginPath), { recursive: true });
|
|
@@ -279,13 +440,13 @@ function installOcPlugin(pluginPath, hookPath, nodePath) {
|
|
|
279
440
|
}
|
|
280
441
|
function uninstallOcPlugin(pluginPath) {
|
|
281
442
|
try {
|
|
282
|
-
if (
|
|
443
|
+
if (readFileSync3(pluginPath, "utf8").includes(OC_MARKER)) unlinkSync(pluginPath);
|
|
283
444
|
} catch {
|
|
284
445
|
}
|
|
285
446
|
}
|
|
286
447
|
function isOcPluginInstalled(pluginPath) {
|
|
287
448
|
try {
|
|
288
|
-
return
|
|
449
|
+
return readFileSync3(pluginPath, "utf8").includes(OC_MARKER);
|
|
289
450
|
} catch {
|
|
290
451
|
return false;
|
|
291
452
|
}
|
|
@@ -293,14 +454,16 @@ function isOcPluginInstalled(pluginPath) {
|
|
|
293
454
|
var OC_NOTICE = " \u5DF2\u5F00\u7740\u7684 opencode \u7A97\u53E3\u91CD\u5F00\u540E\u751F\u6548";
|
|
294
455
|
|
|
295
456
|
// src/core/state.ts
|
|
296
|
-
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";
|
|
297
458
|
import { join as join4 } from "path";
|
|
298
459
|
|
|
299
460
|
// src/core/process.ts
|
|
300
|
-
import { execFileSync } from "child_process";
|
|
461
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
462
|
+
var WIN = process.platform === "win32";
|
|
301
463
|
function processName(pid) {
|
|
464
|
+
if (WIN) return winProcs()?.find((p) => p.pid === pid)?.name ?? null;
|
|
302
465
|
try {
|
|
303
|
-
const out =
|
|
466
|
+
const out = execFileSync2("ps", ["-p", String(pid), "-o", "comm="], {
|
|
304
467
|
encoding: "utf8",
|
|
305
468
|
stdio: ["ignore", "pipe", "ignore"]
|
|
306
469
|
});
|
|
@@ -318,6 +481,12 @@ function isAlive(pid, expectName) {
|
|
|
318
481
|
return false;
|
|
319
482
|
}
|
|
320
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
|
+
}
|
|
321
490
|
const name = processName(pid);
|
|
322
491
|
return name != null && name.toLowerCase().includes(expectName.toLowerCase());
|
|
323
492
|
}
|
|
@@ -332,7 +501,7 @@ function statePath(sessionId) {
|
|
|
332
501
|
}
|
|
333
502
|
function readState(sessionId) {
|
|
334
503
|
try {
|
|
335
|
-
return JSON.parse(
|
|
504
|
+
return JSON.parse(readFileSync4(statePath(sessionId), "utf8"));
|
|
336
505
|
} catch {
|
|
337
506
|
return null;
|
|
338
507
|
}
|
|
@@ -350,7 +519,7 @@ function readAliveStates(tool) {
|
|
|
350
519
|
if (!file.endsWith(".json")) continue;
|
|
351
520
|
let s;
|
|
352
521
|
try {
|
|
353
|
-
s = JSON.parse(
|
|
522
|
+
s = JSON.parse(readFileSync4(join4(stateDir(), file), "utf8"));
|
|
354
523
|
} catch {
|
|
355
524
|
rmSync2(join4(stateDir(), file), { force: true });
|
|
356
525
|
continue;
|
|
@@ -383,7 +552,7 @@ var ALIAS_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
383
552
|
function resolveAlias(sessionId, now = Date.now()) {
|
|
384
553
|
let map = {};
|
|
385
554
|
try {
|
|
386
|
-
map = JSON.parse(
|
|
555
|
+
map = JSON.parse(readFileSync4(join4(deskbuddyHome(), "alias.json"), "utf8")) || {};
|
|
387
556
|
} catch {
|
|
388
557
|
return sessionId;
|
|
389
558
|
}
|
|
@@ -397,13 +566,13 @@ function resolveAlias(sessionId, now = Date.now()) {
|
|
|
397
566
|
}
|
|
398
567
|
|
|
399
568
|
// src/core/api.ts
|
|
400
|
-
import { execFileSync as
|
|
569
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
401
570
|
import { createHash, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
402
|
-
import { readFileSync as
|
|
571
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
403
572
|
import { release } from "os";
|
|
404
573
|
|
|
405
574
|
// src/core/hookFile.ts
|
|
406
|
-
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";
|
|
407
576
|
import { createRequire } from "module";
|
|
408
577
|
import { dirname as dirname3, join as join5 } from "path";
|
|
409
578
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -438,7 +607,7 @@ function hookVersionPath(target = installedHookPath()) {
|
|
|
438
607
|
}
|
|
439
608
|
function installedHookVersion(target = installedHookPath()) {
|
|
440
609
|
try {
|
|
441
|
-
return
|
|
610
|
+
return readFileSync5(hookVersionPath(target), "utf8").trim() || null;
|
|
442
611
|
} catch {
|
|
443
612
|
return null;
|
|
444
613
|
}
|
|
@@ -482,6 +651,8 @@ var SERVICES = {
|
|
|
482
651
|
confirmAuth: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyconfirmauthapi"
|
|
483
652
|
};
|
|
484
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";
|
|
485
656
|
var APPROVAL_SERVICES = {
|
|
486
657
|
create: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcreateapi",
|
|
487
658
|
cancel: "com.shifeng.api.service.api.v2.deskbuddy.deskbuddyapprovalcancelapi",
|
|
@@ -548,12 +719,12 @@ function humanOs(platform = process.platform) {
|
|
|
548
719
|
return known[platform] ?? platform;
|
|
549
720
|
}
|
|
550
721
|
var defaultOsVersionDeps = {
|
|
551
|
-
runSwVers: () =>
|
|
722
|
+
runSwVers: () => execFileSync3("sw_vers", ["-productVersion"], {
|
|
552
723
|
encoding: "utf8",
|
|
553
724
|
stdio: ["ignore", "pipe", "ignore"],
|
|
554
725
|
timeout: 2e3
|
|
555
726
|
}),
|
|
556
|
-
readOsRelease: () =>
|
|
727
|
+
readOsRelease: () => readFileSync6("/etc/os-release", "utf8"),
|
|
557
728
|
kernelRelease: () => release()
|
|
558
729
|
};
|
|
559
730
|
function productOsVersion(platform = process.platform, deps = defaultOsVersionDeps) {
|
|
@@ -619,7 +790,10 @@ var api = {
|
|
|
619
790
|
approvalCreate: (payload) => call(APPROVAL_SERVICES.create, payload),
|
|
620
791
|
approvalCancel: (approvalId, reason) => call(APPROVAL_SERVICES.cancel, { approvalId, reason }),
|
|
621
792
|
approvalSync: (approvalIds) => call(APPROVAL_SERVICES.sync, { approvalIds }),
|
|
622
|
-
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)
|
|
623
797
|
};
|
|
624
798
|
|
|
625
799
|
// src/core/report.ts
|
|
@@ -634,7 +808,8 @@ async function postReport(payload) {
|
|
|
634
808
|
import { basename as basename2 } from "path";
|
|
635
809
|
var KEEPALIVE_MS = 2 * 60 * 1e3;
|
|
636
810
|
var MAX_SESSIONS = 20;
|
|
637
|
-
|
|
811
|
+
var CAPS = ["cmd_result", "window_inject"];
|
|
812
|
+
function buildSnapshot(states, tool, cfg, nowMs, remote) {
|
|
638
813
|
const sessions = states.filter((s) => s.tool === tool).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS).map((s) => ({
|
|
639
814
|
sessionId: s.sessionId,
|
|
640
815
|
title: s.title,
|
|
@@ -643,14 +818,15 @@ function buildSnapshot(states, tool, cfg, nowMs) {
|
|
|
643
818
|
state: s.state,
|
|
644
819
|
startedAt: s.startedAt,
|
|
645
820
|
updatedAt: s.updatedAt,
|
|
646
|
-
interactive: s.interactive === true
|
|
821
|
+
interactive: s.interactive === true,
|
|
822
|
+
remoteWindow: remote ? remote.has(s.sessionId) : s.remoteWindow === true
|
|
647
823
|
}));
|
|
648
|
-
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] };
|
|
649
825
|
}
|
|
650
826
|
|
|
651
827
|
// src/core/dirs.ts
|
|
652
828
|
import { createHmac, randomBytes as randomBytes2 } from "crypto";
|
|
653
|
-
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";
|
|
654
830
|
import { basename as basename3, join as join6 } from "path";
|
|
655
831
|
var DIRS_REPORT_MAX = 10;
|
|
656
832
|
function dirsPath() {
|
|
@@ -659,7 +835,7 @@ function dirsPath() {
|
|
|
659
835
|
function dirSecret() {
|
|
660
836
|
const file = join6(deskbuddyHome(), "dir-secret");
|
|
661
837
|
try {
|
|
662
|
-
const s2 =
|
|
838
|
+
const s2 = readFileSync7(file, "utf8").trim();
|
|
663
839
|
if (s2) return s2;
|
|
664
840
|
} catch {
|
|
665
841
|
}
|
|
@@ -677,7 +853,7 @@ function dirRef(path) {
|
|
|
677
853
|
}
|
|
678
854
|
function readMap() {
|
|
679
855
|
try {
|
|
680
|
-
const m = JSON.parse(
|
|
856
|
+
const m = JSON.parse(readFileSync7(dirsPath(), "utf8"));
|
|
681
857
|
const out = {};
|
|
682
858
|
for (const [k, v] of Object.entries(m ?? {})) if (typeof v === "number") out[k] = v;
|
|
683
859
|
return out;
|
|
@@ -773,7 +949,7 @@ function isExpired(cmd, now = Date.now()) {
|
|
|
773
949
|
}
|
|
774
950
|
|
|
775
951
|
// src/daemon/executor.ts
|
|
776
|
-
import { execFile as execFile2 } from "child_process";
|
|
952
|
+
import { execFile as execFile2, execFileSync as execFileSync4 } from "child_process";
|
|
777
953
|
import { homedir as homedir4 } from "os";
|
|
778
954
|
import { join as join8 } from "path";
|
|
779
955
|
|
|
@@ -787,6 +963,19 @@ function waitModeFor(tool) {
|
|
|
787
963
|
function shQuote(s) {
|
|
788
964
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
789
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
|
+
}
|
|
790
979
|
function defaultGateScript() {
|
|
791
980
|
const candidates = [
|
|
792
981
|
fileURLToPath3(new URL("./gate.cjs", import.meta.url)),
|
|
@@ -794,17 +983,17 @@ function defaultGateScript() {
|
|
|
794
983
|
];
|
|
795
984
|
return candidates.find((p) => existsSync7(p)) ?? candidates[0];
|
|
796
985
|
}
|
|
797
|
-
function gateCommand(t) {
|
|
986
|
+
function gateCommand(t, platform = process.platform) {
|
|
987
|
+
if (platform === "win32") return winGateCommand(t);
|
|
798
988
|
return `${shQuote(t.nodePath)} ${shQuote(t.gateScript)} --run ${t.runId} || exit 2`;
|
|
799
989
|
}
|
|
800
|
-
function
|
|
801
|
-
return
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
});
|
|
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] }] } });
|
|
808
997
|
}
|
|
809
998
|
function codexGateArgs(t) {
|
|
810
999
|
const hook = `hooks.PreToolUse=[{matcher="*",hooks=[{type="command",command=${JSON.stringify(gateCommand(t))},timeout=${GATE_HOOK_TIMEOUT_S}}]}]`;
|
|
@@ -820,7 +1009,7 @@ function codexGateArgs(t) {
|
|
|
820
1009
|
}
|
|
821
1010
|
|
|
822
1011
|
// src/daemon/locate.ts
|
|
823
|
-
import { accessSync, constants, existsSync as existsSync8, statSync } from "fs";
|
|
1012
|
+
import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync8, statSync } from "fs";
|
|
824
1013
|
import { homedir as homedir3 } from "os";
|
|
825
1014
|
import { delimiter, dirname as dirname4, join as join7 } from "path";
|
|
826
1015
|
var defaultLocateDeps = () => ({
|
|
@@ -836,9 +1025,33 @@ var defaultLocateDeps = () => ({
|
|
|
836
1025
|
return false;
|
|
837
1026
|
}
|
|
838
1027
|
},
|
|
839
|
-
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")]
|
|
840
1041
|
});
|
|
841
|
-
|
|
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
|
+
}
|
|
842
1055
|
return [
|
|
843
1056
|
join7(home, ".local", "bin"),
|
|
844
1057
|
join7(home, ".claude", "local"),
|
|
@@ -851,19 +1064,110 @@ function extraBinDirs(home, nodePath = "") {
|
|
|
851
1064
|
];
|
|
852
1065
|
}
|
|
853
1066
|
function whichIn(name, d) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
+
}
|
|
858
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] };
|
|
859
1113
|
return null;
|
|
860
1114
|
}
|
|
861
1115
|
function appDirs(home) {
|
|
862
1116
|
return ["/Applications", join7(home, "Applications")];
|
|
863
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
|
+
}
|
|
864
1167
|
var WORKBUDDY_CLI_IN_APP = "WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy";
|
|
865
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";
|
|
866
1169
|
function locate(tool, d = defaultLocateDeps()) {
|
|
1170
|
+
if (isWin(d)) return locateWin(tool, d);
|
|
867
1171
|
const plain = (name) => {
|
|
868
1172
|
const f = whichIn(name, d);
|
|
869
1173
|
return f ? { file: f, prefixArgs: [] } : null;
|
|
@@ -898,7 +1202,8 @@ function locate(tool, d = defaultLocateDeps()) {
|
|
|
898
1202
|
}
|
|
899
1203
|
function childPath(d = defaultLocateDeps()) {
|
|
900
1204
|
const seen = /* @__PURE__ */ new Set();
|
|
901
|
-
|
|
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);
|
|
902
1207
|
}
|
|
903
1208
|
|
|
904
1209
|
// src/daemon/executor.ts
|
|
@@ -1075,11 +1380,11 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1075
1380
|
startedAt = Date.now();
|
|
1076
1381
|
timer = setTimeout(() => {
|
|
1077
1382
|
timedOut = true;
|
|
1078
|
-
child
|
|
1383
|
+
killTree(child);
|
|
1079
1384
|
}, Math.max(0, remaining));
|
|
1080
1385
|
};
|
|
1081
1386
|
let timedOut = false;
|
|
1082
|
-
const done = new Promise((
|
|
1387
|
+
const done = new Promise((resolve5) => {
|
|
1083
1388
|
child = execFile2(
|
|
1084
1389
|
plan.file,
|
|
1085
1390
|
plan.args,
|
|
@@ -1087,7 +1392,9 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1087
1392
|
cwd: plan.cwd,
|
|
1088
1393
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
1089
1394
|
// agent 自己要读 PATH 里的东西,环境原样继承
|
|
1090
|
-
env:
|
|
1395
|
+
env: mergeEnv(process.env, plan.env),
|
|
1396
|
+
// Windows:别给子进程弹黑框
|
|
1397
|
+
windowsHide: true
|
|
1091
1398
|
},
|
|
1092
1399
|
(err, stdout7) => {
|
|
1093
1400
|
finished = true;
|
|
@@ -1095,10 +1402,10 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1095
1402
|
const out = String(stdout7 ?? "");
|
|
1096
1403
|
if (err || timedOut) {
|
|
1097
1404
|
const msg = timedOut ? `\u6267\u884C\u8D85\u8FC7 ${Math.round(limitMs / 1e3)} \u79D2\u6D3B\u52A8\u65F6\u95F4\uFF0C\u5DF2\u7EC8\u6B62` : err.message;
|
|
1098
|
-
|
|
1405
|
+
resolve5({ ok: false, sessionId: parseSessionId(tool, out), stdout: out, error: msg });
|
|
1099
1406
|
return;
|
|
1100
1407
|
}
|
|
1101
|
-
|
|
1408
|
+
resolve5({ ok: true, sessionId: parseSessionId(tool, out), stdout: out });
|
|
1102
1409
|
}
|
|
1103
1410
|
);
|
|
1104
1411
|
child?.stdin?.end();
|
|
@@ -1120,10 +1427,31 @@ function startCommand(plan, tool, limitMs = EXEC_TIMEOUT_MS) {
|
|
|
1120
1427
|
}
|
|
1121
1428
|
};
|
|
1122
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
|
+
}
|
|
1123
1451
|
|
|
1124
1452
|
// src/daemon/gate/approvals.ts
|
|
1125
1453
|
import { createHash as createHash2 } from "crypto";
|
|
1126
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
1454
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
1127
1455
|
import { join as join9 } from "path";
|
|
1128
1456
|
var RETAIN_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1129
1457
|
function fingerprint(toolName, toolInput) {
|
|
@@ -1145,7 +1473,7 @@ var ApprovalStore = class _ApprovalStore {
|
|
|
1145
1473
|
}
|
|
1146
1474
|
static load() {
|
|
1147
1475
|
try {
|
|
1148
|
-
const raw = JSON.parse(
|
|
1476
|
+
const raw = JSON.parse(readFileSync9(storePath(), "utf8"));
|
|
1149
1477
|
return new _ApprovalStore(Array.isArray(raw) ? raw : []);
|
|
1150
1478
|
} catch {
|
|
1151
1479
|
return new _ApprovalStore();
|
|
@@ -1341,12 +1669,12 @@ function isReadOnlyCommand(command) {
|
|
|
1341
1669
|
}
|
|
1342
1670
|
function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
|
|
1343
1671
|
const roots = (Array.isArray(cwd) ? cwd : [cwd]).filter(Boolean);
|
|
1344
|
-
const
|
|
1672
|
+
const real2 = opts.real;
|
|
1345
1673
|
const inside = (p) => {
|
|
1346
1674
|
if (!roots.some((r) => isInside(r, p))) return false;
|
|
1347
|
-
if (!
|
|
1348
|
-
const rp =
|
|
1349
|
-
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));
|
|
1350
1678
|
};
|
|
1351
1679
|
const rel = (p) => {
|
|
1352
1680
|
const r = roots.find((x) => isInside(x, p)) ?? roots[0] ?? "";
|
|
@@ -1363,7 +1691,7 @@ function classify(toolName, toolInput, cwd, policy = "commands", opts = {}) {
|
|
|
1363
1691
|
if (paths) {
|
|
1364
1692
|
const allInside = paths.length > 0 && paths.every(inside);
|
|
1365
1693
|
const touchesProtected = paths.some(
|
|
1366
|
-
(p) => isProtectedPath(roots[0] ?? "/", p) || (
|
|
1694
|
+
(p) => isProtectedPath(roots[0] ?? "/", p) || (real2 ? isProtectedPath("/", real2(resolve(roots[0] ?? "/", p))) : false)
|
|
1367
1695
|
);
|
|
1368
1696
|
if (policy === "commands" && allInside && !touchesProtected) return { action: "auto" };
|
|
1369
1697
|
const shown = paths.map((p) => inside(p) ? rel(p) : p);
|
|
@@ -1386,11 +1714,14 @@ function describe(toolName, toolInput, cwd) {
|
|
|
1386
1714
|
}
|
|
1387
1715
|
|
|
1388
1716
|
// src/daemon/gate/procs.ts
|
|
1389
|
-
import { execFileSync as
|
|
1717
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1390
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
|
+
}
|
|
1391
1722
|
let out = "";
|
|
1392
1723
|
try {
|
|
1393
|
-
out =
|
|
1724
|
+
out = execFileSync5("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], {
|
|
1394
1725
|
encoding: "utf8",
|
|
1395
1726
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1396
1727
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -1398,19 +1729,28 @@ function listDescendants(rootPid) {
|
|
|
1398
1729
|
} catch {
|
|
1399
1730
|
return [];
|
|
1400
1731
|
}
|
|
1401
|
-
const
|
|
1732
|
+
const rows = [];
|
|
1402
1733
|
for (const line of out.split("\n")) {
|
|
1403
1734
|
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
1404
|
-
if (
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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);
|
|
1409
1746
|
}
|
|
1747
|
+
const seen = /* @__PURE__ */ new Set([rootPid]);
|
|
1410
1748
|
const res = [];
|
|
1411
1749
|
const stack = [rootPid];
|
|
1412
1750
|
while (stack.length > 0) {
|
|
1413
1751
|
for (const c of kids.get(stack.pop()) ?? []) {
|
|
1752
|
+
if (seen.has(c.pid)) continue;
|
|
1753
|
+
seen.add(c.pid);
|
|
1414
1754
|
res.push(c);
|
|
1415
1755
|
stack.push(c.pid);
|
|
1416
1756
|
}
|
|
@@ -1439,9 +1779,9 @@ function pickRememberSuggestion(suggestions) {
|
|
|
1439
1779
|
return null;
|
|
1440
1780
|
}
|
|
1441
1781
|
var DEVICE_FALLBACK = "\u684C\u9762\u642D\u5B50";
|
|
1442
|
-
function windowNotice(decision,
|
|
1782
|
+
function windowNotice(decision, summary2, deviceName = "") {
|
|
1443
1783
|
const who = deviceName ? `\u300C${deviceName}\u300D` : DEVICE_FALLBACK;
|
|
1444
|
-
const what =
|
|
1784
|
+
const what = summary2.length > 60 ? summary2.slice(0, 59) + "\u2026" : summary2;
|
|
1445
1785
|
return decision === "allow" ? `\u2705 \u5DF2\u5728${who}\u4E0A\u5141\u8BB8\uFF1A${what}` : `\u274C \u5DF2\u5728${who}\u4E0A\u62D2\u7EDD\uFF1A${what}`;
|
|
1446
1786
|
}
|
|
1447
1787
|
var WINDOW_SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
@@ -1474,17 +1814,17 @@ var ApprovalCoordinator = class {
|
|
|
1474
1814
|
sessionOf(runId) {
|
|
1475
1815
|
return this.#runs.get(runId)?.sessionId ?? "";
|
|
1476
1816
|
}
|
|
1477
|
-
async onGate(
|
|
1478
|
-
if (
|
|
1479
|
-
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 ?? "");
|
|
1480
1820
|
if (!run2) return { decision: "deny", reason: REASON_UNKNOWN_RUN };
|
|
1481
1821
|
if (run2.superseded) return { decision: "deny", reason: REASON_SUPERSEDED };
|
|
1482
|
-
const toolName = typeof
|
|
1483
|
-
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 || "";
|
|
1484
1824
|
if (sessionId && !run2.sessionId) run2.sessionId = sessionId;
|
|
1485
|
-
const v = classify(toolName,
|
|
1825
|
+
const v = classify(toolName, req2.hook.tool_input, cwdForms(run2.cwd), this.#d.policy(), { real: realpathNearest });
|
|
1486
1826
|
if (v.action === "auto") return { decision: "allow" };
|
|
1487
|
-
const fp = fingerprint(toolName,
|
|
1827
|
+
const fp = fingerprint(toolName, req2.hook.tool_input);
|
|
1488
1828
|
const store = this.#d.store;
|
|
1489
1829
|
if (run2.mode === "defer" && sessionId) {
|
|
1490
1830
|
const prev = store.latestFor(sessionId, fp);
|
|
@@ -1513,7 +1853,7 @@ var ApprovalCoordinator = class {
|
|
|
1513
1853
|
store.save();
|
|
1514
1854
|
let early = null;
|
|
1515
1855
|
let settle;
|
|
1516
|
-
const decided = new Promise((
|
|
1856
|
+
const decided = new Promise((resolve5) => settle = resolve5);
|
|
1517
1857
|
this.#waiters.set(rec.approvalId, (r) => {
|
|
1518
1858
|
early = r;
|
|
1519
1859
|
settle(r);
|
|
@@ -1537,8 +1877,8 @@ var ApprovalCoordinator = class {
|
|
|
1537
1877
|
}
|
|
1538
1878
|
run2.onWaitStart?.();
|
|
1539
1879
|
try {
|
|
1540
|
-
return await new Promise((
|
|
1541
|
-
void decided.then(
|
|
1880
|
+
return await new Promise((resolve5) => {
|
|
1881
|
+
void decided.then(resolve5);
|
|
1542
1882
|
const onAbort = () => {
|
|
1543
1883
|
if (!this.#waiters.delete(rec.approvalId)) return;
|
|
1544
1884
|
if (rec.status === "pending") {
|
|
@@ -1547,7 +1887,7 @@ var ApprovalCoordinator = class {
|
|
|
1547
1887
|
store.save();
|
|
1548
1888
|
void this.#cancelRemote(rec.approvalId, "\u667A\u80FD\u4F53\u8FDB\u7A0B\u5DF2\u7ED3\u675F");
|
|
1549
1889
|
}
|
|
1550
|
-
|
|
1890
|
+
resolve5({ decision: "deny", reason: "\u5DF2\u53D6\u6D88" });
|
|
1551
1891
|
};
|
|
1552
1892
|
if (signal.aborted) onAbort();
|
|
1553
1893
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1570,7 +1910,7 @@ var ApprovalCoordinator = class {
|
|
|
1570
1910
|
if (!toolName || !sessionId || WINDOW_SKIP_TOOLS.has(toolName)) return none;
|
|
1571
1911
|
for (const run2 of this.#runs.values()) if (run2.sessionId === sessionId) return none;
|
|
1572
1912
|
const store = this.#d.store;
|
|
1573
|
-
const { kind, summary } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
1913
|
+
const { kind, summary: summary2 } = describe(toolName, hook.tool_input, cwd ? cwdForms(cwd) : []);
|
|
1574
1914
|
const remember = tool === "claude_code" ? pickRememberSuggestion(hook.permission_suggestions) : null;
|
|
1575
1915
|
const rec = {
|
|
1576
1916
|
approvalId: randomUUID3(),
|
|
@@ -1580,7 +1920,7 @@ var ApprovalCoordinator = class {
|
|
|
1580
1920
|
cwd,
|
|
1581
1921
|
fingerprint: fingerprint(toolName, hook.tool_input),
|
|
1582
1922
|
kind,
|
|
1583
|
-
summary,
|
|
1923
|
+
summary: summary2,
|
|
1584
1924
|
mode: "window",
|
|
1585
1925
|
status: "pending",
|
|
1586
1926
|
createdAt: this.#now(),
|
|
@@ -1599,7 +1939,7 @@ var ApprovalCoordinator = class {
|
|
|
1599
1939
|
...watchProcs ? { pid, cmd, baseline: new Set(this.#descendants(pid).map((p) => p.pid)) } : {}
|
|
1600
1940
|
});
|
|
1601
1941
|
let settle;
|
|
1602
|
-
const decided = new Promise((
|
|
1942
|
+
const decided = new Promise((resolve5) => settle = resolve5);
|
|
1603
1943
|
this.#waiters.set(rec.approvalId, settle);
|
|
1604
1944
|
try {
|
|
1605
1945
|
await this.#d.backend.create(rec);
|
|
@@ -1615,11 +1955,11 @@ var ApprovalCoordinator = class {
|
|
|
1615
1955
|
}
|
|
1616
1956
|
this.#d.log(`\u7A97\u53E3\u6388\u6743 ${rec.approvalId} \u5DF2\u63A8\u5230\u8BBE\u5907 kind=${rec.kind}`);
|
|
1617
1957
|
if (rec.status !== "pending") void this.#cancelRemote(rec.approvalId, "\u7535\u8111\u4E0A\u5DF2\u5904\u7406");
|
|
1618
|
-
return new Promise((
|
|
1619
|
-
void decided.then(
|
|
1958
|
+
return new Promise((resolve5) => {
|
|
1959
|
+
void decided.then(resolve5);
|
|
1620
1960
|
const onAbort = () => {
|
|
1621
1961
|
if (this.#waiters.has(rec.approvalId)) this.#settleWindow(rec, "hook \u5DF2\u7ED3\u675F");
|
|
1622
|
-
|
|
1962
|
+
resolve5(none);
|
|
1623
1963
|
};
|
|
1624
1964
|
if (signal.aborted) onAbort();
|
|
1625
1965
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -1893,8 +2233,8 @@ function realpathNearest(p) {
|
|
|
1893
2233
|
}
|
|
1894
2234
|
function cwdForms(cwd) {
|
|
1895
2235
|
try {
|
|
1896
|
-
const
|
|
1897
|
-
return
|
|
2236
|
+
const real2 = realpathSync(cwd);
|
|
2237
|
+
return real2 === cwd ? [cwd] : [cwd, real2];
|
|
1898
2238
|
} catch {
|
|
1899
2239
|
return [cwd];
|
|
1900
2240
|
}
|
|
@@ -1926,10 +2266,10 @@ var GateServer = class {
|
|
|
1926
2266
|
}
|
|
1927
2267
|
/** 起监听并写 gate.json。返回实际端口 */
|
|
1928
2268
|
async start() {
|
|
1929
|
-
const server = createServer((
|
|
1930
|
-
await new Promise((
|
|
2269
|
+
const server = createServer((req2, res) => void this.#serve(req2, res));
|
|
2270
|
+
await new Promise((resolve5, reject) => {
|
|
1931
2271
|
server.once("error", reject);
|
|
1932
|
-
server.listen(0, "127.0.0.1", () =>
|
|
2272
|
+
server.listen(0, "127.0.0.1", () => resolve5());
|
|
1933
2273
|
});
|
|
1934
2274
|
this.#server = server;
|
|
1935
2275
|
const addr = server.address();
|
|
@@ -1950,21 +2290,21 @@ var GateServer = class {
|
|
|
1950
2290
|
}
|
|
1951
2291
|
const s = this.#server;
|
|
1952
2292
|
this.#server = null;
|
|
1953
|
-
if (s) await new Promise((
|
|
2293
|
+
if (s) await new Promise((resolve5) => s.close(() => resolve5()));
|
|
1954
2294
|
}
|
|
1955
|
-
async #serve(
|
|
2295
|
+
async #serve(req2, res) {
|
|
1956
2296
|
const reply = (status, body2) => {
|
|
1957
2297
|
if (res.writableEnded || res.destroyed) return;
|
|
1958
2298
|
res.writeHead(status, { "content-type": "application/json" });
|
|
1959
2299
|
res.end(body2 ? JSON.stringify(body2) : "");
|
|
1960
2300
|
};
|
|
1961
|
-
if (
|
|
1962
|
-
const token =
|
|
2301
|
+
if (req2.method !== "POST" || req2.url !== "/gate") return reply(404);
|
|
2302
|
+
const token = req2.headers["x-gate-token"];
|
|
1963
2303
|
if (typeof token !== "string" || !tokenEquals(token, this.#token)) return reply(403);
|
|
1964
2304
|
let body = "";
|
|
1965
2305
|
let size = 0;
|
|
1966
2306
|
try {
|
|
1967
|
-
for await (const chunk of
|
|
2307
|
+
for await (const chunk of req2) {
|
|
1968
2308
|
size += chunk.length;
|
|
1969
2309
|
if (size > MAX_BODY_BYTES) return reply(413);
|
|
1970
2310
|
body += String(chunk);
|
|
@@ -1986,12 +2326,12 @@ var GateServer = class {
|
|
|
1986
2326
|
if (!res.writableEnded) ac.abort();
|
|
1987
2327
|
});
|
|
1988
2328
|
try {
|
|
1989
|
-
const
|
|
2329
|
+
const req3 = window ? {
|
|
1990
2330
|
window: true,
|
|
1991
2331
|
tool: typeof parsed.tool === "string" && /^[a-z_]{1,32}$/.test(parsed.tool) ? parsed.tool : "claude_code",
|
|
1992
2332
|
hook: parsed.hook
|
|
1993
2333
|
} : { runId: parsed.runId, hook: parsed.hook };
|
|
1994
|
-
const out = await this.#handler(
|
|
2334
|
+
const out = await this.#handler(req3, ac.signal);
|
|
1995
2335
|
reply(200, out);
|
|
1996
2336
|
} catch {
|
|
1997
2337
|
reply(500);
|
|
@@ -2108,6 +2448,10 @@ var SessionQueue = class {
|
|
|
2108
2448
|
});
|
|
2109
2449
|
return next;
|
|
2110
2450
|
}
|
|
2451
|
+
/** 这个会话有没有在跑 / 在排队的任务 */
|
|
2452
|
+
busy(sessionId) {
|
|
2453
|
+
return this.#tails.has(sessionId || "");
|
|
2454
|
+
}
|
|
2111
2455
|
/** 当前有多少个会话still有在跑/排队的任务。只给测试和诊断用 */
|
|
2112
2456
|
get activeSessions() {
|
|
2113
2457
|
return this.#tails.size;
|
|
@@ -2121,7 +2465,7 @@ var SessionQueue = class {
|
|
|
2121
2465
|
};
|
|
2122
2466
|
|
|
2123
2467
|
// src/daemon/remote.ts
|
|
2124
|
-
import { mkdirSync as mkdirSync10, readFileSync as
|
|
2468
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
2125
2469
|
import { join as join13 } from "path";
|
|
2126
2470
|
function handleDir() {
|
|
2127
2471
|
return join13(deskbuddyHome(), "remote");
|
|
@@ -2132,7 +2476,7 @@ function handlePath(tool) {
|
|
|
2132
2476
|
}
|
|
2133
2477
|
function readHandle(tool) {
|
|
2134
2478
|
try {
|
|
2135
|
-
const h = JSON.parse(
|
|
2479
|
+
const h = JSON.parse(readFileSync10(handlePath(tool), "utf8"));
|
|
2136
2480
|
if (typeof h?.sessionId !== "string" || !h.sessionId) return null;
|
|
2137
2481
|
if (typeof h?.cwd !== "string" || !h.cwd) return null;
|
|
2138
2482
|
return h;
|
|
@@ -2198,7 +2542,7 @@ function failureMessage(reason) {
|
|
|
2198
2542
|
}
|
|
2199
2543
|
|
|
2200
2544
|
// src/daemon/seen.ts
|
|
2201
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
2545
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync11, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
2202
2546
|
import { join as join14 } from "path";
|
|
2203
2547
|
var SEEN_CAPACITY = 200;
|
|
2204
2548
|
function seenPath() {
|
|
@@ -2233,7 +2577,7 @@ var SeenIds = class _SeenIds {
|
|
|
2233
2577
|
}
|
|
2234
2578
|
static load() {
|
|
2235
2579
|
try {
|
|
2236
|
-
const raw = JSON.parse(
|
|
2580
|
+
const raw = JSON.parse(readFileSync11(seenPath(), "utf8"));
|
|
2237
2581
|
return new _SeenIds(Array.isArray(raw?.ids) ? raw.ids.filter((x) => typeof x === "string") : []);
|
|
2238
2582
|
} catch {
|
|
2239
2583
|
return new _SeenIds();
|
|
@@ -2251,11 +2595,25 @@ var SeenIds = class _SeenIds {
|
|
|
2251
2595
|
};
|
|
2252
2596
|
|
|
2253
2597
|
// src/daemon/codexWatch.ts
|
|
2254
|
-
import { execFileSync as
|
|
2598
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2255
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;
|
|
2256
2603
|
var RESUME_RETRY_MS = 1e3;
|
|
2257
2604
|
var RESUME_MAX_TRIES = 120;
|
|
2258
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
|
+
}
|
|
2259
2617
|
function isLoopbackWs(url) {
|
|
2260
2618
|
try {
|
|
2261
2619
|
const u = new URL(url);
|
|
@@ -2274,17 +2632,29 @@ function parseAppServerUrls(psOutput) {
|
|
|
2274
2632
|
return urls;
|
|
2275
2633
|
}
|
|
2276
2634
|
function discoverAppServer(configured) {
|
|
2277
|
-
|
|
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
|
+
}
|
|
2278
2642
|
try {
|
|
2279
|
-
const out =
|
|
2280
|
-
return parseAppServerUrls(out)
|
|
2643
|
+
const out = execFileSync6("ps", ["-A", "-ww", "-o", "args="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
2644
|
+
return parseAppServerUrls(out);
|
|
2281
2645
|
} catch {
|
|
2282
|
-
return
|
|
2646
|
+
return [];
|
|
2283
2647
|
}
|
|
2284
2648
|
}
|
|
2285
2649
|
function unwrapShell(command) {
|
|
2286
2650
|
const m = command.match(/^\/bin\/(?:ba|z)?sh -lc '([\s\S]*)'$/);
|
|
2287
|
-
|
|
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;
|
|
2288
2658
|
}
|
|
2289
2659
|
var CodexWatch = class {
|
|
2290
2660
|
#d;
|
|
@@ -2306,17 +2676,48 @@ var CodexWatch = class {
|
|
|
2306
2676
|
#idle = /* @__PURE__ */ new Set();
|
|
2307
2677
|
/** 连接代数:断线重连后,上一条连接遗留的订阅重试不再跑 */
|
|
2308
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();
|
|
2309
2699
|
constructor(deps) {
|
|
2310
2700
|
this.#d = { connect: (url) => new WebSocket(url), ...deps };
|
|
2311
2701
|
}
|
|
2312
2702
|
get connected() {
|
|
2313
2703
|
return this.#ws !== null;
|
|
2314
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
|
+
}
|
|
2315
2715
|
/** 定时调:没连上就找一次 app-server 连上去;连着时 codex 被 disable 了就断开 */
|
|
2316
2716
|
tick() {
|
|
2317
2717
|
if (this.#stopped) return;
|
|
2318
2718
|
if (this.#ws) {
|
|
2319
2719
|
if (this.#d.enabled && !this.#d.enabled()) this.#teardown();
|
|
2720
|
+
else if (this.#ready) void this.#syncLoaded();
|
|
2320
2721
|
return;
|
|
2321
2722
|
}
|
|
2322
2723
|
const url = (this.#d.discover ?? (() => discoverAppServer()))();
|
|
@@ -2339,10 +2740,10 @@ var CodexWatch = class {
|
|
|
2339
2740
|
this.#gen++;
|
|
2340
2741
|
ws.on("open", () => void this.#onOpen());
|
|
2341
2742
|
ws.on("message", (buf) => this.#onMessage(String(buf)));
|
|
2342
|
-
ws.on("close", () => this.#teardown());
|
|
2343
|
-
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 ?? ""}`));
|
|
2344
2745
|
}
|
|
2345
|
-
#teardown() {
|
|
2746
|
+
#teardown(why = "") {
|
|
2346
2747
|
const ws = this.#ws;
|
|
2347
2748
|
if (!ws) return;
|
|
2348
2749
|
this.#ws = null;
|
|
@@ -2352,7 +2753,7 @@ var CodexWatch = class {
|
|
|
2352
2753
|
}
|
|
2353
2754
|
for (const c of this.#calls.values()) {
|
|
2354
2755
|
clearTimeout(c.timer);
|
|
2355
|
-
c.resolve({ error: { message: "disconnected" } });
|
|
2756
|
+
c.resolve({ error: { message: "disconnected" }, local: true });
|
|
2356
2757
|
}
|
|
2357
2758
|
this.#calls.clear();
|
|
2358
2759
|
for (const { ac } of this.#pending.values()) ac.abort();
|
|
@@ -2362,7 +2763,17 @@ var CodexWatch = class {
|
|
|
2362
2763
|
this.#cwd.clear();
|
|
2363
2764
|
this.#changes.clear();
|
|
2364
2765
|
this.#idle.clear();
|
|
2365
|
-
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` : ""}`);
|
|
2366
2777
|
}
|
|
2367
2778
|
#send(obj) {
|
|
2368
2779
|
try {
|
|
@@ -2372,13 +2783,13 @@ var CodexWatch = class {
|
|
|
2372
2783
|
}
|
|
2373
2784
|
#request(method, params) {
|
|
2374
2785
|
const id = this.#nextId++;
|
|
2375
|
-
return new Promise((
|
|
2786
|
+
return new Promise((resolve5) => {
|
|
2376
2787
|
const timer = setTimeout(() => {
|
|
2377
2788
|
this.#calls.delete(id);
|
|
2378
|
-
|
|
2789
|
+
resolve5({ error: { message: "timeout" }, local: true });
|
|
2379
2790
|
}, REQUEST_TIMEOUT_MS);
|
|
2380
2791
|
timer.unref?.();
|
|
2381
|
-
this.#calls.set(id, { resolve:
|
|
2792
|
+
this.#calls.set(id, { resolve: resolve5, timer });
|
|
2382
2793
|
this.#send({ id, method, params });
|
|
2383
2794
|
});
|
|
2384
2795
|
}
|
|
@@ -2390,7 +2801,64 @@ var CodexWatch = class {
|
|
|
2390
2801
|
return;
|
|
2391
2802
|
}
|
|
2392
2803
|
this.#send({ method: "initialized" });
|
|
2804
|
+
this.#ready = true;
|
|
2393
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
|
+
}
|
|
2394
2862
|
}
|
|
2395
2863
|
#subscribe(threadId) {
|
|
2396
2864
|
if (this.#subscribed.has(threadId) || this.#retrying.has(threadId)) return;
|
|
@@ -2398,16 +2866,26 @@ var CodexWatch = class {
|
|
|
2398
2866
|
const gen = this.#gen;
|
|
2399
2867
|
const attempt = async () => {
|
|
2400
2868
|
if (!this.#ws || gen !== this.#gen || !this.#retrying.has(threadId)) return;
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2403
|
-
|
|
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
|
+
}
|
|
2404
2883
|
return;
|
|
2405
2884
|
}
|
|
2406
2885
|
if (!r.error) {
|
|
2407
2886
|
this.#retrying.delete(threadId);
|
|
2408
2887
|
this.#subscribed.add(threadId);
|
|
2409
|
-
|
|
2410
|
-
if (typeof cwd === "string") this.#cwd.set(threadId, cwd);
|
|
2888
|
+
this.#applyThread(threadId, r);
|
|
2411
2889
|
return;
|
|
2412
2890
|
}
|
|
2413
2891
|
const n = (this.#retrying.get(threadId) ?? 0) + 1;
|
|
@@ -2425,9 +2903,194 @@ var CodexWatch = class {
|
|
|
2425
2903
|
this.#retrying.delete(threadId);
|
|
2426
2904
|
if (!this.#subscribed.has(threadId)) return;
|
|
2427
2905
|
for (const p of this.#pending.values()) if (p.threadId === threadId) return;
|
|
2906
|
+
if ((this.#own.get(threadId) ?? 0) > 0) return;
|
|
2428
2907
|
this.#subscribed.delete(threadId);
|
|
2429
2908
|
void this.#request("thread/unsubscribe", { threadId });
|
|
2430
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
|
+
}
|
|
2431
3094
|
#onMessage(raw) {
|
|
2432
3095
|
let m;
|
|
2433
3096
|
try {
|
|
@@ -2458,29 +3121,64 @@ var CodexWatch = class {
|
|
|
2458
3121
|
case "thread/started": {
|
|
2459
3122
|
const t = p.thread;
|
|
2460
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);
|
|
2461
3125
|
return;
|
|
2462
3126
|
}
|
|
2463
3127
|
case "thread/status/changed": {
|
|
2464
3128
|
const id = typeof p.threadId === "string" ? p.threadId : "";
|
|
2465
3129
|
const type = p.status?.type;
|
|
2466
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);
|
|
2467
3134
|
if (type === "active") {
|
|
2468
3135
|
this.#idle.delete(id);
|
|
2469
3136
|
this.#subscribe(id);
|
|
2470
3137
|
} else {
|
|
2471
3138
|
this.#idle.add(id);
|
|
2472
3139
|
this.#unsubscribe(id);
|
|
3140
|
+
for (const t of this.#turns.values()) if (t.threadId === id) this.#armIdleGrace(t);
|
|
2473
3141
|
}
|
|
3142
|
+
this.#wakeThread(id);
|
|
2474
3143
|
return;
|
|
2475
3144
|
}
|
|
2476
3145
|
case "thread/closed":
|
|
2477
3146
|
if (typeof p.threadId === "string") {
|
|
2478
|
-
|
|
2479
|
-
this.#
|
|
2480
|
-
this.#
|
|
2481
|
-
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);
|
|
2482
3157
|
}
|
|
2483
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);
|
|
3179
|
+
}
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
2484
3182
|
case "item/started": {
|
|
2485
3183
|
const item = p.item;
|
|
2486
3184
|
if (item && item.type === "fileChange" && typeof item.id === "string" && Array.isArray(item.changes)) {
|
|
@@ -2492,6 +3190,8 @@ var CodexWatch = class {
|
|
|
2492
3190
|
case "item/completed": {
|
|
2493
3191
|
const item = p.item;
|
|
2494
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);
|
|
2495
3195
|
return;
|
|
2496
3196
|
}
|
|
2497
3197
|
case "serverRequest/resolved": {
|
|
@@ -2528,7 +3228,7 @@ var CodexWatch = class {
|
|
|
2528
3228
|
const ac = new AbortController();
|
|
2529
3229
|
const entry = { ac, threadId };
|
|
2530
3230
|
this.#pending.set(key, entry);
|
|
2531
|
-
const
|
|
3231
|
+
const req2 = {
|
|
2532
3232
|
window: true,
|
|
2533
3233
|
tool: "codex",
|
|
2534
3234
|
hook: { hook_event_name: "PermissionRequest", session_id: threadId, ...hook }
|
|
@@ -2536,7 +3236,7 @@ var CodexWatch = class {
|
|
|
2536
3236
|
const settled = () => {
|
|
2537
3237
|
if (this.#idle.has(threadId)) this.#unsubscribe(threadId);
|
|
2538
3238
|
};
|
|
2539
|
-
void this.#d.gate(
|
|
3239
|
+
void this.#d.gate(req2, ac.signal).then((reply) => {
|
|
2540
3240
|
if (this.#pending.get(key) !== entry) return;
|
|
2541
3241
|
this.#pending.delete(key);
|
|
2542
3242
|
if (reply.decision === "allow") this.#send({ id, result: { decision: "accept" } });
|
|
@@ -2549,84 +3249,461 @@ var CodexWatch = class {
|
|
|
2549
3249
|
}
|
|
2550
3250
|
};
|
|
2551
3251
|
|
|
2552
|
-
// src/
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
var CODEX_WATCH_MS = 5 * 1e3;
|
|
2558
|
-
function defaultCodexAppServer() {
|
|
2559
|
-
const cfg = loadConfig();
|
|
2560
|
-
return cfg.enabledTools.includes("codex") ? discoverAppServer(cfg.codexAppServer) : null;
|
|
2561
|
-
}
|
|
2562
|
-
function opencodeReady() {
|
|
2563
|
-
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");
|
|
2564
3257
|
}
|
|
2565
|
-
function
|
|
2566
|
-
|
|
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
|
+
}
|
|
2567
3266
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
#deps;
|
|
2573
|
-
#store = ApprovalStore.load();
|
|
2574
|
-
#coord;
|
|
2575
|
-
#gate;
|
|
2576
|
-
/** runId → 这次 run 结束的 promise。defer 续跑要等原进程真退出了再起(见 #scheduleResume) */
|
|
2577
|
-
#runsDone = /* @__PURE__ */ new Map();
|
|
2578
|
-
#attempt = 0;
|
|
2579
|
-
#stopped = false;
|
|
2580
|
-
#ws = null;
|
|
2581
|
-
#idleTimer = null;
|
|
2582
|
-
#windowScan = null;
|
|
2583
|
-
#aliveReport = null;
|
|
2584
|
-
#codexTimer = null;
|
|
2585
|
-
#codex = null;
|
|
2586
|
-
constructor(deps = {}) {
|
|
2587
|
-
this.#deps = {
|
|
2588
|
-
connect: deps.connect ?? ((url) => new WebSocket2(url)),
|
|
2589
|
-
now: deps.now ?? (() => Date.now()),
|
|
2590
|
-
backend: deps.backend ?? serverBackend(),
|
|
2591
|
-
gateScript: deps.gateScript ?? defaultGateScript(),
|
|
2592
|
-
// 🔴 优先用 PATH 上那个稳定的 node(如 /opt/homebrew/bin/node 这种链接),不用 process.execPath:
|
|
2593
|
-
// 后者是 Cellar 里的真实路径,`brew upgrade node` 之后就被删了,而 daemon 还活着、
|
|
2594
|
-
// hook 命令里写的还是旧路径 —— hook 起不来,Qoder 在 bypass 下就是全放行(评审 R3)
|
|
2595
|
-
nodePath: deps.nodePath ?? whichIn("node", defaultLocateDeps()) ?? process.execPath,
|
|
2596
|
-
materialize: deps.materialize ?? ((plan, tool) => materialize(plan, tool)),
|
|
2597
|
-
opencodeReady: deps.opencodeReady ?? opencodeReady,
|
|
2598
|
-
codexAppServer: deps.codexAppServer ?? defaultCodexAppServer
|
|
2599
|
-
};
|
|
2600
|
-
this.#store.prune(this.#deps.now());
|
|
2601
|
-
this.#coord = new ApprovalCoordinator({
|
|
2602
|
-
store: this.#store,
|
|
2603
|
-
backend: this.#deps.backend,
|
|
2604
|
-
policy: () => loadConfig().approvalPolicy ?? "commands",
|
|
2605
|
-
resume: (r, kind) => this.#scheduleResume(r, kind),
|
|
2606
|
-
log,
|
|
2607
|
-
now: this.#deps.now
|
|
2608
|
-
});
|
|
2609
|
-
this.#gate = new GateServer((req, signal) => this.#coord.onGate(req, signal));
|
|
3267
|
+
function removeCodexRemote() {
|
|
3268
|
+
try {
|
|
3269
|
+
rmSync4(codexRemotePath(), { force: true });
|
|
3270
|
+
} catch {
|
|
2610
3271
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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));
|
|
3622
|
+
}
|
|
3623
|
+
/** 起本地授权端点。必须在执行任何指令之前起好 —— 否则 gate 连不上,全部 exit 2 */
|
|
3624
|
+
async startGate() {
|
|
3625
|
+
const port = await this.#gate.start();
|
|
2614
3626
|
log(`\u6388\u6743\u7AEF\u70B9\u5DF2\u5C31\u7EEA 127.0.0.1:${port}`);
|
|
2615
3627
|
this.#windowScan = setInterval(() => this.#coord.scanWindows(), 1e3);
|
|
2616
3628
|
this.#windowScan.unref();
|
|
2617
3629
|
this.#aliveReport = setInterval(() => void this.#reportAlive(), ALIVE_REPORT_MS);
|
|
2618
3630
|
this.#aliveReport.unref();
|
|
2619
|
-
this.#
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
discover: () => this.#deps.codexAppServer(),
|
|
2623
|
-
enabled: () => loadConfig().enabledTools.includes("codex")
|
|
2624
|
-
});
|
|
2625
|
-
this.#codex.tick();
|
|
2626
|
-
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);
|
|
2627
3634
|
this.#codexTimer.unref();
|
|
2628
3635
|
return port;
|
|
2629
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
|
+
}
|
|
2630
3707
|
#lastDirs = "";
|
|
2631
3708
|
#dirsTick = 0;
|
|
2632
3709
|
/**
|
|
@@ -2651,7 +3728,7 @@ var Daemon = class {
|
|
|
2651
3728
|
const cfg = loadConfig();
|
|
2652
3729
|
if (!cfg.clientId || !cfg.boundAt) return;
|
|
2653
3730
|
for (const tool of cfg.enabledTools) {
|
|
2654
|
-
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));
|
|
2655
3732
|
}
|
|
2656
3733
|
if (++this.#dirsTick % 10 === 0) await this.#reportDirs();
|
|
2657
3734
|
}
|
|
@@ -2664,7 +3741,10 @@ var Daemon = class {
|
|
|
2664
3741
|
if (this.#windowScan) clearInterval(this.#windowScan);
|
|
2665
3742
|
if (this.#aliveReport) clearInterval(this.#aliveReport);
|
|
2666
3743
|
if (this.#codexTimer) clearInterval(this.#codexTimer);
|
|
2667
|
-
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();
|
|
2668
3748
|
this.#ws?.close();
|
|
2669
3749
|
void this.#gate.stop();
|
|
2670
3750
|
}
|
|
@@ -2768,8 +3848,139 @@ var Daemon = class {
|
|
|
2768
3848
|
return;
|
|
2769
3849
|
}
|
|
2770
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) {
|
|
2771
3981
|
if (isExpired(cmd, this.#deps.now())) {
|
|
2772
3982
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u5DF2\u8FC7\u671F\uFF0C\u4E22\u5F03`);
|
|
3983
|
+
this.#report(cmd, cmd.sessionId, "rejected", "", "expired");
|
|
2773
3984
|
return;
|
|
2774
3985
|
}
|
|
2775
3986
|
const target = resolveTarget(cmd, {
|
|
@@ -2783,6 +3994,7 @@ var Daemon = class {
|
|
|
2783
3994
|
});
|
|
2784
3995
|
if (!target.ok) {
|
|
2785
3996
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u65E0\u6CD5\u6267\u884C\uFF1A${failureMessage(target.reason)}`);
|
|
3997
|
+
this.#report(cmd, cmd.sessionId, "rejected", "", target.reason);
|
|
2786
3998
|
return;
|
|
2787
3999
|
}
|
|
2788
4000
|
if (target.sessionId) this.#coord.cancelSession(target.sessionId);
|
|
@@ -2793,6 +4005,7 @@ var Daemon = class {
|
|
|
2793
4005
|
const fresh = readState(sessionId);
|
|
2794
4006
|
if (!fresh) {
|
|
2795
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");
|
|
2796
4009
|
return;
|
|
2797
4010
|
}
|
|
2798
4011
|
}
|
|
@@ -2800,17 +4013,22 @@ var Daemon = class {
|
|
|
2800
4013
|
const plan = buildSpawnPlan({ ...cmd, sessionId }, cwd, this.#gateTarget(runId));
|
|
2801
4014
|
if (!plan) {
|
|
2802
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");
|
|
2803
4017
|
return;
|
|
2804
4018
|
}
|
|
2805
4019
|
log(`\u6267\u884C ${cmd.instructionId} tool=${cmd.tool} ${sessionId ? "resume" : "new"} cwd=\u2026/${basename6(cwd)} len=${cmd.text.length}`);
|
|
2806
|
-
const result = await this.#run(plan, cmd.tool, runId, sessionId, cwd);
|
|
2807
|
-
if (
|
|
2808
|
-
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);
|
|
2809
4026
|
return;
|
|
2810
4027
|
}
|
|
2811
4028
|
if (isDeferred(result.stdout)) log(`\u6307\u4EE4 ${cmd.instructionId} \u505C\u5728\u7B49\u8BBE\u5907\u6388\u6743\u4E0A\uFF0C\u8FDB\u7A0B\u5DF2\u9000\u51FA`);
|
|
2812
4029
|
if (!result.ok) {
|
|
2813
4030
|
log(`\u6307\u4EE4 ${cmd.instructionId} \u6267\u884C\u5931\u8D25\uFF1A${result.error}`);
|
|
4031
|
+
this.#report(cmd, sessionId || result.sessionId || "", "failed", "headless", "exec_failed");
|
|
2814
4032
|
}
|
|
2815
4033
|
if (!sessionId) {
|
|
2816
4034
|
const newId = result.sessionId ?? parseSessionId(cmd.tool, result.stdout);
|
|
@@ -2856,28 +4074,29 @@ var Daemon = class {
|
|
|
2856
4074
|
r.consumed = true;
|
|
2857
4075
|
this.#store.save();
|
|
2858
4076
|
}
|
|
2859
|
-
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`}`);
|
|
2860
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`);
|
|
2861
4079
|
else if (!result.ok) log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5931\u8D25\uFF1A${result.error}`);
|
|
2862
4080
|
else log(`\u7EED\u8DD1\u4F1A\u8BDD ${r.sessionId} \u5B8C\u6210`);
|
|
2863
4081
|
}
|
|
2864
4082
|
/**
|
|
2865
4083
|
* 起进程并在授权协调器里登记这次 run(gate 靠 runId 认人)。
|
|
2866
|
-
*
|
|
4084
|
+
* 起不了(关卡不可用 / 找不到可执行文件)返回 {fail};进程起来那一刻调 onStarted
|
|
2867
4085
|
*/
|
|
2868
|
-
async #run(plan, tool, runId, sessionId, cwd) {
|
|
4086
|
+
async #run(plan, tool, runId, sessionId, cwd, onStarted) {
|
|
2869
4087
|
if (!existsSync10(this.#deps.nodePath) || !existsSync10(this.#deps.gateScript)) {
|
|
2870
4088
|
log(`\u6388\u6743\u5173\u5361\u4E0D\u53EF\u7528\uFF08node \u6216 gate.cjs \u627E\u4E0D\u5230\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C ${tool}`);
|
|
2871
|
-
return
|
|
4089
|
+
return { fail: "gate_unavailable" };
|
|
2872
4090
|
}
|
|
2873
4091
|
if (tool === "opencode" && !this.#deps.opencodeReady()) {
|
|
2874
4092
|
log("opencode \u63D2\u4EF6\u4E0D\u5728\uFF08deskbuddy enable opencode \u6CA1\u88C5\u6216\u5DF2\u5378\u8F7D\uFF09\uFF0C\u62D2\u7EDD\u6267\u884C opencode");
|
|
2875
|
-
return
|
|
4093
|
+
return { fail: "gate_unavailable" };
|
|
2876
4094
|
}
|
|
2877
|
-
const
|
|
2878
|
-
if (!
|
|
4095
|
+
const real2 = this.#deps.materialize(plan, tool);
|
|
4096
|
+
if (!real2) return { fail: "binary_not_found" };
|
|
2879
4097
|
const startedAt = this.#deps.now();
|
|
2880
|
-
const rc = startCommand(
|
|
4098
|
+
const rc = startCommand(real2, tool);
|
|
4099
|
+
onStarted?.();
|
|
2881
4100
|
this.#coord.registerRun({
|
|
2882
4101
|
runId,
|
|
2883
4102
|
tool,
|
|
@@ -2918,7 +4137,7 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2918
4137
|
}
|
|
2919
4138
|
if (opts.foreground) process.env.DESKBUDDY_DAEMON_FOREGROUND = "1";
|
|
2920
4139
|
try {
|
|
2921
|
-
|
|
4140
|
+
writeFileSync13(pidFilePath(), String(process.pid), "utf8");
|
|
2922
4141
|
} catch {
|
|
2923
4142
|
}
|
|
2924
4143
|
let daemon;
|
|
@@ -2934,7 +4153,7 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2934
4153
|
log(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`);
|
|
2935
4154
|
daemon.stop();
|
|
2936
4155
|
try {
|
|
2937
|
-
|
|
4156
|
+
rmSync7(pidFilePath(), { force: true });
|
|
2938
4157
|
} catch {
|
|
2939
4158
|
}
|
|
2940
4159
|
process.exit(0);
|
|
@@ -2951,8 +4170,8 @@ async function runDaemonCommand(opts = {}) {
|
|
|
2951
4170
|
}
|
|
2952
4171
|
|
|
2953
4172
|
// src/core/registry.ts
|
|
2954
|
-
import { mkdirSync as
|
|
2955
|
-
import { join as
|
|
4173
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "fs";
|
|
4174
|
+
import { join as join17 } from "path";
|
|
2956
4175
|
var FALLBACK = {
|
|
2957
4176
|
version: "builtin",
|
|
2958
4177
|
tools: {
|
|
@@ -3030,11 +4249,11 @@ var FALLBACK = {
|
|
|
3030
4249
|
}
|
|
3031
4250
|
};
|
|
3032
4251
|
function registryCachePath() {
|
|
3033
|
-
return
|
|
4252
|
+
return join17(deskbuddyHome(), "tool-registry.json");
|
|
3034
4253
|
}
|
|
3035
4254
|
function loadRegistry() {
|
|
3036
4255
|
try {
|
|
3037
|
-
return JSON.parse(
|
|
4256
|
+
return JSON.parse(readFileSync13(registryCachePath(), "utf8"));
|
|
3038
4257
|
} catch {
|
|
3039
4258
|
return FALLBACK;
|
|
3040
4259
|
}
|
|
@@ -3042,15 +4261,15 @@ function loadRegistry() {
|
|
|
3042
4261
|
async function refreshRegistry() {
|
|
3043
4262
|
try {
|
|
3044
4263
|
const registry = await api.toolRegistry();
|
|
3045
|
-
|
|
3046
|
-
|
|
4264
|
+
mkdirSync14(deskbuddyHome(), { recursive: true });
|
|
4265
|
+
writeFileSync14(registryCachePath(), JSON.stringify(registry, null, 2), "utf8");
|
|
3047
4266
|
} catch {
|
|
3048
4267
|
}
|
|
3049
4268
|
}
|
|
3050
4269
|
|
|
3051
4270
|
// src/adapters/ccHook.ts
|
|
3052
|
-
import { chmodSync as
|
|
3053
|
-
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";
|
|
3054
4273
|
var CC_EVENTS = [
|
|
3055
4274
|
"UserPromptSubmit",
|
|
3056
4275
|
"PreToolUse",
|
|
@@ -3061,7 +4280,7 @@ var CC_EVENTS = [
|
|
|
3061
4280
|
];
|
|
3062
4281
|
function readSettings(path) {
|
|
3063
4282
|
if (!existsSync11(path)) return {};
|
|
3064
|
-
const raw =
|
|
4283
|
+
const raw = readFileSync14(path, "utf8");
|
|
3065
4284
|
if (raw.trim() === "") return {};
|
|
3066
4285
|
try {
|
|
3067
4286
|
return JSON.parse(raw);
|
|
@@ -3070,26 +4289,32 @@ function readSettings(path) {
|
|
|
3070
4289
|
}
|
|
3071
4290
|
}
|
|
3072
4291
|
function writeSettings(path, settings) {
|
|
3073
|
-
|
|
4292
|
+
mkdirSync15(dirname6(path), { recursive: true });
|
|
3074
4293
|
let mode;
|
|
3075
4294
|
try {
|
|
3076
|
-
mode =
|
|
4295
|
+
mode = statSync4(path).mode & 511;
|
|
3077
4296
|
} catch {
|
|
3078
4297
|
mode = void 0;
|
|
3079
4298
|
}
|
|
3080
4299
|
const tmp = path + ".deskbuddy.tmp";
|
|
3081
|
-
|
|
3082
|
-
if (mode !== void 0)
|
|
3083
|
-
|
|
4300
|
+
writeFileSync15(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
4301
|
+
if (mode !== void 0) chmodSync4(tmp, mode);
|
|
4302
|
+
renameSync11(tmp, path);
|
|
4303
|
+
}
|
|
4304
|
+
function mentions(command, script) {
|
|
4305
|
+
return !!command && (command.includes(script) || command.includes(fwdSlash(script)));
|
|
3084
4306
|
}
|
|
3085
|
-
function
|
|
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) {
|
|
3086
4311
|
const settings = readSettings(settingsPath);
|
|
3087
4312
|
settings.hooks ??= {};
|
|
3088
4313
|
for (const event of events) {
|
|
3089
4314
|
const groups = settings.hooks[event] ??= [];
|
|
3090
|
-
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)));
|
|
3091
4316
|
if (already) continue;
|
|
3092
|
-
groups.push({ hooks: [{ type: "command", command: `node ${hookPath} ${tool}` }] });
|
|
4317
|
+
groups.push({ hooks: [{ type: "command", command: `node ${scriptArg(hookPath, platform)} ${tool}` }] });
|
|
3093
4318
|
}
|
|
3094
4319
|
writeSettings(settingsPath, settings);
|
|
3095
4320
|
}
|
|
@@ -3097,26 +4322,26 @@ var WINDOW_GATE_TIMEOUT_S = 7 * 24 * 60 * 60;
|
|
|
3097
4322
|
function shQuote2(s) {
|
|
3098
4323
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3099
4324
|
}
|
|
3100
|
-
function installCcWindowGate(settingsPath, gatePath, tool = "claude_code") {
|
|
4325
|
+
function installCcWindowGate(settingsPath, gatePath, tool = "claude_code", platform = process.platform) {
|
|
3101
4326
|
const settings = readSettings(settingsPath);
|
|
3102
4327
|
settings.hooks ??= {};
|
|
3103
4328
|
const groups = settings.hooks.PermissionRequest ??= [];
|
|
3104
|
-
if (groups.some((g) => g.hooks?.some((h) => h.command
|
|
4329
|
+
if (groups.some((g) => g.hooks?.some((h) => mentions(h.command, gatePath)))) return;
|
|
3105
4330
|
const toolArg = tool === "claude_code" ? "" : ` --tool ${tool}`;
|
|
3106
4331
|
groups.push({
|
|
3107
|
-
hooks: [{ type: "command", command: `node ${
|
|
4332
|
+
hooks: [{ type: "command", command: `node ${scriptArg(gatePath, platform, shQuote2)} --window${toolArg}`, timeout: WINDOW_GATE_TIMEOUT_S }]
|
|
3108
4333
|
});
|
|
3109
4334
|
writeSettings(settingsPath, settings);
|
|
3110
4335
|
}
|
|
3111
4336
|
function uninstallCcHook(settingsPath, hookPath) {
|
|
3112
4337
|
const settings = readSettings(settingsPath);
|
|
3113
4338
|
if (!settings.hooks) return;
|
|
3114
|
-
const gatePath =
|
|
4339
|
+
const gatePath = join18(dirname6(hookPath), "gate.cjs");
|
|
3115
4340
|
for (const event of Object.keys(settings.hooks)) {
|
|
3116
4341
|
const groups = settings.hooks[event] ?? [];
|
|
3117
4342
|
const kept = [];
|
|
3118
4343
|
for (const g of groups) {
|
|
3119
|
-
const hooks = (g.hooks ?? []).filter((h) => !h.command
|
|
4344
|
+
const hooks = (g.hooks ?? []).filter((h) => !mentions(h.command, hookPath) && !mentions(h.command, gatePath));
|
|
3120
4345
|
if (hooks.length > 0) kept.push({ ...g, hooks });
|
|
3121
4346
|
}
|
|
3122
4347
|
settings.hooks[event] = kept;
|
|
@@ -3125,8 +4350,8 @@ function uninstallCcHook(settingsPath, hookPath) {
|
|
|
3125
4350
|
}
|
|
3126
4351
|
|
|
3127
4352
|
// src/adapters/codexHook.ts
|
|
3128
|
-
import { chmodSync as
|
|
3129
|
-
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";
|
|
3130
4355
|
var CODEX_MARKER_BEGIN = "# >>> deskbuddy >>>";
|
|
3131
4356
|
var CODEX_MARKER_END = "# <<< deskbuddy <<<";
|
|
3132
4357
|
var CODEX_EVENTS = [
|
|
@@ -3139,13 +4364,13 @@ var CODEX_EVENTS = [
|
|
|
3139
4364
|
];
|
|
3140
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";
|
|
3141
4366
|
function codexConfigPath() {
|
|
3142
|
-
return
|
|
4367
|
+
return join19(codexHome(), "config.toml");
|
|
3143
4368
|
}
|
|
3144
|
-
function renderBlock(hookPath, events, tool) {
|
|
3145
|
-
const command = JSON.stringify(`node ${hookPath} ${tool}`);
|
|
3146
|
-
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];
|
|
3147
4372
|
for (const event of events) {
|
|
3148
|
-
|
|
4373
|
+
lines2.push(
|
|
3149
4374
|
`[[hooks.${event}]]`,
|
|
3150
4375
|
'matcher = ""',
|
|
3151
4376
|
"hooks = [",
|
|
@@ -3154,21 +4379,21 @@ function renderBlock(hookPath, events, tool) {
|
|
|
3154
4379
|
""
|
|
3155
4380
|
);
|
|
3156
4381
|
}
|
|
3157
|
-
|
|
3158
|
-
return
|
|
4382
|
+
lines2.push(CODEX_MARKER_END);
|
|
4383
|
+
return lines2.join("\n");
|
|
3159
4384
|
}
|
|
3160
4385
|
function writeAtomic(path, content) {
|
|
3161
|
-
|
|
4386
|
+
mkdirSync16(dirname7(path), { recursive: true });
|
|
3162
4387
|
let mode;
|
|
3163
4388
|
try {
|
|
3164
|
-
mode =
|
|
4389
|
+
mode = statSync5(path).mode & 511;
|
|
3165
4390
|
} catch {
|
|
3166
4391
|
mode = void 0;
|
|
3167
4392
|
}
|
|
3168
4393
|
const tmp = path + ".deskbuddy.tmp";
|
|
3169
|
-
|
|
3170
|
-
if (mode !== void 0)
|
|
3171
|
-
|
|
4394
|
+
writeFileSync16(tmp, content, "utf8");
|
|
4395
|
+
if (mode !== void 0) chmodSync5(tmp, mode);
|
|
4396
|
+
renameSync12(tmp, path);
|
|
3172
4397
|
}
|
|
3173
4398
|
function assertSafeToAppend(raw, path) {
|
|
3174
4399
|
for (const quote of ['"""', "'''"]) {
|
|
@@ -3177,35 +4402,35 @@ function assertSafeToAppend(raw, path) {
|
|
|
3177
4402
|
}
|
|
3178
4403
|
}
|
|
3179
4404
|
}
|
|
3180
|
-
function installCodexHook(configPath2, hookPath, events = CODEX_EVENTS, tool = "codex") {
|
|
3181
|
-
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") : "";
|
|
3182
4407
|
if (raw.includes(CODEX_MARKER_BEGIN)) return;
|
|
3183
4408
|
assertSafeToAppend(raw, configPath2);
|
|
3184
|
-
const block = renderBlock(hookPath, events, tool);
|
|
4409
|
+
const block = renderBlock(hookPath, events, tool, platform);
|
|
3185
4410
|
const head = raw === "" ? "" : (raw.endsWith("\n") ? raw : raw + "\n") + "\n";
|
|
3186
4411
|
writeAtomic(configPath2, head + block + "\n");
|
|
3187
4412
|
}
|
|
3188
4413
|
function uninstallCodexHook(configPath2) {
|
|
3189
4414
|
if (!existsSync12(configPath2)) return;
|
|
3190
|
-
const raw =
|
|
3191
|
-
const
|
|
3192
|
-
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);
|
|
3193
4418
|
if (begin === -1) return;
|
|
3194
|
-
const end =
|
|
4419
|
+
const end = lines2.findIndex((l, i) => i > begin && l.trim() === CODEX_MARKER_END);
|
|
3195
4420
|
if (end === -1) {
|
|
3196
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`);
|
|
3197
4422
|
}
|
|
3198
|
-
const start = begin > 0 &&
|
|
3199
|
-
|
|
3200
|
-
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"));
|
|
3201
4426
|
}
|
|
3202
4427
|
|
|
3203
4428
|
// src/commands/tools.ts
|
|
3204
|
-
import { existsSync as existsSync13, readFileSync as
|
|
4429
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
|
|
3205
4430
|
function fileContains(path, needle) {
|
|
3206
4431
|
try {
|
|
3207
4432
|
if (!existsSync13(path)) return false;
|
|
3208
|
-
return
|
|
4433
|
+
return readFileSync16(path, "utf8").includes(needle);
|
|
3209
4434
|
} catch {
|
|
3210
4435
|
return false;
|
|
3211
4436
|
}
|
|
@@ -3220,7 +4445,8 @@ var ADAPTERS = {
|
|
|
3220
4445
|
configPath: (tool) => CC_CONFIG_PATHS[tool]?.() ?? null,
|
|
3221
4446
|
install: (p, hook, events, tool) => installCcHook(p, hook, events, tool),
|
|
3222
4447
|
uninstall: (p, hook) => uninstallCcHook(p, hook),
|
|
3223
|
-
|
|
4448
|
+
// Windows 上装出去的是正斜杠形式;JSON 里的反斜杠还会被转义成两个,原样搜不到
|
|
4449
|
+
isInstalled: (p, hook) => fileContains(p, hook) || fileContains(p, fwdSlash(hook)),
|
|
3224
4450
|
defaultEvents: CC_EVENTS,
|
|
3225
4451
|
// 实测 PermissionRequest 与窗口授权框并行的:Claude Code(S10 §2)、Qoder CN 桌面端(二期 V8)。
|
|
3226
4452
|
// WorkBuddy 桌面端弹授权框不触发 PermissionRequest;Codex 的 hook 会阻塞授权框——都不装
|
|
@@ -3271,91 +4497,427 @@ function subscribedEvents(registry, tool, fallback) {
|
|
|
3271
4497
|
return picked.length > 0 ? picked : fallback;
|
|
3272
4498
|
}
|
|
3273
4499
|
|
|
3274
|
-
// src/commands/
|
|
3275
|
-
|
|
3276
|
-
|
|
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, `'\\''`)}'`;
|
|
3277
4524
|
}
|
|
3278
|
-
function
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
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
|
+
];
|
|
3282
4546
|
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
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;
|
|
3295
4574
|
}
|
|
3296
|
-
|
|
3297
|
-
|
|
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) => {
|
|
3298
4766
|
try {
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
out(`\u2717 ${tool}\uFF1A${e.message}`);
|
|
3303
|
-
failed += 1;
|
|
3304
|
-
continue;
|
|
4767
|
+
return readFileSync17(join20(binDir, n), "utf8").includes(SHIM_MARKER);
|
|
4768
|
+
} catch {
|
|
4769
|
+
return false;
|
|
3305
4770
|
}
|
|
3306
|
-
|
|
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 {
|
|
3307
4784
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
if (
|
|
3311
|
-
|
|
3312
|
-
|
|
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
|
+
}
|
|
3313
4806
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
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 {
|
|
3318
4810
|
}
|
|
3319
|
-
|
|
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;
|
|
3320
4869
|
}
|
|
3321
4870
|
|
|
3322
4871
|
// src/commands/enable.ts
|
|
3323
|
-
|
|
3324
|
-
function stdout2(line) {
|
|
4872
|
+
function stdout(line) {
|
|
3325
4873
|
process.stdout.write(line + "\n");
|
|
3326
4874
|
}
|
|
3327
4875
|
function parseTools(arg) {
|
|
3328
4876
|
return (arg ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
3329
4877
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
4878
|
+
function defaultShimEnvForInstall() {
|
|
4879
|
+
let node = null;
|
|
4880
|
+
try {
|
|
4881
|
+
node = whichIn("node", defaultLocateDeps());
|
|
4882
|
+
} catch {
|
|
4883
|
+
node = null;
|
|
3335
4884
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
+
}
|
|
3339
4900
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
4901
|
+
}
|
|
4902
|
+
function installTools(tools, out, shim = defaultShimEnvForInstall()) {
|
|
3342
4903
|
const registry = loadRegistry();
|
|
3343
4904
|
const hookPath = installedHookPath();
|
|
3344
4905
|
syncHookFile();
|
|
3345
4906
|
const cfg = loadConfig();
|
|
3346
4907
|
const enabled = new Set(cfg.enabledTools);
|
|
3347
|
-
|
|
4908
|
+
const installed = [];
|
|
4909
|
+
const failed = [];
|
|
3348
4910
|
for (const tool of tools) {
|
|
3349
4911
|
const adapter = adapterFor(registry, tool);
|
|
3350
4912
|
if (!adapter) {
|
|
3351
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`);
|
|
3352
|
-
failed
|
|
4914
|
+
failed.push({ tool, reason: "\u672C\u5BA2\u6237\u7AEF\u8FD8\u4E0D\u652F\u6301" });
|
|
3353
4915
|
continue;
|
|
3354
4916
|
}
|
|
3355
4917
|
const configPath2 = adapter.configPath(tool);
|
|
3356
4918
|
if (!configPath2) {
|
|
3357
4919
|
out(`\u2717 ${tool}\uFF1A\u672C\u5BA2\u6237\u7AEF\u4E0D\u77E5\u9053\u5B83\u7684\u914D\u7F6E\u6587\u4EF6\u5728\u54EA\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
3358
|
-
failed
|
|
4920
|
+
failed.push({ tool, reason: "\u627E\u4E0D\u5230\u914D\u7F6E\u6587\u4EF6" });
|
|
3359
4921
|
continue;
|
|
3360
4922
|
}
|
|
3361
4923
|
const events = subscribedEvents(registry, tool, adapter.defaultEvents);
|
|
@@ -3363,13 +4925,14 @@ async function runEnable(tools, opts = {}) {
|
|
|
3363
4925
|
adapter.install(configPath2, hookPath, events, tool);
|
|
3364
4926
|
} catch (e) {
|
|
3365
4927
|
out(`\u2717 ${tool}\uFF1A${e.message}`);
|
|
3366
|
-
failed
|
|
4928
|
+
failed.push({ tool, reason: e.message });
|
|
3367
4929
|
continue;
|
|
3368
4930
|
}
|
|
3369
4931
|
enabled.add(tool);
|
|
4932
|
+
installed.push(tool);
|
|
3370
4933
|
out(`\u2705 ${tool}\uFF1Ahook \u5DF2\u88C5\u8FDB ${configPath2}\uFF08${events.length} \u4E2A\u4E8B\u4EF6\uFF09`);
|
|
3371
4934
|
try {
|
|
3372
|
-
if (adapter.installWindowGate?.(configPath2,
|
|
4935
|
+
if (adapter.installWindowGate?.(configPath2, join22(dirname9(hookPath), "gate.cjs"), tool)) {
|
|
3373
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`);
|
|
3374
4937
|
}
|
|
3375
4938
|
} catch (e) {
|
|
@@ -3379,6 +4942,22 @@ async function runEnable(tools, opts = {}) {
|
|
|
3379
4942
|
}
|
|
3380
4943
|
cfg.enabledTools = [...enabled];
|
|
3381
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);
|
|
3382
4961
|
if (!opts.skipAutostart) {
|
|
3383
4962
|
const r = await installAutostart({ execPath: process.execPath, cliPath: defaultCliPath() });
|
|
3384
4963
|
out("");
|
|
@@ -3388,69 +4967,132 @@ async function runEnable(tools, opts = {}) {
|
|
|
3388
4967
|
out("\u88C5\u5B8C\u4E86\u3002\u72B6\u6001\u91C7\u96C6\u9760 hook\uFF0C\u4E0D\u9700\u8981\u5E38\u9A7B\u8FDB\u7A0B\uFF1B");
|
|
3389
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");
|
|
3390
4969
|
out("\u968F\u65F6\u53EF\u4EE5 deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001deskbuddy disable \u5E72\u51C0\u79FB\u9664\u3002");
|
|
3391
|
-
return failed > 0 ? 1 : 0;
|
|
4970
|
+
return failed.length > 0 ? 1 : 0;
|
|
3392
4971
|
}
|
|
3393
4972
|
|
|
3394
|
-
// src/commands/
|
|
3395
|
-
|
|
3396
|
-
import { existsSync as existsSync14, rmSync as rmSync5 } from "fs";
|
|
3397
|
-
function stdout3(line) {
|
|
4973
|
+
// src/commands/disable.ts
|
|
4974
|
+
function stdout2(line) {
|
|
3398
4975
|
process.stdout.write(line + "\n");
|
|
3399
4976
|
}
|
|
3400
|
-
function
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
rl.close();
|
|
3405
|
-
resolve3(/^(y|yes)$/i.test(answer.trim()));
|
|
3406
|
-
});
|
|
3407
|
-
});
|
|
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])];
|
|
3408
4981
|
}
|
|
3409
|
-
async function
|
|
3410
|
-
const out = opts.out ??
|
|
3411
|
-
const
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
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;
|
|
3424
5004
|
}
|
|
5005
|
+
enabled.delete(tool);
|
|
3425
5006
|
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
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
|
+
}
|
|
3429
5021
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
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
|
+
});
|
|
3433
5075
|
}
|
|
3434
5076
|
|
|
3435
5077
|
// src/commands/setup.ts
|
|
3436
5078
|
import qrcode from "qrcode-terminal";
|
|
3437
5079
|
|
|
3438
5080
|
// src/commands/remoteCwd.ts
|
|
3439
|
-
import { existsSync as
|
|
3440
|
-
import { createInterface
|
|
3441
|
-
import { homedir as
|
|
3442
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
3443
|
-
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()) {
|
|
3444
5086
|
const s = (raw || "").trim();
|
|
3445
5087
|
if (!s) return null;
|
|
3446
|
-
const expanded = s === "~" ? home : s.startsWith("~/") ?
|
|
3447
|
-
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);
|
|
3448
5090
|
return abs;
|
|
3449
5091
|
}
|
|
3450
5092
|
function checkPath(abs) {
|
|
3451
|
-
if (!
|
|
5093
|
+
if (!existsSync17(abs)) return "missing";
|
|
3452
5094
|
try {
|
|
3453
|
-
return
|
|
5095
|
+
return statSync7(abs).isDirectory() ? "ok" : "not_a_dir";
|
|
3454
5096
|
} catch {
|
|
3455
5097
|
return "missing";
|
|
3456
5098
|
}
|
|
@@ -3481,7 +5123,7 @@ async function askRemoteCwd(deps) {
|
|
|
3481
5123
|
}
|
|
3482
5124
|
function terminalAsk(prompt) {
|
|
3483
5125
|
if (!process.stdin.isTTY) return Promise.resolve(null);
|
|
3484
|
-
const rl =
|
|
5126
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3485
5127
|
return new Promise((res) => {
|
|
3486
5128
|
rl.question(prompt, (answer) => {
|
|
3487
5129
|
rl.close();
|
|
@@ -3562,11 +5204,11 @@ function parseApiBase(argv) {
|
|
|
3562
5204
|
function sleep(ms) {
|
|
3563
5205
|
return new Promise((r) => setTimeout(r, ms));
|
|
3564
5206
|
}
|
|
3565
|
-
function
|
|
5207
|
+
function stdout3(line) {
|
|
3566
5208
|
process.stdout.write(line + "\n");
|
|
3567
5209
|
}
|
|
3568
5210
|
async function runSetup(opts = {}) {
|
|
3569
|
-
const out = opts.out ??
|
|
5211
|
+
const out = opts.out ?? stdout3;
|
|
3570
5212
|
const clientId = ensureClientId();
|
|
3571
5213
|
if (opts.apiBase) applyApiBase(opts.apiBase);
|
|
3572
5214
|
const cfg = loadConfig();
|
|
@@ -3599,22 +5241,247 @@ async function runSetup(opts = {}) {
|
|
|
3599
5241
|
return 0;
|
|
3600
5242
|
}
|
|
3601
5243
|
|
|
3602
|
-
// src/commands/
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
var STATE_LABELS = {
|
|
3612
|
-
watch: "\u8FDB\u884C\u4E2D",
|
|
3613
|
-
ask_approval: "\u5F85\u786E\u8BA4",
|
|
3614
|
-
done: "\u5DF2\u5B8C\u6210",
|
|
3615
|
-
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
|
|
3616
5253
|
};
|
|
3617
|
-
|
|
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) {
|
|
3618
5485
|
const sec = Math.max(0, Math.round((now - from) / 1e3));
|
|
3619
5486
|
if (sec < 60) return `${sec} \u79D2\u524D`;
|
|
3620
5487
|
const min = Math.round(sec / 60);
|
|
@@ -3628,48 +5495,58 @@ function pad(s, width) {
|
|
|
3628
5495
|
return s + " ".repeat(Math.max(1, width - w));
|
|
3629
5496
|
}
|
|
3630
5497
|
function formatStatus(input) {
|
|
3631
|
-
const
|
|
5498
|
+
const lines2 = [];
|
|
3632
5499
|
const cfg = input.config;
|
|
3633
|
-
|
|
5500
|
+
lines2.push("DeskBuddy \u72B6\u6001");
|
|
3634
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";
|
|
3635
|
-
|
|
3636
|
-
|
|
5502
|
+
lines2.push(` ${pad("\u8D26\u53F7", 10)}${account}`);
|
|
5503
|
+
lines2.push(` ${pad("\u670D\u52A1\u7AEF", 10)}${cfg.apiBase}\uFF08${environmentLabel(cfg.apiBase)}\uFF09`);
|
|
3637
5504
|
if (cfg.boundAt && cfg.boundApiBase && cfg.boundApiBase !== cfg.apiBase) {
|
|
3638
|
-
|
|
5505
|
+
lines2.push(
|
|
3639
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`
|
|
3640
5507
|
);
|
|
3641
5508
|
}
|
|
3642
5509
|
if (input.envOverride?.active) {
|
|
3643
|
-
|
|
5510
|
+
lines2.push(
|
|
3644
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`
|
|
3645
5512
|
);
|
|
3646
5513
|
}
|
|
3647
5514
|
if (input.tools.length === 0) {
|
|
3648
|
-
|
|
5515
|
+
lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}\u65E0 \u2014\u2014 \u8FD0\u884C deskbuddy enable claude_code,codex`);
|
|
3649
5516
|
} else {
|
|
3650
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");
|
|
3651
|
-
|
|
5518
|
+
lines2.push(` ${pad("\u5DF2\u542F\u7528", 10)}${desc}`);
|
|
3652
5519
|
}
|
|
3653
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`;
|
|
3654
|
-
|
|
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
|
+
}
|
|
3655
5532
|
for (const t of input.tools) {
|
|
3656
|
-
if (t.notice)
|
|
5533
|
+
if (t.notice) lines2.push(` ${t.notice}`);
|
|
3657
5534
|
}
|
|
3658
|
-
|
|
5535
|
+
lines2.push("");
|
|
3659
5536
|
const sorted = [...input.sessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
3660
|
-
|
|
5537
|
+
lines2.push(`\u5F53\u524D\u672C\u5730\u4F1A\u8BDD\uFF08${sorted.length}\uFF09`);
|
|
3661
5538
|
if (sorted.length === 0) {
|
|
3662
|
-
|
|
5539
|
+
lines2.push(" \u6682\u65E0 \u2014\u2014 \u667A\u80FD\u4F53\u8DD1\u8D77\u6765\u4E4B\u540E\u8FD9\u91CC\u5C31\u4F1A\u6709\u5185\u5BB9");
|
|
3663
5540
|
}
|
|
3664
5541
|
for (const s of sorted) {
|
|
3665
|
-
|
|
5542
|
+
lines2.push(
|
|
3666
5543
|
` ${pad(s.tool, 14)}${pad(s.title || s.project || "-", 24)}${pad(STATE_LABELS[s.state] ?? s.state, 8)}${relativeTime(s.updatedAt, input.now)}`
|
|
3667
5544
|
);
|
|
3668
5545
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
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");
|
|
3673
5550
|
}
|
|
3674
5551
|
function collectStatus(now = Date.now()) {
|
|
3675
5552
|
const config = loadConfig();
|
|
@@ -3691,7 +5568,21 @@ function collectStatus(now = Date.now()) {
|
|
|
3691
5568
|
});
|
|
3692
5569
|
sessions.push(...readAliveStates(tool));
|
|
3693
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
|
+
}
|
|
3694
5583
|
return {
|
|
5584
|
+
windowWrap,
|
|
5585
|
+
codexWrap,
|
|
3695
5586
|
config,
|
|
3696
5587
|
hookVersion: installedHookVersion(),
|
|
3697
5588
|
currentVersion: packageVersion(),
|
|
@@ -3727,10 +5618,1008 @@ async function runUpdate(opts = {}) {
|
|
|
3727
5618
|
return runEnable([...cfg.enabledTools], opts);
|
|
3728
5619
|
}
|
|
3729
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
|
+
|
|
3730
6617
|
// src/cli.ts
|
|
3731
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
|
|
3732
6619
|
|
|
3733
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
|
|
3734
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
|
|
3735
6624
|
deskbuddy enable <tool[,tool]> \u5B89\u88C5\u91C7\u96C6 hook\uFF08\u5982 claude_code,codex\uFF09
|
|
3736
6625
|
deskbuddy disable [tool[,tool]] \u79FB\u9664\u91C7\u96C6 hook\uFF08\u4E0D\u5E26\u53C2\u6570=\u5168\u90E8\uFF09
|
|
@@ -3739,6 +6628,8 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
|
|
|
3739
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
|
|
3740
6629
|
deskbuddy status \u770B\u91C7\u96C6\u5230\u4E86\u4EC0\u4E48\u3001\u4E0A\u62A5\u4E86\u54EA\u4E9B\u5B57\u6BB5
|
|
3741
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
|
|
3742
6633
|
|
|
3743
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
|
|
3744
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
|
|
@@ -3747,7 +6638,7 @@ var USAGE = `deskbuddy \u2014\u2014 \u628A AI \u7F16\u7801\u667A\u80FD\u4F53\u6B
|
|
|
3747
6638
|
async function main(argv) {
|
|
3748
6639
|
const cmd = argv[2];
|
|
3749
6640
|
const arg = argv[3];
|
|
3750
|
-
if (cmd && !["-h", "--help", "help", "-v", "--version"].includes(cmd)) {
|
|
6641
|
+
if (cmd && !["-h", "--help", "help", "-v", "--version", "wrap"].includes(cmd)) {
|
|
3751
6642
|
try {
|
|
3752
6643
|
syncHookFile();
|
|
3753
6644
|
} catch {
|
|
@@ -3764,6 +6655,8 @@ async function main(argv) {
|
|
|
3764
6655
|
case "--version":
|
|
3765
6656
|
process.stdout.write(packageVersion() + "\n");
|
|
3766
6657
|
return 0;
|
|
6658
|
+
case "install":
|
|
6659
|
+
return runInstall({ code: parseCode(argv), apiBase: parseApiBase(argv) });
|
|
3767
6660
|
case "setup":
|
|
3768
6661
|
return runSetup({ apiBase: parseApiBase(argv) });
|
|
3769
6662
|
case "enable":
|
|
@@ -3778,6 +6671,8 @@ async function main(argv) {
|
|
|
3778
6671
|
return runStatus();
|
|
3779
6672
|
case "logout":
|
|
3780
6673
|
return runLogout({ yes: argv.includes("--yes") || argv.includes("-y") });
|
|
6674
|
+
case "wrap":
|
|
6675
|
+
return runWrap(argv.slice(3));
|
|
3781
6676
|
default:
|
|
3782
6677
|
process.stderr.write(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
|
|
3783
6678
|
|