@wrongstack/tools 0.5.7 → 0.6.1

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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs4 from 'fs/promises';
2
+ import { stat } from 'fs/promises';
2
3
  import * as path from 'path';
3
4
  import { dirname } from 'path';
4
5
  import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, buildChildEnv, stripAnsi, loadPlan, emptyPlan, clearPlan, savePlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, formatPlan } from '@wrongstack/core';
@@ -8,12 +9,7 @@ import * as dns from 'dns/promises';
8
9
  import * as net from 'net';
9
10
  import { statSync } from 'fs';
10
11
 
11
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
- }) : x)(function(x) {
14
- if (typeof require !== "undefined") return require.apply(this, arguments);
15
- throw Error('Dynamic require of "' + x + '" is not supported');
16
- });
12
+ // src/read.ts
17
13
  function resolvePath(input, ctx) {
18
14
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
19
15
  }
@@ -67,17 +63,17 @@ var readTool = {
67
63
  async execute(input, ctx) {
68
64
  if (!input?.path) throw new Error("read: path is required");
69
65
  const absPath = safeResolve(input.path, ctx);
70
- let stat9;
66
+ let stat10;
71
67
  try {
72
- stat9 = await fs4.stat(absPath);
68
+ stat10 = await fs4.stat(absPath);
73
69
  } catch (err) {
74
70
  const code = err.code;
75
71
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
76
72
  throw new Error(`read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`);
77
73
  }
78
- if (!stat9.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
79
- if (stat9.size > MAX_BYTES) {
80
- throw new Error(`read: file too large (${stat9.size} bytes, limit ${MAX_BYTES})`);
74
+ if (!stat10.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
75
+ if (stat10.size > MAX_BYTES) {
76
+ throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES})`);
81
77
  }
82
78
  const buf = await fs4.readFile(absPath);
83
79
  if (isBinaryBuffer(buf)) {
@@ -89,14 +85,14 @@ var readTool = {
89
85
  const offset = Math.max(1, input.offset ?? 1);
90
86
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
91
87
  if (limit === 0) {
92
- ctx.recordRead(absPath, stat9.mtimeMs);
88
+ ctx.recordRead(absPath, stat10.mtimeMs);
93
89
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
94
90
  }
95
91
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
96
92
  const truncated = offset - 1 + slice.length < total;
97
93
  const width = String(offset + slice.length - 1).length;
98
94
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
99
- ctx.recordRead(absPath, stat9.mtimeMs);
95
+ ctx.recordRead(absPath, stat10.mtimeMs);
100
96
  return {
101
97
  text: numbered,
102
98
  total_lines: total,
@@ -128,12 +124,12 @@ var writeTool = {
128
124
  let existed = false;
129
125
  let prev = "";
130
126
  try {
131
- const stat10 = await fs4.stat(absPath);
132
- existed = stat10.isFile();
127
+ const stat11 = await fs4.stat(absPath);
128
+ existed = stat11.isFile();
133
129
  if (existed) {
134
130
  if (!ctx.hasRead(absPath)) {
135
131
  prev = await fs4.readFile(absPath, "utf8");
136
- ctx.recordRead(absPath, stat10.mtimeMs);
132
+ ctx.recordRead(absPath, stat11.mtimeMs);
137
133
  } else {
138
134
  prev = await fs4.readFile(absPath, "utf8");
139
135
  }
@@ -146,8 +142,8 @@ var writeTool = {
146
142
  await atomicWrite(absPath, input.content);
147
143
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
148
144
  + (new file, ${input.content.split("\n").length} lines)`;
149
- const stat9 = await fs4.stat(absPath);
150
- ctx.recordRead(absPath, stat9.mtimeMs);
145
+ const stat10 = await fs4.stat(absPath);
146
+ ctx.recordRead(absPath, stat10.mtimeMs);
151
147
  ctx.session.recordFileChange({
152
148
  path: absPath,
153
149
  action: existed ? "modified" : "created",
@@ -186,13 +182,13 @@ var editTool = {
186
182
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
187
183
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
188
184
  const absPath = safeResolve(input.path, ctx);
189
- const stat9 = await fs4.stat(absPath).catch((err) => {
185
+ const stat10 = await fs4.stat(absPath).catch((err) => {
190
186
  if (err.code === "ENOENT") {
191
187
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
192
188
  }
193
189
  throw err;
194
190
  });
195
- if (!stat9.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
191
+ if (!stat10.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
196
192
  if (!ctx.hasRead(absPath)) {
197
193
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
198
194
  }
@@ -281,12 +277,17 @@ function findSimilarity(haystack, needle) {
281
277
  }
282
278
 
283
279
  // src/_regex.ts
284
- var MAX_PATTERN_LEN = 512;
280
+ var MAX_PATTERN_LEN = 256;
285
281
  var DANGEROUS_PATTERNS = [
286
- /(\([^)]*[+*][^)]*\))[+*]/,
287
282
  // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
288
- /(\(\?:[^)]*[+*][^)]*\))[+*]/
289
- // same, with non-capturing group
283
+ /(\([^)]*[+*][^)]*\))[+*]/,
284
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/,
285
+ // Adjacent quantifiers: a++ a*+
286
+ /[+*]{2,}/,
287
+ // Quantifier on alternation with length 2+
288
+ /\([^|)]+\|[^)]+\)[+*][+*]/,
289
+ // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
290
+ /[\(\[][^)\]]*[+*][^)\]]*[\)\]][^)]*\?\??/
290
291
  ];
