@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/pack.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import { buildChildEnv, stripAnsi, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
3
+ import * as fs11 from 'node:fs/promises';
4
+ import { stat } from 'node:fs/promises';
3
5
  import * as path from 'node:path';
4
6
  import { resolve, sep, dirname } from 'node:path';
5
7
  import * as os from 'node:os';
6
- import * as fs11 from 'node:fs/promises';
7
- import { stat } from 'node:fs/promises';
8
8
  import { createRequire } from 'node:module';
9
9
  import * as fs from 'node:fs';
10
10
  import { statSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -112,6 +112,36 @@ function ensureInsideRoot(absPath, ctx) {
112
112
  function safeResolve(input, ctx) {
113
113
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
114
114
  }
115
+ async function assertRealInsideRoot(absPath, ctx) {
116
+ const realRoot = await fs11.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
117
+ let probe = absPath;
118
+ for (; ; ) {
119
+ let real;
120
+ try {
121
+ real = await fs11.realpath(probe);
122
+ } catch (err) {
123
+ if (err.code === "ENOENT") {
124
+ const parent = path.dirname(probe);
125
+ if (parent === probe) return;
126
+ probe = parent;
127
+ continue;
128
+ }
129
+ throw err;
130
+ }
131
+ const rel = path.relative(realRoot, real);
132
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
133
+ throw new Error(
134
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
135
+ );
136
+ }
137
+ return;
138
+ }
139
+ }
140
+ async function safeResolveReal(input, ctx) {
141
+ const abs = safeResolve(input, ctx);
142
+ await assertRealInsideRoot(abs, ctx);
143
+ return abs;
144
+ }
115
145
  function truncateMiddle(s, max) {
116
146
  if (Buffer.byteLength(s, "utf8") <= max) return s;
117
147
  const half = Math.floor(max / 2);
@@ -258,8 +288,6 @@ var CircuitBreaker = class {
258
288
  lastSlowAt = null;
259
289
  /** Timestamp when the breaker was opened (for cooldown calculation). */
260
290
  openedAt = null;
261
- /** Timestamp when the last call ran (for half-open gate). */
262
- lastCallAt = null;
263
291
  constructor(config = {}) {
264
292
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
265
293
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -317,7 +345,6 @@ var CircuitBreaker = class {
317
345
  */
318
346
  afterCall(durationMs, failed) {
319
347
  const now = Date.now();
320
- this.lastCallAt = now;
321
348
  if (this.state === "half-open") {
322
349
  if (failed) {
323
350
  this._trip();
@@ -2112,7 +2139,7 @@ function regexParse(opts) {
2112
2139
  }
2113
2140
  return lo + 1;
2114
2141
  }
2115
- function extractDeclaration(lineIdx, match) {
2142
+ function extractDeclaration(lineIdx, _match) {
2116
2143
  const line = lines[lineIdx] ?? "";
2117
2144
  return line.trim().slice(0, 500);
2118
2145
  }
@@ -2577,7 +2604,7 @@ async function parseFile(file, content, lang) {
2577
2604
  return { file, lang, symbols: [], mtimeMs: Date.now() };
2578
2605
  }
2579
2606
  }
2580
- async function runIndexer(ctx, opts) {
2607
+ async function runIndexer(_ctx, opts) {
2581
2608
  const { projectRoot, force = false, langs, ignore = [] } = opts;
2582
2609
  const store = new IndexStore(projectRoot);
2583
2610
  const startMs = Date.now();
@@ -2964,6 +2991,12 @@ var diffTool = {
2964
2991
  }
2965
2992
  };
2966
2993
  async function gitDiff(input, ctx, signal) {
2994
+ if (input.a?.startsWith("-")) {
2995
+ throw new Error(`diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`);
2996
+ }
2997
+ if (input.b?.startsWith("-")) {
2998
+ throw new Error(`diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`);
2999
+ }
2967
3000
  const gitDir = findGitDir(ctx.cwd);
2968
3001
  if (!gitDir) {
2969
3002
  return { diff: "", files: [], truncated: false, mode: "unified" };
@@ -3002,7 +3035,12 @@ function runGit(args, cwd, signal) {
3002
3035
  return new Promise((resolve7) => {
3003
3036
  let stdout = "";
3004
3037
  let stderr = "";
3005
- const child = spawn("git", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
3038
+ const child = spawn("git", args, {
3039
+ cwd,
3040
+ signal,
3041
+ env: buildChildEnv(),
3042
+ stdio: ["ignore", "pipe", "pipe"]
3043
+ });
3006
3044
  child.stdout?.on("data", (c) => {
3007
3045
  stdout += c.toString();
3008
3046
  });
@@ -3013,8 +3051,7 @@ function runGit(args, cwd, signal) {
3013
3051
  child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
3014
3052
  });
3015
3053
  }
3016
- async function fileDiff(input, ctx, signal) {
3017
- input.path ? safeResolve(input.path, ctx) : ctx.cwd;
3054
+ async function fileDiff(input, ctx, _signal) {
3018
3055
  input.context ?? 3;
3019
3056
  const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()).filter(Boolean) : [];
3020
3057
  if (files.length === 0) {
@@ -3043,8 +3080,8 @@ ${formatUnified(lines)}`);
3043
3080
  mode: input.mode ?? "unified"
3044
3081
  };
3045
3082
  }
3046
- function formatUnified(lines, context) {
3047
- return lines.map((line, i) => ` ${line}`).join("\n");
3083
+ function formatUnified(lines, _context) {
3084
+ return lines.map((line, _i) => ` ${line}`).join("\n");
3048
3085
  }
3049
3086
  var documentTool = {
3050
3087
  name: "document",
@@ -3134,9 +3171,8 @@ async function resolveFiles(filesInput, cwd) {
3134
3171
  }
3135
3172
  return resolved;
3136
3173
  }
3137
- function processFile(content, absPath, style, overwrite, target) {
3174
+ function processFile(content, absPath, _style, _overwrite, target) {
3138
3175
  const results = [];
3139
- content.split("\n");
3140
3176
  const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
3141
3177
  const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
3142
3178
  const classRegex = /class\s+(\w+)/g;
@@ -3218,7 +3254,7 @@ var editTool = {
3218
3254
  if (input.old_string === void 0) throw new Error("edit: old_string is required");
3219
3255
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
3220
3256
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
3221
- const absPath = safeResolve(input.path, ctx);
3257
+ const absPath = await safeResolveReal(input.path, ctx);
3222
3258
  const stat11 = await fs11.stat(absPath).catch((err) => {
3223
3259
  if (err.code === "ENOENT") {
3224
3260
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
@@ -3603,11 +3639,10 @@ function getPinnedDispatcher() {
3603
3639
  }
3604
3640
  return pinnedAgent;
3605
3641
  }
3606
- async function fetchWithRedirectLimit(url, maxRedirects, signal) {
3607
- const headers = {
3608
- "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
3609
- accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
3610
- };
3642
+ async function guardedFetch(url, maxRedirects, signal, headers = {
3643
+ "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
3644
+ accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
3645
+ }) {
3611
3646
  let redirectCount = 0;
3612
3647
  let currentUrl = url;
3613
3648
  for (; ; ) {
@@ -3685,7 +3720,7 @@ var fetchTool = {
3685
3720
  const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS2);
3686
3721
  const combined = combineSignals(opts.signal, ctrl.signal);
3687
3722
  try {
3688
- const res = await fetchWithRedirectLimit(input.url, 5, combined);
3723
+ const res = await guardedFetch(input.url, 5, combined);
3689
3724
  const ct = res.headers.get("content-type") ?? "application/octet-stream";
3690
3725
  if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
3691
3726
  throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
@@ -5050,6 +5085,10 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5050
5085
  child.stderr?.on("data", (c) => {
5051
5086
  if (stderr.length < MAX) stderr += c.toString();
5052
5087
  });
5088
+ child.stdout?.on("error", () => {
5089
+ });
5090
+ child.stderr?.on("error", () => {
5091
+ });
5053
5092
  child.on("close", () => {
5054
5093
  const output = stdout + stderr;
5055
5094
  const entries = parseLogLines(output, filterRe);
@@ -5515,14 +5554,16 @@ var readTool = {
5515
5554
  },
5516
5555
  async execute(input, ctx) {
5517
5556
  if (!input?.path) throw new Error("read: path is required");
5518
- const absPath = safeResolve(input.path, ctx);
5557
+ const absPath = await safeResolveReal(input.path, ctx);
5519
5558
  let stat11;
5520
5559
  try {
5521
5560
  stat11 = await fs11.stat(absPath);
5522
5561
  } catch (err) {
5523
5562
  const code = err.code;
5524
5563
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
5525
- throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
5564
+ throw new Error(
5565
+ `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
5566
+ );
5526
5567
  }
5527
5568
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
5528
5569
  if (stat11.size > MAX_BYTES2) {
@@ -5677,7 +5718,6 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
5677
5718
  return resolved;
5678
5719
  }
5679
5720
  async function globFiles(pattern, base, extraGlob) {
5680
- const { spawn: spawn11 } = await import('node:child_process');
5681
5721
  const rgAvailable = await checkRg();
5682
5722
  if (rgAvailable) {
5683
5723
  try {
@@ -6119,11 +6159,8 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
6119
6159
  const timer = setTimeout(() => controller.abort(), timeoutMs);
6120
6160
  const fetchSignal = anySignal(signal, controller.signal);
6121
6161
  try {
6122
- const res = await fetch(url, {
6123
- headers: {
6124
- "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"
6125
- },
6126
- signal: fetchSignal
6162
+ const res = await guardedFetch(url, 5, fetchSignal, {
6163
+ "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"
6127
6164
  });
6128
6165
  clearTimeout(timer);
6129
6166
  return res;
@@ -6875,7 +6912,7 @@ var writeTool = {
6875
6912
  async execute(input, ctx) {
6876
6913
  if (!input?.path) throw new Error("write: path is required");
6877
6914
  if (input.content === void 0) throw new Error("write: content is required");
6878
- const absPath = safeResolve(input.path, ctx);
6915
+ const absPath = await safeResolveReal(input.path, ctx);
6879
6916
  let existed = false;
6880
6917
  let prev = "";
6881
6918
  try {