@agentdevjs/shell-feature 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.js +1516 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/bash.render.d.ts +29 -0
- package/dist/templates/bash.render.js +101 -0
- package/dist/templates/bash.render.js.map +1 -0
- package/dist/templates/trash-delete.render.d.ts +14 -0
- package/dist/templates/trash-delete.render.js +36 -0
- package/dist/templates/trash-delete.render.js.map +1 -0
- package/dist/templates/trash-list.render.d.ts +19 -0
- package/dist/templates/trash-list.render.js +46 -0
- package/dist/templates/trash-list.render.js.map +1 -0
- package/dist/templates/trash-restore.render.d.ts +17 -0
- package/dist/templates/trash-restore.render.js +45 -0
- package/dist/templates/trash-restore.render.js.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1516 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { resolve as resolve3 } from "path";
|
|
5
|
+
import { getPackageInfoFromSource } from "@agentdevjs/core";
|
|
6
|
+
|
|
7
|
+
// src/tools.ts
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import * as path2 from "path";
|
|
11
|
+
import { createTool } from "@agentdevjs/core";
|
|
12
|
+
|
|
13
|
+
// src/shellQuoting.ts
|
|
14
|
+
function containsHeredoc(command) {
|
|
15
|
+
if (/\d\s*<<\s*\d/.test(command) || /\[\[\s*\d+\s*<<\s*\d+\s*\]\]/.test(command) || /\$\(\(.*<<.*\)\)/.test(command)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const heredocRegex = /<<-?\s*(?:(['"]?)(\w+)\1|\\(\w+))/;
|
|
19
|
+
return heredocRegex.test(command);
|
|
20
|
+
}
|
|
21
|
+
function containsMultilineString(command) {
|
|
22
|
+
const singleQuoteMultiline = /'(?:[^'\\]|\\.)*\n(?:[^'\\]|\\.)*'/;
|
|
23
|
+
const doubleQuoteMultiline = /"(?:[^"\\]|\\.)*\n(?:[^"\\]|\\.)*"/;
|
|
24
|
+
return singleQuoteMultiline.test(command) || doubleQuoteMultiline.test(command);
|
|
25
|
+
}
|
|
26
|
+
function hasStdinRedirect(command) {
|
|
27
|
+
return /(?:^|[\s;&|])<(?![<(])\s*\S+/.test(command);
|
|
28
|
+
}
|
|
29
|
+
function shouldAddStdinRedirect(command) {
|
|
30
|
+
if (containsHeredoc(command)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (hasStdinRedirect(command)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
var NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g;
|
|
39
|
+
function rewriteWindowsNullRedirect(command) {
|
|
40
|
+
return command.replace(NUL_REDIRECT_REGEX, "$1/dev/null");
|
|
41
|
+
}
|
|
42
|
+
function escapeForSingleQuote(str) {
|
|
43
|
+
return str.replace(/'/g, `'"'"'`);
|
|
44
|
+
}
|
|
45
|
+
function wrapSingleQuote(str) {
|
|
46
|
+
return `'${escapeForSingleQuote(str)}'`;
|
|
47
|
+
}
|
|
48
|
+
function quoteShellCommand(command, addStdinRedirect = true) {
|
|
49
|
+
if (containsHeredoc(command) || containsMultilineString(command)) {
|
|
50
|
+
const quoted2 = wrapSingleQuote(command);
|
|
51
|
+
if (containsHeredoc(command)) {
|
|
52
|
+
return quoted2;
|
|
53
|
+
}
|
|
54
|
+
return addStdinRedirect ? `${quoted2} < /dev/null` : quoted2;
|
|
55
|
+
}
|
|
56
|
+
const quoted = wrapSingleQuote(command);
|
|
57
|
+
if (addStdinRedirect) {
|
|
58
|
+
return `${quoted} < /dev/null`;
|
|
59
|
+
}
|
|
60
|
+
return quoted;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/shell-core.ts
|
|
64
|
+
import { spawn } from "child_process";
|
|
65
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
66
|
+
import * as path from "path";
|
|
67
|
+
import { emitNotification, createToolProgress } from "@agentdevjs/core";
|
|
68
|
+
var TERMINATION_DRAIN_FALLBACK_MS = 1e3;
|
|
69
|
+
var TERMINATION_DRAIN_SAFETY_MS = 50;
|
|
70
|
+
var PROGRESS_EMIT_INTERVAL_MS = 300;
|
|
71
|
+
var PROGRESS_TAIL_LINES = 5;
|
|
72
|
+
var SHELL_METADATA_OPEN = "<shell_metadata>";
|
|
73
|
+
var SHELL_METADATA_CLOSE = "</shell_metadata>";
|
|
74
|
+
function tailLines(text, lines) {
|
|
75
|
+
if (!text) return "";
|
|
76
|
+
const parts = text.split("\n");
|
|
77
|
+
const tail = parts.slice(-lines).join("\n");
|
|
78
|
+
return tail.length > 500 ? tail.slice(-500) : tail;
|
|
79
|
+
}
|
|
80
|
+
var MAX_OUTPUT_LENGTH = 3e4;
|
|
81
|
+
function timestampSlug() {
|
|
82
|
+
const now = /* @__PURE__ */ new Date();
|
|
83
|
+
const ts = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "-" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
|
|
84
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
85
|
+
return `${ts}-${suffix}`;
|
|
86
|
+
}
|
|
87
|
+
async function processOutputWithPersistence(output, workdir, limit = MAX_OUTPUT_LENGTH, forcePersist = false) {
|
|
88
|
+
if (!forcePersist && output.length <= limit) return [output, null];
|
|
89
|
+
let filePath = null;
|
|
90
|
+
try {
|
|
91
|
+
const tempDir = path.join(workdir, ".agentdev", "temp");
|
|
92
|
+
const fileName = `bash-output-${timestampSlug()}.log`;
|
|
93
|
+
filePath = path.join(tempDir, fileName);
|
|
94
|
+
await mkdir(tempDir, { recursive: true });
|
|
95
|
+
await writeFile(filePath, output, "utf-8");
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[shell] Failed to persist output: ${err}`);
|
|
98
|
+
filePath = null;
|
|
99
|
+
}
|
|
100
|
+
if (output.length <= limit) return [output, filePath];
|
|
101
|
+
const headSize = Math.floor(limit * 0.6);
|
|
102
|
+
const tailSize = limit - headSize;
|
|
103
|
+
const head = output.slice(0, headSize);
|
|
104
|
+
const tail = output.slice(-tailSize);
|
|
105
|
+
const omitted = output.length - limit;
|
|
106
|
+
const totalKB = Math.round(output.length / 1024);
|
|
107
|
+
const persistNotice = filePath ? `[Full output (${totalKB}KB) saved to: ${filePath}]
|
|
108
|
+
Use the read tool to access the full output if needed.
|
|
109
|
+
` : "";
|
|
110
|
+
return [
|
|
111
|
+
head + `
|
|
112
|
+
|
|
113
|
+
... [truncated: omitted ${omitted} characters (${totalKB}KB total)] ...
|
|
114
|
+
${persistNotice}
|
|
115
|
+
` + tail,
|
|
116
|
+
filePath
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
function makeKillChild(child) {
|
|
120
|
+
return () => {
|
|
121
|
+
try {
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
124
|
+
killer.once("error", (error) => {
|
|
125
|
+
console.warn(`[shell] taskkill failed for PID=${child.pid}:`, error);
|
|
126
|
+
child.kill("SIGKILL");
|
|
127
|
+
});
|
|
128
|
+
killer.once("close", (code) => {
|
|
129
|
+
if (code !== 0) {
|
|
130
|
+
console.warn(`[shell] taskkill exited with code ${code} for PID=${child.pid}; killing the direct child`);
|
|
131
|
+
child.kill("SIGKILL");
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
process.kill(-child.pid, "SIGKILL");
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function drainToEof(child, timeoutMs) {
|
|
142
|
+
return new Promise((resolve4) => {
|
|
143
|
+
let done = false;
|
|
144
|
+
const finish = () => {
|
|
145
|
+
if (done) return;
|
|
146
|
+
done = true;
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolve4();
|
|
149
|
+
};
|
|
150
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
151
|
+
timer.unref();
|
|
152
|
+
child.once("close", finish);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function formatShellMetadata(meta) {
|
|
156
|
+
return [
|
|
157
|
+
SHELL_METADATA_OPEN,
|
|
158
|
+
`terminated: ${meta.terminated}`,
|
|
159
|
+
`reason: ${meta.reason}`,
|
|
160
|
+
`durationMs: ${Math.round(meta.durationMs)}`,
|
|
161
|
+
`exitCode: ${meta.exitCode === null ? "null" : meta.exitCode}`,
|
|
162
|
+
`outputBytes: ${meta.outputBytes}`,
|
|
163
|
+
`truncated: ${meta.truncated}`,
|
|
164
|
+
`logPath: ${meta.logPath ?? "null"}`,
|
|
165
|
+
SHELL_METADATA_CLOSE
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
function runCollectedProcess(opts) {
|
|
169
|
+
const { workdir, logPrefix, signal, termination, progress } = opts;
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
const cleanForRun = (raw) => opts.cleanStderr ? opts.cleanStderr(raw) : raw.trim();
|
|
172
|
+
const readTermination = () => termination?.() ?? "user";
|
|
173
|
+
return new Promise((resolve4, reject) => {
|
|
174
|
+
let stdout = "";
|
|
175
|
+
let stderr = "";
|
|
176
|
+
let settled = false;
|
|
177
|
+
let lastProgressAt = 0;
|
|
178
|
+
let progressTimer = null;
|
|
179
|
+
const stopProgressTimer = () => {
|
|
180
|
+
if (progressTimer !== null) {
|
|
181
|
+
clearInterval(progressTimer);
|
|
182
|
+
progressTimer = null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const emitProgress = () => {
|
|
186
|
+
if (!progress) return;
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if (now - lastProgressAt < PROGRESS_EMIT_INTERVAL_MS) return;
|
|
189
|
+
lastProgressAt = now;
|
|
190
|
+
try {
|
|
191
|
+
emitNotification(createToolProgress({
|
|
192
|
+
callId: progress.callId ?? "",
|
|
193
|
+
toolName: progress.toolName,
|
|
194
|
+
startedAt,
|
|
195
|
+
elapsedMs: now - startedAt,
|
|
196
|
+
timeoutMs: progress.timeoutMs,
|
|
197
|
+
outputTail: tailLines(stdout || stderr, PROGRESS_TAIL_LINES)
|
|
198
|
+
}));
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const finishSettled = (fn) => {
|
|
203
|
+
stopProgressTimer();
|
|
204
|
+
fn();
|
|
205
|
+
};
|
|
206
|
+
const finishTerminated = async () => {
|
|
207
|
+
const combined = [stdout, cleanForRun(stderr)].filter(Boolean).join("\n");
|
|
208
|
+
const [text, logPath] = await processOutputWithPersistence(
|
|
209
|
+
combined || "",
|
|
210
|
+
workdir,
|
|
211
|
+
MAX_OUTPUT_LENGTH,
|
|
212
|
+
true
|
|
213
|
+
);
|
|
214
|
+
const meta = {
|
|
215
|
+
terminated: true,
|
|
216
|
+
reason: readTermination(),
|
|
217
|
+
durationMs: Date.now() - startedAt,
|
|
218
|
+
exitCode: null,
|
|
219
|
+
outputBytes: Buffer.byteLength(combined, "utf-8"),
|
|
220
|
+
truncated: text.length < combined.length,
|
|
221
|
+
logPath
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
stdout: text,
|
|
225
|
+
stderr: "",
|
|
226
|
+
output: text ? `${text}
|
|
227
|
+
${formatShellMetadata(meta)}` : formatShellMetadata(meta)
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
const child = spawn(opts.execPath, opts.args, {
|
|
231
|
+
cwd: workdir,
|
|
232
|
+
env: opts.env ?? { ...process.env },
|
|
233
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
234
|
+
windowsHide: true,
|
|
235
|
+
// On Linux/macOS, detached: true puts the child in its own process group
|
|
236
|
+
// so that process.kill(-pid) can terminate the entire group on abort.
|
|
237
|
+
...process.platform !== "win32" ? { detached: true } : {}
|
|
238
|
+
});
|
|
239
|
+
const killChild = makeKillChild(child);
|
|
240
|
+
if (progress) {
|
|
241
|
+
emitProgress();
|
|
242
|
+
progressTimer = setInterval(emitProgress, PROGRESS_EMIT_INTERVAL_MS);
|
|
243
|
+
progressTimer.unref?.();
|
|
244
|
+
}
|
|
245
|
+
const cleanupSignal = () => {
|
|
246
|
+
signal?.removeEventListener("abort", onAbort);
|
|
247
|
+
};
|
|
248
|
+
const terminateAndCollect = () => {
|
|
249
|
+
if (settled) return;
|
|
250
|
+
settled = true;
|
|
251
|
+
cleanupSignal();
|
|
252
|
+
stopProgressTimer();
|
|
253
|
+
console.log(`${logPrefix} signal abort detected, killing child PID=${child.pid}`);
|
|
254
|
+
killChild();
|
|
255
|
+
const deadline = opts.terminationDeadline?.();
|
|
256
|
+
const drainMs = Math.max(
|
|
257
|
+
0,
|
|
258
|
+
(deadline ?? Date.now() + TERMINATION_DRAIN_FALLBACK_MS) - Date.now() - TERMINATION_DRAIN_SAFETY_MS
|
|
259
|
+
);
|
|
260
|
+
void drainToEof(child, drainMs).then(finishTerminated).then((value) => finishSettled(() => resolve4(value))).catch((err) => finishSettled(() => reject(err)));
|
|
261
|
+
};
|
|
262
|
+
const onAbort = () => terminateAndCollect();
|
|
263
|
+
if (signal) {
|
|
264
|
+
if (signal.aborted) {
|
|
265
|
+
terminateAndCollect();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
269
|
+
}
|
|
270
|
+
child.stdout?.on("data", (data) => {
|
|
271
|
+
stdout += data.toString();
|
|
272
|
+
});
|
|
273
|
+
child.stderr?.on("data", (data) => {
|
|
274
|
+
stderr += data.toString();
|
|
275
|
+
});
|
|
276
|
+
child.on("close", (code) => {
|
|
277
|
+
if (settled) return;
|
|
278
|
+
settled = true;
|
|
279
|
+
cleanupSignal();
|
|
280
|
+
stopProgressTimer();
|
|
281
|
+
const cleanStderr = cleanForRun(stderr);
|
|
282
|
+
if (signal?.aborted) {
|
|
283
|
+
void finishTerminated().then((value) => finishSettled(() => resolve4(value))).catch((err) => finishSettled(() => reject(err)));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (code === 0) {
|
|
287
|
+
Promise.all([
|
|
288
|
+
processOutputWithPersistence(stdout || "", workdir),
|
|
289
|
+
processOutputWithPersistence(cleanStderr || "", workdir)
|
|
290
|
+
]).then(([truncatedStdout, truncatedStderr]) => {
|
|
291
|
+
finishSettled(() => resolve4({
|
|
292
|
+
stdout: truncatedStdout[0],
|
|
293
|
+
stderr: truncatedStderr[0],
|
|
294
|
+
output: truncatedStdout[0] || truncatedStderr[0]
|
|
295
|
+
}));
|
|
296
|
+
}).catch((err) => {
|
|
297
|
+
finishSettled(() => reject(err));
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
const combined = [stdout, cleanStderr].filter(Boolean).join("\n\n--- stderr ---\n");
|
|
301
|
+
processOutputWithPersistence(combined, workdir).then(([truncated]) => {
|
|
302
|
+
finishSettled(() => reject(new Error(truncated || `Command failed with exit code ${code}`)));
|
|
303
|
+
}).catch((err) => {
|
|
304
|
+
finishSettled(() => reject(err));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
child.on("error", (err) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
cleanupSignal();
|
|
312
|
+
finishSettled(() => reject(err));
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/tools.ts
|
|
318
|
+
var cachedBashPath = null;
|
|
319
|
+
function findGitBashPath(configuredPath) {
|
|
320
|
+
if (cachedBashPath) return cachedBashPath;
|
|
321
|
+
if (configuredPath && existsSync(configuredPath)) {
|
|
322
|
+
cachedBashPath = configuredPath;
|
|
323
|
+
return cachedBashPath;
|
|
324
|
+
}
|
|
325
|
+
if (process.platform !== "win32") {
|
|
326
|
+
cachedBashPath = process.env.SHELL || "/bin/bash";
|
|
327
|
+
return cachedBashPath;
|
|
328
|
+
}
|
|
329
|
+
const candidates = [];
|
|
330
|
+
if (process.env.AGENTDEV_GIT_BASH_PATH) {
|
|
331
|
+
candidates.push(process.env.AGENTDEV_GIT_BASH_PATH);
|
|
332
|
+
}
|
|
333
|
+
candidates.push("C:\\Program Files\\Git\\bin\\bash.exe");
|
|
334
|
+
candidates.push("C:\\Program Files (x86)\\Git\\bin\\bash.exe");
|
|
335
|
+
try {
|
|
336
|
+
const result = execSync("where bash", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
337
|
+
for (const line of result.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
338
|
+
if (line.toLowerCase().includes("git")) {
|
|
339
|
+
candidates.push(line);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const gitPath = execSync("where git", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0]?.trim();
|
|
346
|
+
if (gitPath) {
|
|
347
|
+
const derived = path2.join(path2.dirname(path2.dirname(gitPath)), "bin", "bash.exe");
|
|
348
|
+
candidates.push(derived);
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
for (const candidate of candidates) {
|
|
353
|
+
if (candidate && existsSync(candidate)) {
|
|
354
|
+
cachedBashPath = candidate;
|
|
355
|
+
return cachedBashPath;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
cachedBashPath = null;
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
362
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
363
|
+
async function runShellCommand(command, options = {}, context) {
|
|
364
|
+
const workspaceDir = options.workspaceDir || process.cwd();
|
|
365
|
+
const workdir = options.workdir || workspaceDir;
|
|
366
|
+
const resourceRoot = options.resourceRoot || process.cwd();
|
|
367
|
+
const bashrcPath = resourceRoot.replace(/\\/g, "/") + "/.agentdev/bashrc";
|
|
368
|
+
console.log(`[shell] ${command}`);
|
|
369
|
+
const normalizedCommand = rewriteWindowsNullRedirect(command);
|
|
370
|
+
const addStdinRedirect = shouldAddStdinRedirect(normalizedCommand);
|
|
371
|
+
const quotedCommand = quoteShellCommand(normalizedCommand, addStdinRedirect);
|
|
372
|
+
const quotedBashrc = `'${bashrcPath.replace(/'/g, `'\\''`)}'`;
|
|
373
|
+
const commandString = `source ${quotedBashrc} 2>/dev/null || true; eval ${quotedCommand}`;
|
|
374
|
+
const bashPath = options.bashPath || findGitBashPath();
|
|
375
|
+
if (!bashPath) {
|
|
376
|
+
const hint = process.platform === "win32" ? "Git Bash not found. Please install Git for Windows or configure the path in settings." : "Bash not found. Please ensure bash is installed or configure the path in settings.";
|
|
377
|
+
throw new Error(hint);
|
|
378
|
+
}
|
|
379
|
+
const isWin = process.platform === "win32";
|
|
380
|
+
return runCollectedProcess({
|
|
381
|
+
workdir,
|
|
382
|
+
execPath: bashPath,
|
|
383
|
+
args: ["-c", commandString],
|
|
384
|
+
env: {
|
|
385
|
+
...process.env,
|
|
386
|
+
// MSYSTEM is only meaningful for Git Bash (MSYS2/MinGW) on Windows.
|
|
387
|
+
...isWin ? { MSYSTEM: process.env.MSYSTEM || "MINGW64" } : {}
|
|
388
|
+
},
|
|
389
|
+
logPrefix: "[shell]",
|
|
390
|
+
signal: context?.signal,
|
|
391
|
+
termination: context?.termination,
|
|
392
|
+
terminationDeadline: context?.terminationDeadline,
|
|
393
|
+
progress: context?.progress,
|
|
394
|
+
cleanStderr: (stderr) => stderr.split("\n").filter((line) => !line.includes("process group") && !line.includes("job control")).join("\n").trim()
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function createShellCommandTool(description, options = {}) {
|
|
398
|
+
const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
399
|
+
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? MAX_TIMEOUT_MS, defaultTimeoutMs, 1);
|
|
400
|
+
return createTool({
|
|
401
|
+
name: "bash",
|
|
402
|
+
description,
|
|
403
|
+
parameters: {
|
|
404
|
+
type: "object",
|
|
405
|
+
properties: {
|
|
406
|
+
command: { type: "string" },
|
|
407
|
+
timeout: { type: "number", description: `Optional timeout in milliseconds (max ${maxTimeoutMs}). Defaults to ${defaultTimeoutMs}.` }
|
|
408
|
+
},
|
|
409
|
+
required: ["command"]
|
|
410
|
+
},
|
|
411
|
+
render: { call: "bash", result: "bash" },
|
|
412
|
+
// 计时职责归框架 executor(ticket 023):args.timeout 经 fromArg 消费并 clamp
|
|
413
|
+
timeout: {
|
|
414
|
+
defaultMs: defaultTimeoutMs,
|
|
415
|
+
maxMs: maxTimeoutMs,
|
|
416
|
+
fromArg: "timeout"
|
|
417
|
+
},
|
|
418
|
+
execute: async (args, context) => {
|
|
419
|
+
const { command } = args;
|
|
420
|
+
const effectiveTimeoutMs = typeof context?.timeoutMs === "number" ? context.timeoutMs : null;
|
|
421
|
+
const result = await runShellCommand(command, options, {
|
|
422
|
+
signal: context?.signal,
|
|
423
|
+
termination: context?.termination,
|
|
424
|
+
terminationDeadline: context?.terminationDeadline,
|
|
425
|
+
progress: {
|
|
426
|
+
callId: context?.callId,
|
|
427
|
+
toolName: "bash",
|
|
428
|
+
timeoutMs: effectiveTimeoutMs
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return result.output;
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/powershell.ts
|
|
437
|
+
import { execSync as execSync2 } from "child_process";
|
|
438
|
+
import { existsSync as existsSync2 } from "fs";
|
|
439
|
+
import { createTool as createTool2 } from "@agentdevjs/core";
|
|
440
|
+
var cachedPsPath = void 0;
|
|
441
|
+
function findPowerShellPath(configuredPath) {
|
|
442
|
+
if (cachedPsPath !== void 0) return cachedPsPath;
|
|
443
|
+
if (configuredPath && existsSync2(configuredPath)) {
|
|
444
|
+
cachedPsPath = configuredPath;
|
|
445
|
+
return cachedPsPath;
|
|
446
|
+
}
|
|
447
|
+
if (process.env.AGENTDEV_POWERSHELL_PATH && existsSync2(process.env.AGENTDEV_POWERSHELL_PATH)) {
|
|
448
|
+
cachedPsPath = process.env.AGENTDEV_POWERSHELL_PATH;
|
|
449
|
+
return cachedPsPath;
|
|
450
|
+
}
|
|
451
|
+
const isWin = process.platform === "win32";
|
|
452
|
+
const whereCmd = isWin ? "where" : "which";
|
|
453
|
+
try {
|
|
454
|
+
const result = execSync2(`${whereCmd} pwsh`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
455
|
+
const p = result.split("\n").map((l) => l.trim()).filter(Boolean)[0];
|
|
456
|
+
if (p && existsSync2(p)) {
|
|
457
|
+
cachedPsPath = p;
|
|
458
|
+
return cachedPsPath;
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
if (isWin) {
|
|
463
|
+
try {
|
|
464
|
+
const result = execSync2(`${whereCmd} powershell`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
465
|
+
const p = result.split("\n").map((l) => l.trim()).filter(Boolean)[0];
|
|
466
|
+
if (p && existsSync2(p)) {
|
|
467
|
+
cachedPsPath = p;
|
|
468
|
+
return cachedPsPath;
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
const sysPath = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
473
|
+
if (existsSync2(sysPath)) {
|
|
474
|
+
cachedPsPath = sysPath;
|
|
475
|
+
return cachedPsPath;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
cachedPsPath = null;
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
var DEFAULT_TIMEOUT_MS2 = 12e4;
|
|
482
|
+
var MAX_TIMEOUT_MS2 = 6e5;
|
|
483
|
+
async function runPowerShellCommand(command, options = {}, context) {
|
|
484
|
+
const workspaceDir = options.workspaceDir || process.cwd();
|
|
485
|
+
const workdir = options.workdir || workspaceDir;
|
|
486
|
+
const psPath = options.psPath || findPowerShellPath();
|
|
487
|
+
if (!psPath) {
|
|
488
|
+
throw new Error("PowerShell not found.");
|
|
489
|
+
}
|
|
490
|
+
console.log(`[powershell] ${command}`);
|
|
491
|
+
return runCollectedProcess({
|
|
492
|
+
workdir,
|
|
493
|
+
execPath: psPath,
|
|
494
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", command],
|
|
495
|
+
env: { ...process.env },
|
|
496
|
+
logPrefix: "[powershell]",
|
|
497
|
+
signal: context?.signal,
|
|
498
|
+
termination: context?.termination,
|
|
499
|
+
terminationDeadline: context?.terminationDeadline,
|
|
500
|
+
progress: context?.progress
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function createPowerShellTool(description, options = {}) {
|
|
504
|
+
const defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
505
|
+
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? MAX_TIMEOUT_MS2, defaultTimeoutMs, 1);
|
|
506
|
+
return createTool2({
|
|
507
|
+
name: "powershell",
|
|
508
|
+
description,
|
|
509
|
+
parameters: {
|
|
510
|
+
type: "object",
|
|
511
|
+
properties: {
|
|
512
|
+
command: { type: "string" },
|
|
513
|
+
timeout: { type: "number", description: `Optional timeout in milliseconds (max ${maxTimeoutMs}). Defaults to ${defaultTimeoutMs}.` }
|
|
514
|
+
},
|
|
515
|
+
required: ["command"]
|
|
516
|
+
},
|
|
517
|
+
render: { call: "bash", result: "bash" },
|
|
518
|
+
// 与 bash 一致:计时职责归框架 executor(ticket 023)
|
|
519
|
+
timeout: {
|
|
520
|
+
defaultMs: defaultTimeoutMs,
|
|
521
|
+
maxMs: maxTimeoutMs,
|
|
522
|
+
fromArg: "timeout"
|
|
523
|
+
},
|
|
524
|
+
execute: async (args, context) => {
|
|
525
|
+
const { command } = args;
|
|
526
|
+
const effectiveTimeoutMs = typeof context?.timeoutMs === "number" ? context.timeoutMs : null;
|
|
527
|
+
const result = await runPowerShellCommand(command, options, {
|
|
528
|
+
signal: context?.signal,
|
|
529
|
+
termination: context?.termination,
|
|
530
|
+
terminationDeadline: context?.terminationDeadline,
|
|
531
|
+
progress: {
|
|
532
|
+
callId: context?.callId,
|
|
533
|
+
toolName: "powershell",
|
|
534
|
+
timeoutMs: effectiveTimeoutMs
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
return result.output;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/tools-trash.ts
|
|
543
|
+
import { join as join7 } from "path";
|
|
544
|
+
import { createTool as createTool3 } from "@agentdevjs/core";
|
|
545
|
+
|
|
546
|
+
// src/lib/errors.ts
|
|
547
|
+
var SafeRmError = class extends Error {
|
|
548
|
+
code;
|
|
549
|
+
details;
|
|
550
|
+
constructor(code, message, details) {
|
|
551
|
+
super(message);
|
|
552
|
+
this.name = "SafeRmError";
|
|
553
|
+
this.code = code;
|
|
554
|
+
this.details = details || {};
|
|
555
|
+
}
|
|
556
|
+
toDict() {
|
|
557
|
+
return {
|
|
558
|
+
success: false,
|
|
559
|
+
error_code: this.code,
|
|
560
|
+
error_message: this.message,
|
|
561
|
+
error_details: this.details
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
toSafeRmResult() {
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
moved: [],
|
|
568
|
+
failed: [this.message],
|
|
569
|
+
movedCount: 0,
|
|
570
|
+
failedCount: 1
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
function throwPathTooLongError(path3, length, max) {
|
|
575
|
+
throw new SafeRmError("ERROR_PATH_TOO_LONG" /* ERROR_PATH_TOO_LONG */, `\u8DEF\u5F84\u8FC7\u957F\uFF08\u6700\u5927 ${max} \u5B57\u7B26\uFF09`, { path: path3, length, max_length: max });
|
|
576
|
+
}
|
|
577
|
+
function throwIllegalCharError(path3, char) {
|
|
578
|
+
throw new SafeRmError("ERROR_INVALID_PATH" /* ERROR_INVALID_PATH */, `\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26: '${char}'`, { path: path3, illegal_char: char });
|
|
579
|
+
}
|
|
580
|
+
function throwFileNotFoundError(path3) {
|
|
581
|
+
throw new SafeRmError("ERROR_FILE_NOT_FOUND" /* ERROR_FILE_NOT_FOUND */, "\u6587\u4EF6\u4E0D\u5B58\u5728", { path: path3 });
|
|
582
|
+
}
|
|
583
|
+
function throwPermissionDeniedError(path3, action) {
|
|
584
|
+
throw new SafeRmError("ERROR_PERMISSION_DENIED" /* ERROR_PERMISSION_DENIED */, `\u6743\u9650\u4E0D\u8DB3\uFF0C\u65E0\u6CD5${action}`, { path: path3 });
|
|
585
|
+
}
|
|
586
|
+
function throwNoTrashDirError(trashDir) {
|
|
587
|
+
throw new SafeRmError("ERROR_NO_TRASH_DIR" /* ERROR_NO_TRASH_DIR */, "\u5783\u573E\u76EE\u5F55\u4E0D\u5B58\u5728", { trash_dir: trashDir });
|
|
588
|
+
}
|
|
589
|
+
function throwNoFilesSpecifiedError() {
|
|
590
|
+
throw new SafeRmError("ERROR_NO_FILES_SPECIFIED" /* ERROR_NO_FILES_SPECIFIED */, "\u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u6587\u4EF6", {});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/lib/fs.ts
|
|
594
|
+
import {
|
|
595
|
+
existsSync as existsSync3,
|
|
596
|
+
lstatSync,
|
|
597
|
+
statSync,
|
|
598
|
+
readdirSync,
|
|
599
|
+
mkdirSync,
|
|
600
|
+
renameSync,
|
|
601
|
+
rmSync,
|
|
602
|
+
readFileSync,
|
|
603
|
+
writeFileSync,
|
|
604
|
+
copyFileSync
|
|
605
|
+
} from "fs";
|
|
606
|
+
import { join as join3, dirname as dirname2, basename, normalize } from "path";
|
|
607
|
+
var MAX_PATH = 260;
|
|
608
|
+
var ILLEGAL_CHARS = '<>:"|?*';
|
|
609
|
+
var SYSTEM_DIRS = [
|
|
610
|
+
process.env.SystemRoot || "C:\\Windows",
|
|
611
|
+
"C:\\Windows",
|
|
612
|
+
"C:\\Program Files",
|
|
613
|
+
"C:\\Program Files (x86)",
|
|
614
|
+
"C:\\ProgramData"
|
|
615
|
+
];
|
|
616
|
+
var FileSystem = class _FileSystem {
|
|
617
|
+
static validatePath(path3) {
|
|
618
|
+
if (!path3 || typeof path3 !== "string") {
|
|
619
|
+
throw new SafeRmError("ERROR_INVALID_PATH" /* ERROR_INVALID_PATH */, "\u8DEF\u5F84\u65E0\u6548", { path: String(path3) });
|
|
620
|
+
}
|
|
621
|
+
if (path3.length > MAX_PATH) {
|
|
622
|
+
throwPathTooLongError(path3, path3.length, MAX_PATH);
|
|
623
|
+
}
|
|
624
|
+
for (const char of ILLEGAL_CHARS) {
|
|
625
|
+
if (path3.includes(char)) {
|
|
626
|
+
const colonContext = path3.length >= 2 && path3[1] === ":" && char === ":";
|
|
627
|
+
if (!colonContext) {
|
|
628
|
+
throwIllegalCharError(path3, char);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
static exists(path3) {
|
|
634
|
+
return existsSync3(path3);
|
|
635
|
+
}
|
|
636
|
+
static lexists(path3) {
|
|
637
|
+
try {
|
|
638
|
+
lstatSync(path3);
|
|
639
|
+
return true;
|
|
640
|
+
} catch {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
static isdir(path3) {
|
|
645
|
+
try {
|
|
646
|
+
return statSync(path3).isDirectory();
|
|
647
|
+
} catch {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
static isfile(path3) {
|
|
652
|
+
try {
|
|
653
|
+
return statSync(path3).isFile();
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
static islink(path3) {
|
|
659
|
+
try {
|
|
660
|
+
return lstatSync(path3).isSymbolicLink();
|
|
661
|
+
} catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
static getsize(path3) {
|
|
666
|
+
try {
|
|
667
|
+
return statSync(path3).size;
|
|
668
|
+
} catch {
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
static makedirs(path3, mode = 493) {
|
|
673
|
+
if (existsSync3(path3) && _FileSystem.isdir(path3)) return;
|
|
674
|
+
try {
|
|
675
|
+
mkdirSync(path3, { mode, recursive: true });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
678
|
+
throwPermissionDeniedError(path3, "\u521B\u5EFA\u76EE\u5F55");
|
|
679
|
+
}
|
|
680
|
+
throw new SafeRmError("ERROR_CREATE_PARENT_FAILED" /* ERROR_CREATE_PARENT_FAILED */, `\u65E0\u6CD5\u521B\u5EFA\u76EE\u5F55: ${error.message}`, { path: path3, os_error: error.code });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
static move(src, dst) {
|
|
684
|
+
try {
|
|
685
|
+
const dstDir = dirname2(dst);
|
|
686
|
+
if (!existsSync3(dstDir)) {
|
|
687
|
+
this.makedirs(dstDir);
|
|
688
|
+
}
|
|
689
|
+
renameSync(src, dst);
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (error.code === "EXDEV" || error.code === "ENOENT") {
|
|
692
|
+
try {
|
|
693
|
+
this._copyAndDelete(src, dst);
|
|
694
|
+
} catch (copyError) {
|
|
695
|
+
if (copyError.code === "EACCES" || copyError.code === "EPERM") {
|
|
696
|
+
throw new SafeRmError("ERROR_FILE_LOCKED" /* ERROR_FILE_LOCKED */, "\u6587\u4EF6\u88AB\u5360\u7528\u6216\u65E0\u6743\u9650\u8BBF\u95EE", { source: src, destination: dst });
|
|
697
|
+
}
|
|
698
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u8DE8\u5377\u79FB\u52A8\u6587\u4EF6\u5931\u8D25: ${copyError.message}`, { source: src, destination: dst });
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
703
|
+
throw new SafeRmError("ERROR_FILE_LOCKED" /* ERROR_FILE_LOCKED */, "\u6587\u4EF6\u88AB\u5360\u7528\u6216\u65E0\u6743\u9650\u8BBF\u95EE", { source: src, destination: dst });
|
|
704
|
+
}
|
|
705
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u79FB\u52A8\u6587\u4EF6\u5931\u8D25: ${error.message}`, { source: src, destination: dst });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
static _copyAndDelete(src, dst) {
|
|
709
|
+
const srcStat = statSync(src);
|
|
710
|
+
if (srcStat.isDirectory()) {
|
|
711
|
+
if (!existsSync3(dst)) {
|
|
712
|
+
mkdirSync(dst, { recursive: true });
|
|
713
|
+
}
|
|
714
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
715
|
+
for (const entry of entries) {
|
|
716
|
+
const srcPath = join3(src, entry.name);
|
|
717
|
+
const dstPath = join3(dst, entry.name);
|
|
718
|
+
this._copyAndDelete(srcPath, dstPath);
|
|
719
|
+
}
|
|
720
|
+
rmSync(src, { recursive: true, force: true });
|
|
721
|
+
} else {
|
|
722
|
+
copyFileSync(src, dst);
|
|
723
|
+
rmSync(src, { force: true });
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
static remove(path3) {
|
|
727
|
+
try {
|
|
728
|
+
if (_FileSystem.isdir(path3)) {
|
|
729
|
+
rmSync(path3, { recursive: true, force: true });
|
|
730
|
+
} else {
|
|
731
|
+
rmSync(path3, { force: true });
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
735
|
+
throwPermissionDeniedError(path3, "\u5220\u9664");
|
|
736
|
+
}
|
|
737
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u5220\u9664\u5931\u8D25: ${error.message}`, { path: path3 });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
static isSystemFile(path3) {
|
|
741
|
+
try {
|
|
742
|
+
const absPath = normalize(path3).toLowerCase();
|
|
743
|
+
for (const sysDir of SYSTEM_DIRS) {
|
|
744
|
+
if (sysDir && absPath.startsWith(normalize(sysDir).toLowerCase())) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
static readFile(path3) {
|
|
753
|
+
try {
|
|
754
|
+
return readFileSync(path3, "utf-8");
|
|
755
|
+
} catch (error) {
|
|
756
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${error.message}`, { path: path3 });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
static writeFile(path3, content) {
|
|
760
|
+
try {
|
|
761
|
+
const dir = dirname2(path3);
|
|
762
|
+
if (!existsSync3(dir)) {
|
|
763
|
+
this.makedirs(dir);
|
|
764
|
+
}
|
|
765
|
+
writeFileSync(path3, content, "utf-8");
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
768
|
+
throwPermissionDeniedError(path3, "\u5199\u5165");
|
|
769
|
+
}
|
|
770
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u5199\u5165\u6587\u4EF6\u5931\u8D25: ${error.message}`, { path: path3 });
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
static listDir(path3) {
|
|
774
|
+
try {
|
|
775
|
+
return readdirSync(path3);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u5217\u51FA\u76EE\u5F55\u5931\u8D25: ${error.message}`, { path: path3 });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
static normalizePath(path3) {
|
|
781
|
+
return normalize(path3).replace(/\\/g, "/");
|
|
782
|
+
}
|
|
783
|
+
static shouldSkipBySpecs(path3) {
|
|
784
|
+
const base = basename(path3);
|
|
785
|
+
return base === "." || base === "..";
|
|
786
|
+
}
|
|
787
|
+
static getFileSize(path3) {
|
|
788
|
+
if (!existsSync3(path3)) return 0;
|
|
789
|
+
if (_FileSystem.isfile(path3)) return _FileSystem.getsize(path3);
|
|
790
|
+
if (_FileSystem.isdir(path3)) {
|
|
791
|
+
let total = 0;
|
|
792
|
+
try {
|
|
793
|
+
const entries = readdirSync(path3, { withFileTypes: true });
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
const fullPath = join3(path3, entry.name);
|
|
796
|
+
try {
|
|
797
|
+
if (entry.isDirectory()) {
|
|
798
|
+
total += this.getFileSize(fullPath);
|
|
799
|
+
} else if (entry.isFile()) {
|
|
800
|
+
total += _FileSystem.getsize(fullPath);
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
return total;
|
|
808
|
+
}
|
|
809
|
+
return 0;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/lib/trashinfo.ts
|
|
814
|
+
import { join as join4, basename as basename2 } from "path";
|
|
815
|
+
function validateOperator(operator) {
|
|
816
|
+
if (operator === null || operator === void 0) return;
|
|
817
|
+
if (typeof operator !== "string") {
|
|
818
|
+
throw new SafeRmError("ERROR_INVALID_OPERATOR" /* ERROR_INVALID_OPERATOR */, "\u64CD\u4F5C\u8005\u540D\u79F0\u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
819
|
+
}
|
|
820
|
+
if (!operator) {
|
|
821
|
+
throw new SafeRmError("ERROR_INVALID_OPERATOR" /* ERROR_INVALID_OPERATOR */, "\u64CD\u4F5C\u8005\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
|
|
822
|
+
}
|
|
823
|
+
const illegalChars = '<>:"|?*\\/()[]{}';
|
|
824
|
+
for (const char of illegalChars) {
|
|
825
|
+
if (operator.includes(char)) {
|
|
826
|
+
throw new SafeRmError("ERROR_INVALID_OPERATOR" /* ERROR_INVALID_OPERATOR */, `\u64CD\u4F5C\u8005\u540D\u79F0\u5305\u542B\u975E\u6CD5\u5B57\u7B26: '${char}'`, { operator, illegal_char: char });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (operator.length > 64) {
|
|
830
|
+
throw new SafeRmError("ERROR_INVALID_OPERATOR" /* ERROR_INVALID_OPERATOR */, "\u64CD\u4F5C\u8005\u540D\u79F0\u8FC7\u957F\uFF08\u6700\u5927 64 \u5B57\u7B26\uFF09", { operator, length: operator.length });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function encodePath(path3) {
|
|
834
|
+
const normalized = FileSystem.normalizePath(path3);
|
|
835
|
+
return encodeURIComponent(normalized).replace(/%2F/g, "/");
|
|
836
|
+
}
|
|
837
|
+
function decodePath(encoded) {
|
|
838
|
+
try {
|
|
839
|
+
return decodeURIComponent(encoded);
|
|
840
|
+
} catch {
|
|
841
|
+
return encoded;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function generateTrashName(originalPath) {
|
|
845
|
+
const base = basename2(originalPath);
|
|
846
|
+
const extIndex = base.lastIndexOf(".");
|
|
847
|
+
let name;
|
|
848
|
+
let ext;
|
|
849
|
+
if (extIndex > 0) {
|
|
850
|
+
name = base.substring(0, extIndex);
|
|
851
|
+
ext = base.substring(extIndex);
|
|
852
|
+
} else {
|
|
853
|
+
name = base;
|
|
854
|
+
ext = "";
|
|
855
|
+
}
|
|
856
|
+
const now = /* @__PURE__ */ new Date();
|
|
857
|
+
const timestamp = now.getFullYear().toString() + (now.getMonth() + 1).toString().padStart(2, "0") + now.getDate().toString().padStart(2, "0") + now.getHours().toString().padStart(2, "0") + now.getMinutes().toString().padStart(2, "0") + now.getSeconds().toString().padStart(2, "0");
|
|
858
|
+
const randomSuffix = Math.floor(Math.random() * 9e3) + 1e3;
|
|
859
|
+
return `${name}_${timestamp}_${randomSuffix}${ext}`;
|
|
860
|
+
}
|
|
861
|
+
function createTrashInfo(originalPath, trashName, trashDir, operator) {
|
|
862
|
+
const infoDir = join4(trashDir, "info");
|
|
863
|
+
const infoPath = join4(infoDir, `${trashName}.trashinfo`);
|
|
864
|
+
const encodedPath = encodePath(originalPath);
|
|
865
|
+
const now = /* @__PURE__ */ new Date();
|
|
866
|
+
const deletionDate = now.toISOString().replace(/\.\d{3}Z$/, "");
|
|
867
|
+
let content = `[Trash Info]
|
|
868
|
+
Path=${encodedPath}
|
|
869
|
+
DeletionDate=${deletionDate}
|
|
870
|
+
`;
|
|
871
|
+
if (operator) {
|
|
872
|
+
content += `Operator=${operator}
|
|
873
|
+
`;
|
|
874
|
+
}
|
|
875
|
+
FileSystem.writeFile(infoPath, content);
|
|
876
|
+
return infoPath;
|
|
877
|
+
}
|
|
878
|
+
function parseTrashInfo(infoPath, trashDir) {
|
|
879
|
+
try {
|
|
880
|
+
const content = FileSystem.readFile(infoPath);
|
|
881
|
+
let originalPath = null;
|
|
882
|
+
let deletionDate = null;
|
|
883
|
+
let operator = null;
|
|
884
|
+
for (const line of content.split("\n")) {
|
|
885
|
+
const trimmed = line.trim();
|
|
886
|
+
if (trimmed.startsWith("Path=")) {
|
|
887
|
+
originalPath = decodePath(trimmed.substring(5));
|
|
888
|
+
} else if (trimmed.startsWith("DeletionDate=")) {
|
|
889
|
+
deletionDate = trimmed.substring(13);
|
|
890
|
+
} else if (trimmed.startsWith("Operator=")) {
|
|
891
|
+
operator = trimmed.substring(9);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (!originalPath || !deletionDate) return null;
|
|
895
|
+
try {
|
|
896
|
+
FileSystem.validatePath(originalPath);
|
|
897
|
+
} catch {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
const infoBasename = basename2(infoPath);
|
|
901
|
+
const trashBasename = infoBasename.replace(/\.trashinfo$/, "");
|
|
902
|
+
const trashFile = join4(trashDir, "files", trashBasename);
|
|
903
|
+
if (!FileSystem.exists(trashFile)) return null;
|
|
904
|
+
const size = FileSystem.getFileSize(trashFile);
|
|
905
|
+
return {
|
|
906
|
+
index: -1,
|
|
907
|
+
originalPath,
|
|
908
|
+
deletionDate,
|
|
909
|
+
operator,
|
|
910
|
+
trashFile,
|
|
911
|
+
infoFile: infoPath,
|
|
912
|
+
size
|
|
913
|
+
};
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function parseRmCommand(rmCommand) {
|
|
919
|
+
const trimmed = rmCommand.trim();
|
|
920
|
+
if (!trimmed) {
|
|
921
|
+
return { files: [], force: false, interactive: false, recursive: false };
|
|
922
|
+
}
|
|
923
|
+
const parts = [];
|
|
924
|
+
let current = "";
|
|
925
|
+
let inQuote = false;
|
|
926
|
+
let quoteChar = "";
|
|
927
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
928
|
+
const char = trimmed[i];
|
|
929
|
+
const nextChar = trimmed[i + 1] || "";
|
|
930
|
+
if (inQuote) {
|
|
931
|
+
if (char === quoteChar) {
|
|
932
|
+
if (nextChar === quoteChar) {
|
|
933
|
+
current += char;
|
|
934
|
+
i++;
|
|
935
|
+
} else {
|
|
936
|
+
inQuote = false;
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
current += char;
|
|
940
|
+
}
|
|
941
|
+
} else if (char === '"' || char === "'" || char === "`") {
|
|
942
|
+
inQuote = true;
|
|
943
|
+
quoteChar = char;
|
|
944
|
+
} else if (char === " " || char === " ") {
|
|
945
|
+
if (current) {
|
|
946
|
+
parts.push(current);
|
|
947
|
+
current = "";
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
current += char;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (current) parts.push(current);
|
|
954
|
+
const files = [];
|
|
955
|
+
let force = false;
|
|
956
|
+
let interactive = false;
|
|
957
|
+
let recursive = false;
|
|
958
|
+
for (let part of parts) {
|
|
959
|
+
if (part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'") || part.startsWith("`") && part.endsWith("`")) {
|
|
960
|
+
part = part.slice(1, -1);
|
|
961
|
+
}
|
|
962
|
+
if (!part) continue;
|
|
963
|
+
if (part.startsWith("-")) {
|
|
964
|
+
if (part.includes("f") || part === "--force") force = true;
|
|
965
|
+
if (part.includes("i") || part === "--interactive") interactive = true;
|
|
966
|
+
if (part.includes("r") || part.includes("R") || part === "--recursive") recursive = true;
|
|
967
|
+
} else {
|
|
968
|
+
files.push(part);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return { files, force, interactive, recursive };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/lib/trash.ts
|
|
975
|
+
import { isAbsolute, join as join5, resolve } from "path";
|
|
976
|
+
var SafeRm = class {
|
|
977
|
+
trashDir;
|
|
978
|
+
workingDir;
|
|
979
|
+
operator;
|
|
980
|
+
verbose;
|
|
981
|
+
constructor(options) {
|
|
982
|
+
this.trashDir = resolve(options.trashDir);
|
|
983
|
+
this.workingDir = options.workingDir || null;
|
|
984
|
+
this.operator = options.operator || null;
|
|
985
|
+
this.verbose = options.verbose || 0;
|
|
986
|
+
validateOperator(this.operator);
|
|
987
|
+
this._ensureTrashDir();
|
|
988
|
+
}
|
|
989
|
+
_ensureTrashDir() {
|
|
990
|
+
const parentDir = join5(this.trashDir, "..");
|
|
991
|
+
if (!FileSystem.exists(parentDir)) {
|
|
992
|
+
throw new SafeRmError("ERROR_NO_TRASH_DIR" /* ERROR_NO_TRASH_DIR */, "\u5783\u573E\u76EE\u5F55\u7684\u7236\u76EE\u5F55\u4E0D\u5B58\u5728", { trash_dir: this.trashDir, parent_dir: parentDir });
|
|
993
|
+
}
|
|
994
|
+
FileSystem.makedirs(this.trashDir);
|
|
995
|
+
FileSystem.makedirs(join5(this.trashDir, "info"));
|
|
996
|
+
FileSystem.makedirs(join5(this.trashDir, "files"));
|
|
997
|
+
const testFile = join5(this.trashDir, "info", ".write_test");
|
|
998
|
+
try {
|
|
999
|
+
FileSystem.writeFile(testFile, "test");
|
|
1000
|
+
FileSystem.remove(testFile);
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new SafeRmError("ERROR_TRASH_NOT_WRITABLE" /* ERROR_TRASH_NOT_WRITABLE */, "\u5783\u573E\u76EE\u5F55\u4E0D\u53EF\u5199", { trash_dir: this.trashDir });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
trashSingle(path3, mode) {
|
|
1006
|
+
FileSystem.validatePath(path3);
|
|
1007
|
+
if (FileSystem.shouldSkipBySpecs(path3)) return "Failure" /* FAILURE */;
|
|
1008
|
+
const absPath = resolve(path3);
|
|
1009
|
+
if (FileSystem.isSystemFile(absPath)) {
|
|
1010
|
+
throw new SafeRmError("ERROR_SYSTEM_FILE" /* ERROR_SYSTEM_FILE */, "\u7CFB\u7EDF\u6587\u4EF6\u53D7\u4FDD\u62A4\uFF0C\u65E0\u6CD5\u5220\u9664", { path: path3 });
|
|
1011
|
+
}
|
|
1012
|
+
if (!FileSystem.lexists(path3)) {
|
|
1013
|
+
if (mode === "mode_force" /* MODE_FORCE */) return "Success" /* SUCCESS */;
|
|
1014
|
+
throwFileNotFoundError(path3);
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const trashName = generateTrashName(path3);
|
|
1018
|
+
const destPath = join5(this.trashDir, "files", trashName);
|
|
1019
|
+
createTrashInfo(path3, trashName, this.trashDir, this.operator);
|
|
1020
|
+
FileSystem.move(path3, destPath);
|
|
1021
|
+
return "Success" /* SUCCESS */;
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error instanceof SafeRmError) throw error;
|
|
1024
|
+
throw new SafeRmError("ERROR_UNKNOWN" /* ERROR_UNKNOWN */, `\u5220\u9664\u6587\u4EF6\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, { path: path3 });
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
trashAll(paths, mode) {
|
|
1028
|
+
const failedPaths = [];
|
|
1029
|
+
const movedPaths = [];
|
|
1030
|
+
for (const path3 of paths) {
|
|
1031
|
+
try {
|
|
1032
|
+
const result = this.trashSingle(path3, mode);
|
|
1033
|
+
if (result === "Success" /* SUCCESS */) {
|
|
1034
|
+
movedPaths.push(path3);
|
|
1035
|
+
} else {
|
|
1036
|
+
failedPaths.push(path3);
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
failedPaths.push(path3);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
success: failedPaths.length === 0,
|
|
1044
|
+
moved: movedPaths,
|
|
1045
|
+
failed: failedPaths,
|
|
1046
|
+
movedCount: movedPaths.length,
|
|
1047
|
+
failedCount: failedPaths.length
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
function safeRm(workingDir, rmCommand, trashDir, operator = null, verbose = 0) {
|
|
1052
|
+
validateOperator(operator);
|
|
1053
|
+
const { files: parsedFiles, force, interactive } = parseRmCommand(rmCommand);
|
|
1054
|
+
const files = parsedFiles.map((file) => {
|
|
1055
|
+
if (!workingDir || isAbsolute(file)) {
|
|
1056
|
+
return file;
|
|
1057
|
+
}
|
|
1058
|
+
return resolve(workingDir, file);
|
|
1059
|
+
});
|
|
1060
|
+
if (files.length === 0) throwNoFilesSpecifiedError();
|
|
1061
|
+
let mode;
|
|
1062
|
+
if (force) mode = "mode_force" /* MODE_FORCE */;
|
|
1063
|
+
else if (interactive) mode = "mode_interactive" /* MODE_INTERACTIVE */;
|
|
1064
|
+
else mode = "mode_unspecified" /* MODE_UNSPECIFIED */;
|
|
1065
|
+
if (workingDir) {
|
|
1066
|
+
if (!FileSystem.exists(workingDir)) {
|
|
1067
|
+
throw new SafeRmError("ERROR_WORKING_DIR_NOT_FOUND" /* ERROR_WORKING_DIR_NOT_FOUND */, "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728", { working_dir: workingDir });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const safeRmInstance = new SafeRm({ trashDir, workingDir, operator, verbose });
|
|
1071
|
+
return safeRmInstance.trashAll(files, mode);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/lib/restore.ts
|
|
1075
|
+
import { join as join6, resolve as resolve2, dirname as dirname3 } from "path";
|
|
1076
|
+
function matchesPattern(actualPath, pattern) {
|
|
1077
|
+
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1078
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
1079
|
+
return regex.test(actualPath) || regex.test(actualPath.replace(/\//g, "\\"));
|
|
1080
|
+
}
|
|
1081
|
+
function parseIndexSpec(spec) {
|
|
1082
|
+
const indices = [];
|
|
1083
|
+
for (const part of spec.split(",")) {
|
|
1084
|
+
const trimmed = part.trim();
|
|
1085
|
+
if (trimmed.includes("-")) {
|
|
1086
|
+
const [startStr, endStr] = trimmed.split("-", 2);
|
|
1087
|
+
const start = parseInt(startStr, 10);
|
|
1088
|
+
const end = parseInt(endStr, 10);
|
|
1089
|
+
if (isNaN(start) || isNaN(end)) {
|
|
1090
|
+
throw new SafeRmError("ERROR_INVALID_INDEX" /* ERROR_INVALID_INDEX */, `\u65E0\u6548\u7684\u7D22\u5F15\u8303\u56F4: ${trimmed}`, { spec });
|
|
1091
|
+
}
|
|
1092
|
+
for (let i = start; i <= end; i++) indices.push(i);
|
|
1093
|
+
} else {
|
|
1094
|
+
const index = parseInt(trimmed, 10);
|
|
1095
|
+
if (isNaN(index)) {
|
|
1096
|
+
throw new SafeRmError("ERROR_INVALID_INDEX" /* ERROR_INVALID_INDEX */, `\u65E0\u6548\u7684\u7D22\u5F15: ${trimmed}`, { spec });
|
|
1097
|
+
}
|
|
1098
|
+
indices.push(index);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return indices;
|
|
1102
|
+
}
|
|
1103
|
+
var SafeRestore = class {
|
|
1104
|
+
trashDir;
|
|
1105
|
+
operator;
|
|
1106
|
+
verbose;
|
|
1107
|
+
constructor(options) {
|
|
1108
|
+
this.trashDir = resolve2(options.trashDir);
|
|
1109
|
+
this.operator = options.operator || null;
|
|
1110
|
+
this.verbose = options.verbose || 0;
|
|
1111
|
+
validateOperator(this.operator);
|
|
1112
|
+
if (!FileSystem.exists(this.trashDir)) throwNoTrashDirError(this.trashDir);
|
|
1113
|
+
}
|
|
1114
|
+
_scanInfoFiles() {
|
|
1115
|
+
const infoDir = join6(this.trashDir, "info");
|
|
1116
|
+
const files = [];
|
|
1117
|
+
if (!FileSystem.exists(infoDir)) return files;
|
|
1118
|
+
const entries = FileSystem.listDir(infoDir);
|
|
1119
|
+
for (const filename of entries) {
|
|
1120
|
+
if (!filename.endsWith(".trashinfo")) continue;
|
|
1121
|
+
const infoPath = join6(infoDir, filename);
|
|
1122
|
+
const info = parseTrashInfo(infoPath, this.trashDir);
|
|
1123
|
+
if (info && (this.operator === null || info.operator === this.operator)) {
|
|
1124
|
+
files.push(info);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return files;
|
|
1128
|
+
}
|
|
1129
|
+
listTrashed() {
|
|
1130
|
+
const files = this._scanInfoFiles();
|
|
1131
|
+
files.sort((a, b) => a.deletionDate.localeCompare(b.deletionDate));
|
|
1132
|
+
for (let i = 0; i < files.length; i++) files[i] = { ...files[i], index: i };
|
|
1133
|
+
return files;
|
|
1134
|
+
}
|
|
1135
|
+
_matchesTarget(info, target) {
|
|
1136
|
+
if (typeof target === "number") return info.index === target;
|
|
1137
|
+
return matchesPattern(info.originalPath, target) || matchesPattern(info.originalPath.replace(/\//g, "\\"), target);
|
|
1138
|
+
}
|
|
1139
|
+
restore(target, overwrite = false, dryRun = false) {
|
|
1140
|
+
const allFiles = this.listTrashed();
|
|
1141
|
+
const restored = [];
|
|
1142
|
+
const failed = [];
|
|
1143
|
+
const skipped = [];
|
|
1144
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
1145
|
+
const filesToRestore = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const t of targets) {
|
|
1147
|
+
let matched = false;
|
|
1148
|
+
for (const info of allFiles) {
|
|
1149
|
+
if (this._matchesTarget(info, t)) {
|
|
1150
|
+
filesToRestore.add(info);
|
|
1151
|
+
matched = true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!matched && typeof t === "number") {
|
|
1155
|
+
failed.push({ path: `index:${t}`, error: "\u7D22\u5F15\u6CA1\u6709\u627E\u5230\u5BF9\u5E94\u7684\u6587\u4EF6" });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const info of Array.from(filesToRestore)) {
|
|
1159
|
+
try {
|
|
1160
|
+
if (FileSystem.exists(info.originalPath) && !overwrite) {
|
|
1161
|
+
failed.push({ path: info.originalPath, error: "file already exists" });
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const parentDir = dirname3(info.originalPath);
|
|
1165
|
+
if (parentDir && !FileSystem.exists(parentDir)) FileSystem.makedirs(parentDir);
|
|
1166
|
+
if (dryRun) {
|
|
1167
|
+
restored.push(info.originalPath);
|
|
1168
|
+
} else {
|
|
1169
|
+
FileSystem.move(info.trashFile, info.originalPath);
|
|
1170
|
+
FileSystem.remove(info.infoFile);
|
|
1171
|
+
restored.push(info.originalPath);
|
|
1172
|
+
}
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
failed.push({ path: info.originalPath, error: error instanceof Error ? error.message : String(error) });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
success: failed.length === 0,
|
|
1179
|
+
restored,
|
|
1180
|
+
failed,
|
|
1181
|
+
skipped,
|
|
1182
|
+
restoredCount: restored.length,
|
|
1183
|
+
failedCount: failed.length,
|
|
1184
|
+
skippedCount: skipped.length
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
function listTrashed(trashDir, operator = null) {
|
|
1189
|
+
validateOperator(operator);
|
|
1190
|
+
const restorer = new SafeRestore({ trashDir, operator });
|
|
1191
|
+
const infoList = restorer.listTrashed();
|
|
1192
|
+
return { success: true, total: infoList.length, files: infoList };
|
|
1193
|
+
}
|
|
1194
|
+
function restore(trashDir, target, operator = null, overwrite = false, dryRun = false, parseIndexRanges = true) {
|
|
1195
|
+
validateOperator(operator);
|
|
1196
|
+
const restorer = new SafeRestore({ trashDir, operator });
|
|
1197
|
+
let actualTarget = target;
|
|
1198
|
+
if (parseIndexRanges && typeof target === "string") {
|
|
1199
|
+
if (/^[\d,\s\-]+$/.test(target.trim())) {
|
|
1200
|
+
try {
|
|
1201
|
+
actualTarget = parseIndexSpec(target);
|
|
1202
|
+
} catch {
|
|
1203
|
+
actualTarget = target;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return restorer.restore(actualTarget, overwrite, dryRun);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/tools-trash.ts
|
|
1211
|
+
function formatSize(bytes) {
|
|
1212
|
+
if (bytes === 0) return "0 B";
|
|
1213
|
+
const k = 1024;
|
|
1214
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
1215
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1216
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
|
1217
|
+
}
|
|
1218
|
+
function createSafeTrashDeleteTool(workspaceDir = process.cwd()) {
|
|
1219
|
+
const defaultTrashDir = join7(workspaceDir, ".trash");
|
|
1220
|
+
return createTool3({
|
|
1221
|
+
name: "safe_trash_delete",
|
|
1222
|
+
description: "\u5B89\u5168\u5220\u9664\u6587\u4EF6\u6216\u76EE\u5F55\uFF0C\u79FB\u52A8\u5230\u5783\u573E\u76EE\u5F55\u800C\u975E\u6C38\u4E45\u5220\u9664\u3002",
|
|
1223
|
+
parameters: {
|
|
1224
|
+
type: "object",
|
|
1225
|
+
properties: {
|
|
1226
|
+
paths: { type: "array", items: { type: "string" } },
|
|
1227
|
+
trashDir: { type: "string" }
|
|
1228
|
+
},
|
|
1229
|
+
required: ["paths"]
|
|
1230
|
+
},
|
|
1231
|
+
render: { call: "trash-delete", result: "trash-delete" },
|
|
1232
|
+
execute: async (args) => {
|
|
1233
|
+
const { paths, trashDir } = args;
|
|
1234
|
+
const trashDirPath = trashDir || defaultTrashDir;
|
|
1235
|
+
const result = safeRm(workspaceDir, paths.join(" "), trashDirPath, null, 0);
|
|
1236
|
+
return {
|
|
1237
|
+
success: result.success,
|
|
1238
|
+
moved_count: result.movedCount,
|
|
1239
|
+
moved: result.moved,
|
|
1240
|
+
failed: result.failed
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function createSafeTrashListTool(workspaceDir = process.cwd()) {
|
|
1246
|
+
const defaultTrashDir = join7(workspaceDir, ".trash");
|
|
1247
|
+
return createTool3({
|
|
1248
|
+
name: "safe_trash_list",
|
|
1249
|
+
description: "\u5217\u51FA\u5783\u573E\u76EE\u5F55\u4E2D\u7684\u6240\u6709\u53EF\u6062\u590D\u6587\u4EF6\u3002",
|
|
1250
|
+
parallelizable: true,
|
|
1251
|
+
parameters: {
|
|
1252
|
+
type: "object",
|
|
1253
|
+
properties: {
|
|
1254
|
+
trashDir: { type: "string" }
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
render: { call: "trash-list", result: "trash-list" },
|
|
1258
|
+
execute: async (args) => {
|
|
1259
|
+
const { trashDir } = args;
|
|
1260
|
+
const trashDirPath = trashDir || defaultTrashDir;
|
|
1261
|
+
const result = listTrashed(trashDirPath, null);
|
|
1262
|
+
return {
|
|
1263
|
+
success: result.success,
|
|
1264
|
+
total: result.total,
|
|
1265
|
+
files: result.files.map((f) => ({ ...f, size_formatted: formatSize(f.size || 0) }))
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
function createSafeTrashRestoreTool(workspaceDir = process.cwd()) {
|
|
1271
|
+
const defaultTrashDir = join7(workspaceDir, ".trash");
|
|
1272
|
+
return createTool3({
|
|
1273
|
+
name: "safe_trash_restore",
|
|
1274
|
+
description: "\u4ECE\u5783\u573E\u76EE\u5F55\u6062\u590D\u6587\u4EF6\u5230\u539F\u4F4D\u7F6E\u3002",
|
|
1275
|
+
parameters: {
|
|
1276
|
+
type: "object",
|
|
1277
|
+
properties: {
|
|
1278
|
+
target: {
|
|
1279
|
+
oneOf: [{ type: "string" }, { type: "number" }, { type: "array", items: { oneOf: [{ type: "number" }, { type: "string" }] } }]
|
|
1280
|
+
},
|
|
1281
|
+
trashDir: { type: "string" },
|
|
1282
|
+
overwrite: { type: "boolean" }
|
|
1283
|
+
},
|
|
1284
|
+
required: ["target"]
|
|
1285
|
+
},
|
|
1286
|
+
render: { call: "trash-restore", result: "trash-restore" },
|
|
1287
|
+
execute: async (args) => {
|
|
1288
|
+
const { target, trashDir, overwrite } = args;
|
|
1289
|
+
const trashDirPath = trashDir || defaultTrashDir;
|
|
1290
|
+
const result = restore(trashDirPath, target, null, overwrite || false, false, true);
|
|
1291
|
+
return {
|
|
1292
|
+
success: result.success,
|
|
1293
|
+
restored_count: result.restoredCount,
|
|
1294
|
+
restored: result.restored,
|
|
1295
|
+
failed: result.failed
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
var safeTrashDeleteTool = createSafeTrashDeleteTool();
|
|
1301
|
+
var safeTrashListTool = createSafeTrashListTool();
|
|
1302
|
+
var safeTrashRestoreTool = createSafeTrashRestoreTool();
|
|
1303
|
+
|
|
1304
|
+
// src/index.ts
|
|
1305
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1306
|
+
var DEFAULT_TIMEOUT_MS3 = 12e4;
|
|
1307
|
+
var MAX_TIMEOUT_MS3 = 6e5;
|
|
1308
|
+
function resolvePositiveNumber(value, fallback) {
|
|
1309
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1310
|
+
}
|
|
1311
|
+
var ShellFeature = class {
|
|
1312
|
+
name = "shell";
|
|
1313
|
+
dependencies = [];
|
|
1314
|
+
source = __filename.replace(/\\/g, "/");
|
|
1315
|
+
description = "\u63D0\u4F9B Bash/PowerShell \u547D\u4EE4\u6267\u884C\u80FD\u529B\uFF0C\u4EE5\u53CA\u5B89\u5168\u5220\u9664\u3001\u6062\u590D\u548C\u67E5\u770B\u5783\u573E\u6876\u5DE5\u5177\u3002";
|
|
1316
|
+
bashDescription;
|
|
1317
|
+
powershellDescription;
|
|
1318
|
+
_packageInfo = null;
|
|
1319
|
+
workspaceDir;
|
|
1320
|
+
workdir;
|
|
1321
|
+
resourceRoot;
|
|
1322
|
+
constructor(config = {}) {
|
|
1323
|
+
this.workspaceDir = config.workspaceDir || process.cwd();
|
|
1324
|
+
this.workdir = config.workdir || this.workspaceDir;
|
|
1325
|
+
this.resourceRoot = config.resourceRoot || process.cwd();
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* 获取同步工具(垃圾桶工具)
|
|
1329
|
+
*/
|
|
1330
|
+
getTools() {
|
|
1331
|
+
return [
|
|
1332
|
+
createSafeTrashDeleteTool(this.workdir),
|
|
1333
|
+
createSafeTrashListTool(this.workdir),
|
|
1334
|
+
createSafeTrashRestoreTool(this.workdir)
|
|
1335
|
+
];
|
|
1336
|
+
}
|
|
1337
|
+
getFeatureManifest() {
|
|
1338
|
+
return {
|
|
1339
|
+
schemaVersion: 1,
|
|
1340
|
+
settings: {
|
|
1341
|
+
properties: {
|
|
1342
|
+
bashEnabled: {
|
|
1343
|
+
type: "boolean",
|
|
1344
|
+
title: "\u542F\u7528 Bash",
|
|
1345
|
+
description: "\u542F\u7528\u540E\uFF0CAgent \u5C06\u83B7\u5F97 Bash \u5DE5\u5177\u3002Windows \u9700\u8981 Git for Windows\uFF1BLinux/macOS \u4F7F\u7528\u7CFB\u7EDF\u81EA\u5E26 Shell\u3002",
|
|
1346
|
+
default: true
|
|
1347
|
+
},
|
|
1348
|
+
bashPath: {
|
|
1349
|
+
type: "file",
|
|
1350
|
+
title: "Bash \u8DEF\u5F84",
|
|
1351
|
+
description: "Bash \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\u3002\u7559\u7A7A\u65F6\u81EA\u52A8\u68C0\u6D4B\u3002",
|
|
1352
|
+
placeholder: "\u81EA\u52A8\u68C0\u6D4B"
|
|
1353
|
+
},
|
|
1354
|
+
powershellEnabled: {
|
|
1355
|
+
type: "boolean",
|
|
1356
|
+
title: "\u542F\u7528 PowerShell",
|
|
1357
|
+
description: "\u542F\u7528\u540E\uFF0CAgent \u5C06\u83B7\u5F97 PowerShell \u5DE5\u5177\u3002Windows \u81EA\u5E26 PowerShell 5.1\uFF1BLinux/macOS \u9700\u5B89\u88C5 PowerShell Core (pwsh)\u3002",
|
|
1358
|
+
default: true
|
|
1359
|
+
},
|
|
1360
|
+
powershellPath: {
|
|
1361
|
+
type: "file",
|
|
1362
|
+
title: "PowerShell \u8DEF\u5F84",
|
|
1363
|
+
description: "PowerShell \u53EF\u6267\u884C\u6587\u4EF6\u8DEF\u5F84\u3002\u7559\u7A7A\u65F6\u81EA\u52A8\u68C0\u6D4B\u3002",
|
|
1364
|
+
placeholder: "\u81EA\u52A8\u68C0\u6D4B"
|
|
1365
|
+
},
|
|
1366
|
+
defaultTimeoutMs: {
|
|
1367
|
+
type: "number",
|
|
1368
|
+
title: "\u9ED8\u8BA4\u547D\u4EE4\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
|
|
1369
|
+
description: "\u547D\u4EE4\u6267\u884C\u7684\u9ED8\u8BA4\u8D85\u65F6\u65F6\u95F4\u3002\u6A21\u578B\u53EF\u901A\u8FC7 timeout \u53C2\u6570\u8986\u76D6\uFF08\u4E0D\u8D85\u8FC7\u6700\u5927\u8D85\u65F6\uFF09\u3002",
|
|
1370
|
+
default: DEFAULT_TIMEOUT_MS3,
|
|
1371
|
+
min: 1,
|
|
1372
|
+
max: MAX_TIMEOUT_MS3,
|
|
1373
|
+
step: 1e3
|
|
1374
|
+
},
|
|
1375
|
+
maxTimeoutMs: {
|
|
1376
|
+
type: "number",
|
|
1377
|
+
title: "\u6700\u5927\u547D\u4EE4\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
|
|
1378
|
+
description: "\u547D\u4EE4\u8D85\u65F6\u7684\u786C\u4E0A\u9650\uFF0C\u4EFB\u4F55\u6765\u6E90\u7684\u8D85\u65F6\u503C\u90FD\u4F1A\u88AB\u9650\u5236\u5230\u6B64\u503C\u4EE5\u5185\u3002",
|
|
1379
|
+
default: MAX_TIMEOUT_MS3,
|
|
1380
|
+
min: 1,
|
|
1381
|
+
step: 1e3
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
resolveShellConfig(featureConfig) {
|
|
1388
|
+
if (!featureConfig || typeof featureConfig !== "object") {
|
|
1389
|
+
return {
|
|
1390
|
+
bashEnabled: true,
|
|
1391
|
+
powershellEnabled: true,
|
|
1392
|
+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
1393
|
+
maxTimeoutMs: MAX_TIMEOUT_MS3
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
const c = featureConfig;
|
|
1397
|
+
const maxTimeoutMs = Math.max(
|
|
1398
|
+
resolvePositiveNumber(c.maxTimeoutMs, MAX_TIMEOUT_MS3),
|
|
1399
|
+
1
|
|
1400
|
+
);
|
|
1401
|
+
return {
|
|
1402
|
+
bashEnabled: c.bashEnabled !== false,
|
|
1403
|
+
bashPath: typeof c.bashPath === "string" && c.bashPath.trim() ? c.bashPath.trim() : void 0,
|
|
1404
|
+
powershellEnabled: c.powershellEnabled !== false,
|
|
1405
|
+
powershellPath: typeof c.powershellPath === "string" && c.powershellPath.trim() ? c.powershellPath.trim() : void 0,
|
|
1406
|
+
// default 不允许超过 max(配置面板顺序无关时的自洽保护)
|
|
1407
|
+
defaultTimeoutMs: Math.min(
|
|
1408
|
+
resolvePositiveNumber(c.defaultTimeoutMs, DEFAULT_TIMEOUT_MS3),
|
|
1409
|
+
maxTimeoutMs
|
|
1410
|
+
),
|
|
1411
|
+
maxTimeoutMs
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* 获取异步工具(bash/powershell 工具,条件注册)
|
|
1416
|
+
*/
|
|
1417
|
+
async getAsyncTools(ctx) {
|
|
1418
|
+
const config = this.resolveShellConfig(ctx.featureConfig);
|
|
1419
|
+
const tools = [];
|
|
1420
|
+
if (config.bashEnabled) {
|
|
1421
|
+
const bashPath = findGitBashPath(config.bashPath);
|
|
1422
|
+
if (bashPath) {
|
|
1423
|
+
if (!this.bashDescription) {
|
|
1424
|
+
try {
|
|
1425
|
+
const descriptionPath = resolve3(this.resourceRoot, ".agentdev/prompts/tool-bash.md");
|
|
1426
|
+
this.bashDescription = await readFile(descriptionPath, "utf-8");
|
|
1427
|
+
} catch {
|
|
1428
|
+
this.bashDescription = "\u6267\u884C Shell \u547D\u4EE4";
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
tools.push(createShellCommandTool(this.bashDescription, {
|
|
1432
|
+
workspaceDir: this.workspaceDir,
|
|
1433
|
+
workdir: this.workdir,
|
|
1434
|
+
resourceRoot: this.resourceRoot,
|
|
1435
|
+
bashPath,
|
|
1436
|
+
timeoutMs: config.defaultTimeoutMs,
|
|
1437
|
+
maxTimeoutMs: config.maxTimeoutMs
|
|
1438
|
+
}));
|
|
1439
|
+
} else {
|
|
1440
|
+
console.warn("[shell] Bash is enabled but was not found on this system. Skipping Bash tool.");
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (config.powershellEnabled) {
|
|
1444
|
+
const psPath = findPowerShellPath(config.powershellPath);
|
|
1445
|
+
if (psPath) {
|
|
1446
|
+
if (!this.powershellDescription) {
|
|
1447
|
+
try {
|
|
1448
|
+
const descriptionPath = resolve3(this.resourceRoot, ".agentdev/prompts/tool-powershell.md");
|
|
1449
|
+
this.powershellDescription = await readFile(descriptionPath, "utf-8");
|
|
1450
|
+
} catch {
|
|
1451
|
+
this.powershellDescription = "\u6267\u884C PowerShell \u547D\u4EE4";
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
tools.push(createPowerShellTool(this.powershellDescription, {
|
|
1455
|
+
workspaceDir: this.workspaceDir,
|
|
1456
|
+
workdir: this.workdir,
|
|
1457
|
+
resourceRoot: this.resourceRoot,
|
|
1458
|
+
psPath,
|
|
1459
|
+
timeoutMs: config.defaultTimeoutMs,
|
|
1460
|
+
maxTimeoutMs: config.maxTimeoutMs
|
|
1461
|
+
}));
|
|
1462
|
+
} else {
|
|
1463
|
+
console.warn("[shell] PowerShell is enabled but was not found on this system. Skipping PowerShell tool.");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return tools;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* 获取包信息(统一打包方案)
|
|
1470
|
+
*/
|
|
1471
|
+
getPackageInfo() {
|
|
1472
|
+
if (!this._packageInfo) {
|
|
1473
|
+
this._packageInfo = getPackageInfoFromSource(this.source);
|
|
1474
|
+
}
|
|
1475
|
+
return this._packageInfo;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* 获取模板名称列表(统一打包方案)
|
|
1479
|
+
*/
|
|
1480
|
+
getTemplateNames() {
|
|
1481
|
+
return [
|
|
1482
|
+
"bash",
|
|
1483
|
+
"trash-delete",
|
|
1484
|
+
"trash-list",
|
|
1485
|
+
"trash-restore"
|
|
1486
|
+
];
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
export {
|
|
1490
|
+
SHELL_METADATA_CLOSE,
|
|
1491
|
+
SHELL_METADATA_OPEN,
|
|
1492
|
+
ShellFeature,
|
|
1493
|
+
containsHeredoc,
|
|
1494
|
+
createPowerShellTool,
|
|
1495
|
+
createSafeTrashDeleteTool,
|
|
1496
|
+
createSafeTrashListTool,
|
|
1497
|
+
createSafeTrashRestoreTool,
|
|
1498
|
+
createShellCommandTool,
|
|
1499
|
+
findGitBashPath,
|
|
1500
|
+
findPowerShellPath,
|
|
1501
|
+
formatShellMetadata,
|
|
1502
|
+
hasStdinRedirect,
|
|
1503
|
+
listTrashed,
|
|
1504
|
+
processOutputWithPersistence,
|
|
1505
|
+
quoteShellCommand,
|
|
1506
|
+
restore,
|
|
1507
|
+
rewriteWindowsNullRedirect,
|
|
1508
|
+
runPowerShellCommand,
|
|
1509
|
+
runShellCommand,
|
|
1510
|
+
safeRm,
|
|
1511
|
+
safeTrashDeleteTool,
|
|
1512
|
+
safeTrashListTool,
|
|
1513
|
+
safeTrashRestoreTool,
|
|
1514
|
+
shouldAddStdinRedirect
|
|
1515
|
+
};
|
|
1516
|
+
//# sourceMappingURL=index.js.map
|