@wrongstack/tools 0.9.7 → 0.9.20

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.
Files changed (53) hide show
  1. package/dist/audit.js.map +1 -1
  2. package/dist/bash.js +0 -3
  3. package/dist/bash.js.map +1 -1
  4. package/dist/builtin.js +67 -30
  5. package/dist/builtin.js.map +1 -1
  6. package/dist/circuit-breaker.d.ts +0 -2
  7. package/dist/circuit-breaker.js +0 -3
  8. package/dist/circuit-breaker.js.map +1 -1
  9. package/dist/codebase-index/index.d.ts +2 -2
  10. package/dist/codebase-index/index.js +2 -2
  11. package/dist/codebase-index/index.js.map +1 -1
  12. package/dist/{codebase-stats-tool-BLhQmPNc.d.ts → codebase-stats-tool-C8ApERbn.d.ts} +0 -11
  13. package/dist/diff.js +15 -5
  14. package/dist/diff.js.map +1 -1
  15. package/dist/document.js +1 -2
  16. package/dist/document.js.map +1 -1
  17. package/dist/edit.js +31 -1
  18. package/dist/edit.js.map +1 -1
  19. package/dist/exec.js +0 -3
  20. package/dist/exec.js.map +1 -1
  21. package/dist/fetch.d.ts +10 -1
  22. package/dist/fetch.js +6 -7
  23. package/dist/fetch.js.map +1 -1
  24. package/dist/format.js.map +1 -1
  25. package/dist/glob.js.map +1 -1
  26. package/dist/grep.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +65 -28
  29. package/dist/index.js.map +1 -1
  30. package/dist/install.js.map +1 -1
  31. package/dist/json.js.map +1 -1
  32. package/dist/lint.js.map +1 -1
  33. package/dist/logs.js +4 -0
  34. package/dist/logs.js.map +1 -1
  35. package/dist/outdated.js.map +1 -1
  36. package/dist/pack.js +67 -30
  37. package/dist/pack.js.map +1 -1
  38. package/dist/patch.js.map +1 -1
  39. package/dist/process-registry.js +0 -3
  40. package/dist/process-registry.js.map +1 -1
  41. package/dist/read.js +37 -5
  42. package/dist/read.js.map +1 -1
  43. package/dist/replace.js +0 -1
  44. package/dist/replace.js.map +1 -1
  45. package/dist/scaffold.js.map +1 -1
  46. package/dist/search.js +177 -5
  47. package/dist/search.js.map +1 -1
  48. package/dist/test.js.map +1 -1
  49. package/dist/tree.js.map +1 -1
  50. package/dist/typecheck.js.map +1 -1
  51. package/dist/write.js +31 -1
  52. package/dist/write.js.map +1 -1
  53. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -34,6 +34,36 @@ function ensureInsideRoot(absPath, ctx) {
34
34
  function safeResolve(input, ctx) {
35
35
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
36
36
  }
37
+ async function assertRealInsideRoot(absPath, ctx) {
38
+ const realRoot = await fs4.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
39
+ let probe = absPath;
40
+ for (; ; ) {
41
+ let real;
42
+ try {
43
+ real = await fs4.realpath(probe);
44
+ } catch (err) {
45
+ if (err.code === "ENOENT") {
46
+ const parent = path.dirname(probe);
47
+ if (parent === probe) return;
48
+ probe = parent;
49
+ continue;
50
+ }
51
+ throw err;
52
+ }
53
+ const rel = path.relative(realRoot, real);
54
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
55
+ throw new Error(
56
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
57
+ );
58
+ }
59
+ return;
60
+ }
61
+ }
62
+ async function safeResolveReal(input, ctx) {
63
+ const abs = safeResolve(input, ctx);
64
+ await assertRealInsideRoot(abs, ctx);
65
+ return abs;
66
+ }
37
67
  function truncateMiddle(s, max) {
38
68
  if (Buffer.byteLength(s, "utf8") <= max) return s;
39
69
  const half = Math.floor(max / 2);
@@ -71,14 +101,16 @@ var readTool = {
71
101
  },
72
102
  async execute(input, ctx) {
73
103
  if (!input?.path) throw new Error("read: path is required");
74
- const absPath = safeResolve(input.path, ctx);
104
+ const absPath = await safeResolveReal(input.path, ctx);
75
105
  let stat11;
76
106
  try {
77
107
  stat11 = await fs4.stat(absPath);
78
108
  } catch (err) {
79
109
  const code = err.code;
80
110
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
81
- throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
111
+ throw new Error(
112
+ `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
113
+ );
82
114
  }
83
115
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
84
116
  if (stat11.size > MAX_BYTES) {
@@ -129,7 +161,7 @@ var writeTool = {
129
161
  async execute(input, ctx) {
130
162
  if (!input?.path) throw new Error("write: path is required");
131
163
  if (input.content === void 0) throw new Error("write: content is required");
132
- const absPath = safeResolve(input.path, ctx);
164
+ const absPath = await safeResolveReal(input.path, ctx);
133
165
  let existed = false;
134
166
  let prev = "";
135
167
  try {
@@ -190,7 +222,7 @@ var editTool = {
190
222
  if (input.old_string === void 0) throw new Error("edit: old_string is required");
191
223
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
192
224
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
193
- const absPath = safeResolve(input.path, ctx);
225
+ const absPath = await safeResolveReal(input.path, ctx);
194
226
  const stat11 = await fs4.stat(absPath).catch((err) => {
195
227
  if (err.code === "ENOENT") {
196
228
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
@@ -454,7 +486,6 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
454
486
  return resolved;
455
487
  }
456
488
  async function globFiles(pattern, base, extraGlob) {
457
- const { spawn: spawn11 } = await import('node:child_process');
458
489
  const rgAvailable = await checkRg();
459
490
  if (rgAvailable) {
460
491
  try {
@@ -886,8 +917,6 @@ var CircuitBreaker = class {
886
917
  lastSlowAt = null;
887
918
  /** Timestamp when the breaker was opened (for cooldown calculation). */
888
919
  openedAt = null;
889
- /** Timestamp when the last call ran (for half-open gate). */
890
- lastCallAt = null;
891
920
  constructor(config = {}) {
892
921
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
893
922
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -945,7 +974,6 @@ var CircuitBreaker = class {
945
974
  */
946
975
  afterCall(durationMs, failed) {
947
976
  const now = Date.now();
948
- this.lastCallAt = now;
949
977
  if (this.state === "half-open") {
950
978
  if (failed) {
951
979
  this._trip();
@@ -1760,11 +1788,10 @@ function getPinnedDispatcher() {
1760
1788
  }
1761
1789
  return pinnedAgent;
1762
1790
  }
1763
- async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1764
- const headers = {
1765
- "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
1766
- accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
1767
- };
1791
+ async function guardedFetch(url, maxRedirects, signal, headers = {
1792
+ "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
1793
+ accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
1794
+ }) {
1768
1795
  let redirectCount = 0;
1769
1796
  let currentUrl = url;
1770
1797
  for (; ; ) {
@@ -1842,7 +1869,7 @@ var fetchTool = {
1842
1869
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS2);
1843
1870
  const combined = combineSignals(opts.signal, ctrl.signal);
1844
1871
  try {
1845
- const res = await fetchWithRedirectLimit(input.url, 5, combined);
1872
+ const res = await guardedFetch(input.url, 5, combined);
1846
1873
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
1847
1874
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
1848
1875
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -2234,11 +2261,8 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
2234
2261
  const timer = setTimeout(() => controller.abort(), timeoutMs);
2235
2262
  const fetchSignal = anySignal(signal, controller.signal);
2236
2263
  try {
2237
- const res = await fetch(url, {
2238
- headers: {
2239
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
2240
- },
2241
- signal: fetchSignal
2264
+ const res = await guardedFetch(url, 5, fetchSignal, {
2265
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
2242
2266
  });
2243
2267
  clearTimeout(timer);
2244
2268
  return res;
@@ -2957,6 +2981,12 @@ var diffTool = {
2957
2981
  }
2958
2982
  };
2959
2983
  async function gitDiff(input, ctx, signal) {
2984
+ if (input.a?.startsWith("-")) {
2985
+ throw new Error(`diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`);
2986
+ }
2987
+ if (input.b?.startsWith("-")) {
2988
+ throw new Error(`diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`);
2989
+ }
2960
2990
  const gitDir = findGitDir2(ctx.cwd);
2961
2991
  if (!gitDir) {
2962
2992
  return { diff: "", files: [], truncated: false, mode: "unified" };
@@ -2995,7 +3025,12 @@ function runGit2(args, cwd, signal) {
2995
3025
  return new Promise((resolve7) => {
2996
3026
  let stdout = "";
2997
3027
  let stderr = "";
2998
- const child = spawn("git", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
3028
+ const child = spawn("git", args, {
3029
+ cwd,
3030
+ signal,
3031
+ env: buildChildEnv(),
3032
+ stdio: ["ignore", "pipe", "pipe"]
3033
+ });
2999
3034
  child.stdout?.on("data", (c) => {
3000
3035
  stdout += c.toString();
3001
3036
  });
@@ -3006,8 +3041,7 @@ function runGit2(args, cwd, signal) {
3006
3041
  child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
3007
3042
  });
3008
3043
  }
3009
- async function fileDiff(input, ctx, signal) {
3010
- input.path ? safeResolve(input.path, ctx) : ctx.cwd;
3044
+ async function fileDiff(input, ctx, _signal) {
3011
3045
  input.context ?? 3;
3012
3046
  const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
3013
3047
  if (files.length === 0) {
@@ -3036,8 +3070,8 @@ ${formatUnified(lines)}`);
3036
3070
  mode: input.mode ?? "unified"
3037
3071
  };
3038
3072
  }
3039
- function formatUnified(lines, context) {
3040
- return lines.map((line, i) => ` ${line}`).join("\n");
3073
+ function formatUnified(lines, _context) {
3074
+ return lines.map((line, _i) => ` ${line}`).join("\n");
3041
3075
  }
3042
3076
  var DEFAULT_IGNORE4 = [
3043
3077
  "node_modules",
@@ -4143,6 +4177,10 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4143
4177
  child.stderr?.on("data", (c) => {
4144
4178
  if (stderr.length < MAX) stderr += c.toString();
4145
4179
  });
4180
+ child.stdout?.on("error", () => {
4181
+ });
4182
+ child.stderr?.on("error", () => {
4183
+ });
4146
4184
  child.on("close", () => {
4147
4185
  const output = stdout + stderr;
4148
4186
  const entries = parseLogLines(output, filterRe);
@@ -4319,9 +4357,8 @@ async function resolveFiles2(filesInput, cwd) {
4319
4357
  }
4320
4358
  return resolved;
4321
4359
  }
4322
- function processFile(content, absPath, style, overwrite, target) {
4360
+ function processFile(content, absPath, _style, _overwrite, target) {
4323
4361
  const results = [];
4324
- content.split("\n");
4325
4362
  const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
4326
4363
  const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
4327
4364
  const classRegex = /class\s+(\w+)/g;
@@ -6236,7 +6273,7 @@ function regexParse(opts) {
6236
6273
  }
6237
6274
  return lo + 1;
6238
6275
  }
6239
- function extractDeclaration(lineIdx, match) {
6276
+ function extractDeclaration(lineIdx, _match) {
6240
6277
  const line = lines[lineIdx] ?? "";
6241
6278
  return line.trim().slice(0, 500);
6242
6279
  }
@@ -6701,7 +6738,7 @@ async function parseFile(file, content, lang) {
6701
6738
  return { file, lang, symbols: [], mtimeMs: Date.now() };
6702
6739
  }
6703
6740
  }
6704
- async function runIndexer(ctx, opts) {
6741
+ async function runIndexer(_ctx, opts) {
6705
6742
  const { projectRoot, force = false, langs, ignore = [] } = opts;
6706
6743
  const store = new IndexStore(projectRoot);
6707
6744
  const startMs = Date.now();