weavatrix 0.3.8 → 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.
- package/docs/releases/v0.3.9.md +36 -0
- package/package.json +2 -1
- package/scripts/run-agent-task-benchmark.mjs +0 -1
- package/src/analysis/change-classification.js +9 -19
- package/src/analysis/git-history/collector.js +1 -43
- package/src/analysis/git-history.js +2 -1
- package/src/analysis/git-ref-graph.js +4 -10
- package/src/analysis/internal-audit/repo-files.js +2 -6
- package/src/git-exec.js +67 -0
- package/src/graph/freshness-probe.js +2 -5
- package/src/graph/internal-builder.langs.js +11 -24
- package/src/mcp/git-output.mjs +2 -5
- package/src/mcp/graph/context-state.mjs +4 -5
- package/src/mcp/health/audit-format.mjs +2 -5
- package/src/mcp/tools-impact-change.mjs +2 -3
- package/src/mcp-rg.mjs +2 -4
- package/src/mcp-source-tools.mjs +4 -5
- package/src/process.js +18 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Weavatrix 0.3.9
|
|
2
|
+
|
|
3
|
+
0.3.9 consolidates every subprocess launch behind a small number of audited
|
|
4
|
+
chokepoints. Tool behavior and output are unchanged — verified byte-identical
|
|
5
|
+
against 0.3.8 on real repositories.
|
|
6
|
+
|
|
7
|
+
## Single-chokepoint subprocess execution
|
|
8
|
+
|
|
9
|
+
Git and other subprocess launches were scattered across roughly fifteen modules,
|
|
10
|
+
each with its own near-identical spawn wrapper. They now route through two
|
|
11
|
+
focused modules:
|
|
12
|
+
|
|
13
|
+
- **git** — every git command runs through `src/git-exec.js`: `runGit` for
|
|
14
|
+
synchronous run-and-collect calls and `boundedGitCommand` for the streaming
|
|
15
|
+
history collector.
|
|
16
|
+
- **everything else** — ripgrep resolution and search, `tar` archive extraction,
|
|
17
|
+
and `where`/`which` binary probing run through `src/process.js`: the existing
|
|
18
|
+
async `runCommand` plus a new synchronous `runCommandSync`.
|
|
19
|
+
|
|
20
|
+
Every launch now applies the same credential-stripped child environment,
|
|
21
|
+
`windowsHide`, bounded timeout, and `-C <repoRoot>`/`cwd` containment in one
|
|
22
|
+
place. The number of modules importing `node:child_process` drops from fifteen
|
|
23
|
+
to four — the two runners plus the long-lived language-server client — removing
|
|
24
|
+
duplicated wrappers and making the whole process surface auditable from a single
|
|
25
|
+
point.
|
|
26
|
+
|
|
27
|
+
This is a pure refactor: graph construction, change impact, Health, history and
|
|
28
|
+
search produce identical results. A byte-level comparison of the entire graph
|
|
29
|
+
(every node, edge and external import, hashed with SHA-256) against the published
|
|
30
|
+
0.3.8 matched exactly on the analytics, controller-rest-api and Weavatrix
|
|
31
|
+
repositories.
|
|
32
|
+
|
|
33
|
+
## Minor
|
|
34
|
+
|
|
35
|
+
Removed two redundant module imports (`node:perf_hooks`, `node:process`) that
|
|
36
|
+
re-imported values already available as Node globals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local repository intelligence MCP for AI coding agents: reusable architecture graph for fast application understanding, Health, dead code, clones, history, blast radius and architecture safeguards.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"docs/releases/v0.3.6.md",
|
|
43
43
|
"docs/releases/v0.3.7.md",
|
|
44
44
|
"docs/releases/v0.3.8.md",
|
|
45
|
+
"docs/releases/v0.3.9.md",
|
|
45
46
|
"docs/releases/v0.2.19.md",
|
|
46
47
|
"README.md",
|
|
47
48
|
"SECURITY.md",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Symbol-aware git-diff classification for change_impact.
|
|
2
|
-
import {
|
|
3
|
-
import {childProcessEnv} from '../child-env.js'
|
|
2
|
+
import {runGit} from '../git-exec.js'
|
|
4
3
|
import {createPathClassifier} from '../path-classification.js'
|
|
5
4
|
import {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
6
5
|
import {
|
|
@@ -33,13 +32,10 @@ function recoverBoundedDiff(repoRoot, base, files, limits) {
|
|
|
33
32
|
fallbackFiles.push(...files.slice(index))
|
|
34
33
|
break
|
|
35
34
|
}
|
|
36
|
-
const result =
|
|
37
|
-
'
|
|
35
|
+
const result = runGit(repoRoot, [
|
|
36
|
+
'diff', '--no-ext-diff', '--find-renames', '--no-color',
|
|
38
37
|
'--unified=0', String(base), '--', files[index],
|
|
39
|
-
], {
|
|
40
|
-
encoding: 'utf8', windowsHide: true, timeout: Math.min(2_000, remainingMs),
|
|
41
|
-
maxBuffer: remainingBytes + 1, env: childProcessEnv(),
|
|
42
|
-
})
|
|
38
|
+
], {timeout: Math.min(2_000, remainingMs), maxBuffer: remainingBytes + 1})
|
|
43
39
|
const chunk = String(result.stdout || '')
|
|
44
40
|
if (result.status !== 0 || !chunk || Buffer.byteLength(chunk) > remainingBytes) {
|
|
45
41
|
fallbackFiles.push(files[index])
|
|
@@ -51,11 +47,8 @@ function recoverBoundedDiff(repoRoot, base, files, limits) {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
function runGitDiff(repoRoot, base, limits) {
|
|
54
|
-
const args = ['
|
|
55
|
-
const result =
|
|
56
|
-
encoding: 'utf8', windowsHide: true, timeout: 12_000,
|
|
57
|
-
maxBuffer: limits.maxDiffBytes + 1, env: childProcessEnv(),
|
|
58
|
-
})
|
|
50
|
+
const args = ['diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
|
|
51
|
+
const result = runGit(repoRoot, args, {timeout: 12_000, maxBuffer: limits.maxDiffBytes + 1})
|
|
59
52
|
const output = String(result.stdout || '')
|
|
60
53
|
const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(output) > limits.maxDiffBytes
|
|
61
54
|
if (result.status === 0 && !oversized) return {available: true, text: output, error: null}
|
|
@@ -63,12 +56,9 @@ function runGitDiff(repoRoot, base, limits) {
|
|
|
63
56
|
let fallbackTruncated = false
|
|
64
57
|
let recoveredText = ''
|
|
65
58
|
if (oversized) {
|
|
66
|
-
const names =
|
|
67
|
-
'
|
|
68
|
-
], {
|
|
69
|
-
encoding: 'utf8', windowsHide: true, timeout: 12_000,
|
|
70
|
-
maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096), env: childProcessEnv(),
|
|
71
|
-
})
|
|
59
|
+
const names = runGit(repoRoot, [
|
|
60
|
+
'diff', '--name-only', '-z', '--no-ext-diff', '--find-renames', String(base), '--',
|
|
61
|
+
], {timeout: 12_000, maxBuffer: Math.max(64 * 1024, limits.maxFiles * 4_096)})
|
|
72
62
|
if (names.status === 0) {
|
|
73
63
|
const all = String(names.stdout || '').split('\0').map(normalizeChangePath).filter(Boolean)
|
|
74
64
|
const boundedFiles = [...new Set(all)].sort((a, b) => a.localeCompare(b)).slice(0, limits.maxFiles)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {spawn} from 'node:child_process'
|
|
2
1
|
import {isWeavatrixIgnored} from '../../path-ignore.js'
|
|
3
2
|
import {
|
|
4
3
|
boundedHistoryInteger,
|
|
@@ -10,48 +9,7 @@ import {
|
|
|
10
9
|
const HEADER_SEPARATOR = '\x1e'
|
|
11
10
|
const FIELD_SEPARATOR = '\x1f'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn(command, args, {
|
|
16
|
-
cwd: options.cwd, env: options.env, shell: false, windowsHide: true,
|
|
17
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
|
-
})
|
|
19
|
-
const stdout = [], stderr = []
|
|
20
|
-
let stdoutBytes = 0, stderrBytes = 0, truncated = false, timedOut = false, settled = false
|
|
21
|
-
const finish = (callback) => {
|
|
22
|
-
if (settled) return
|
|
23
|
-
settled = true
|
|
24
|
-
clearTimeout(timer)
|
|
25
|
-
callback()
|
|
26
|
-
}
|
|
27
|
-
const stop = () => { try { child.kill('SIGKILL') } catch { /* process already exited */ } }
|
|
28
|
-
const timer = setTimeout(() => { timedOut = true; stop() }, options.timeoutMs)
|
|
29
|
-
child.stdout?.on('data', (chunk) => {
|
|
30
|
-
if (truncated) return
|
|
31
|
-
const remaining = options.maxOutputBytes - stdoutBytes
|
|
32
|
-
if (remaining <= 0) { truncated = true; stop(); return }
|
|
33
|
-
const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining)
|
|
34
|
-
stdout.push(kept); stdoutBytes += kept.length
|
|
35
|
-
if (kept.length !== chunk.length) { truncated = true; stop() }
|
|
36
|
-
})
|
|
37
|
-
child.stderr?.on('data', (chunk) => {
|
|
38
|
-
const remaining = 64 * 1024 - stderrBytes
|
|
39
|
-
if (remaining <= 0) return
|
|
40
|
-
const kept = chunk.length <= remaining ? chunk : chunk.subarray(0, remaining)
|
|
41
|
-
stderr.push(kept); stderrBytes += kept.length
|
|
42
|
-
})
|
|
43
|
-
child.on('error', (error) => finish(() => reject(error)))
|
|
44
|
-
child.on('close', (exitCode) => finish(() => {
|
|
45
|
-
if (timedOut) return reject(new Error('git history collection timed out'))
|
|
46
|
-
resolve({
|
|
47
|
-
stdout: Buffer.concat(stdout),
|
|
48
|
-
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
49
|
-
exitCode: Number(exitCode ?? 1),
|
|
50
|
-
truncated,
|
|
51
|
-
})
|
|
52
|
-
}))
|
|
53
|
-
})
|
|
54
|
-
}
|
|
12
|
+
// git subprocess execution (sync runGit + streaming boundedGitCommand) lives in ../../git-exec.js.
|
|
55
13
|
|
|
56
14
|
const statNumber = (value) => value === '-'
|
|
57
15
|
? {value: 0, binary: true}
|
|
@@ -3,7 +3,8 @@ import {childProcessEnv} from '../child-env.js'
|
|
|
3
3
|
import {createRepoBoundary} from '../repo-path.js'
|
|
4
4
|
import {loadWeavatrixIgnore} from '../path-ignore.js'
|
|
5
5
|
import {buildGitHistoryAnalytics} from './git-history/analytics.js'
|
|
6
|
-
import {
|
|
6
|
+
import {parseGitNumstatLog} from './git-history/collector.js'
|
|
7
|
+
import {boundedGitCommand} from '../git-exec.js'
|
|
7
8
|
import {
|
|
8
9
|
boundedHistoryInteger,
|
|
9
10
|
GIT_FORMAT,
|
|
@@ -4,18 +4,15 @@
|
|
|
4
4
|
import {mkdtempSync, mkdirSync, rmSync} from 'node:fs'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
6
|
import {tmpdir} from 'node:os'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import {runGit} from '../git-exec.js'
|
|
8
|
+
import {runCommandSync} from '../process.js'
|
|
9
9
|
import {buildInternalGraph} from '../graph/internal-builder.js'
|
|
10
10
|
import {filterGraphForMode} from '../graph/graph-filter.js'
|
|
11
11
|
|
|
12
12
|
const SAFE_REF = /^(?!-)[A-Za-z0-9][A-Za-z0-9._\/@{}+~^-]{0,199}$/
|
|
13
13
|
|
|
14
14
|
function git(repoRoot, args, timeout = 15_000) {
|
|
15
|
-
return
|
|
16
|
-
encoding: 'utf8', timeout, env: childProcessEnv(), windowsHide: true,
|
|
17
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
18
|
-
})
|
|
15
|
+
return runGit(repoRoot, args, {timeout, maxBuffer: 4 * 1024 * 1024})
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export function resolveGitCommit(repoRoot, requestedRef) {
|
|
@@ -41,10 +38,7 @@ export async function withGitRefCheckout(repoRoot, requestedRef, operation) {
|
|
|
41
38
|
if (archived.status !== 0) {
|
|
42
39
|
return {ok: false, error: `git archive failed for ${resolved.ref}: ${String(archived.stderr || '').trim() || 'unknown error'}`}
|
|
43
40
|
}
|
|
44
|
-
const extracted =
|
|
45
|
-
encoding: 'utf8', timeout: 60_000, env: childProcessEnv(), windowsHide: true,
|
|
46
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
47
|
-
})
|
|
41
|
+
const extracted = runCommandSync('tar', ['-xf', archive, '-C', checkout], {timeout: 60_000, maxBuffer: 4 * 1024 * 1024})
|
|
48
42
|
if (extracted.status !== 0) {
|
|
49
43
|
return {ok: false, error: `temporary Git archive extraction failed: ${String(extracted.stderr || '').trim() || 'tar unavailable'}`}
|
|
50
44
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {readFileSync, readdirSync} from 'node:fs'
|
|
2
2
|
import {join} from 'node:path'
|
|
3
|
-
import {
|
|
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 =
|
|
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)
|
package/src/git-exec.js
ADDED
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
package/src/mcp/git-output.mjs
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
|
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 =
|
|
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 */ }
|
package/src/mcp-source-tools.mjs
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
+
}
|