@zqagent/core 0.1.30 → 0.2.5
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/CONTRIBUTING.md +65 -0
- package/LICENSE +190 -0
- package/README.md +102 -146
- package/README.oss.en.md +161 -0
- package/README.oss.md +165 -0
- package/README.oss.zh-TW.md +165 -0
- package/SECURITY.md +51 -0
- package/dist/{ToolRegistry-BmvPxgMr.d.cts → ToolRegistry-C3pTFiaJ.d.cts} +13 -4
- package/dist/{ToolRegistry-BmvPxgMr.d.ts → ToolRegistry-C3pTFiaJ.d.ts} +13 -4
- package/dist/cli/index.cjs +17799 -2062
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +16729 -985
- package/dist/cli/index.js.map +1 -1
- package/dist/{explore-OI6htHN1.d.cts → explore-D4lCKXbA.d.ts} +160 -19
- package/dist/{explore-Pmhyk4gv.d.ts → explore-Dj1j_GyN.d.cts} +160 -19
- package/dist/index.cjs +3626 -453
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -27
- package/dist/index.d.ts +337 -27
- package/dist/index.js +3617 -452
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14029 -1482
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +14036 -1482
- package/dist/server/index.js.map +1 -1
- package/dist/tools/ask.cjs +14 -0
- package/dist/tools/ask.cjs.map +1 -1
- package/dist/tools/ask.d.cts +8 -2
- package/dist/tools/ask.d.ts +8 -2
- package/dist/tools/ask.js +13 -0
- package/dist/tools/ask.js.map +1 -1
- package/dist/tools/diff.cjs +59 -30
- package/dist/tools/diff.cjs.map +1 -1
- package/dist/tools/diff.d.cts +1 -1
- package/dist/tools/diff.d.ts +1 -1
- package/dist/tools/diff.js +59 -30
- package/dist/tools/diff.js.map +1 -1
- package/dist/tools/directories.d.cts +1 -1
- package/dist/tools/directories.d.ts +1 -1
- package/dist/tools/explore.cjs +1050 -309
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +2 -2
- package/dist/tools/explore.d.ts +2 -2
- package/dist/tools/explore.js +1057 -309
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/files.cjs +58 -43
- package/dist/tools/files.cjs.map +1 -1
- package/dist/tools/files.d.cts +10 -4
- package/dist/tools/files.d.ts +10 -4
- package/dist/tools/files.js +56 -42
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/git.cjs +816 -63
- package/dist/tools/git.cjs.map +1 -1
- package/dist/tools/git.d.cts +200 -2
- package/dist/tools/git.d.ts +200 -2
- package/dist/tools/git.js +788 -62
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/media.cjs +1191 -0
- package/dist/tools/media.cjs.map +1 -0
- package/dist/tools/media.d.cts +31 -0
- package/dist/tools/media.d.ts +31 -0
- package/dist/tools/media.js +1159 -0
- package/dist/tools/media.js.map +1 -0
- package/dist/tools/plan.d.cts +1 -1
- package/dist/tools/plan.d.ts +1 -1
- package/dist/tools/search.cjs +513 -33
- package/dist/tools/search.cjs.map +1 -1
- package/dist/tools/search.d.cts +12 -1
- package/dist/tools/search.d.ts +12 -1
- package/dist/tools/search.js +508 -33
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.cjs +631 -41
- package/dist/tools/shell.cjs.map +1 -1
- package/dist/tools/shell.d.cts +37 -4
- package/dist/tools/shell.d.ts +37 -4
- package/dist/tools/shell.js +626 -40
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/todo.d.cts +1 -1
- package/dist/tools/todo.d.ts +1 -1
- package/dist/tools/web.cjs +51 -18
- package/dist/tools/web.cjs.map +1 -1
- package/dist/tools/web.d.cts +1 -1
- package/dist/tools/web.d.ts +1 -1
- package/dist/tools/web.js +51 -18
- package/dist/tools/web.js.map +1 -1
- package/package.json +43 -8
package/dist/tools/shell.js
CHANGED
|
@@ -1,81 +1,667 @@
|
|
|
1
1
|
// src/tools/shell.ts
|
|
2
|
-
import
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path2 from "path";
|
|
4
|
+
|
|
5
|
+
// src/tools/execAsync.ts
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
|
|
8
|
+
// src/tools/killTree.ts
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
function killTree(pid) {
|
|
11
|
+
if (!pid) return;
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
try {
|
|
14
|
+
spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
process.kill(-pid, "SIGKILL");
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, "SIGKILL");
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/tools/execAsync.ts
|
|
30
|
+
var liveChildren = /* @__PURE__ */ new Set();
|
|
31
|
+
var exitCleanupInstalled = false;
|
|
32
|
+
function trackChild(pid) {
|
|
33
|
+
if (!pid) return;
|
|
34
|
+
liveChildren.add(pid);
|
|
35
|
+
if (exitCleanupInstalled) return;
|
|
36
|
+
exitCleanupInstalled = true;
|
|
37
|
+
process.once("exit", () => {
|
|
38
|
+
for (const p of liveChildren) killTree(p);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
var DEFAULT_MAX_BUFFER = 1024 * 1024 * 5;
|
|
42
|
+
var KILL_GRACE_MS = 1e3;
|
|
43
|
+
function makeSink(maxBuffer) {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let len = 0;
|
|
46
|
+
let over = false;
|
|
47
|
+
return {
|
|
48
|
+
push(chunk) {
|
|
49
|
+
if (len >= maxBuffer) {
|
|
50
|
+
over = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const room = maxBuffer - len;
|
|
54
|
+
if (chunk.length > room) {
|
|
55
|
+
chunks.push(chunk.subarray(0, room));
|
|
56
|
+
len = maxBuffer;
|
|
57
|
+
over = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
chunks.push(chunk);
|
|
61
|
+
len += chunk.length;
|
|
62
|
+
},
|
|
63
|
+
done: () => Buffer.concat(chunks),
|
|
64
|
+
over: () => over
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function execAsync(file, argv, opts = {}) {
|
|
68
|
+
return new Promise((resolve2) => {
|
|
69
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
70
|
+
const out = makeSink(maxBuffer);
|
|
71
|
+
const err = makeSink(maxBuffer);
|
|
72
|
+
if (opts.signal?.aborted) {
|
|
73
|
+
resolve2({
|
|
74
|
+
status: null,
|
|
75
|
+
stdout: Buffer.alloc(0),
|
|
76
|
+
stderr: Buffer.alloc(0),
|
|
77
|
+
timedOut: false,
|
|
78
|
+
truncated: false,
|
|
79
|
+
aborted: true
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let child;
|
|
84
|
+
try {
|
|
85
|
+
child = spawn(file, argv, {
|
|
86
|
+
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
87
|
+
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
88
|
+
// 自成进程组(POSIX):超时/中断时用 killTree 一次收掉整棵进程树
|
|
89
|
+
detached: process.platform !== "win32",
|
|
90
|
+
stdio: [opts.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
|
|
91
|
+
windowsHide: true
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
resolve2({
|
|
95
|
+
status: null,
|
|
96
|
+
stdout: Buffer.alloc(0),
|
|
97
|
+
stderr: Buffer.alloc(0),
|
|
98
|
+
error: e,
|
|
99
|
+
timedOut: false,
|
|
100
|
+
truncated: false
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
trackChild(child.pid);
|
|
105
|
+
let settled = false;
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
let aborted = false;
|
|
108
|
+
let spawnError;
|
|
109
|
+
let timer = null;
|
|
110
|
+
let graceTimer = null;
|
|
111
|
+
const onAbort = () => {
|
|
112
|
+
aborted = true;
|
|
113
|
+
killAndSettle();
|
|
114
|
+
};
|
|
115
|
+
const finish = (status) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
settled = true;
|
|
118
|
+
if (timer) clearTimeout(timer);
|
|
119
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
120
|
+
if (child.pid !== void 0) liveChildren.delete(child.pid);
|
|
121
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
122
|
+
const result = {
|
|
123
|
+
status,
|
|
124
|
+
stdout: out.done(),
|
|
125
|
+
stderr: err.done(),
|
|
126
|
+
timedOut,
|
|
127
|
+
truncated: out.over() || err.over()
|
|
128
|
+
};
|
|
129
|
+
if (aborted) result.aborted = true;
|
|
130
|
+
if (spawnError) result.error = spawnError;
|
|
131
|
+
resolve2(result);
|
|
132
|
+
};
|
|
133
|
+
const killAndSettle = () => {
|
|
134
|
+
killTree(child.pid);
|
|
135
|
+
if (settled || graceTimer) return;
|
|
136
|
+
graceTimer = setTimeout(() => {
|
|
137
|
+
child.stdout?.destroy();
|
|
138
|
+
child.stderr?.destroy();
|
|
139
|
+
finish(null);
|
|
140
|
+
}, KILL_GRACE_MS);
|
|
141
|
+
};
|
|
142
|
+
if (opts.timeoutMs !== void 0 && opts.timeoutMs > 0) {
|
|
143
|
+
timer = setTimeout(() => {
|
|
144
|
+
timedOut = true;
|
|
145
|
+
killAndSettle();
|
|
146
|
+
}, opts.timeoutMs);
|
|
147
|
+
}
|
|
148
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
child.stdout?.on("data", (c) => out.push(c));
|
|
150
|
+
child.stderr?.on("data", (c) => err.push(c));
|
|
151
|
+
child.on("error", (e) => {
|
|
152
|
+
spawnError = e;
|
|
153
|
+
if (child.pid === void 0) finish(null);
|
|
154
|
+
});
|
|
155
|
+
if (opts.input !== void 0 && child.stdin) {
|
|
156
|
+
child.stdin.on("error", () => {
|
|
157
|
+
});
|
|
158
|
+
child.stdin.end(opts.input);
|
|
159
|
+
}
|
|
160
|
+
child.on("close", (code) => finish(code));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/tools/shellRuntime.ts
|
|
3
165
|
import * as fs from "fs";
|
|
4
166
|
import * as path from "path";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return
|
|
167
|
+
var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
|
|
168
|
+
function pathApi(platform) {
|
|
169
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
170
|
+
}
|
|
171
|
+
function defaultFileExists(p) {
|
|
172
|
+
try {
|
|
173
|
+
return fs.statSync(p).isFile();
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function joinPath(platform, ...parts) {
|
|
179
|
+
return pathApi(platform).join(...parts);
|
|
180
|
+
}
|
|
181
|
+
function findExecutable(name, ctx) {
|
|
182
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
183
|
+
return ctx.fileExists(name) ? name : null;
|
|
184
|
+
}
|
|
185
|
+
const suffixes = ctx.platform === "win32" ? WINDOWS_SUFFIXES : [""];
|
|
186
|
+
const delimiter = ctx.platform === "win32" ? ";" : ":";
|
|
187
|
+
const dirs = (ctx.env.PATH ?? "").split(delimiter).filter((d) => d.length > 0);
|
|
188
|
+
for (const dir of dirs) {
|
|
189
|
+
for (const suffix of suffixes) {
|
|
190
|
+
const candidate = joinPath(ctx.platform, dir, name + suffix);
|
|
191
|
+
if (ctx.fileExists(candidate)) return candidate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function kindFromBasename(base) {
|
|
197
|
+
const b = base.toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
|
|
198
|
+
if (b === "busybox") return "busybox";
|
|
199
|
+
if (b === "powershell" || b === "pwsh") return "powershell";
|
|
200
|
+
if (b === "cmd") return "cmd";
|
|
201
|
+
return "sh";
|
|
202
|
+
}
|
|
203
|
+
function busyboxArgs(file, command) {
|
|
204
|
+
const base = (file ? path.win32.basename(file) : "busybox").toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
|
|
205
|
+
return base === "busybox" ? ["sh", "-c", command] : ["-c", command];
|
|
206
|
+
}
|
|
207
|
+
function argsFor(kind, command) {
|
|
208
|
+
switch (kind) {
|
|
209
|
+
case "busybox":
|
|
210
|
+
return ["sh", "-c", command];
|
|
211
|
+
case "powershell":
|
|
212
|
+
return ["-NoProfile", "-NonInteractive", "-Command", command];
|
|
213
|
+
case "cmd":
|
|
214
|
+
return ["/d", "/s", "/c", command];
|
|
215
|
+
default:
|
|
216
|
+
return ["-c", command];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function makeSpec(kind, file, label) {
|
|
220
|
+
return {
|
|
221
|
+
kind,
|
|
222
|
+
file,
|
|
223
|
+
label,
|
|
224
|
+
buildArgs: (command) => kind === "busybox" ? busyboxArgs(file, command) : argsFor(kind, command)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function defaultLabelFor(kind) {
|
|
228
|
+
switch (kind) {
|
|
229
|
+
case "busybox":
|
|
230
|
+
return "busybox ash";
|
|
231
|
+
case "powershell":
|
|
232
|
+
return "PowerShell";
|
|
233
|
+
case "cmd":
|
|
234
|
+
return "cmd.exe";
|
|
235
|
+
case "default":
|
|
236
|
+
return "\u7CFB\u7EDF\u9ED8\u8BA4 shell";
|
|
237
|
+
default:
|
|
238
|
+
return "sh";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function findBusybox(ctx, execPath) {
|
|
242
|
+
const explicit = (ctx.env.ZQAGENT_BUSYBOX ?? "").trim();
|
|
243
|
+
if (explicit) return ctx.fileExists(explicit) ? explicit : null;
|
|
244
|
+
const exeDir = pathApi(ctx.platform).dirname(execPath);
|
|
245
|
+
const candidates = [
|
|
246
|
+
joinPath(ctx.platform, exeDir, "busybox.exe"),
|
|
247
|
+
joinPath(ctx.platform, exeDir, "sh.exe"),
|
|
248
|
+
joinPath(ctx.platform, exeDir, "bin", "busybox.exe"),
|
|
249
|
+
joinPath(ctx.platform, exeDir, "bin", "sh.exe")
|
|
250
|
+
];
|
|
251
|
+
for (const candidate of candidates) {
|
|
252
|
+
if (ctx.fileExists(candidate)) return candidate;
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function resolveRequested(requested, ctx, execPath) {
|
|
257
|
+
const lower = requested.toLowerCase();
|
|
258
|
+
if (lower === "cmd") {
|
|
259
|
+
const file = findExecutable("cmd", ctx) ?? ctx.env.ComSpec ?? ctx.env.COMSPEC ?? "cmd.exe";
|
|
260
|
+
return makeSpec("cmd", file, "cmd.exe");
|
|
261
|
+
}
|
|
262
|
+
if (lower === "powershell" || lower === "pwsh") {
|
|
263
|
+
const file = findExecutable(lower, ctx) ?? findExecutable("powershell", ctx) ?? `${lower}.exe`;
|
|
264
|
+
return makeSpec("powershell", file, "PowerShell");
|
|
265
|
+
}
|
|
266
|
+
if (lower === "busybox") {
|
|
267
|
+
const found = findBusybox(ctx, execPath) ?? findExecutable("busybox", ctx);
|
|
268
|
+
return makeSpec("busybox", found ?? requested, "busybox ash");
|
|
269
|
+
}
|
|
270
|
+
if (["sh", "bash", "dash", "zsh", "ash"].includes(lower)) {
|
|
271
|
+
return makeSpec("sh", findExecutable(lower, ctx) ?? lower, lower);
|
|
272
|
+
}
|
|
273
|
+
const base = pathApi(ctx.platform).basename(requested);
|
|
274
|
+
const kind = kindFromBasename(base);
|
|
275
|
+
return makeSpec(kind, requested, base || defaultLabelFor(kind));
|
|
276
|
+
}
|
|
277
|
+
function resolveShellSpec(opts = {}) {
|
|
278
|
+
const platform = opts.platform ?? process.platform;
|
|
279
|
+
const env = opts.env ?? process.env;
|
|
280
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
281
|
+
const ctx = {
|
|
282
|
+
platform,
|
|
283
|
+
env,
|
|
284
|
+
fileExists: opts.fileExists ?? defaultFileExists
|
|
285
|
+
};
|
|
286
|
+
const requested = (env.ZQAGENT_SHELL ?? "").trim();
|
|
287
|
+
if (requested && requested.toLowerCase() !== "auto") {
|
|
288
|
+
return resolveRequested(requested, ctx, execPath);
|
|
289
|
+
}
|
|
290
|
+
if (platform !== "win32") {
|
|
291
|
+
return makeSpec("default", null, "/bin/sh");
|
|
292
|
+
}
|
|
293
|
+
const busybox = findBusybox(ctx, execPath);
|
|
294
|
+
if (busybox) return makeSpec("busybox", busybox, "busybox ash (POSIX)");
|
|
295
|
+
const bash = findExecutable("bash", ctx);
|
|
296
|
+
if (bash) return makeSpec("sh", bash, "bash (Git for Windows)");
|
|
297
|
+
const shell = findExecutable("pwsh", ctx) ?? findExecutable("powershell", ctx);
|
|
298
|
+
if (shell) return makeSpec("powershell", shell, "PowerShell");
|
|
299
|
+
return makeSpec("default", null, env.ComSpec ?? env.COMSPEC ?? "cmd.exe");
|
|
300
|
+
}
|
|
301
|
+
function wrapCommand(kind, command, platform = process.platform) {
|
|
302
|
+
if (kind === "powershell") {
|
|
303
|
+
return "$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8; " + command + "\nif ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }";
|
|
304
|
+
}
|
|
305
|
+
if (kind === "cmd" || kind === "default" && platform === "win32") {
|
|
306
|
+
return `chcp 65001>nul & ${command}`;
|
|
307
|
+
}
|
|
308
|
+
return command;
|
|
309
|
+
}
|
|
310
|
+
function describeShell(spec, platform) {
|
|
311
|
+
if (spec.kind === "sh" || spec.kind === "busybox") {
|
|
312
|
+
return `\u5F53\u524D\u5E73\u53F0 ${platform}\uFF0Cshell=${spec.label}\uFF1A\u8BF7\u4F7F\u7528 POSIX \u98CE\u683C\u547D\u4EE4\u3002`;
|
|
313
|
+
}
|
|
314
|
+
if (spec.kind === "powershell") {
|
|
315
|
+
return `\u5F53\u524D\u5E73\u53F0 ${platform}\uFF0Cshell=PowerShell\uFF1Als/cat/rm/cp/mv/pwd \u7B49\u522B\u540D\u53EF\u7528\uFF0C\u4E5F\u53EF\u7528 cmdlet\uFF1B\u8DEF\u5F84\u5206\u9694\u7B26\u4E3A "\\"\u3002`;
|
|
316
|
+
}
|
|
317
|
+
if (spec.kind === "default" && platform !== "win32") {
|
|
318
|
+
return `\u5F53\u524D\u5E73\u53F0 ${platform}\uFF0Cshell=${spec.label}\uFF1A\u8BF7\u4F7F\u7528 POSIX \u98CE\u683C\u547D\u4EE4\u3002`;
|
|
319
|
+
}
|
|
320
|
+
return `\u5F53\u524D\u5E73\u53F0 ${platform}\uFF0Cshell=${spec.label}\uFF1A\u8BF7\u4F7F\u7528 Windows \u547D\u4EE4\uFF08dir/type/del/copy/move/findstr\uFF09\uFF0C\u591A\u6761\u547D\u4EE4\u7528 & \u6216 && \u8FDE\u63A5\uFF1B\u4E0D\u8981\u4F7F\u7528 ls/cat/rm/grep \u7B49 POSIX \u547D\u4EE4\u3002`;
|
|
321
|
+
}
|
|
322
|
+
function parseCdChain(command, opts = {}) {
|
|
323
|
+
const platform = opts.platform ?? process.platform;
|
|
324
|
+
const trimmed = command.trim();
|
|
325
|
+
const chain = /^([\s\S]*?)\s*&&\s*([\s\S]+)$/.exec(trimmed);
|
|
326
|
+
const head = (chain ? chain[1] : trimmed).trim();
|
|
327
|
+
const rest = chain ? chain[2].trim() : null;
|
|
328
|
+
const m = /^(?:cd|set-location|sl|pushd|push-location)\b\s*([\s\S]*)$/i.exec(head);
|
|
329
|
+
if (!m) return { target: null, rest: null };
|
|
330
|
+
let target = m[1].trim();
|
|
331
|
+
target = target.replace(/^-(?:path|literalpath)\s+/i, "");
|
|
332
|
+
if (platform === "win32") target = target.replace(/^\/d\s+/i, "");
|
|
333
|
+
target = target.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
|
|
334
|
+
if (!target || target === "-") return { target: null, rest: null };
|
|
335
|
+
return { target, rest };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/tools/shellBackground.ts
|
|
339
|
+
import { spawn as spawn2 } from "child_process";
|
|
340
|
+
var DEFAULT_BUFFER_LIMIT = 64 * 1024;
|
|
341
|
+
var RingBuffer = class {
|
|
342
|
+
constructor(limit = DEFAULT_BUFFER_LIMIT) {
|
|
343
|
+
this.limit = limit;
|
|
344
|
+
}
|
|
345
|
+
limit;
|
|
346
|
+
chunks = [];
|
|
347
|
+
size = 0;
|
|
348
|
+
add(chunk) {
|
|
349
|
+
if (!chunk) return;
|
|
350
|
+
this.chunks.push(chunk);
|
|
351
|
+
this.size += chunk.length;
|
|
352
|
+
while (this.size > this.limit && this.chunks.length > 1) {
|
|
353
|
+
this.size -= this.chunks.shift().length;
|
|
354
|
+
}
|
|
355
|
+
if (this.size > this.limit) {
|
|
356
|
+
const joined = this.chunks.join("");
|
|
357
|
+
this.chunks = [joined.slice(-this.limit)];
|
|
358
|
+
this.size = this.chunks[0].length;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
get() {
|
|
362
|
+
const joined = this.chunks.join("");
|
|
363
|
+
return joined.length > this.limit ? joined.slice(-this.limit) : joined;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
var ShellTaskManager = class {
|
|
367
|
+
tasks = /* @__PURE__ */ new Map();
|
|
368
|
+
seq = 0;
|
|
369
|
+
cleanupInstalled = false;
|
|
370
|
+
/** 启动一个后台任务,立即返回快照(不等退出) */
|
|
371
|
+
start(opts) {
|
|
372
|
+
this.installCleanup();
|
|
373
|
+
const id = `shell-${++this.seq}-${Date.now().toString(36)}`;
|
|
374
|
+
const child = spawn2(opts.file, opts.argv, {
|
|
375
|
+
cwd: opts.cwd,
|
|
376
|
+
env: opts.env,
|
|
377
|
+
detached: process.platform !== "win32",
|
|
378
|
+
windowsHide: true,
|
|
379
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
380
|
+
});
|
|
381
|
+
const task = {
|
|
382
|
+
id,
|
|
383
|
+
command: opts.command,
|
|
384
|
+
scope: opts.scope ?? null,
|
|
385
|
+
child,
|
|
386
|
+
stdout: new RingBuffer(),
|
|
387
|
+
stderr: new RingBuffer(),
|
|
388
|
+
status: "running",
|
|
389
|
+
exitCode: null,
|
|
390
|
+
signal: null,
|
|
391
|
+
startedAt: Date.now(),
|
|
392
|
+
endedAt: null
|
|
393
|
+
};
|
|
394
|
+
this.tasks.set(id, task);
|
|
395
|
+
child.stdout?.on("data", (d) => task.stdout.add(d.toString("utf-8")));
|
|
396
|
+
child.stderr?.on("data", (d) => task.stderr.add(d.toString("utf-8")));
|
|
397
|
+
child.on("error", (err) => task.stderr.add(`[spawn error] ${err.message}
|
|
398
|
+
`));
|
|
399
|
+
child.on("exit", (code, signal) => {
|
|
400
|
+
task.exitCode = code;
|
|
401
|
+
task.signal = signal;
|
|
402
|
+
task.endedAt = Date.now();
|
|
403
|
+
if (task.status !== "killed") task.status = "exited";
|
|
404
|
+
});
|
|
405
|
+
child.unref();
|
|
406
|
+
return this.snapshot(task);
|
|
407
|
+
}
|
|
408
|
+
/** 读取任务输出与状态 */
|
|
409
|
+
output(taskId) {
|
|
410
|
+
const task = this.tasks.get(taskId);
|
|
411
|
+
if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
412
|
+
return this.snapshot(task);
|
|
413
|
+
}
|
|
414
|
+
/** 终止任务(进程树)并返回快照;传 scope 时只允许终止该归属下的任务 */
|
|
415
|
+
kill(taskId, scope) {
|
|
416
|
+
const task = this.tasks.get(taskId);
|
|
417
|
+
if (!task) return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
418
|
+
if (scope !== void 0 && task.scope !== scope) {
|
|
419
|
+
return { error: `\u672A\u77E5\u7684\u540E\u53F0\u4EFB\u52A1: ${taskId}` };
|
|
420
|
+
}
|
|
421
|
+
if (task.status === "running") {
|
|
422
|
+
task.status = "killed";
|
|
423
|
+
task.endedAt = Date.now();
|
|
424
|
+
killTree(task.child.pid);
|
|
425
|
+
}
|
|
426
|
+
return this.snapshot(task);
|
|
427
|
+
}
|
|
428
|
+
/** 列出任务快照(调试/展示用);传 scope 时只返回该归属下的任务 */
|
|
429
|
+
list(scope) {
|
|
430
|
+
const all = [...this.tasks.values()];
|
|
431
|
+
return (scope === void 0 ? all : all.filter((t) => t.scope === scope)).map(
|
|
432
|
+
(t) => this.snapshot(t)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
/** 终止全部任务(进程退出时调用) */
|
|
436
|
+
disposeAll() {
|
|
437
|
+
for (const task of this.tasks.values()) {
|
|
438
|
+
if (task.status === "running") {
|
|
439
|
+
task.status = "killed";
|
|
440
|
+
killTree(task.child.pid);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
snapshot(task) {
|
|
445
|
+
return {
|
|
446
|
+
task_id: task.id,
|
|
447
|
+
command: task.command,
|
|
448
|
+
status: task.status,
|
|
449
|
+
running: task.status === "running",
|
|
450
|
+
exit_code: task.exitCode,
|
|
451
|
+
signal: task.signal,
|
|
452
|
+
stdout: task.stdout.get(),
|
|
453
|
+
stderr: task.stderr.get(),
|
|
454
|
+
duration_ms: (task.endedAt ?? Date.now()) - task.startedAt
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
installCleanup() {
|
|
458
|
+
if (this.cleanupInstalled) return;
|
|
459
|
+
this.cleanupInstalled = true;
|
|
460
|
+
process.once("exit", () => this.disposeAll());
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/tools/shell.ts
|
|
465
|
+
var MAX_STDOUT = 1e4;
|
|
466
|
+
var MAX_STDERR = 5e3;
|
|
467
|
+
var MAX_BUFFER = 1024 * 1024 * 5;
|
|
468
|
+
var shellSessions = /* @__PURE__ */ new Map();
|
|
469
|
+
function sessionFor(workspace) {
|
|
470
|
+
const key = path2.resolve(workspace);
|
|
471
|
+
let s = shellSessions.get(key);
|
|
472
|
+
if (!s) {
|
|
473
|
+
s = { cwd: key };
|
|
474
|
+
shellSessions.set(key, s);
|
|
475
|
+
}
|
|
476
|
+
return s;
|
|
477
|
+
}
|
|
478
|
+
function resetShellSession(workspace) {
|
|
479
|
+
shellSessions.delete(path2.resolve(workspace));
|
|
480
|
+
}
|
|
481
|
+
function resolveInvocation(spec, prepared) {
|
|
482
|
+
if (spec.file !== null) {
|
|
483
|
+
return { file: spec.file, argv: spec.buildArgs(prepared) };
|
|
484
|
+
}
|
|
485
|
+
if (process.platform === "win32") {
|
|
486
|
+
const comspec = process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe";
|
|
487
|
+
return { file: comspec, argv: ["/d", "/s", "/c", prepared] };
|
|
488
|
+
}
|
|
489
|
+
return { file: "/bin/sh", argv: ["-c", prepared] };
|
|
490
|
+
}
|
|
491
|
+
function shellEnv() {
|
|
492
|
+
return {
|
|
493
|
+
...process.env,
|
|
494
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
495
|
+
PYTHONUNBUFFERED: "1",
|
|
496
|
+
PYTHONUTF8: "1",
|
|
497
|
+
PYTHONIOENCODING: "utf-8"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function decodeOutput(buf) {
|
|
501
|
+
if (!buf || buf.length === 0) return "";
|
|
502
|
+
try {
|
|
503
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
504
|
+
} catch {
|
|
505
|
+
for (const enc of ["gbk", "big5", "shift_jis", "latin1"]) {
|
|
506
|
+
try {
|
|
507
|
+
return new TextDecoder(enc).decode(buf);
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return buf.toString("latin1");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function makeRunShell(workspace, spec, tasks, scope) {
|
|
515
|
+
const session = sessionFor(workspace);
|
|
516
|
+
return async function run_shell(args, ctx) {
|
|
8
517
|
const command = String(args.command ?? "");
|
|
9
518
|
const description = String(args.description ?? "").slice(0, 100);
|
|
519
|
+
const background = args.run_in_background === true;
|
|
10
520
|
const rawTimeout = Number(args.timeout_seconds ?? 120);
|
|
11
521
|
const timeoutSec = Number.isFinite(rawTimeout) ? rawTimeout : 120;
|
|
12
522
|
const timeout = Math.min(Math.max(timeoutSec, 1), 600) * 1e3;
|
|
13
523
|
if (!command.trim()) {
|
|
14
524
|
return { error: "\u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
15
525
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// 关闭 git 的交互式凭据提示;Python 输出不缓冲
|
|
24
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", PYTHONUNBUFFERED: "1" }
|
|
25
|
-
});
|
|
26
|
-
if (command.trim().startsWith("cd ")) {
|
|
27
|
-
const newDir = command.trim().slice(3).trim();
|
|
28
|
-
try {
|
|
29
|
-
const resolved = path.resolve(cwd, newDir);
|
|
30
|
-
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
31
|
-
cwd = resolved;
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
526
|
+
const { target, rest } = parseCdChain(command);
|
|
527
|
+
let toRun = command;
|
|
528
|
+
if (target) {
|
|
529
|
+
try {
|
|
530
|
+
const resolved = path2.resolve(session.cwd, target);
|
|
531
|
+
if (fs2.existsSync(resolved) && fs2.statSync(resolved).isDirectory()) {
|
|
532
|
+
session.cwd = resolved;
|
|
34
533
|
}
|
|
534
|
+
} catch {
|
|
35
535
|
}
|
|
536
|
+
if (rest) toRun = rest;
|
|
537
|
+
}
|
|
538
|
+
const prepared = wrapCommand(spec.kind, toRun);
|
|
539
|
+
const { file, argv } = resolveInvocation(spec, prepared);
|
|
540
|
+
const env = shellEnv();
|
|
541
|
+
if (background) {
|
|
542
|
+
const snap = tasks.start({ command, file, argv, cwd: session.cwd, env, scope });
|
|
36
543
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
cwd
|
|
544
|
+
task_id: snap.task_id,
|
|
545
|
+
status: snap.status,
|
|
546
|
+
cwd: session.cwd,
|
|
547
|
+
note: "\u5DF2\u5728\u540E\u53F0\u542F\u52A8\u3002\u7528 shell_task_output \u67E5\u770B\u8F93\u51FA\uFF0Cshell_task_kill \u7EC8\u6B62\u3002"
|
|
42
548
|
};
|
|
43
|
-
}
|
|
44
|
-
|
|
549
|
+
}
|
|
550
|
+
const result = await execAsync(file, argv, {
|
|
551
|
+
cwd: session.cwd,
|
|
552
|
+
timeoutMs: timeout,
|
|
553
|
+
maxBuffer: MAX_BUFFER,
|
|
554
|
+
env,
|
|
555
|
+
signal: ctx?.signal
|
|
556
|
+
});
|
|
557
|
+
const stdout = decodeOutput(result.stdout).slice(-MAX_STDOUT);
|
|
558
|
+
const stderr = decodeOutput(result.stderr).slice(-MAX_STDERR);
|
|
559
|
+
if (result.aborted) {
|
|
45
560
|
return {
|
|
46
|
-
stdout
|
|
47
|
-
stderr
|
|
48
|
-
exit_code:
|
|
49
|
-
|
|
50
|
-
|
|
561
|
+
stdout,
|
|
562
|
+
stderr,
|
|
563
|
+
exit_code: result.status ?? 1,
|
|
564
|
+
cwd: session.cwd,
|
|
565
|
+
interrupted: true,
|
|
566
|
+
error: "\u547D\u4EE4\u5DF2\u88AB\u7528\u6237\u4E2D\u65AD"
|
|
51
567
|
};
|
|
52
568
|
}
|
|
569
|
+
if (result.error && !result.timedOut) {
|
|
570
|
+
const message = result.error.message;
|
|
571
|
+
return {
|
|
572
|
+
stdout,
|
|
573
|
+
stderr: stderr || message,
|
|
574
|
+
exit_code: 1,
|
|
575
|
+
cwd: session.cwd,
|
|
576
|
+
error: message.slice(0, 200)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
stdout,
|
|
581
|
+
stderr,
|
|
582
|
+
exit_code: result.status ?? 1,
|
|
583
|
+
cwd: session.cwd,
|
|
584
|
+
...result.timedOut ? { timed_out: true, error: `\u547D\u4EE4\u8D85\u65F6\uFF08${timeoutSec}s\uFF09\u540E\u5DF2\u7EC8\u6B62` } : {}
|
|
585
|
+
};
|
|
53
586
|
};
|
|
54
587
|
}
|
|
55
|
-
|
|
56
|
-
|
|
588
|
+
var shellTasks = new ShellTaskManager();
|
|
589
|
+
function listShellTasks(scope) {
|
|
590
|
+
return shellTasks.list(scope);
|
|
591
|
+
}
|
|
592
|
+
function killShellTask(taskId, scope) {
|
|
593
|
+
return shellTasks.kill(taskId, scope);
|
|
594
|
+
}
|
|
595
|
+
function registerShellTool(registry, workspace, scope) {
|
|
596
|
+
const spec = resolveShellSpec();
|
|
597
|
+
const tasks = shellTasks;
|
|
598
|
+
const shell = makeRunShell(workspace, spec, tasks, scope ?? null);
|
|
57
599
|
registry.register(
|
|
58
|
-
(args) => shell(args),
|
|
600
|
+
(args, ctx) => shell(args, ctx),
|
|
59
601
|
{ category: "shell", riskLevel: "high", capabilities: ["run_shell"], requiresApproval: true },
|
|
60
602
|
{
|
|
61
603
|
type: "function",
|
|
62
604
|
function: {
|
|
63
605
|
name: "run_shell",
|
|
64
|
-
description:
|
|
606
|
+
description: `\u5728\u6301\u4E45\u5316 shell \u4F1A\u8BDD\u4E2D\u8FD0\u884C\u547D\u4EE4 (cwd \u548C\u73AF\u5883\u53D8\u91CF\u5728\u591A\u6B21\u8C03\u7528\u4E4B\u95F4\u4FDD\u6301)\u3002\u8F93\u51FA\u622A\u65AD\u5230\u6700\u540E\u90E8\u5206\uFF0Cstdout \u4E0E stderr \u90FD\u4F1A\u8FD4\u56DE\u3002\u4F20\u9012 description \u8BF4\u660E\u547D\u4EE4\u7528\u9014 (\u663E\u793A\u5728\u5BA1\u6279\u63D0\u793A\u4E2D)\uFF0C\u4F20\u9012 timeout_seconds \u63A7\u5236\u8D85\u65F6\u3002\u5BF9\u4E8E\u5F00\u53D1\u670D\u52A1\u5668 / watch \u7B49\u957F\u9A7B\u8FDB\u7A0B\uFF0C\u8BBE\u7F6E run_in_background=true\uFF0C\u968F\u540E\u7528 shell_task_output \u8F6E\u8BE2\u8F93\u51FA\u3001shell_task_kill \u7EC8\u6B62\u3002\u8FD0\u884C\u73AF\u5883\uFF1A${describeShell(spec, process.platform)}`,
|
|
65
607
|
parameters: {
|
|
66
608
|
type: "object",
|
|
67
609
|
properties: {
|
|
68
610
|
command: { type: "string", description: "\u8981\u6267\u884C\u7684 shell \u547D\u4EE4" },
|
|
69
611
|
description: { type: "string", description: "\u547D\u4EE4\u7528\u9014\u7684\u7B80\u77ED\u63CF\u8FF0 (\u663E\u793A\u5728\u5BA1\u6279\u4E2D)" },
|
|
70
|
-
timeout_seconds: { type: "integer", description: "\u8D85\u65F6\u79D2\u6570 (\u9ED8\u8BA4 120\uFF0C\u6700\u5927 600)" }
|
|
612
|
+
timeout_seconds: { type: "integer", description: "\u8D85\u65F6\u79D2\u6570 (\u9ED8\u8BA4 120\uFF0C\u6700\u5927 600)" },
|
|
613
|
+
run_in_background: {
|
|
614
|
+
type: "boolean",
|
|
615
|
+
description: "\u662F\u5426\u540E\u53F0\u8FD0\u884C (\u9ED8\u8BA4 false)\u3002\u9002\u5408\u5F00\u53D1\u670D\u52A1\u5668\u7B49\u957F\u9A7B\u8FDB\u7A0B\u3002"
|
|
616
|
+
}
|
|
71
617
|
},
|
|
72
618
|
required: ["command"]
|
|
73
619
|
}
|
|
74
620
|
}
|
|
75
621
|
}
|
|
76
622
|
);
|
|
623
|
+
registry.register(
|
|
624
|
+
(args) => tasks.output(String(args.task_id ?? "")),
|
|
625
|
+
{ category: "shell", riskLevel: "low", capabilities: ["run_shell"], requiresApproval: false },
|
|
626
|
+
{
|
|
627
|
+
type: "function",
|
|
628
|
+
function: {
|
|
629
|
+
name: "shell_task_output",
|
|
630
|
+
description: "\u67E5\u770B run_shell(run_in_background=true) \u542F\u52A8\u7684\u540E\u53F0\u4EFB\u52A1\u7684\u7D2F\u8BA1\u8F93\u51FA\u4E0E\u72B6\u6001\u3002\u8FD4\u56DE status (running/exited/killed)\u3001exit_code\u3001stdout\u3001stderr\u3002",
|
|
631
|
+
parameters: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
task_id: { type: "string", description: "run_shell \u8FD4\u56DE\u7684 task_id" }
|
|
635
|
+
},
|
|
636
|
+
required: ["task_id"]
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
registry.register(
|
|
642
|
+
(args) => tasks.kill(String(args.task_id ?? ""), scope ?? null),
|
|
643
|
+
{ category: "shell", riskLevel: "medium", capabilities: ["run_shell"], requiresApproval: false },
|
|
644
|
+
{
|
|
645
|
+
type: "function",
|
|
646
|
+
function: {
|
|
647
|
+
name: "shell_task_kill",
|
|
648
|
+
description: "\u7EC8\u6B62\u540E\u53F0\u4EFB\u52A1\uFF08\u8FDE\u540C\u5176\u5B50\u8FDB\u7A0B\u6811\uFF09\u3002\u8FD4\u56DE\u4EFB\u52A1\u7684\u6700\u7EC8\u72B6\u6001\u3002",
|
|
649
|
+
parameters: {
|
|
650
|
+
type: "object",
|
|
651
|
+
properties: {
|
|
652
|
+
task_id: { type: "string", description: "run_shell \u8FD4\u56DE\u7684 task_id" }
|
|
653
|
+
},
|
|
654
|
+
required: ["task_id"]
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
);
|
|
77
659
|
}
|
|
78
660
|
export {
|
|
79
|
-
|
|
661
|
+
decodeOutput,
|
|
662
|
+
killShellTask,
|
|
663
|
+
listShellTasks,
|
|
664
|
+
registerShellTool,
|
|
665
|
+
resetShellSession
|
|
80
666
|
};
|
|
81
667
|
//# sourceMappingURL=shell.js.map
|