weavatrix 0.3.7 → 0.3.9

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.
@@ -1,7 +1,6 @@
1
1
  import {readFileSync, readdirSync} from 'node:fs'
2
2
  import {join} from 'node:path'
3
- import {spawnSync} from 'node:child_process'
4
- import {childProcessEnv} from '../../child-env.js'
3
+ import {runGit} from '../../git-exec.js'
5
4
  import {filterWeavatrixIgnored} from '../../path-ignore.js'
6
5
 
7
6
  export const readText = (path) => {
@@ -29,10 +28,7 @@ const SKIP_DIRS = new Set([
29
28
 
30
29
  export function listRepoFiles(repoRoot) {
31
30
  try {
32
- const result = spawnSync('git', ['-C', repoRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
33
- encoding: 'utf8', windowsHide: true, timeout: 15_000, maxBuffer: 32 * 1024 * 1024,
34
- env: childProcessEnv(),
35
- })
31
+ const result = runGit(repoRoot, ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], {timeout: 15_000, maxBuffer: 32 * 1024 * 1024})
36
32
  if (result.status === 0) {
37
33
  const files = String(result.stdout || '').split('\0').filter(Boolean).map((file) => file.replace(/\\/g, '/'))
38
34
  return filterWeavatrixIgnored(repoRoot, files)
@@ -0,0 +1,67 @@
1
+ // The single chokepoint for launching git. Every git subprocess in the package routes through here,
2
+ // so there is one place that applies `-C <repoRoot>` containment, a credential-stripped child
3
+ // environment, a hidden Windows window and a bounded timeout — instead of a dozen near-identical
4
+ // spawnSync wrappers scattered across the analysis, graph and MCP layers.
5
+ import { spawnSync, spawn } from "node:child_process";
6
+ import { childProcessEnv } from "./child-env.js";
7
+
8
+ // Synchronous git. Returns the raw spawnSync result ({status, stdout, stderr, error}) so callers keep
9
+ // their existing status/stdout/error handling. encoding defaults to "utf8" (pass "buffer" for binary
10
+ // stdout); maxBuffer and stdio are forwarded only when provided so Node's defaults are preserved.
11
+ export function runGit(repoRoot, args, options = {}) {
12
+ const spawnOptions = {
13
+ encoding: options.encoding ?? "utf8",
14
+ windowsHide: true,
15
+ timeout: options.timeout ?? 8000,
16
+ env: childProcessEnv(options.env),
17
+ };
18
+ if (options.maxBuffer != null) spawnOptions.maxBuffer = options.maxBuffer;
19
+ if (options.stdio != null) spawnOptions.stdio = options.stdio;
20
+ return spawnSync("git", ["-C", repoRoot, ...args], spawnOptions);
21
+ }
22
+
23
+ // Streaming git for the history collector: byte-bounded stdout, a hard timeout that SIGKILLs the child,
24
+ // and a resolved {stdout, stderr, exitCode, truncated}. The caller owns cwd/env so reads can be scoped
25
+ // to a specific worktree.
26
+ export function boundedGitCommand(command, args, options) {
27
+ return new Promise((resolve, reject) => {
28
+ const child = spawn(command, args, {
29
+ cwd: options.cwd, env: options.env, shell: false, windowsHide: true,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ });
32
+ const stdout = [], stderr = [];
33
+ let stdoutBytes = 0, stderrBytes = 0, truncated = false, timedOut = false, settled = false;
34
+ const finish = (callback) => {
35
+ if (settled) return;
36
+ settled = true;
37
+ clearTimeout(timer);
38
+ callback();
39
+ };
40
+ const stop = () => { try { child.kill("SIGKILL"); } catch { /* process already exited */ } };
41
+ const timer = setTimeout(() => { timedOut = true; stop(); }, options.timeoutMs);
42
+ child.stdout?.on("data", (chunk) => {
43
+ if (truncated) return;
44
+ const remaining = options.maxOutputBytes - stdoutBytes;
45
+ if (remaining <= 0) { truncated = true; stop(); return; }
46
+ const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
47
+ stdout.push(kept); stdoutBytes += kept.length;
48
+ if (kept.length !== chunk.length) { truncated = true; stop(); }
49
+ });
50
+ child.stderr?.on("data", (chunk) => {
51
+ const remaining = 64 * 1024 - stderrBytes;
52
+ if (remaining <= 0) return;
53
+ const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining);
54
+ stderr.push(kept); stderrBytes += kept.length;
55
+ });
56
+ child.on("error", (error) => finish(() => reject(error)));
57
+ child.on("close", (exitCode) => finish(() => {
58
+ if (timedOut) return reject(new Error("git history collection timed out"));
59
+ resolve({
60
+ stdout: Buffer.concat(stdout),
61
+ stderr: Buffer.concat(stderr).toString("utf8"),
62
+ exitCode: Number(exitCode ?? 1),
63
+ truncated,
64
+ });
65
+ }));
66
+ });
67
+ }
@@ -2,10 +2,9 @@
2
2
  // authority when something changed; this exact Git/worktree token is safe to cache in-process and in
3
3
  // versioned graph metadata when HEAD + dirty/untracked/control-file content remain unchanged.
4
4
  import {createHash} from 'node:crypto'
5
- import {spawnSync} from 'node:child_process'
6
5
  import {readFileSync, statSync} from 'node:fs'
7
6
  import {createRequire} from 'node:module'
8
- import {childProcessEnv} from '../child-env.js'
7
+ import {runGit} from '../git-exec.js'
9
8
  import {createRepoBoundary} from '../repo-path.js'
10
9
 
11
10
  const REPOSITORY_FRESHNESS_PROBE_V = 1
@@ -34,9 +33,7 @@ const CONTROL_FILES = ['.gitignore', '.weavatrixignore', '.weavatrix.json']
34
33
  const MAX_CONTROL_BYTES = 1_000_000
35
34
 
36
35
  function git(repoRoot, args) {
37
- const result = spawnSync('git', ['-C', repoRoot, ...args], {
38
- encoding: 'buffer', timeout: 8000, windowsHide: true, env: childProcessEnv(), maxBuffer: 16 * 1024 * 1024,
39
- })
36
+ const result = runGit(repoRoot, args, {encoding: 'buffer', maxBuffer: 16 * 1024 * 1024})
40
37
  return result.status === 0 ? Buffer.from(result.stdout || '') : null
41
38
  }
42
39
 
@@ -5,10 +5,9 @@
5
5
  // build works — and Electron main runs Node, so this needs no external runtime.
6
6
  import { readdirSync, statSync, realpathSync } from "node:fs";
7
7
  import { join, extname, dirname } from "node:path";
8
- import { execFileSync } from "node:child_process";
9
8
  import { createRequire } from "node:module";
10
9
  import { isPathInside } from "../repo-path.js";
11
- import { childProcessEnv } from "../child-env.js";
10
+ import { runGit } from "../git-exec.js";
12
11
  import { filterWeavatrixIgnored } from "../path-ignore.js";
13
12
  import {trustedGrammarWasm, trustedRuntimeWasm} from './parser-artifact-boundary.js'
14
13
  import LANG_JS from "./builder/lang-js.js";
@@ -112,17 +111,11 @@ function walkFallback(dir, acc = [], seen = new Set(), depth = 0, rootReal = nul
112
111
  // A repository may be opened without Git (or Git may be unavailable), so failure is deliberately a signal to
113
112
  // use the boundary-safe walker above rather than a build failure.
114
113
  function gitFileUniverse(dir) {
115
- let raw;
116
- try {
117
- raw = execFileSync("git", ["-C", dir, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
118
- encoding: "utf8",
119
- windowsHide: true,
120
- stdio: ["ignore", "pipe", "ignore"],
121
- timeout: 15_000,
122
- maxBuffer: 64 * 1024 * 1024,
123
- env: childProcessEnv(),
124
- });
125
- } catch { return null; }
114
+ const lsFiles = runGit(dir, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
115
+ stdio: ["ignore", "pipe", "ignore"], timeout: 15_000, maxBuffer: 64 * 1024 * 1024,
116
+ });
117
+ if (lsFiles.status !== 0 || lsFiles.error) return null;
118
+ const raw = lsFiles.stdout;
126
119
 
127
120
  // `git -C <dir>` also succeeds when <dir> is merely nested under some parent repository. If that
128
121
  // nested directory is ignored by the parent, `ls-files` is empty even though <dir> can be a valid
@@ -130,17 +123,11 @@ function gitFileUniverse(dir) {
130
123
  // that one ambiguous case, fall back to the boundary-safe walker. Preserve an empty universe for
131
124
  // an actual Git root so ignored files do not leak back into its graph.
132
125
  if (!raw) {
133
- try {
134
- const top = execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
135
- encoding: "utf8",
136
- windowsHide: true,
137
- stdio: ["ignore", "pipe", "ignore"],
138
- timeout: 15_000,
139
- maxBuffer: 1024 * 1024,
140
- env: childProcessEnv(),
141
- }).trim();
142
- if (realpathSync.native(top) !== realpathSync.native(dir)) return null;
143
- } catch { return null; }
126
+ const top = runGit(dir, ["rev-parse", "--show-toplevel"], {
127
+ stdio: ["ignore", "pipe", "ignore"], timeout: 15_000, maxBuffer: 1024 * 1024,
128
+ });
129
+ if (top.status !== 0 || top.error) return null;
130
+ if (realpathSync.native(String(top.stdout).trim()) !== realpathSync.native(dir)) return null;
144
131
  }
145
132
 
146
133
  let rootReal;
@@ -1,10 +1,7 @@
1
- import {spawnSync} from 'node:child_process'
2
- import {childProcessEnv} from '../child-env.js'
1
+ import {runGit} from '../git-exec.js'
3
2
 
4
3
  export function gitLines(repoRoot, args) {
5
- const result = spawnSync('git', ['-C', repoRoot, ...args], {
6
- encoding: 'utf8', timeout: 8000, env: childProcessEnv(), windowsHide: true,
7
- })
4
+ const result = runGit(repoRoot, args)
8
5
  if (result.status !== 0) return null
9
6
  return String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
10
7
  }
@@ -1,7 +1,6 @@
1
1
  import {readFileSync, statSync} from 'node:fs'
2
2
  import {join} from 'node:path'
3
- import {spawnSync} from 'node:child_process'
4
- import {childProcessEnv} from '../../child-env.js'
3
+ import {runGit} from '../../git-exec.js'
5
4
  import {resolveRepoPath} from '../../repo-path.js'
6
5
  import {mergePrecisionOverlay, precisionSemanticInputsMatch, readPrecisionOverlay} from '../../precision/lsp-overlay.js'
7
6
 
@@ -14,19 +13,19 @@ export function graphStaleness(ctx) {
14
13
  try { info.builtAt = statSync(ctx.graphPath).mtime } catch { /* no graph file */ }
15
14
  if (ctx.repoRoot && info.builtAt) {
16
15
  try {
17
- const head = spawnSync('git', ['-C', ctx.repoRoot, 'log', '-1', '--format=%cI'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
16
+ const head = runGit(ctx.repoRoot, ['log', '-1', '--format=%cI'], {timeout: 4000})
18
17
  const iso = (head.stdout || '').trim()
19
18
  if (head.status === 0 && iso) {
20
19
  info.headAt = new Date(iso)
21
20
  if (info.headAt > info.builtAt) {
22
21
  info.stale = true
23
- const count = spawnSync('git', ['-C', ctx.repoRoot, 'rev-list', '--count', `--since=${info.builtAt.toISOString()}`, 'HEAD'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
22
+ const count = runGit(ctx.repoRoot, ['rev-list', '--count', `--since=${info.builtAt.toISOString()}`, 'HEAD'], {timeout: 4000})
24
23
  if (count.status === 0) info.behind = Number(count.stdout.trim()) || null
25
24
  }
26
25
  }
27
26
  } catch { /* git unavailable */ }
28
27
  try {
29
- const status = spawnSync('git', ['-C', ctx.repoRoot, 'status', '--porcelain'], {encoding: 'utf8', timeout: 4000, env: childProcessEnv()})
28
+ const status = runGit(ctx.repoRoot, ['status', '--porcelain'], {timeout: 4000})
30
29
  if (status.status === 0) {
31
30
  let newer = 0
32
31
  for (const line of String(status.stdout || '').split(/\r?\n/).filter(Boolean).slice(0, 200)) {
@@ -1,6 +1,5 @@
1
- import {spawnSync} from 'node:child_process'
2
1
  import {summarizeFindings} from '../../analysis/findings.js'
3
- import {childProcessEnv} from '../../child-env.js'
2
+ import {runGit} from '../../git-exec.js'
4
3
  import {createPathClassifier, hasPathClass} from '../../path-classification.js'
5
4
 
6
5
  const SEVERITY_RANK = {critical: 0, high: 1, medium: 2, low: 3, info: 4}
@@ -179,9 +178,7 @@ export const formatOrdinaryAudit = (audit, args, findings = audit.findings, head
179
178
  }
180
179
 
181
180
  export const gitUntracked = (repoRoot) => {
182
- const result = spawnSync('git', ['-C', repoRoot, 'ls-files', '--others', '--exclude-standard'], {
183
- encoding: 'utf8', timeout: 8000, maxBuffer: 2 * 1024 * 1024, env: childProcessEnv(), windowsHide: true,
184
- })
181
+ const result = runGit(repoRoot, ['ls-files', '--others', '--exclude-standard'], {maxBuffer: 2 * 1024 * 1024})
185
182
  if (result.status !== 0) return {
186
183
  ok: false,
187
184
  files: [],
@@ -1,7 +1,6 @@
1
1
  // Symbol-aware change_impact implementation, isolated from the other hot impact tools so its
2
2
  // classifier/evidence contract can be tested without perturbing get_dependents or graph_diff.
3
- import {spawnSync} from 'node:child_process'
4
- import {childProcessEnv} from '../child-env.js'
3
+ import {runGit} from '../git-exec.js'
5
4
  import {fileOfId} from '../graph/node-id.js'
6
5
  import {classifyChangeImpact} from '../analysis/change-classification.js'
7
6
  import {readCoverageForRepo} from '../analysis/coverage-reports.js'
@@ -12,7 +11,7 @@ import {gitLines} from './git-output.mjs'
12
11
  import {toolResult} from './tool-result.mjs'
13
12
 
14
13
  function gitValue(repoRoot, args) {
15
- const result = spawnSync('git', ['-C', repoRoot, ...args], {encoding: 'utf8', timeout: 8000, env: childProcessEnv()})
14
+ const result = runGit(repoRoot, args)
16
15
  if (result.status !== 0) return null
17
16
  return String(result.stdout || '').trim() || null
18
17
  }
package/src/mcp-rg.mjs CHANGED
@@ -1,9 +1,7 @@
1
1
  import { readdirSync, existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { spawnSync } from 'node:child_process'
4
- import { childProcessEnv } from './child-env.js'
5
3
  import { createRequire } from 'node:module'
6
- import process from 'node:process'
4
+ import { runCommandSync } from './process.js'
7
5
 
8
6
  const rgInInstall = (base) => [
9
7
  join(base, 'resources', 'app', 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg.exe'),
@@ -43,7 +41,7 @@ export function createRgResolver(selfDir) {
43
41
  } catch { /* optional packaged ripgrep path */ }
44
42
  for (const c of editorRgCandidates()) if (existsSync(c)) return (rgPath = c)
45
43
  try {
46
- const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['rg'], {encoding: 'utf8', env: childProcessEnv(), timeout: 3000, windowsHide: true})
44
+ const probe = runCommandSync(process.platform === 'win32' ? 'where' : 'which', ['rg'], {timeout: 3000})
47
45
  const p = probe.status === 0 ? probe.stdout.split(/\r?\n/)[0].trim() : ''
48
46
  if (p && existsSync(p)) return (rgPath = p)
49
47
  } catch { /* optional PATH probe */ }
@@ -1,8 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
- import { spawnSync } from 'node:child_process'
3
2
  import { extname, isAbsolute, join, relative, resolve } from 'node:path'
4
3
  import { resolveRepoPath } from './repo-path.js'
5
- import { childProcessEnv } from './child-env.js'
4
+ import { runCommandSync } from './process.js'
6
5
  import { toolResult } from './mcp/tool-result.mjs'
7
6
 
8
7
  const SEARCH_SKIP = new Set(['.git', 'node_modules', 'dist', 'build', 'out', '.next', 'coverage', 'vendor', '.venv', 'venv', 'env', 'target', '__pycache__', '.idea', '.vscode', '.cache', 'bin', 'obj', 'weavatrix-graphs'])
@@ -11,7 +10,7 @@ const MAX_SEARCH_FILE_BYTES = 1024 * 1024
11
10
  const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024
12
11
  const MAX_SOURCE_CONTEXT_LINES = 1000
13
12
 
14
- function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spawnRg = spawnSync) {
13
+ function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spawnRg = runCommandSync) {
15
14
  const rg = resolveRg()
16
15
  if (!rg) return null
17
16
  // Exclude node_modules explicitly: --hidden + the default gitignore stack is the only thing that
@@ -24,7 +23,7 @@ function rgSearch(repoRoot, resolveRg, query, { isRegex, glob, maxResults }, spa
24
23
  // against repository-relative paths on Windows too. Passing an absolute search root makes rg
25
24
  // compare the glob with a drive-prefixed path and silently return no matches.
26
25
  args.push('--', query, '.')
27
- const res = spawnRg(rg, args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 15000, env: childProcessEnv() })
26
+ const res = spawnRg(rg, args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 15000 })
28
27
  if (res.status !== 0 && res.status !== 1) return null
29
28
  const out = []
30
29
  for (const line of (res.stdout || '').split(/\r?\n/)) {
@@ -93,7 +92,7 @@ function nodeGrep(repoRoot, query, { isRegex, glob, maxResults }) {
93
92
  return out
94
93
  }
95
94
 
96
- export function searchCode({ repoRoot, resolveRg, spawnRg = spawnSync }, { query, is_regex = false, max_results = 40, glob } = {}) {
95
+ export function searchCode({ repoRoot, resolveRg, spawnRg = runCommandSync }, { query, is_regex = false, max_results = 40, glob } = {}) {
97
96
  if (!query) return 'Provide a "query" string.'
98
97
  if (!repoRoot || !existsSync(repoRoot)) return 'Source search unavailable: repo root not provided to this MCP server.'
99
98
  const max = Math.max(1, Math.min(200, Number(max_results) || 40))
package/src/process.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Subprocess runner shared by the search engines and security sweeps.
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import { childProcessEnv } from "./child-env.js";
4
4
 
5
5
  // Windows: .cmd/.ps1 shims (npx, etc.) can't be spawned directly by Node — they need a
@@ -76,3 +76,20 @@ export function runCommand(command, args = [], options = {}) {
76
76
  child.on("close", (code) => finish(() => (timedOut ? reject(new Error("Command timed out")) : resolve({ stdout, stderr, exitCode: code }))));
77
77
  });
78
78
  }
79
+
80
+ // Synchronous run-and-collect variant for callers that resolve a real binary and read its output in
81
+ // one shot (ripgrep resolution/search, tar extraction). These launch a resolved .exe or bare binary
82
+ // directly, so no shell quoting is needed; the child env is stripped of connector secrets and a
83
+ // bounded timeout/buffer applies. Returns the raw spawnSync result ({status, stdout, stderr, error}).
84
+ export function runCommandSync(command, args = [], options = {}) {
85
+ const spawnOptions = {
86
+ cwd: options.cwd || undefined,
87
+ encoding: options.encoding ?? "utf8",
88
+ env: childProcessEnv(options.env),
89
+ windowsHide: true,
90
+ };
91
+ if (options.timeout != null) spawnOptions.timeout = options.timeout;
92
+ if (options.maxBuffer != null) spawnOptions.maxBuffer = options.maxBuffer;
93
+ if (options.stdio != null) spawnOptions.stdio = options.stdio;
94
+ return spawnSync(command, args, spawnOptions);
95
+ }