291
292
  function compileUserRegex(pattern, flags) {
292
293
  if (typeof pattern !== "string") {
@@ -379,8 +380,8 @@ var replaceTool = {
379
380
  }
380
381
  const rel = path.relative(ctx.projectRoot, realPath);
381
382
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
382
- const stat9 = await fs4.stat(realPath).catch(() => null);
383
- if (!stat9 || !stat9.isFile()) continue;
383
+ const stat10 = await fs4.stat(realPath).catch(() => null);
384
+ if (!stat10 || !stat10.isFile()) continue;
384
385
  let content;
385
386
  try {
386
387
  const buf = await fs4.readFile(realPath);
@@ -405,7 +406,7 @@ var replaceTool = {
405
406
  totalReplacements += count;
406
407
  if (!dryRun) {
407
408
  const newContent = toStyle(newContentLf, style);
408
- await atomicWrite(realPath, newContent, { mode: stat9.mode & 511 });
409
+ await atomicWrite(realPath, newContent, { mode: stat10.mode & 511 });
409
410
  }
410
411
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
411
412
  fromFile: absPath,
@@ -435,8 +436,8 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
435
436
  const resolved = [];
436
437
  for (const p of parts) {
437
438
  const absPath = safeResolve(p, ctx);
438
- const stat9 = await fs4.stat(absPath).catch(() => null);
439
- if (stat9?.isFile()) {
439
+ const stat10 = await fs4.stat(absPath).catch(() => null);
440
+ if (stat10?.isFile()) {
440
441
  resolved.push(absPath);
441
442
  }
442
443
  }
@@ -495,8 +496,8 @@ async function globNative(pattern, base, extraGlob) {
495
496
  if (DEFAULT_IGNORE.includes(e.name)) continue;
496
497
  const full = path.join(dir, e.name);
497
498
  try {
498
- const stat9 = await fs4.lstat(full);
499
- if (stat9.isSymbolicLink()) continue;
499
+ const stat10 = await fs4.lstat(full);
500
+ if (stat10.isSymbolicLink()) continue;
500
501
  } catch {
501
502
  continue;
502
503
  }
@@ -812,8 +813,8 @@ async function runNative(input, base, mode, limit, signal) {
812
813
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
813
814
  if (globRe) globRe.lastIndex = 0;
814
815
  try {
815
- const stat9 = await fs4.stat(full);
816
- if (stat9.size > 1e6) continue;
816
+ const stat10 = await fs4.stat(full);
817
+ if (stat10.size > 1e6) continue;
817
818
  const head = await fs4.readFile(full);
818
819
  if (isBinaryBuffer(head)) continue;
819
820
  const text = head.toString("utf8");
@@ -1485,14 +1486,21 @@ var BLOCKED_ARG_PATTERNS = {
1485
1486
  // go run could execute arbitrary .go files; -ldflags could inject build-time code
1486
1487
  go: [/^-ldflags$/],
1487
1488
  // bun --preload is similar to node --require
1488
- bun: [/^--preload$/],
1489
+ bun: [/^--preload$/, /^run$/, /^bunx$/, /^create$/, /^init$/],
1489
1490
  // docker build/run can create containers with host access;
1490
1491
  // only allow read-only commands (ps, images, version)
1491
1492
  docker: [/^build$/, /^run$/, /^exec$/, /^push$/, /^pull$/],
1492
1493
  // find -exec/-ok/-execdir execute arbitrary commands
1493
1494
  find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
1494
1495
  // rm -rf / is catastrophic — block root and home targets
1495
- rm: [/^\/$/, /^\/\*$/, /^~$/]
1496
+ rm: [/^\/$/, /^\/\*$/, /^~$/],
1497
+ // npm run/exec/create/pack/publish can execute arbitrary scripts or publish malware
1498
+ npm: [/^run$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
1499
+ // pnpm run/dlx/exec/create can execute arbitrary scripts
1500
+ pnpm: [/^run$/, /^dlx$/, /^exec$/, /^create$/, /^init$/, /^pack$/, /^publish$/, /^deploy$/],
1501
+ // npx should only be used for --version; any package name is a vector for
1502
+ // malicious package execution (typosquatting, dependency confusion)
1503
+ npx: [/^[^\s]+$/]
1496
1504
  };
1497
1505
  function validateArgs(cmd, args) {
1498
1506
  const blocked = BLOCKED_ARG_PATTERNS[cmd];
@@ -2412,8 +2420,8 @@ function findGitDir(cwd, projectRoot) {
2412
2420
  let dir = cwd;
2413
2421
  for (let i = 0; i < 20; i++) {
2414
2422
  try {
2415
- const stat9 = statSync(`${dir}/.git`);
2416
- if (stat9.isDirectory()) return dir;
2423
+ const stat10 = statSync(`${dir}/.git`);
2424
+ if (stat10.isDirectory()) return dir;
2417
2425
  } catch {
2418
2426
  }
2419
2427
  if (dir === root) break;
@@ -2798,8 +2806,8 @@ function findGitDir2(cwd) {
2798
2806
  let dir = cwd;
2799
2807
  for (let i = 0; i < 20; i++) {
2800
2808
  try {
2801
- const stat9 = statSync(path.join(dir, ".git"));
2802
- if (stat9.isDirectory()) return dir;
2809
+ const stat10 = statSync(path.join(dir, ".git"));
2810
+ if (stat10.isDirectory()) return dir;
2803
2811
  } catch {
2804
2812
  }
2805
2813
  const parent = path.dirname(dir);
@@ -2838,8 +2846,8 @@ async function fileDiff(input, ctx, signal) {
2838
2846
  const results = [];
2839
2847
  for (const file of files) {
2840
2848
  const absPath = safeResolve(file, ctx);
2841
- const stat9 = await fs4.stat(absPath).catch(() => null);
2842
- if (!stat9?.isFile()) continue;
2849
+ const stat10 = await fs4.stat(absPath).catch(() => null);
2850
+ if (!stat10?.isFile()) continue;
2843
2851
  const content = await fs4.readFile(absPath, "utf8");
2844
2852
  const lines = content.split(/\r?\n/);
2845
2853
  results.push(`--- ${file}
@@ -3181,11 +3189,11 @@ var lintTool = {
3181
3189
  }
3182
3190
  };
3183
3191
  async function detectLinter(cwd) {
3184
- const { stat: stat9 } = await import('fs/promises');
3192
+ const { stat: stat10 } = await import('fs/promises');
3185
3193
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
3186
3194
  for (const f of checks) {
3187
3195
  try {
3188
- await stat9(`${cwd}/${f}`);
3196
+ await stat10(`${cwd}/${f}`);
3189
3197
  if (f.includes("biome")) return "biome";
3190
3198
  if (f.includes("eslint")) return "eslint";
3191
3199
  if (f.includes("tslint")) return "tslint";
@@ -3280,13 +3288,13 @@ var formatTool = {
3280
3288
  }
3281
3289
  };
3282
3290
  async function detectFixer(cwd) {
3283
- const { stat: stat9 } = await import('fs/promises');
3291
+ const { stat: stat10 } = await import('fs/promises');
3284
3292
  try {
3285
- await stat9(`${cwd}/biome.json`);
3293
+ await stat10(`${cwd}/biome.json`);
3286
3294
  return "biome";
3287
3295
  } catch {
3288
3296
  try {
3289
- await stat9(`${cwd}/.prettierrc`);
3297
+ await stat10(`${cwd}/.prettierrc`);
3290
3298
  return "prettier";
3291
3299
  } catch {
3292
3300
  return "biome";
@@ -3362,11 +3370,11 @@ var typecheckTool = {
3362
3370
  }
3363
3371
  };
3364
3372
  async function findTsConfig(cwd) {
3365
- const { stat: stat9 } = await import('fs/promises');
3373
+ const { stat: stat10 } = await import('fs/promises');
3366
3374
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
3367
3375
  for (const f of candidates) {
3368
3376
  try {
3369
- const s = await stat9(path.join(cwd, f));
3377
+ const s = await stat10(path.join(cwd, f));
3370
3378
  if (s.isFile()) return path.join(cwd, f);
3371
3379
  } catch {
3372
3380
  }
@@ -3443,11 +3451,11 @@ var testTool = {
3443
3451
  }
3444
3452
  };
3445
3453
  async function detectRunner(cwd) {
3446
- const { stat: stat9 } = await import('fs/promises');
3454
+ const { stat: stat10 } = await import('fs/promises');
3447
3455
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
3448
3456
  for (const f of candidates) {
3449
3457
  try {
3450
- await stat9(path.join(cwd, f));
3458
+ await stat10(path.join(cwd, f));
3451
3459
  if (f.includes("vitest")) return "vitest";
3452
3460
  if (f.includes("jest")) return "jest";
3453
3461
  if (f.includes("mocha")) return "mocha";
@@ -3618,13 +3626,13 @@ var installTool = {
3618
3626
  }
3619
3627
  };
3620
3628
  async function detectPackageManager(cwd) {
3621
- const { stat: stat9 } = await import('fs/promises');
3629
+ const { stat: stat10 } = await import('fs/promises');
3622
3630
  try {
3623
- await stat9(`${cwd}/pnpm-lock.yaml`);
3631
+ await stat10(`${cwd}/pnpm-lock.yaml`);
3624
3632
  return "pnpm";
3625
3633
  } catch {
3626
3634
  try {
3627
- await stat9(`${cwd}/yarn.lock`);
3635
+ await stat10(`${cwd}/yarn.lock`);
3628
3636
  return "yarn";
3629
3637
  } catch {
3630
3638
  return "npm";
@@ -3683,14 +3691,14 @@ var auditTool = {
3683
3691
  }
3684
3692
  };
3685
3693
  async function detectManager(cwd) {
3686
- const { stat: stat9 } = await import('fs/promises');
3694
+ const { stat: stat10 } = await import('fs/promises');
3687
3695
  try {
3688
- await stat9(`${cwd}/pnpm-lock.yaml`);
3696
+ await stat10(`${cwd}/pnpm-lock.yaml`);
3689
3697
  return "pnpm";
3690
3698
  } catch {
3691
3699
  }
3692
3700
  try {
3693
- await stat9(`${cwd}/yarn.lock`);
3701
+ await stat10(`${cwd}/yarn.lock`);
3694
3702
  return "yarn";
3695
3703
  } catch {
3696
3704
  }
@@ -3778,14 +3786,13 @@ var outdatedTool = {
3778
3786
  }
3779
3787
  };
3780
3788
  async function detectManager2(cwd) {
3781
- const { stat: stat9 } = __require("fs/promises");
3782
3789
  try {
3783
- await stat9(`${cwd}/pnpm-lock.yaml`);
3790
+ await stat(`${cwd}/pnpm-lock.yaml`);
3784
3791
  return "pnpm";
3785
3792
  } catch {
3786
3793
  }
3787
3794
  try {
3788
- await stat9(`${cwd}/yarn.lock`);
3795
+ await stat(`${cwd}/yarn.lock`);
3789
3796
  return "yarn";
3790
3797
  } catch {
3791
3798
  }
@@ -4121,8 +4128,8 @@ async function resolveFiles2(filesInput, cwd) {
4121
4128
  for (const f of files) {
4122
4129
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4123
4130
  try {
4124
- const stat9 = await fs4.stat(absPath);
4125
- if (stat9.isFile()) resolved.push(absPath);
4131
+ const stat10 = await fs4.stat(absPath);
4132
+ if (stat10.isFile()) resolved.push(absPath);
4126
4133
  } catch {
4127
4134
  }
4128
4135
  }
@@ -4345,7 +4352,7 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
4345
4352
  const fullPath = target;
4346
4353
  if (!dryRun) {
4347
4354
  await fs4.mkdir(path.dirname(fullPath), { recursive: true });
4348
- await fs4.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
4355
+ await atomicWrite(fullPath, substituteVars(content, name, vars));
4349
4356
  }
4350
4357
  files.push(resolvedPath);
4351
4358
  filesCreated++;