@wrongstack/tools 0.8.4 → 0.8.6

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/builtin.js CHANGED
@@ -1,7 +1,7 @@
1
- import { spawn, execSync, spawnSync } from 'node:child_process';
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
3
  import * as path from 'node:path';
4
- import { dirname } from 'node:path';
4
+ import { resolve, sep, dirname } from 'node:path';
5
5
  import * as os from 'node:os';
6
6
  import * as fs11 from 'node:fs/promises';
7
7
  import { stat } from 'node:fs/promises';
@@ -11,6 +11,7 @@ import { statSync, mkdirSync, writeFileSync } from 'node:fs';
11
11
  import * as ts from 'typescript';
12
12
  import * as dns from 'node:dns/promises';
13
13
  import * as net from 'node:net';
14
+ import { Agent } from 'undici';
14
15
 
15
16
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
16
17
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -65,8 +66,8 @@ async function* spawnStream(opts) {
65
66
  let spawnFailed = false;
66
67
  for (; ; ) {
67
68
  while (queue.length === 0) {
68
- await new Promise((resolve6) => {
69
- waiter = resolve6;
69
+ await new Promise((resolve7) => {
70
+ waiter = resolve7;
70
71
  });
71
72
  }
72
73
  const chunk = queue.shift();
@@ -741,10 +742,10 @@ var bashTool = {
741
742
  queue.push(c);
742
743
  }
743
744
  };
744
- const next = () => new Promise((resolve6) => {
745
+ const next = () => new Promise((resolve7) => {
745
746
  const c = queue.shift();
746
- if (c) resolve6(c);
747
- else resolveNext = resolve6;
747
+ if (c) resolve7(c);
748
+ else resolveNext = resolve7;
748
749
  });
749
750
  let lastFlush = Date.now();
750
751
  const flush = () => {
@@ -1716,7 +1717,7 @@ function syncGoParse(filePath, content, lang) {
1716
1717
  mkdirSync(tmpDir, { recursive: true });
1717
1718
  const scriptPath = path.join(tmpDir, "parse.go");
1718
1719
  writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
1719
- const stdout = execSync(`go run "${scriptPath}"`, {
1720
+ const stdout = execFileSync("go", ["run", scriptPath], {
1720
1721
  input: content,
1721
1722
  timeout: 15e3,
1722
1723
  encoding: "utf8",
@@ -1962,7 +1963,7 @@ function syncPyParse(filePath, lang) {
1962
1963
  mkdirSync(tmpDir, { recursive: true });
1963
1964
  const scriptPath = path.join(tmpDir, "parse.py");
1964
1965
  writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
1965
- const stdout = execSync(`python "${scriptPath}" "${filePath}"`, {
1966
+ const stdout = execFileSync("python", [scriptPath, filePath], {
1966
1967
  timeout: 15e3,
1967
1968
  encoding: "utf8",
1968
1969
  windowsHide: true
@@ -2000,11 +2001,19 @@ function parseSymbols4(opts) {
2000
2001
  }
2001
2002
  function checkNativeParser() {
2002
2003
  try {
2003
- execSync("rustc --version", { stdio: "pipe" });
2004
+ execFileSync("rustc", ["--version"], { stdio: "pipe" });
2004
2005
  const toolsDir = path.join(process.cwd(), "tools");
2005
2006
  try {
2006
- execSync(
2007
- "cargo metadata --no-deps --format-version 1 --manifest-path " + path.join(toolsDir, "Cargo.toml"),
2007
+ execFileSync(
2008
+ "cargo",
2009
+ [
2010
+ "metadata",
2011
+ "--no-deps",
2012
+ "--format-version",
2013
+ "1",
2014
+ "--manifest-path",
2015
+ path.join(toolsDir, "Cargo.toml")
2016
+ ],
2008
2017
  { stdio: "pipe" }
2009
2018
  );
2010
2019
  return true;
@@ -2961,7 +2970,7 @@ function findGitDir(cwd) {
2961
2970
  return null;
2962
2971
  }
2963
2972
  function runGit(args, cwd, signal) {
2964
- return new Promise((resolve6) => {
2973
+ return new Promise((resolve7) => {
2965
2974
  let stdout = "";
2966
2975
  let stderr = "";
2967
2976
  const child = spawn("git", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
@@ -2971,8 +2980,8 @@ function runGit(args, cwd, signal) {
2971
2980
  child.stderr?.on("data", (c) => {
2972
2981
  stderr += c.toString();
2973
2982
  });
2974
- child.on("close", (code) => resolve6({ stdout, stderr, exitCode: code ?? 0 }));
2975
- child.on("error", (e) => resolve6({ stdout: "", stderr: e.message, exitCode: 1 }));
2983
+ child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
2984
+ child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
2976
2985
  });
2977
2986
  }
2978
2987
  async function fileDiff(input, ctx, signal) {
@@ -3324,8 +3333,20 @@ var BLOCKED_ARG_PATTERNS = {
3324
3333
  // python -c/--command executes arbitrary code; python -m runs modules
3325
3334
  python: [/-c$/, /^--command$/, /^-m$/, /^--module$/],
3326
3335
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
3327
- // -C <dir> changes working directory, bypassing cwd sandbox
3328
- git: [/^--exec=/, /^--upload-pack=/, /^--receive-pack=/, /^-C$/],
3336
+ // -C <dir> changes working directory, bypassing cwd sandbox;
3337
+ // -c/--config <k>=<v> injects config that runs commands
3338
+ // (e.g. core.sshCommand, core.pager, http.proxy, alias.x=!cmd).
3339
+ git: [
3340
+ /^--exec=/,
3341
+ /^--upload-pack=/,
3342
+ /^--receive-pack=/,
3343
+ /^-C$/,
3344
+ /^-c$/,
3345
+ /^--config$/,
3346
+ /^-c=/,
3347
+ /^--config=/,
3348
+ /^--config-env=/
3349
+ ],
3329
3350
  // node -r/--require preloads arbitrary modules; --eval executes code
3330
3351
  node: [/^-r$/, /^--require$/, /^-e$/, /^--eval$/, /^--prof-process$/],
3331
3352
  // go run could execute arbitrary .go files; -ldflags could inject build-time code
@@ -3448,7 +3469,7 @@ var execTool = {
3448
3469
  }
3449
3470
  };
3450
3471
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3451
- return new Promise((resolve6) => {
3472
+ return new Promise((resolve7) => {
3452
3473
  let stdout = "";
3453
3474
  let stderr = "";
3454
3475
  let killed = false;
@@ -3482,7 +3503,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3482
3503
  const durationMs = Date.now() - startedAt;
3483
3504
  const exitCode = killed ? 124 : code ?? 1;
3484
3505
  registry.afterCall(durationMs, exitCode !== 0);
3485
- resolve6({
3506
+ resolve7({
3486
3507
  command: cmd,
3487
3508
  args,
3488
3509
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -3496,7 +3517,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3496
3517
  clearTimeout(timer);
3497
3518
  if (typeof pid === "number") registry.unregister(pid);
3498
3519
  registry.afterCall(Date.now() - startedAt, true);
3499
- resolve6({
3520
+ resolve7({
3500
3521
  command: cmd,
3501
3522
  args,
3502
3523
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -3511,6 +3532,48 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3511
3532
  var MAX_BYTES = 131072;
3512
3533
  var TIMEOUT_MS2 = 2e4;
3513
3534
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
3535
+ function guardedLookup(hostname, options, callback) {
3536
+ dns.lookup(hostname, { all: true }).then((records) => {
3537
+ const family = options?.family;
3538
+ const byFamily = family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;
3539
+ const list = byFamily.length > 0 ? byFamily : records;
3540
+ if (!ALLOW_PRIVATE) {
3541
+ for (const r of list) {
3542
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
3543
+ if (bad) {
3544
+ callback(
3545
+ Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {
3546
+ code: "EAI_FAIL"
3547
+ })
3548
+ );
3549
+ return;
3550
+ }
3551
+ }
3552
+ }
3553
+ if (options?.all) {
3554
+ callback(
3555
+ null,
3556
+ list.map((r) => ({ address: r.address, family: r.family }))
3557
+ );
3558
+ return;
3559
+ }
3560
+ const first = list[0];
3561
+ if (!first) {
3562
+ callback(
3563
+ Object.assign(new Error(`fetch: no address for ${hostname}`), { code: "ENOTFOUND" })
3564
+ );
3565
+ return;
3566
+ }
3567
+ callback(null, first.address, first.family);
3568
+ }).catch((err) => callback(err));
3569
+ }
3570
+ var pinnedAgent;
3571
+ function getPinnedDispatcher() {
3572
+ if (!pinnedAgent) {
3573
+ pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
3574
+ }
3575
+ return pinnedAgent;
3576
+ }
3514
3577
  async function fetchWithRedirectLimit(url, maxRedirects, signal) {
3515
3578
  const headers = {
3516
3579
  "user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
@@ -3527,11 +3590,13 @@ async function fetchWithRedirectLimit(url, maxRedirects, signal) {
3527
3590
  throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
3528
3591
  }
3529
3592
  await assertNotPrivate(parsed.hostname);
3530
- const res = await fetch(currentUrl, {
3593
+ const init = {
3531
3594
  redirect: "manual",
3532
3595
  signal,
3533
- headers
3534
- });
3596
+ headers,
3597
+ dispatcher: getPinnedDispatcher()
3598
+ };
3599
+ const res = await fetch(currentUrl, init);
3535
3600
  if (res.status < 300 || res.status > 399) {
3536
3601
  return res;
3537
3602
  }
@@ -3973,6 +4038,10 @@ var gitTool = {
3973
4038
  truncated: false
3974
4039
  };
3975
4040
  }
4041
+ if (input.command === "worktree") {
4042
+ const guard = validateWorktreeInput(input, ctx.projectRoot);
4043
+ if (guard) return guard;
4044
+ }
3976
4045
  const gitDir = findGitDir2(ctx.cwd, ctx.projectRoot);
3977
4046
  if (!gitDir) {
3978
4047
  return {
@@ -3987,13 +4056,34 @@ var gitTool = {
3987
4056
  return await runGit2(args, gitDir, opts.signal);
3988
4057
  }
3989
4058
  };
4059
+ function validateWorktreeInput(input, projectRoot) {
4060
+ const reject = (stderr) => ({
4061
+ command: "worktree",
4062
+ stdout: "",
4063
+ stderr,
4064
+ exitCode: 1,
4065
+ truncated: false
4066
+ });
4067
+ if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
4068
+ if (input.worktreePath?.startsWith("-")) {
4069
+ return reject(`unsafe worktree path: ${input.worktreePath}`);
4070
+ }
4071
+ if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
4072
+ const root = resolve(projectRoot);
4073
+ const abs = resolve(root, input.worktreePath);
4074
+ if (abs !== root && !abs.startsWith(root + sep)) {
4075
+ return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
4076
+ }
4077
+ }
4078
+ return null;
4079
+ }
3990
4080
  function findGitDir2(cwd, projectRoot) {
3991
4081
  const root = projectRoot;
3992
4082
  let dir = cwd;
3993
4083
  for (let i = 0; i < 20; i++) {
3994
4084
  try {
3995
4085
  const stat11 = statSync(`${dir}/.git`);
3996
- if (stat11.isDirectory()) return dir;
4086
+ if (stat11.isDirectory() || stat11.isFile()) return dir;
3997
4087
  } catch {
3998
4088
  }
3999
4089
  if (dir === root) break;
@@ -4049,14 +4139,14 @@ function buildArgs(input) {
4049
4139
  switch (input.worktreeAction) {
4050
4140
  case "list":
4051
4141
  return ["worktree", "list"];
4052
- case "add":
4053
- return [
4054
- "worktree",
4055
- "add",
4056
- ...input.newBranch ? ["-b"] : [],
4057
- ...input.branch ? [input.branch] : [],
4058
- input.worktreePath ?? ""
4059
- ].filter(Boolean);
4142
+ case "add": {
4143
+ if (!input.worktreePath) return ["worktree", "list"];
4144
+ const add = ["worktree", "add"];
4145
+ if (input.newBranch && input.branch) add.push("-b", input.branch);
4146
+ add.push(input.worktreePath);
4147
+ if (!input.newBranch && input.branch) add.push(input.branch);
4148
+ return add;
4149
+ }
4060
4150
  case "remove":
4061
4151
  return [
4062
4152
  "worktree",
@@ -4074,7 +4164,7 @@ function buildArgs(input) {
4074
4164
  }
4075
4165
  }
4076
4166
  function runGit2(args, cwd, signal) {
4077
- return new Promise((resolve6) => {
4167
+ return new Promise((resolve7) => {
4078
4168
  let stdout = "";
4079
4169
  let stderr = "";
4080
4170
  const child = spawn("git", args, {
@@ -4094,7 +4184,7 @@ function runGit2(args, cwd, signal) {
4094
4184
  }
4095
4185
  });
4096
4186
  child.on("error", (err) => {
4097
- resolve6({
4187
+ resolve7({
4098
4188
  command: args[0],
4099
4189
  stdout,
4100
4190
  stderr: err.message,
@@ -4103,7 +4193,7 @@ function runGit2(args, cwd, signal) {
4103
4193
  });
4104
4194
  });
4105
4195
  child.on("close", (code) => {
4106
- resolve6({
4196
+ resolve7({
4107
4197
  command: args[0],
4108
4198
  stdout: stdout.slice(0, MAX_OUTPUT3),
4109
4199
  stderr: stderr.slice(0, MAX_OUTPUT3),
@@ -4289,13 +4379,13 @@ var grepTool = {
4289
4379
  }
4290
4380
  };
4291
4381
  async function detectRg(signal) {
4292
- return new Promise((resolve6) => {
4382
+ return new Promise((resolve7) => {
4293
4383
  try {
4294
4384
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal });
4295
- p.on("error", () => resolve6(false));
4296
- p.on("close", (code) => resolve6(code === 0));
4385
+ p.on("error", () => resolve7(false));
4386
+ p.on("close", (code) => resolve7(code === 0));
4297
4387
  } catch {
4298
- resolve6(false);
4388
+ resolve7(false);
4299
4389
  }
4300
4390
  });
4301
4391
  }
@@ -4902,21 +4992,39 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4902
4992
  };
4903
4993
  }
4904
4994
  args.push("--timestamps", service);
4905
- return new Promise((resolve6) => {
4995
+ return new Promise((resolve7) => {
4906
4996
  let stdout = "";
4907
4997
  let stderr = "";
4908
4998
  const MAX = 2e5;
4999
+ let settled = false;
5000
+ const empty = () => ({
5001
+ source: `docker:${service}`,
5002
+ entries: [],
5003
+ total: 0,
5004
+ truncated: false,
5005
+ stream_mode: false
5006
+ });
5007
+ const finish = (result) => {
5008
+ if (settled) return;
5009
+ settled = true;
5010
+ clearTimeout(timer);
5011
+ resolve7(result);
5012
+ };
4909
5013
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"] });
5014
+ const timer = setTimeout(() => {
5015
+ child.kill("SIGTERM");
5016
+ finish(empty());
5017
+ }, DOCKER_LOGS_TIMEOUT_MS);
4910
5018
  child.stdout?.on("data", (c) => {
4911
5019
  if (stdout.length < MAX) stdout += c.toString();
4912
5020
  });
4913
5021
  child.stderr?.on("data", (c) => {
4914
5022
  if (stderr.length < MAX) stderr += c.toString();
4915
5023
  });
4916
- child.on("close", (code) => {
5024
+ child.on("close", () => {
4917
5025
  const output = stdout + stderr;
4918
5026
  const entries = parseLogLines(output, filterRe);
4919
- resolve6({
5027
+ finish({
4920
5028
  source: `docker:${service}`,
4921
5029
  entries,
4922
5030
  total: entries.length,
@@ -4924,17 +5032,10 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4924
5032
  stream_mode: false
4925
5033
  });
4926
5034
  });
4927
- child.on("error", (e) => {
4928
- resolve6({
4929
- source: `docker:${service}`,
4930
- entries: [],
4931
- total: 0,
4932
- truncated: false,
4933
- stream_mode: false
4934
- });
4935
- });
5035
+ child.on("error", () => finish(empty()));
4936
5036
  });
4937
5037
  }
5038
+ var DOCKER_LOGS_TIMEOUT_MS = 3e3;
4938
5039
  var MAX_TAIL_LINES = 1e5;
4939
5040
  async function fileLogs(path18, lines, filterRe, stream) {
4940
5041
  const { createInterface } = await import('node:readline');
@@ -5058,7 +5159,7 @@ async function detectManager2(cwd) {
5058
5159
  return "npm";
5059
5160
  }
5060
5161
  function runOutdated(manager, args, cwd, signal) {
5061
- return new Promise((resolve6) => {
5162
+ return new Promise((resolve7) => {
5062
5163
  let stdout = "";
5063
5164
  let stderr = "";
5064
5165
  const MAX = 1e5;
@@ -5071,10 +5172,10 @@ function runOutdated(manager, args, cwd, signal) {
5071
5172
  });
5072
5173
  child.on("close", (code) => {
5073
5174
  const result = parseOutdatedOutput(stdout, code ?? 0);
5074
- resolve6(result);
5175
+ resolve7(result);
5075
5176
  });
5076
5177
  child.on("error", (e) => {
5077
- resolve6({
5178
+ resolve7({
5078
5179
  exit_code: 1,
5079
5180
  packages: [],
5080
5181
  total: 0,
@@ -5204,7 +5305,7 @@ function stripPathComponents(p, strip) {
5204
5305
  return parts.slice(strip).join("/");
5205
5306
  }
5206
5307
  function runPatch(args, cwd, signal) {
5207
- return new Promise((resolve6) => {
5308
+ return new Promise((resolve7) => {
5208
5309
  let stdout = "";
5209
5310
  let stderr = "";
5210
5311
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -5215,8 +5316,8 @@ function runPatch(args, cwd, signal) {
5215
5316
  child.stderr?.on("data", (c) => {
5216
5317
  stderr += c.toString();
5217
5318
  });
5218
- child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
5219
- child.on("error", (e) => resolve6({ exitCode: 1, stdout: "", stderr: e.message }));
5319
+ child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
5320
+ child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
5220
5321
  });
5221
5322
  }
5222
5323
  function extractPatchedFiles(output) {
@@ -5465,6 +5566,7 @@ var replaceTool = {
5465
5566
  const dryRun = input.dry_run ?? false;
5466
5567
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
5467
5568
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
5569
+ const realRoot = await fs11.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
5468
5570
  const results = [];
5469
5571
  let totalReplacements = 0;
5470
5572
  for (const absPath of fileList) {
@@ -5480,7 +5582,7 @@ var replaceTool = {
5480
5582
  } catch {
5481
5583
  continue;
5482
5584
  }
5483
- const rel = path.relative(ctx.projectRoot, realPath);
5585
+ const rel = path.relative(realRoot, realPath);
5484
5586
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
5485
5587
  const stat11 = await fs11.stat(realPath).catch(() => null);
5486
5588
  if (!stat11 || !stat11.isFile()) continue;
@@ -5558,13 +5660,13 @@ async function globFiles(pattern, base, extraGlob) {
5558
5660
  return await globNative(pattern, base, extraGlob);
5559
5661
  }
5560
5662
  function checkRg() {
5561
- return new Promise((resolve6) => {
5663
+ return new Promise((resolve7) => {
5562
5664
  try {
5563
5665
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore" });
5564
- p.on("error", () => resolve6(false));
5565
- p.on("close", (code) => resolve6(code === 0));
5666
+ p.on("error", () => resolve7(false));
5667
+ p.on("close", (code) => resolve7(code === 0));
5566
5668
  } catch {
5567
- resolve6(false);
5669
+ resolve7(false);
5568
5670
  }
5569
5671
  });
5570
5672
  }
@@ -5576,10 +5678,10 @@ function spawnRgFind(pattern, base) {
5576
5678
  buf += chunk.toString();
5577
5679
  });
5578
5680
  return {
5579
- promise: new Promise((resolve6, reject) => {
5681
+ promise: new Promise((resolve7, reject) => {
5580
5682
  child.on("error", reject);
5581
5683
  child.on("close", () => {
5582
- resolve6(buf.split("\n").filter(Boolean));
5684
+ resolve7(buf.split("\n").filter(Boolean));
5583
5685
  });
5584
5686
  })
5585
5687
  };