@wrongstack/tools 0.1.4 → 0.1.7
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 +127 -0
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +103 -5
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +546 -245
- package/dist/builtin.js.map +1 -1
- package/dist/diff.js +5 -9
- package/dist/diff.js.map +1 -1
- package/dist/document.js +0 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.js +2 -2
- package/dist/edit.js.map +1 -1
- package/dist/exec.d.ts +0 -1
- package/dist/exec.js +105 -44
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +110 -25
- package/dist/fetch.js.map +1 -1
- package/dist/format.js.map +1 -1
- package/dist/git.d.ts +0 -1
- package/dist/git.js +9 -9
- package/dist/git.js.map +1 -1
- package/dist/glob.js +0 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +58 -3
- package/dist/grep.js.map +1 -1
- package/dist/index.js +545 -244
- package/dist/index.js.map +1 -1
- package/dist/install.js.map +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +61 -6
- package/dist/logs.js.map +1 -1
- package/dist/outdated.js.map +1 -1
- package/dist/patch.js +68 -29
- package/dist/patch.js.map +1 -1
- package/dist/read.js +0 -1
- package/dist/read.js.map +1 -1
- package/dist/replace.js +59 -9
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +0 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -1
- package/dist/todo.js.map +1 -1
- package/dist/tree.js +9 -5
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +0 -1
- package/dist/write.js.map +1 -1
- package/package.json +7 -4
package/dist/builtin.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as fs4 from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
4
|
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, stripAnsi } from '@wrongstack/core';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import * as dns from 'dns/promises';
|
|
8
|
+
import * as net from 'net';
|
|
8
9
|
import * as fsSync from 'fs';
|
|
9
10
|
import { statSync } from 'fs';
|
|
10
11
|
|
|
@@ -43,83 +44,6 @@ function isBinaryBuffer(buf) {
|
|
|
43
44
|
}
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
|
-
async function* spawnStream(opts) {
|
|
47
|
-
const max = opts.maxBytes ?? 2e5;
|
|
48
|
-
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
49
|
-
let stdout = "";
|
|
50
|
-
let stderr = "";
|
|
51
|
-
let pending = "";
|
|
52
|
-
let error;
|
|
53
|
-
const child = spawn(opts.cmd, opts.args, {
|
|
54
|
-
cwd: opts.cwd,
|
|
55
|
-
signal: opts.signal,
|
|
56
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
57
|
-
});
|
|
58
|
-
const queue = [];
|
|
59
|
-
let waiter;
|
|
60
|
-
const wake = () => {
|
|
61
|
-
if (waiter) {
|
|
62
|
-
const w = waiter;
|
|
63
|
-
waiter = void 0;
|
|
64
|
-
w();
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
child.stdout?.on("data", (c) => {
|
|
68
|
-
const s = c.toString();
|
|
69
|
-
if (stdout.length < max) stdout += s;
|
|
70
|
-
queue.push({ kind: "out", data: s });
|
|
71
|
-
wake();
|
|
72
|
-
});
|
|
73
|
-
child.stderr?.on("data", (c) => {
|
|
74
|
-
const s = c.toString();
|
|
75
|
-
if (stderr.length < max) stderr += s;
|
|
76
|
-
queue.push({ kind: "err", data: s });
|
|
77
|
-
wake();
|
|
78
|
-
});
|
|
79
|
-
child.on("error", (e) => {
|
|
80
|
-
error = e.message;
|
|
81
|
-
queue.push({ kind: "error", data: e.message });
|
|
82
|
-
wake();
|
|
83
|
-
});
|
|
84
|
-
child.on("close", (code) => {
|
|
85
|
-
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
86
|
-
wake();
|
|
87
|
-
});
|
|
88
|
-
let exitCode = 0;
|
|
89
|
-
let spawnFailed = false;
|
|
90
|
-
for (; ; ) {
|
|
91
|
-
while (queue.length === 0) {
|
|
92
|
-
await new Promise((resolve2) => {
|
|
93
|
-
waiter = resolve2;
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
const chunk = queue.shift();
|
|
97
|
-
if (chunk.kind === "close") {
|
|
98
|
-
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
if (chunk.kind === "error") {
|
|
102
|
-
spawnFailed = true;
|
|
103
|
-
exitCode = 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
pending += chunk.data;
|
|
107
|
-
if (pending.length >= flushAt) {
|
|
108
|
-
yield { type: "partial_output", text: pending };
|
|
109
|
-
pending = "";
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (pending.length > 0) {
|
|
113
|
-
yield { type: "partial_output", text: pending };
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
stdout,
|
|
117
|
-
stderr,
|
|
118
|
-
exitCode,
|
|
119
|
-
truncated: stdout.length >= max || stderr.length >= max,
|
|
120
|
-
error
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
47
|
|
|
124
48
|
// src/read.ts
|
|
125
49
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -256,7 +180,8 @@ var editTool = {
|
|
|
256
180
|
);
|
|
257
181
|
}
|
|
258
182
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
259
|
-
|
|
183
|
+
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
184
|
+
if (lastReadMtime !== void 0 && stat9.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
260
185
|
throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
|
|
261
186
|
}
|
|
262
187
|
const original = await fs4.readFile(absPath, "utf8");
|
|
@@ -331,6 +256,48 @@ function findSimilarity(haystack, needle) {
|
|
|
331
256
|
}
|
|
332
257
|
return line;
|
|
333
258
|
}
|
|
259
|
+
|
|
260
|
+
// src/_regex.ts
|
|
261
|
+
var MAX_PATTERN_LEN = 512;
|
|
262
|
+
var DANGEROUS_PATTERNS = [
|
|
263
|
+
/(\([^)]*[+*][^)]*\))[+*]/,
|
|
264
|
+
// (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
|
|
265
|
+
/(\(\?:[^)]*[+*][^)]*\))[+*]/
|
|
266
|
+
// same, with non-capturing group
|
|
267
|
+
];
|
|
268
|
+
function compileUserRegex(pattern, flags) {
|
|
269
|
+
if (typeof pattern !== "string") {
|
|
270
|
+
return { ok: false, reason: "pattern must be a string" };
|
|
271
|
+
}
|
|
272
|
+
if (pattern.length === 0) {
|
|
273
|
+
return { ok: false, reason: "pattern is empty" };
|
|
274
|
+
}
|
|
275
|
+
if (pattern.length > MAX_PATTERN_LEN) {
|
|
276
|
+
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
277
|
+
}
|
|
278
|
+
for (const rx of DANGEROUS_PATTERNS) {
|
|
279
|
+
if (rx.test(pattern)) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
reason: err instanceof Error ? err.message : "invalid regex"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var MAX_SUBJECT_LEN = 64 * 1024;
|
|
296
|
+
function capSubject(line) {
|
|
297
|
+
return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/replace.ts
|
|
334
301
|
var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
335
302
|
var replaceTool = {
|
|
336
303
|
name: "replace",
|
|
@@ -358,23 +325,38 @@ var replaceTool = {
|
|
|
358
325
|
if (!input?.pattern) throw new Error("replace: pattern is required");
|
|
359
326
|
if (input.replacement === void 0) throw new Error("replace: replacement is required");
|
|
360
327
|
if (!input?.files) throw new Error("replace: files is required");
|
|
361
|
-
const
|
|
328
|
+
const replaceAll = input.replace_all ?? true;
|
|
329
|
+
const compiled = compileUserRegex(input.pattern, replaceAll ? "g" : "");
|
|
330
|
+
if (!compiled.ok) {
|
|
331
|
+
throw new Error(`replace: ${compiled.reason}`);
|
|
332
|
+
}
|
|
333
|
+
const re = compiled.regex;
|
|
362
334
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
363
335
|
const dryRun = input.dry_run ?? false;
|
|
364
|
-
const replaceAll = input.replace_all ?? true;
|
|
365
336
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
366
337
|
const fileList = await resolveFiles(filesInput, ctx, globRe);
|
|
367
338
|
const results = [];
|
|
368
339
|
let totalReplacements = 0;
|
|
369
340
|
for (const absPath of fileList) {
|
|
370
|
-
const
|
|
341
|
+
const lstat2 = await fs4.lstat(absPath).catch((err) => {
|
|
371
342
|
if (err.code === "ENOENT") return null;
|
|
372
343
|
throw err;
|
|
373
344
|
});
|
|
345
|
+
if (!lstat2 || !lstat2.isFile()) continue;
|
|
346
|
+
if (lstat2.isSymbolicLink()) continue;
|
|
347
|
+
let realPath;
|
|
348
|
+
try {
|
|
349
|
+
realPath = await fs4.realpath(absPath);
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const rel = path.relative(ctx.projectRoot, realPath);
|
|
354
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
355
|
+
const stat9 = await fs4.stat(realPath).catch(() => null);
|
|
374
356
|
if (!stat9 || !stat9.isFile()) continue;
|
|
375
357
|
let content;
|
|
376
358
|
try {
|
|
377
|
-
const buf = await fs4.readFile(
|
|
359
|
+
const buf = await fs4.readFile(realPath);
|
|
378
360
|
if (isBinaryBuffer(buf)) continue;
|
|
379
361
|
content = buf.toString("utf8");
|
|
380
362
|
} catch {
|
|
@@ -385,13 +367,12 @@ var replaceTool = {
|
|
|
385
367
|
re.lastIndex = 0;
|
|
386
368
|
const matches = [...contentLf.matchAll(re)];
|
|
387
369
|
if (matches.length === 0) continue;
|
|
388
|
-
const newContentLf =
|
|
370
|
+
const newContentLf = contentLf.replace(re, input.replacement);
|
|
389
371
|
re.lastIndex = 0;
|
|
390
|
-
|
|
391
|
-
totalReplacements += actualCount;
|
|
372
|
+
totalReplacements += matches.length;
|
|
392
373
|
if (!dryRun) {
|
|
393
374
|
const newContent = toStyle(newContentLf, style);
|
|
394
|
-
await atomicWrite(
|
|
375
|
+
await atomicWrite(realPath, newContent, { mode: stat9.mode & 511 });
|
|
395
376
|
}
|
|
396
377
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath }) : void 0;
|
|
397
378
|
results.push({
|
|
@@ -438,13 +419,13 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
438
419
|
return await globNative(pattern, base, extraGlob);
|
|
439
420
|
}
|
|
440
421
|
function checkRg() {
|
|
441
|
-
return new Promise((
|
|
422
|
+
return new Promise((resolve4) => {
|
|
442
423
|
try {
|
|
443
424
|
const p = spawn("rg", ["--version"], { stdio: "ignore" });
|
|
444
|
-
p.on("error", () =>
|
|
445
|
-
p.on("close", (code) =>
|
|
425
|
+
p.on("error", () => resolve4(false));
|
|
426
|
+
p.on("close", (code) => resolve4(code === 0));
|
|
446
427
|
} catch {
|
|
447
|
-
|
|
428
|
+
resolve4(false);
|
|
448
429
|
}
|
|
449
430
|
});
|
|
450
431
|
}
|
|
@@ -456,10 +437,10 @@ function spawnRgFind(pattern, base) {
|
|
|
456
437
|
buf += chunk.toString();
|
|
457
438
|
});
|
|
458
439
|
return {
|
|
459
|
-
promise: new Promise((
|
|
440
|
+
promise: new Promise((resolve4, reject) => {
|
|
460
441
|
child.on("error", reject);
|
|
461
442
|
child.on("close", () => {
|
|
462
|
-
|
|
443
|
+
resolve4(buf.split("\n").filter(Boolean));
|
|
463
444
|
});
|
|
464
445
|
})
|
|
465
446
|
};
|
|
@@ -616,13 +597,13 @@ var grepTool = {
|
|
|
616
597
|
}
|
|
617
598
|
};
|
|
618
599
|
async function detectRg(signal) {
|
|
619
|
-
return new Promise((
|
|
600
|
+
return new Promise((resolve4) => {
|
|
620
601
|
try {
|
|
621
602
|
const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
|
|
622
|
-
p.on("error", () =>
|
|
623
|
-
p.on("close", (code) =>
|
|
603
|
+
p.on("error", () => resolve4(false));
|
|
604
|
+
p.on("close", (code) => resolve4(code === 0));
|
|
624
605
|
} catch {
|
|
625
|
-
|
|
606
|
+
resolve4(false);
|
|
626
607
|
}
|
|
627
608
|
});
|
|
628
609
|
}
|
|
@@ -642,6 +623,8 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
642
623
|
let totalLines = 0;
|
|
643
624
|
let batchSinceFlush = 0;
|
|
644
625
|
const FLUSH_AT = 16;
|
|
626
|
+
const MAX_BUF_BYTES = 1e6;
|
|
627
|
+
let bufOverflow = false;
|
|
645
628
|
const child = spawn("rg", args, { signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
646
629
|
const queue = [];
|
|
647
630
|
let waiter;
|
|
@@ -679,6 +662,14 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
679
662
|
}
|
|
680
663
|
if (c.kind === "close") break;
|
|
681
664
|
buf += c.data;
|
|
665
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
666
|
+
bufOverflow = true;
|
|
667
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
668
|
+
try {
|
|
669
|
+
child.kill("SIGTERM");
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
}
|
|
682
673
|
const idx = buf.lastIndexOf("\n");
|
|
683
674
|
if (idx === -1) continue;
|
|
684
675
|
const ready = buf.slice(0, idx);
|
|
@@ -725,14 +716,18 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
725
716
|
output: {
|
|
726
717
|
matches,
|
|
727
718
|
count: totalLines,
|
|
728
|
-
truncated: totalLines > limit,
|
|
719
|
+
truncated: totalLines > limit || bufOverflow,
|
|
729
720
|
used: "rg"
|
|
730
721
|
}
|
|
731
722
|
};
|
|
732
723
|
}
|
|
733
724
|
async function runNative(input, base, mode, limit, signal) {
|
|
734
725
|
const flags = input.case_insensitive ? "i" : "";
|
|
735
|
-
const
|
|
726
|
+
const compiled = compileUserRegex(input.pattern, flags);
|
|
727
|
+
if (!compiled.ok) {
|
|
728
|
+
throw new Error(`grep: ${compiled.reason}`);
|
|
729
|
+
}
|
|
730
|
+
const re = compiled.regex;
|
|
736
731
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
737
732
|
const matches = [];
|
|
738
733
|
const fileMatches = /* @__PURE__ */ new Map();
|
|
@@ -749,6 +744,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
749
744
|
for (const e of entries) {
|
|
750
745
|
if (stopped) return;
|
|
751
746
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
747
|
+
if (e.isSymbolicLink()) continue;
|
|
752
748
|
const full = path.join(dir, e.name);
|
|
753
749
|
if (e.isDirectory()) {
|
|
754
750
|
await walk(full);
|
|
@@ -764,7 +760,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
764
760
|
const lines = text.split(/\r?\n/);
|
|
765
761
|
let fileHits = 0;
|
|
766
762
|
for (let i = 0; i < lines.length; i++) {
|
|
767
|
-
const ln = lines[i] ?? "";
|
|
763
|
+
const ln = capSubject(lines[i] ?? "");
|
|
768
764
|
re.lastIndex = 0;
|
|
769
765
|
if (re.test(ln)) {
|
|
770
766
|
fileHits++;
|
|
@@ -797,6 +793,86 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
797
793
|
used: "native"
|
|
798
794
|
};
|
|
799
795
|
}
|
|
796
|
+
|
|
797
|
+
// src/_env.ts
|
|
798
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
799
|
+
"PATH",
|
|
800
|
+
"HOME",
|
|
801
|
+
"USER",
|
|
802
|
+
"USERNAME",
|
|
803
|
+
"LOGNAME",
|
|
804
|
+
"SHELL",
|
|
805
|
+
"LANG",
|
|
806
|
+
"LC_ALL",
|
|
807
|
+
"LC_CTYPE",
|
|
808
|
+
"TERM",
|
|
809
|
+
"TZ",
|
|
810
|
+
"TMPDIR",
|
|
811
|
+
"TEMP",
|
|
812
|
+
"TMP",
|
|
813
|
+
"PWD",
|
|
814
|
+
"OLDPWD",
|
|
815
|
+
"COMSPEC",
|
|
816
|
+
"SYSTEMROOT",
|
|
817
|
+
"SYSTEMDRIVE",
|
|
818
|
+
"WINDIR",
|
|
819
|
+
"PROGRAMFILES",
|
|
820
|
+
"PROGRAMFILES(X86)",
|
|
821
|
+
"PROGRAMDATA",
|
|
822
|
+
"APPDATA",
|
|
823
|
+
"LOCALAPPDATA",
|
|
824
|
+
"USERPROFILE",
|
|
825
|
+
"PUBLIC",
|
|
826
|
+
"PATHEXT"
|
|
827
|
+
]);
|
|
828
|
+
var SECRET_NAME_PARTS = [
|
|
829
|
+
"TOKEN",
|
|
830
|
+
"SECRET",
|
|
831
|
+
"PASSWORD",
|
|
832
|
+
"PASSWD",
|
|
833
|
+
"AUTH",
|
|
834
|
+
"CRED",
|
|
835
|
+
"BEARER",
|
|
836
|
+
"COOKIE",
|
|
837
|
+
"PRIVATE"
|
|
838
|
+
];
|
|
839
|
+
function looksSecret(name) {
|
|
840
|
+
const upper = name.toUpperCase();
|
|
841
|
+
for (const p of SECRET_NAME_PARTS) {
|
|
842
|
+
if (upper.includes(p)) return true;
|
|
843
|
+
}
|
|
844
|
+
if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
|
|
845
|
+
if (/API[_-]?KEY/i.test(upper)) return true;
|
|
846
|
+
if (/ACCESS[_-]?KEY/i.test(upper)) return true;
|
|
847
|
+
if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
function buildChildEnv(sessionId) {
|
|
853
|
+
const passthrough = process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
|
|
854
|
+
const out = {};
|
|
855
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
856
|
+
if (v === void 0) continue;
|
|
857
|
+
if (passthrough) {
|
|
858
|
+
out[k] = v;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const upper = k.toUpperCase();
|
|
862
|
+
if (ALLOWED_KEYS.has(upper)) {
|
|
863
|
+
out[k] = v;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (looksSecret(upper)) continue;
|
|
867
|
+
if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
|
|
868
|
+
out[k] = v;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (sessionId) out["WRONGSTACK_SESSION_ID"] = sessionId;
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/bash.ts
|
|
800
876
|
var MAX_OUTPUT = 32768;
|
|
801
877
|
var DEFAULT_TIMEOUT = 3e4;
|
|
802
878
|
var STREAM_FLUSH_INTERVAL_MS = 200;
|
|
@@ -807,6 +883,10 @@ var bashTool = {
|
|
|
807
883
|
usageHint: "Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.",
|
|
808
884
|
permission: "confirm",
|
|
809
885
|
mutating: true,
|
|
886
|
+
// Trust rules match on the literal `command` string. Without subjectKey
|
|
887
|
+
// the policy heuristic would have done the same here, but declaring it
|
|
888
|
+
// explicitly removes the implicit cross-tool aliasing.
|
|
889
|
+
subjectKey: "command",
|
|
810
890
|
timeoutMs: 3e4,
|
|
811
891
|
maxOutputBytes: MAX_OUTPUT,
|
|
812
892
|
estimatedDurationMs: 3e3,
|
|
@@ -833,13 +913,13 @@ var bashTool = {
|
|
|
833
913
|
const isWin = os.platform() === "win32";
|
|
834
914
|
const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
835
915
|
const args = isWin ? ["/c", input.command] : ["-c", input.command];
|
|
836
|
-
const env =
|
|
837
|
-
|
|
916
|
+
const env = buildChildEnv(ctx.session?.id);
|
|
917
|
+
const detached = isWin ? !!input.background : true;
|
|
838
918
|
const child = spawn(shell, args, {
|
|
839
919
|
cwd: ctx.projectRoot,
|
|
840
920
|
env,
|
|
841
921
|
stdio: input.background ? "ignore" : ["ignore", "pipe", "pipe"],
|
|
842
|
-
detached
|
|
922
|
+
detached,
|
|
843
923
|
signal: opts.signal
|
|
844
924
|
});
|
|
845
925
|
if (input.background) {
|
|
@@ -869,10 +949,26 @@ var bashTool = {
|
|
|
869
949
|
}
|
|
870
950
|
} else {
|
|
871
951
|
try {
|
|
872
|
-
child.
|
|
952
|
+
if (typeof child.pid === "number") {
|
|
953
|
+
try {
|
|
954
|
+
process.kill(-child.pid, "SIGTERM");
|
|
955
|
+
} catch {
|
|
956
|
+
child.kill("SIGTERM");
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
child.kill("SIGTERM");
|
|
960
|
+
}
|
|
873
961
|
const killTimer = setTimeout(() => {
|
|
874
962
|
try {
|
|
875
|
-
child.
|
|
963
|
+
if (typeof child.pid === "number") {
|
|
964
|
+
try {
|
|
965
|
+
process.kill(-child.pid, "SIGKILL");
|
|
966
|
+
} catch {
|
|
967
|
+
child.kill("SIGKILL");
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
child.kill("SIGKILL");
|
|
971
|
+
}
|
|
876
972
|
} catch {
|
|
877
973
|
}
|
|
878
974
|
}, 2e3);
|
|
@@ -894,10 +990,10 @@ var bashTool = {
|
|
|
894
990
|
queue.push(c);
|
|
895
991
|
}
|
|
896
992
|
};
|
|
897
|
-
const next = () => new Promise((
|
|
993
|
+
const next = () => new Promise((resolve4) => {
|
|
898
994
|
const c = queue.shift();
|
|
899
|
-
if (c)
|
|
900
|
-
else resolveNext =
|
|
995
|
+
if (c) resolve4(c);
|
|
996
|
+
else resolveNext = resolve4;
|
|
901
997
|
});
|
|
902
998
|
let lastFlush = Date.now();
|
|
903
999
|
const flush = () => {
|
|
@@ -989,32 +1085,13 @@ var ALLOWED_COMMANDS = {
|
|
|
989
1085
|
docker: ["--version", "ps", "images", "build"],
|
|
990
1086
|
kubectl: ["version", "get", "describe", "logs"]
|
|
991
1087
|
};
|
|
992
|
-
var FORBIDDEN_PATTERNS = [
|
|
993
|
-
/;\s*rm\s+-rf/i,
|
|
994
|
-
/\|\s*rm\s/i,
|
|
995
|
-
/\&\&\s*rm/i,
|
|
996
|
-
/\$\(.*rm/s,
|
|
997
|
-
/`.*rm/s,
|
|
998
|
-
/eval\s*\(/i,
|
|
999
|
-
/exec\s+/i,
|
|
1000
|
-
/nc\s+-e/i,
|
|
1001
|
-
/bash\s+-i/i,
|
|
1002
|
-
/\/dev\/tcp\//i,
|
|
1003
|
-
/curl\s+.*\|/i,
|
|
1004
|
-
/wget\s+.*\|/i,
|
|
1005
|
-
/chmod\s+777/i,
|
|
1006
|
-
/chmod\s+4755/i,
|
|
1007
|
-
/>\s*\/dev\//i,
|
|
1008
|
-
/2>\s*\/dev\//i,
|
|
1009
|
-
/tee\s+/i
|
|
1010
|
-
];
|
|
1011
1088
|
var MAX_ARGS = 20;
|
|
1012
1089
|
var MAX_OUTPUT2 = 2e5;
|
|
1013
1090
|
var TIMEOUT_MS = 3e4;
|
|
1014
1091
|
var execTool = {
|
|
1015
1092
|
name: "exec",
|
|
1016
1093
|
description: "Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.",
|
|
1017
|
-
usageHint: "Set `command` (must be in allowlist). `args` passed through.
|
|
1094
|
+
usageHint: "Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.",
|
|
1018
1095
|
permission: "confirm",
|
|
1019
1096
|
mutating: false,
|
|
1020
1097
|
timeoutMs: TIMEOUT_MS,
|
|
@@ -1023,57 +1100,56 @@ var execTool = {
|
|
|
1023
1100
|
properties: {
|
|
1024
1101
|
command: { type: "string", description: "Command to run (must be in allowlist)" },
|
|
1025
1102
|
args: { type: "array", items: { type: "string" }, description: "Arguments" },
|
|
1026
|
-
cwd: { type: "string", description: "Working directory" },
|
|
1027
|
-
timeout: { type: "integer", description: "Timeout in ms (default: 30000)" }
|
|
1028
|
-
allow_unknown: {
|
|
1029
|
-
type: "boolean",
|
|
1030
|
-
description: "Allow commands not in allowlist (DANGEROUS, use with caution)"
|
|
1031
|
-
}
|
|
1103
|
+
cwd: { type: "string", description: "Working directory (must resolve inside project root)" },
|
|
1104
|
+
timeout: { type: "integer", description: "Timeout in ms (default: 30000)" }
|
|
1032
1105
|
},
|
|
1033
1106
|
required: ["command"]
|
|
1034
1107
|
},
|
|
1035
1108
|
async execute(input, ctx, opts) {
|
|
1036
1109
|
const cmd = input.command.trim();
|
|
1037
1110
|
if (!cmd) return { command: cmd, args: [], stdout: "", stderr: "Empty command", exitCode: 1, truncated: false, allowed: false };
|
|
1038
|
-
if (
|
|
1111
|
+
if (!(cmd in ALLOWED_COMMANDS)) {
|
|
1039
1112
|
return {
|
|
1040
1113
|
command: cmd,
|
|
1041
1114
|
args: input.args ?? [],
|
|
1042
1115
|
stdout: "",
|
|
1043
|
-
stderr: `Command
|
|
1116
|
+
stderr: `Command "${cmd}" not in allowlist. Use the bash tool for arbitrary commands.`,
|
|
1044
1117
|
exitCode: 1,
|
|
1045
1118
|
truncated: false,
|
|
1046
1119
|
allowed: false
|
|
1047
1120
|
};
|
|
1048
1121
|
}
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (
|
|
1122
|
+
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
1123
|
+
const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);
|
|
1124
|
+
const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
|
|
1125
|
+
const rel = path.relative(ctx.projectRoot, requestedCwd);
|
|
1126
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
1054
1127
|
return {
|
|
1055
1128
|
command: cmd,
|
|
1056
|
-
args
|
|
1129
|
+
args,
|
|
1057
1130
|
stdout: "",
|
|
1058
|
-
stderr: `
|
|
1131
|
+
stderr: `cwd "${input.cwd}" resolves outside project root`,
|
|
1059
1132
|
exitCode: 1,
|
|
1060
1133
|
truncated: false,
|
|
1061
1134
|
allowed: false
|
|
1062
1135
|
};
|
|
1063
1136
|
}
|
|
1064
|
-
const
|
|
1065
|
-
const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);
|
|
1066
|
-
const cwd = input.cwd ?? ctx.cwd;
|
|
1137
|
+
const cwd = requestedCwd;
|
|
1067
1138
|
const signal = opts.signal;
|
|
1068
|
-
return runCommand(cmd, args, cwd, timeout, signal);
|
|
1139
|
+
return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
|
|
1069
1140
|
}
|
|
1070
1141
|
};
|
|
1071
|
-
function runCommand(cmd, args, cwd, timeout, signal) {
|
|
1072
|
-
return new Promise((
|
|
1142
|
+
function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
1143
|
+
return new Promise((resolve4) => {
|
|
1073
1144
|
let stdout = "";
|
|
1074
1145
|
let stderr = "";
|
|
1075
1146
|
let killed = false;
|
|
1076
|
-
const child = spawn(cmd, args, {
|
|
1147
|
+
const child = spawn(cmd, args, {
|
|
1148
|
+
cwd,
|
|
1149
|
+
signal,
|
|
1150
|
+
env: buildChildEnv(sessionId),
|
|
1151
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1152
|
+
});
|
|
1077
1153
|
const timer = setTimeout(() => {
|
|
1078
1154
|
killed = true;
|
|
1079
1155
|
child.kill("SIGTERM");
|
|
@@ -1086,7 +1162,7 @@ function runCommand(cmd, args, cwd, timeout, signal) {
|
|
|
1086
1162
|
});
|
|
1087
1163
|
child.on("close", (code) => {
|
|
1088
1164
|
clearTimeout(timer);
|
|
1089
|
-
|
|
1165
|
+
resolve4({
|
|
1090
1166
|
command: cmd,
|
|
1091
1167
|
args,
|
|
1092
1168
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1098,7 +1174,7 @@ function runCommand(cmd, args, cwd, timeout, signal) {
|
|
|
1098
1174
|
});
|
|
1099
1175
|
child.on("error", (err) => {
|
|
1100
1176
|
clearTimeout(timer);
|
|
1101
|
-
|
|
1177
|
+
resolve4({
|
|
1102
1178
|
command: cmd,
|
|
1103
1179
|
args,
|
|
1104
1180
|
stdout: stdout.slice(0, MAX_OUTPUT2),
|
|
@@ -1112,17 +1188,6 @@ function runCommand(cmd, args, cwd, timeout, signal) {
|
|
|
1112
1188
|
}
|
|
1113
1189
|
var MAX_BYTES2 = 131072;
|
|
1114
1190
|
var TIMEOUT_MS2 = 2e4;
|
|
1115
|
-
var PRIVATE_RANGES = [
|
|
1116
|
-
/^10\./,
|
|
1117
|
-
/^192\.168\./,
|
|
1118
|
-
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
1119
|
-
/^127\./,
|
|
1120
|
-
/^0\./,
|
|
1121
|
-
/^169\.254\./,
|
|
1122
|
-
/^::1$/,
|
|
1123
|
-
/^fc/i,
|
|
1124
|
-
/^fe80:/i
|
|
1125
|
-
];
|
|
1126
1191
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
1127
1192
|
async function fetchWithRedirectLimit(url, maxRedirects, signal) {
|
|
1128
1193
|
const headers = {
|
|
@@ -1132,6 +1197,14 @@ async function fetchWithRedirectLimit(url, maxRedirects, signal) {
|
|
|
1132
1197
|
let redirectCount = 0;
|
|
1133
1198
|
let currentUrl = url;
|
|
1134
1199
|
for (; ; ) {
|
|
1200
|
+
const parsed = new URL(currentUrl);
|
|
1201
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
1202
|
+
throw new Error(`fetch: redirect to unsupported protocol "${parsed.protocol}"`);
|
|
1203
|
+
}
|
|
1204
|
+
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
1205
|
+
throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
|
|
1206
|
+
}
|
|
1207
|
+
await assertNotPrivate(parsed.hostname);
|
|
1135
1208
|
const res = await fetch(currentUrl, {
|
|
1136
1209
|
redirect: "manual",
|
|
1137
1210
|
signal,
|
|
@@ -1157,6 +1230,11 @@ var fetchTool = {
|
|
|
1157
1230
|
usageHint: "HTTPS only by default. Localhost and RFC1918 ranges blocked unless WRONGSTACK_FETCH_ALLOW_PRIVATE=1. Max 5 redirects, 20s timeout, 128KB cap.",
|
|
1158
1231
|
permission: "confirm",
|
|
1159
1232
|
mutating: false,
|
|
1233
|
+
// Trust rules for fetch match on the literal URL — declare it explicitly
|
|
1234
|
+
// so a user can trust `https://api.example.com/*` without accidentally
|
|
1235
|
+
// matching that pattern on any other tool that happens to have a `url`
|
|
1236
|
+
// input field.
|
|
1237
|
+
subjectKey: "url",
|
|
1160
1238
|
timeoutMs: TIMEOUT_MS2,
|
|
1161
1239
|
maxOutputBytes: MAX_BYTES2,
|
|
1162
1240
|
inputSchema: {
|
|
@@ -1244,35 +1322,118 @@ var fetchTool = {
|
|
|
1244
1322
|
};
|
|
1245
1323
|
async function assertNotPrivate(hostname) {
|
|
1246
1324
|
if (ALLOW_PRIVATE) return;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
1325
|
+
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1326
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
1251
1327
|
throw new Error("fetch: blocked localhost target");
|
|
1252
1328
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1329
|
+
const ipVersion = net.isIP(host);
|
|
1330
|
+
if (ipVersion === 4) {
|
|
1331
|
+
if (isPrivateIPv4(host)) {
|
|
1332
|
+
throw new Error(`fetch: blocked private/loopback address "${host}"`);
|
|
1333
|
+
}
|
|
1334
|
+
} else if (ipVersion === 6) {
|
|
1335
|
+
if (isPrivateIPv6(host)) {
|
|
1336
|
+
throw new Error(`fetch: blocked private/loopback address "${host}"`);
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
try {
|
|
1340
|
+
const records = await dns.lookup(host, { all: true });
|
|
1341
|
+
for (const r of records) {
|
|
1342
|
+
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
1343
|
+
if (bad) {
|
|
1344
|
+
throw new Error(`fetch: resolved to private address ${r.address}`);
|
|
1345
|
+
}
|
|
1258
1346
|
}
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
|
|
1259
1349
|
}
|
|
1260
|
-
} catch (err) {
|
|
1261
|
-
if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
|
|
1262
1350
|
}
|
|
1263
1351
|
}
|
|
1352
|
+
function isPrivateIPv4(addr) {
|
|
1353
|
+
const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
|
|
1354
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
const [a, b, c] = parts;
|
|
1358
|
+
if (a === 0) return true;
|
|
1359
|
+
if (a === 10) return true;
|
|
1360
|
+
if (a === 127) return true;
|
|
1361
|
+
if (a === 169 && b === 254) return true;
|
|
1362
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1363
|
+
if (a === 192 && b === 168) return true;
|
|
1364
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
1365
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
1366
|
+
if (a >= 224) return true;
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
function isPrivateIPv6(addr) {
|
|
1370
|
+
const lower = addr.toLowerCase();
|
|
1371
|
+
if (lower === "::" || lower === "::1") return true;
|
|
1372
|
+
const groups = expandIPv6(lower);
|
|
1373
|
+
if (!groups) return true;
|
|
1374
|
+
if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
|
|
1375
|
+
const a = (groups[6] ?? 0) >> 8;
|
|
1376
|
+
const b = (groups[6] ?? 0) & 255;
|
|
1377
|
+
const c = (groups[7] ?? 0) >> 8;
|
|
1378
|
+
const d = (groups[7] ?? 0) & 255;
|
|
1379
|
+
return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
|
|
1380
|
+
}
|
|
1381
|
+
const high = groups[0] ?? 0;
|
|
1382
|
+
if ((high & 65024) === 64512) return true;
|
|
1383
|
+
if ((high & 65472) === 65152) return true;
|
|
1384
|
+
if ((high & 65280) === 65280) return true;
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
function expandIPv6(addr) {
|
|
1388
|
+
const parts = addr.split("::");
|
|
1389
|
+
if (parts.length > 2) return null;
|
|
1390
|
+
const parseGroups = (s) => {
|
|
1391
|
+
if (s === "") return [];
|
|
1392
|
+
const out = [];
|
|
1393
|
+
for (const g of s.split(":")) {
|
|
1394
|
+
if (g.length === 0 || g.length > 4) return null;
|
|
1395
|
+
const n = Number.parseInt(g, 16);
|
|
1396
|
+
if (Number.isNaN(n) || n < 0 || n > 65535) return null;
|
|
1397
|
+
out.push(n);
|
|
1398
|
+
}
|
|
1399
|
+
return out;
|
|
1400
|
+
};
|
|
1401
|
+
if (parts.length === 1) {
|
|
1402
|
+
const groups = parseGroups(parts[0] ?? "");
|
|
1403
|
+
if (!groups || groups.length !== 8) return null;
|
|
1404
|
+
return groups;
|
|
1405
|
+
}
|
|
1406
|
+
const head = parseGroups(parts[0] ?? "");
|
|
1407
|
+
const tail = parseGroups(parts[1] ?? "");
|
|
1408
|
+
if (!head || !tail) return null;
|
|
1409
|
+
const fill = 8 - head.length - tail.length;
|
|
1410
|
+
if (fill < 0) return null;
|
|
1411
|
+
return [...head, ...new Array(fill).fill(0), ...tail];
|
|
1412
|
+
}
|
|
1264
1413
|
function combineSignals(...sigs) {
|
|
1265
1414
|
if (typeof AbortSignal.any === "function") {
|
|
1266
1415
|
return AbortSignal.any(sigs);
|
|
1267
1416
|
}
|
|
1268
1417
|
const ctrl = new AbortController();
|
|
1418
|
+
const cleanups = [];
|
|
1419
|
+
const detach = () => {
|
|
1420
|
+
for (const fn of cleanups) fn();
|
|
1421
|
+
cleanups.length = 0;
|
|
1422
|
+
};
|
|
1269
1423
|
for (const s of sigs) {
|
|
1270
1424
|
if (s.aborted) {
|
|
1425
|
+
detach();
|
|
1271
1426
|
ctrl.abort(s.reason);
|
|
1272
|
-
|
|
1427
|
+
return ctrl.signal;
|
|
1273
1428
|
}
|
|
1274
|
-
|
|
1429
|
+
const onAbort = () => {
|
|
1430
|
+
detach();
|
|
1431
|
+
ctrl.abort(s.reason);
|
|
1432
|
+
};
|
|
1433
|
+
s.addEventListener("abort", onAbort, { once: true });
|
|
1434
|
+
cleanups.push(() => s.removeEventListener("abort", onAbort));
|
|
1275
1435
|
}
|
|
1436
|
+
ctrl.signal.addEventListener("abort", detach, { once: true });
|
|
1276
1437
|
return ctrl.signal;
|
|
1277
1438
|
}
|
|
1278
1439
|
function prettyJson(s) {
|
|
@@ -1557,7 +1718,7 @@ var todoTool = {
|
|
|
1557
1718
|
}
|
|
1558
1719
|
}
|
|
1559
1720
|
}
|
|
1560
|
-
ctx.
|
|
1721
|
+
ctx.state.replaceTodos(items);
|
|
1561
1722
|
return {
|
|
1562
1723
|
count: items.length,
|
|
1563
1724
|
in_progress: items.filter((t) => t.status === "in_progress").length
|
|
@@ -1571,10 +1732,10 @@ var gitTool = {
|
|
|
1571
1732
|
description: "Run git commands. Wraps common operations: status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset.",
|
|
1572
1733
|
usageHint: "Prefer built-in subcommands over raw args. `command` is required. `message` for commits. `branch` for checkout/branch. `files` for status/diff. `format` for log.",
|
|
1573
1734
|
permission: "confirm",
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1735
|
+
// Conservative: any of these may mutate. The non-mutating commands
|
|
1736
|
+
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
1737
|
+
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
1738
|
+
mutating: true,
|
|
1578
1739
|
timeoutMs: TIMEOUT_MS4,
|
|
1579
1740
|
inputSchema: {
|
|
1580
1741
|
type: "object",
|
|
@@ -1596,7 +1757,6 @@ var gitTool = {
|
|
|
1596
1757
|
],
|
|
1597
1758
|
description: "Git subcommand"
|
|
1598
1759
|
},
|
|
1599
|
-
args: { type: "string", description: "Raw args string (bypasses subcommand logic)" },
|
|
1600
1760
|
files: {
|
|
1601
1761
|
type: "string",
|
|
1602
1762
|
description: 'File(s) for status/diff: single path, comma-separated list, or "**/*.ts" glob'
|
|
@@ -1615,12 +1775,12 @@ var gitTool = {
|
|
|
1615
1775
|
},
|
|
1616
1776
|
async execute(input, ctx, opts) {
|
|
1617
1777
|
if (!input?.command) throw new Error("git: command is required");
|
|
1618
|
-
const gitDir = findGitDir(ctx.cwd);
|
|
1778
|
+
const gitDir = findGitDir(ctx.cwd, ctx.projectRoot);
|
|
1619
1779
|
if (!gitDir) {
|
|
1620
1780
|
return {
|
|
1621
1781
|
command: input.command,
|
|
1622
1782
|
stdout: "",
|
|
1623
|
-
stderr: "Not in a git repository",
|
|
1783
|
+
stderr: "Not in a git repository (within project root)",
|
|
1624
1784
|
exitCode: 128,
|
|
1625
1785
|
truncated: false
|
|
1626
1786
|
};
|
|
@@ -1629,7 +1789,8 @@ var gitTool = {
|
|
|
1629
1789
|
return await runGit(args, gitDir, opts.signal);
|
|
1630
1790
|
}
|
|
1631
1791
|
};
|
|
1632
|
-
function findGitDir(cwd) {
|
|
1792
|
+
function findGitDir(cwd, projectRoot) {
|
|
1793
|
+
const root = projectRoot;
|
|
1633
1794
|
let dir = cwd;
|
|
1634
1795
|
for (let i = 0; i < 20; i++) {
|
|
1635
1796
|
try {
|
|
@@ -1637,6 +1798,7 @@ function findGitDir(cwd) {
|
|
|
1637
1798
|
if (stat9.isDirectory()) return dir;
|
|
1638
1799
|
} catch {
|
|
1639
1800
|
}
|
|
1801
|
+
if (dir === root) break;
|
|
1640
1802
|
const parent = dirname(dir);
|
|
1641
1803
|
if (parent === dir) break;
|
|
1642
1804
|
dir = parent;
|
|
@@ -1644,7 +1806,6 @@ function findGitDir(cwd) {
|
|
|
1644
1806
|
return null;
|
|
1645
1807
|
}
|
|
1646
1808
|
function buildArgs(input) {
|
|
1647
|
-
if (input.args) return input.args.split(/\s+/).filter(Boolean);
|
|
1648
1809
|
const limit = input.limit ?? 20;
|
|
1649
1810
|
const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim()).filter(Boolean) : [];
|
|
1650
1811
|
switch (input.command) {
|
|
@@ -1691,7 +1852,7 @@ function buildArgs(input) {
|
|
|
1691
1852
|
}
|
|
1692
1853
|
}
|
|
1693
1854
|
function runGit(args, cwd, signal) {
|
|
1694
|
-
return new Promise((
|
|
1855
|
+
return new Promise((resolve4) => {
|
|
1695
1856
|
let stdout = "";
|
|
1696
1857
|
let stderr = "";
|
|
1697
1858
|
const child = spawn("git", args, {
|
|
@@ -1710,7 +1871,7 @@ function runGit(args, cwd, signal) {
|
|
|
1710
1871
|
}
|
|
1711
1872
|
});
|
|
1712
1873
|
child.on("error", (err) => {
|
|
1713
|
-
|
|
1874
|
+
resolve4({
|
|
1714
1875
|
command: args[0],
|
|
1715
1876
|
stdout,
|
|
1716
1877
|
stderr: err.message,
|
|
@@ -1719,7 +1880,7 @@ function runGit(args, cwd, signal) {
|
|
|
1719
1880
|
});
|
|
1720
1881
|
});
|
|
1721
1882
|
child.on("close", (code) => {
|
|
1722
|
-
|
|
1883
|
+
resolve4({
|
|
1723
1884
|
command: args[0],
|
|
1724
1885
|
stdout: stdout.slice(0, MAX_OUTPUT3),
|
|
1725
1886
|
stderr: stderr.slice(0, MAX_OUTPUT3),
|
|
@@ -1749,51 +1910,90 @@ var patchTool = {
|
|
|
1749
1910
|
async execute(input, ctx, opts) {
|
|
1750
1911
|
if (!input?.patch) throw new Error("patch: patch content is required");
|
|
1751
1912
|
const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
|
|
1752
|
-
const strip = input.strip ?? 1;
|
|
1913
|
+
const strip = Math.max(1, input.strip ?? 1);
|
|
1753
1914
|
const dryRun = input.dry_run ?? false;
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
"
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1915
|
+
const targets = extractDiffTargets(input.patch);
|
|
1916
|
+
for (const t of targets) {
|
|
1917
|
+
const stripped = stripPathComponents(t, strip);
|
|
1918
|
+
if (!stripped) continue;
|
|
1919
|
+
const candidate = path.resolve(dir, stripped);
|
|
1920
|
+
const rel = path.relative(ctx.projectRoot, candidate);
|
|
1921
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
1922
|
+
return {
|
|
1923
|
+
applied: 0,
|
|
1924
|
+
rejected: 1,
|
|
1925
|
+
files: [],
|
|
1926
|
+
dry_run: dryRun,
|
|
1927
|
+
message: `patch refused: target "${t}" resolves outside project root`
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
const tmpDir = await fs4.mkdtemp(path.join(dir, ".wstack_patch_"));
|
|
1932
|
+
try {
|
|
1933
|
+
await fs4.chmod(tmpDir, 448).catch(() => {
|
|
1934
|
+
});
|
|
1935
|
+
const patchFile = path.join(tmpDir, "in.diff");
|
|
1936
|
+
await fs4.writeFile(patchFile, input.patch, { mode: 384 });
|
|
1937
|
+
const args = [
|
|
1938
|
+
`-p${strip}`,
|
|
1939
|
+
"--merge",
|
|
1940
|
+
...dryRun ? ["--dry-run"] : [],
|
|
1941
|
+
"-i",
|
|
1942
|
+
patchFile
|
|
1943
|
+
];
|
|
1944
|
+
const result = await runPatch(args, dir, opts.signal);
|
|
1945
|
+
if (result.exitCode !== 0 && !dryRun) {
|
|
1946
|
+
return {
|
|
1947
|
+
applied: 0,
|
|
1948
|
+
rejected: 1,
|
|
1949
|
+
files: [],
|
|
1950
|
+
dry_run: dryRun,
|
|
1951
|
+
message: `patch failed: ${result.stderr || result.stdout}`
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
const patched = extractPatchedFiles(result.stdout);
|
|
1767
1955
|
return {
|
|
1768
|
-
applied:
|
|
1769
|
-
rejected:
|
|
1770
|
-
files:
|
|
1956
|
+
applied: patched.length,
|
|
1957
|
+
rejected: 0,
|
|
1958
|
+
files: patched,
|
|
1771
1959
|
dry_run: dryRun,
|
|
1772
|
-
message:
|
|
1960
|
+
message: result.stdout || "patch applied"
|
|
1773
1961
|
};
|
|
1962
|
+
} finally {
|
|
1963
|
+
await fs4.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
1964
|
+
});
|
|
1774
1965
|
}
|
|
1775
|
-
return {
|
|
1776
|
-
applied: result.stdout.includes("patching file") ? 1 : 0,
|
|
1777
|
-
rejected: 0,
|
|
1778
|
-
files: extractPatchedFiles(result.stdout),
|
|
1779
|
-
dry_run: dryRun,
|
|
1780
|
-
message: result.stdout || "patch applied"
|
|
1781
|
-
};
|
|
1782
1966
|
}
|
|
1783
1967
|
};
|
|
1968
|
+
function extractDiffTargets(patch) {
|
|
1969
|
+
const out = [];
|
|
1970
|
+
const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
|
|
1971
|
+
for (const m of patch.matchAll(re)) {
|
|
1972
|
+
const target = m[1]?.trim();
|
|
1973
|
+
if (!target || target === "/dev/null") continue;
|
|
1974
|
+
out.push(target);
|
|
1975
|
+
}
|
|
1976
|
+
return out;
|
|
1977
|
+
}
|
|
1978
|
+
function stripPathComponents(p, strip) {
|
|
1979
|
+
const parts = p.replace(/\\/g, "/").split("/");
|
|
1980
|
+
if (parts.length <= strip) return void 0;
|
|
1981
|
+
return parts.slice(strip).join("/");
|
|
1982
|
+
}
|
|
1784
1983
|
function runPatch(args, cwd, signal) {
|
|
1785
|
-
return new Promise((
|
|
1984
|
+
return new Promise((resolve4) => {
|
|
1786
1985
|
let stdout = "";
|
|
1787
1986
|
let stderr = "";
|
|
1788
|
-
const
|
|
1987
|
+
const env = { ...process.env, LANG: "C", LC_ALL: "C" };
|
|
1988
|
+
const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
1789
1989
|
child.stdout?.on("data", (c) => {
|
|
1790
1990
|
stdout += c.toString();
|
|
1791
1991
|
});
|
|
1792
1992
|
child.stderr?.on("data", (c) => {
|
|
1793
1993
|
stderr += c.toString();
|
|
1794
1994
|
});
|
|
1795
|
-
child.on("close", (code) =>
|
|
1796
|
-
child.on("error", (e) =>
|
|
1995
|
+
child.on("close", (code) => resolve4({ exitCode: code ?? 1, stdout, stderr }));
|
|
1996
|
+
child.on("error", (e) => resolve4({ exitCode: 1, stdout: "", stderr: e.message }));
|
|
1797
1997
|
});
|
|
1798
1998
|
}
|
|
1799
1999
|
function extractPatchedFiles(output) {
|
|
@@ -1872,8 +2072,8 @@ var jsonTool = {
|
|
|
1872
2072
|
};
|
|
1873
2073
|
}
|
|
1874
2074
|
};
|
|
1875
|
-
function query(data,
|
|
1876
|
-
const parts =
|
|
2075
|
+
function query(data, path12) {
|
|
2076
|
+
const parts = path12.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
1877
2077
|
let current = data;
|
|
1878
2078
|
for (const part of parts) {
|
|
1879
2079
|
if (current === null || current === void 0) return void 0;
|
|
@@ -1979,18 +2179,18 @@ function findGitDir2(cwd) {
|
|
|
1979
2179
|
let dir = cwd;
|
|
1980
2180
|
for (let i = 0; i < 20; i++) {
|
|
1981
2181
|
try {
|
|
1982
|
-
const stat9 =
|
|
2182
|
+
const stat9 = statSync(path.join(dir, ".git"));
|
|
1983
2183
|
if (stat9.isDirectory()) return dir;
|
|
1984
2184
|
} catch {
|
|
1985
2185
|
}
|
|
1986
|
-
const parent =
|
|
2186
|
+
const parent = path.dirname(dir);
|
|
1987
2187
|
if (parent === dir) break;
|
|
1988
2188
|
dir = parent;
|
|
1989
2189
|
}
|
|
1990
2190
|
return null;
|
|
1991
2191
|
}
|
|
1992
2192
|
function runGit2(args, cwd, signal) {
|
|
1993
|
-
return new Promise((
|
|
2193
|
+
return new Promise((resolve4) => {
|
|
1994
2194
|
let stdout = "";
|
|
1995
2195
|
let stderr = "";
|
|
1996
2196
|
const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -2000,8 +2200,8 @@ function runGit2(args, cwd, signal) {
|
|
|
2000
2200
|
child.stderr?.on("data", (c) => {
|
|
2001
2201
|
stderr += c.toString();
|
|
2002
2202
|
});
|
|
2003
|
-
child.on("close", (code) =>
|
|
2004
|
-
child.on("error", (e) =>
|
|
2203
|
+
child.on("close", (code) => resolve4({ stdout, stderr, exitCode: code ?? 0 }));
|
|
2204
|
+
child.on("error", (e) => resolve4({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
2005
2205
|
});
|
|
2006
2206
|
}
|
|
2007
2207
|
async function fileDiff(input, ctx, signal) {
|
|
@@ -2124,10 +2324,15 @@ var treeTool = {
|
|
|
2124
2324
|
if (queue.length > 0) {
|
|
2125
2325
|
yield queue.shift();
|
|
2126
2326
|
} else {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2327
|
+
let pollTimer;
|
|
2328
|
+
const poll = new Promise((r) => {
|
|
2329
|
+
pollTimer = setTimeout(r, 50);
|
|
2330
|
+
});
|
|
2331
|
+
try {
|
|
2332
|
+
await Promise.race([walkPromise, poll]).catch(() => void 0);
|
|
2333
|
+
} finally {
|
|
2334
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
2335
|
+
}
|
|
2131
2336
|
}
|
|
2132
2337
|
}
|
|
2133
2338
|
await walkPromise;
|
|
@@ -2182,6 +2387,83 @@ async function walkDir(dir, depth, opts) {
|
|
|
2182
2387
|
}
|
|
2183
2388
|
}
|
|
2184
2389
|
}
|
|
2390
|
+
async function* spawnStream(opts) {
|
|
2391
|
+
const max = opts.maxBytes ?? 2e5;
|
|
2392
|
+
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
2393
|
+
let stdout = "";
|
|
2394
|
+
let stderr = "";
|
|
2395
|
+
let pending = "";
|
|
2396
|
+
let error;
|
|
2397
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
2398
|
+
cwd: opts.cwd,
|
|
2399
|
+
signal: opts.signal,
|
|
2400
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2401
|
+
});
|
|
2402
|
+
const queue = [];
|
|
2403
|
+
let waiter;
|
|
2404
|
+
const wake = () => {
|
|
2405
|
+
if (waiter) {
|
|
2406
|
+
const w = waiter;
|
|
2407
|
+
waiter = void 0;
|
|
2408
|
+
w();
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
child.stdout?.on("data", (c) => {
|
|
2412
|
+
const s = c.toString();
|
|
2413
|
+
if (stdout.length < max) stdout += s;
|
|
2414
|
+
queue.push({ kind: "out", data: s });
|
|
2415
|
+
wake();
|
|
2416
|
+
});
|
|
2417
|
+
child.stderr?.on("data", (c) => {
|
|
2418
|
+
const s = c.toString();
|
|
2419
|
+
if (stderr.length < max) stderr += s;
|
|
2420
|
+
queue.push({ kind: "err", data: s });
|
|
2421
|
+
wake();
|
|
2422
|
+
});
|
|
2423
|
+
child.on("error", (e) => {
|
|
2424
|
+
error = e.message;
|
|
2425
|
+
queue.push({ kind: "error", data: e.message });
|
|
2426
|
+
wake();
|
|
2427
|
+
});
|
|
2428
|
+
child.on("close", (code) => {
|
|
2429
|
+
queue.push({ kind: "close", data: "", code: code ?? 0 });
|
|
2430
|
+
wake();
|
|
2431
|
+
});
|
|
2432
|
+
let exitCode = 0;
|
|
2433
|
+
let spawnFailed = false;
|
|
2434
|
+
for (; ; ) {
|
|
2435
|
+
while (queue.length === 0) {
|
|
2436
|
+
await new Promise((resolve4) => {
|
|
2437
|
+
waiter = resolve4;
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
const chunk = queue.shift();
|
|
2441
|
+
if (chunk.kind === "close") {
|
|
2442
|
+
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
if (chunk.kind === "error") {
|
|
2446
|
+
spawnFailed = true;
|
|
2447
|
+
exitCode = 1;
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
pending += chunk.data;
|
|
2451
|
+
if (pending.length >= flushAt) {
|
|
2452
|
+
yield { type: "partial_output", text: pending };
|
|
2453
|
+
pending = "";
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
if (pending.length > 0) {
|
|
2457
|
+
yield { type: "partial_output", text: pending };
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
stdout,
|
|
2461
|
+
stderr,
|
|
2462
|
+
exitCode,
|
|
2463
|
+
truncated: stdout.length >= max || stderr.length >= max,
|
|
2464
|
+
error
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2185
2467
|
|
|
2186
2468
|
// src/lint.ts
|
|
2187
2469
|
var lintTool = {
|
|
@@ -2836,7 +3118,7 @@ async function detectManager2(cwd) {
|
|
|
2836
3118
|
return "npm";
|
|
2837
3119
|
}
|
|
2838
3120
|
function runOutdated(manager, args, cwd, signal) {
|
|
2839
|
-
return new Promise((
|
|
3121
|
+
return new Promise((resolve4) => {
|
|
2840
3122
|
let stdout = "";
|
|
2841
3123
|
let stderr = "";
|
|
2842
3124
|
const MAX = 1e5;
|
|
@@ -2849,9 +3131,9 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
2849
3131
|
});
|
|
2850
3132
|
child.on("close", (code) => {
|
|
2851
3133
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
2852
|
-
|
|
3134
|
+
resolve4(result);
|
|
2853
3135
|
});
|
|
2854
|
-
child.on("error", (e) =>
|
|
3136
|
+
child.on("error", (e) => resolve4({
|
|
2855
3137
|
exit_code: 1,
|
|
2856
3138
|
packages: [],
|
|
2857
3139
|
total: 0,
|
|
@@ -2937,7 +3219,14 @@ var logsTool = {
|
|
|
2937
3219
|
async execute(input, ctx, opts) {
|
|
2938
3220
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
2939
3221
|
const lines = input.lines ?? 100;
|
|
2940
|
-
|
|
3222
|
+
let filterRe = null;
|
|
3223
|
+
if (input.filter) {
|
|
3224
|
+
const compiled = compileUserRegex(input.filter, "i");
|
|
3225
|
+
if (!compiled.ok) {
|
|
3226
|
+
throw new Error(`logs: ${compiled.reason}`);
|
|
3227
|
+
}
|
|
3228
|
+
filterRe = compiled.regex;
|
|
3229
|
+
}
|
|
2941
3230
|
if (input.service) {
|
|
2942
3231
|
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
2943
3232
|
}
|
|
@@ -2957,7 +3246,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2957
3246
|
const args = ["logs"];
|
|
2958
3247
|
if (lines > 0) args.push("--tail", String(lines));
|
|
2959
3248
|
args.push("--timestamps", service);
|
|
2960
|
-
return new Promise((
|
|
3249
|
+
return new Promise((resolve4) => {
|
|
2961
3250
|
let stdout = "";
|
|
2962
3251
|
let stderr = "";
|
|
2963
3252
|
const MAX = 2e5;
|
|
@@ -2971,7 +3260,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2971
3260
|
child.on("close", (code) => {
|
|
2972
3261
|
const output = stdout + stderr;
|
|
2973
3262
|
const entries = parseLogLines(output, filterRe);
|
|
2974
|
-
|
|
3263
|
+
resolve4({
|
|
2975
3264
|
source: `docker:${service}`,
|
|
2976
3265
|
entries,
|
|
2977
3266
|
total: entries.length,
|
|
@@ -2979,7 +3268,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2979
3268
|
stream_mode: false
|
|
2980
3269
|
});
|
|
2981
3270
|
});
|
|
2982
|
-
child.on("error", (e) =>
|
|
3271
|
+
child.on("error", (e) => resolve4({
|
|
2983
3272
|
source: `docker:${service}`,
|
|
2984
3273
|
entries: [],
|
|
2985
3274
|
total: 0,
|
|
@@ -2988,29 +3277,41 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
2988
3277
|
}));
|
|
2989
3278
|
});
|
|
2990
3279
|
}
|
|
2991
|
-
|
|
3280
|
+
var MAX_TAIL_LINES = 1e5;
|
|
3281
|
+
async function fileLogs(path12, lines, filterRe, stream) {
|
|
2992
3282
|
const { createInterface } = await import('readline');
|
|
2993
3283
|
const { createReadStream } = await import('fs');
|
|
2994
3284
|
const entries = [];
|
|
2995
|
-
const
|
|
3285
|
+
const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
|
|
3286
|
+
const window = new Array(effLines);
|
|
3287
|
+
let writeIdx = 0;
|
|
3288
|
+
let totalLines = 0;
|
|
2996
3289
|
const rl = createInterface({
|
|
2997
|
-
input: createReadStream(
|
|
3290
|
+
input: createReadStream(path12),
|
|
2998
3291
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
2999
3292
|
});
|
|
3000
3293
|
for await (const line of rl) {
|
|
3001
3294
|
if (filterRe && !filterRe.test(line)) continue;
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3295
|
+
window[writeIdx] = line;
|
|
3296
|
+
writeIdx = (writeIdx + 1) % effLines;
|
|
3297
|
+
totalLines++;
|
|
3298
|
+
}
|
|
3299
|
+
const ordered = [];
|
|
3300
|
+
const start = totalLines >= effLines ? writeIdx : 0;
|
|
3301
|
+
const count = Math.min(totalLines, effLines);
|
|
3302
|
+
for (let i = 0; i < count; i++) {
|
|
3303
|
+
const v = window[(start + i) % effLines];
|
|
3304
|
+
if (v !== void 0) ordered.push(v);
|
|
3305
|
+
}
|
|
3306
|
+
for (const line of ordered) {
|
|
3006
3307
|
const parsed = parseLine(line);
|
|
3007
3308
|
if (parsed) entries.push(parsed);
|
|
3008
3309
|
}
|
|
3009
3310
|
return {
|
|
3010
|
-
source:
|
|
3311
|
+
source: path12,
|
|
3011
3312
|
entries,
|
|
3012
3313
|
total: entries.length,
|
|
3013
|
-
truncated:
|
|
3314
|
+
truncated: totalLines > effLines,
|
|
3014
3315
|
stream_mode: stream
|
|
3015
3316
|
};
|
|
3016
3317
|
}
|