@vibecheckai/cli 3.8.0 → 3.9.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/bin/runners/lib/agent-firewall/enforcement/index.js +98 -98
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -318
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -484
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -418
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -333
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -622
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -102
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -352
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -283
- package/bin/runners/lib/agent-firewall/interceptor/base.js +7 -3
- package/bin/runners/lib/engine/ast-cache.js +210 -210
- package/bin/runners/lib/engine/auth-extractor.js +211 -211
- package/bin/runners/lib/engine/billing-extractor.js +112 -112
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -100
- package/bin/runners/lib/engine/env-extractor.js +207 -207
- package/bin/runners/lib/engine/express-extractor.js +208 -208
- package/bin/runners/lib/engine/extractors.js +849 -849
- package/bin/runners/lib/engine/index.js +207 -207
- package/bin/runners/lib/engine/repo-index.js +514 -514
- package/bin/runners/lib/engine/types.js +124 -124
- package/bin/runners/lib/unified-cli-output.js +16 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +2 -2
- package/bin/runners/runIntent.js +906 -906
- package/bin/runners/runPacks.js +2089 -2089
- package/bin/runners/runReality.js +178 -1
- package/bin/runners/runShield.js +1282 -1282
- package/mcp-server/handlers/index.ts +2 -2
- package/mcp-server/handlers/tool-handler.ts +47 -8
- package/mcp-server/lib/executor.ts +5 -5
- package/mcp-server/lib/index.ts +14 -4
- package/mcp-server/lib/sandbox.test.ts +4 -4
- package/mcp-server/lib/sandbox.ts +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry.test.ts +18 -12
- package/mcp-server/tsconfig.json +1 -0
- package/package.json +2 -1
|
@@ -1,514 +1,514 @@
|
|
|
1
|
-
// bin/runners/lib/engine/repo-index.js
|
|
2
|
-
// Single-pass file indexer with content cache, token prefilter, and framework detection
|
|
3
|
-
|
|
4
|
-
const fg = require("fast-glob");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const crypto = require("crypto");
|
|
8
|
-
|
|
9
|
-
// Default ignore patterns (performance-critical)
|
|
10
|
-
const DEFAULT_IGNORE = [
|
|
11
|
-
"**/node_modules/**",
|
|
12
|
-
"**/.next/**",
|
|
13
|
-
"**/dist/**",
|
|
14
|
-
"**/build/**",
|
|
15
|
-
"**/.turbo/**",
|
|
16
|
-
"**/.git/**",
|
|
17
|
-
"**/.vibecheck/**",
|
|
18
|
-
"**/.guardrail/**",
|
|
19
|
-
"**/coverage/**",
|
|
20
|
-
"**/.nyc_output/**",
|
|
21
|
-
"**/vendor/**",
|
|
22
|
-
"**/__pycache__/**",
|
|
23
|
-
"**/.venv/**",
|
|
24
|
-
"**/venv/**",
|
|
25
|
-
"**/env/**",
|
|
26
|
-
"**/site-packages/**",
|
|
27
|
-
"**/*.min.js",
|
|
28
|
-
"**/*.min.css",
|
|
29
|
-
"**/*.map",
|
|
30
|
-
"**/*.d.ts",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Tokens we search for to build a prefilter index
|
|
34
|
-
const PREFILTER_TOKENS = [
|
|
35
|
-
// Route definitions
|
|
36
|
-
"fastify", "Fastify", "register", "express", "Express", "app.get", "app.post",
|
|
37
|
-
"router.get", "router.post", "hono", "Hono", "koa", "Koa",
|
|
38
|
-
// Client calls
|
|
39
|
-
"fetch(", "axios", "useSWR", "useQuery",
|
|
40
|
-
// Python
|
|
41
|
-
"@app.route", "@router", "FastAPI", "Flask", "Django",
|
|
42
|
-
// Go
|
|
43
|
-
"gin.Default", "echo.New", "fiber.New",
|
|
44
|
-
// Auth
|
|
45
|
-
"requireAuth", "isAuthenticated", "checkPermission", "authorize",
|
|
46
|
-
// Env
|
|
47
|
-
"process.env", "import.meta.env", "os.environ", "getenv",
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Compute SHA256 hash of content
|
|
52
|
-
* @param {string} content
|
|
53
|
-
* @returns {string}
|
|
54
|
-
*/
|
|
55
|
-
function sha256(content) {
|
|
56
|
-
return crypto.createHash("sha256").update(content).digest("hex");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check if a file exists
|
|
61
|
-
* @param {string} p - Path to check
|
|
62
|
-
* @returns {boolean}
|
|
63
|
-
*/
|
|
64
|
-
function fileExists(p) {
|
|
65
|
-
try {
|
|
66
|
-
return fs.statSync(p).isFile();
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check if a directory exists
|
|
74
|
-
* @param {string} p - Path to check
|
|
75
|
-
* @returns {boolean}
|
|
76
|
-
*/
|
|
77
|
-
function dirExists(p) {
|
|
78
|
-
try {
|
|
79
|
-
return fs.statSync(p).isDirectory();
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* RepoIndex - Single-pass file indexer
|
|
87
|
-
*
|
|
88
|
-
* Usage:
|
|
89
|
-
* const index = new RepoIndex(repoRoot, { ignorePatterns: [...] });
|
|
90
|
-
* await index.build();
|
|
91
|
-
*
|
|
92
|
-
* // Get all files
|
|
93
|
-
* const allFiles = index.files;
|
|
94
|
-
*
|
|
95
|
-
* // Get files by extension
|
|
96
|
-
* const tsFiles = index.getByExtension('.ts');
|
|
97
|
-
*
|
|
98
|
-
* // Get files containing a token
|
|
99
|
-
* const fastifyFiles = index.getByToken('fastify');
|
|
100
|
-
*
|
|
101
|
-
* // Get file content (cached)
|
|
102
|
-
* const content = index.getContent(fileAbs);
|
|
103
|
-
*
|
|
104
|
-
* // Check signals
|
|
105
|
-
* if (index.signals.detectedFrameworks.has('fastify')) { ... }
|
|
106
|
-
*/
|
|
107
|
-
class RepoIndex {
|
|
108
|
-
/**
|
|
109
|
-
* @param {string} repoRoot - Absolute path to repo root
|
|
110
|
-
* @param {Object} [options]
|
|
111
|
-
* @param {string[]} [options.ignorePatterns] - Additional ignore patterns
|
|
112
|
-
* @param {string[]} [options.includePatterns] - File patterns to include (default: all code files)
|
|
113
|
-
* @param {boolean} [options.buildTokenIndex] - Whether to build token index (default: true)
|
|
114
|
-
* @param {number} [options.maxFileSize] - Max file size to index in bytes (default: 1MB)
|
|
115
|
-
*/
|
|
116
|
-
constructor(repoRoot, options = {}) {
|
|
117
|
-
this.repoRoot = repoRoot;
|
|
118
|
-
this.ignorePatterns = [...DEFAULT_IGNORE, ...(options.ignorePatterns || [])];
|
|
119
|
-
this.includePatterns = options.includePatterns || [
|
|
120
|
-
"**/*.{ts,tsx,js,jsx,mjs,cjs}",
|
|
121
|
-
"**/*.py",
|
|
122
|
-
"**/*.go",
|
|
123
|
-
"**/*.rb",
|
|
124
|
-
"**/*.json",
|
|
125
|
-
"**/*.yaml",
|
|
126
|
-
"**/*.yml",
|
|
127
|
-
];
|
|
128
|
-
this.buildTokenIndex = options.buildTokenIndex !== false;
|
|
129
|
-
this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB
|
|
130
|
-
|
|
131
|
-
/** @type {import('./types').FileRecord[]} */
|
|
132
|
-
this.files = [];
|
|
133
|
-
|
|
134
|
-
/** @type {Map<string, string>} abs -> content */
|
|
135
|
-
this._contentCache = new Map();
|
|
136
|
-
|
|
137
|
-
/** @type {Map<string, Set<string>>} token -> Set<abs> */
|
|
138
|
-
this._tokenIndex = new Map();
|
|
139
|
-
|
|
140
|
-
/** @type {Map<string, import('./types').FileRecord[]>} ext -> FileRecord[] */
|
|
141
|
-
this._byExtension = new Map();
|
|
142
|
-
|
|
143
|
-
/** @type {import('./types').RepoSignals} */
|
|
144
|
-
this.signals = {
|
|
145
|
-
hasPackageJson: false,
|
|
146
|
-
hasRequirementsTxt: false,
|
|
147
|
-
hasPyprojectToml: false,
|
|
148
|
-
hasGoMod: false,
|
|
149
|
-
hasGemfile: false,
|
|
150
|
-
hasNextApp: false,
|
|
151
|
-
hasNextPages: false,
|
|
152
|
-
hasOpenAPI: false,
|
|
153
|
-
hasGraphQL: false,
|
|
154
|
-
detectedFrameworks: new Set(),
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
this.stats = {
|
|
158
|
-
totalFiles: 0,
|
|
159
|
-
totalSize: 0,
|
|
160
|
-
indexTimeMs: 0,
|
|
161
|
-
contentCacheHits: 0,
|
|
162
|
-
contentCacheMisses: 0,
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
this._built = false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Build the index (single glob pass)
|
|
170
|
-
* @returns {Promise<this>}
|
|
171
|
-
*/
|
|
172
|
-
async build() {
|
|
173
|
-
const startTime = Date.now();
|
|
174
|
-
|
|
175
|
-
// Single glob pass
|
|
176
|
-
const filePaths = await fg(this.includePatterns, {
|
|
177
|
-
cwd: this.repoRoot,
|
|
178
|
-
absolute: true,
|
|
179
|
-
ignore: this.ignorePatterns,
|
|
180
|
-
stats: true,
|
|
181
|
-
onlyFiles: true,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Process files
|
|
185
|
-
for (const entry of filePaths) {
|
|
186
|
-
const abs = typeof entry === "string" ? entry : entry.path;
|
|
187
|
-
const rel = path.relative(this.repoRoot, abs).replace(/\\/g, "/");
|
|
188
|
-
const ext = path.extname(abs).toLowerCase();
|
|
189
|
-
|
|
190
|
-
let stat;
|
|
191
|
-
if (typeof entry === "object" && entry.stats) {
|
|
192
|
-
stat = entry.stats;
|
|
193
|
-
} else {
|
|
194
|
-
try {
|
|
195
|
-
stat = fs.statSync(abs);
|
|
196
|
-
} catch {
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Skip files that are too large
|
|
202
|
-
if (stat.size > this.maxFileSize) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const content = this._readFile(abs);
|
|
207
|
-
if (content === null) continue;
|
|
208
|
-
|
|
209
|
-
const hash = sha256(content);
|
|
210
|
-
|
|
211
|
-
/** @type {import('./types').FileRecord} */
|
|
212
|
-
const record = {
|
|
213
|
-
abs,
|
|
214
|
-
rel,
|
|
215
|
-
size: stat.size,
|
|
216
|
-
mtime: stat.mtimeMs,
|
|
217
|
-
hash,
|
|
218
|
-
ext,
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
this.files.push(record);
|
|
222
|
-
this._contentCache.set(abs, content);
|
|
223
|
-
this.stats.totalSize += stat.size;
|
|
224
|
-
|
|
225
|
-
// Index by extension
|
|
226
|
-
if (!this._byExtension.has(ext)) {
|
|
227
|
-
this._byExtension.set(ext, []);
|
|
228
|
-
}
|
|
229
|
-
this._byExtension.get(ext).push(record);
|
|
230
|
-
|
|
231
|
-
// Build token index
|
|
232
|
-
if (this.buildTokenIndex) {
|
|
233
|
-
this._indexTokens(abs, content);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.stats.totalFiles = this.files.length;
|
|
238
|
-
|
|
239
|
-
// Detect signals
|
|
240
|
-
this._detectSignals();
|
|
241
|
-
|
|
242
|
-
this.stats.indexTimeMs = Date.now() - startTime;
|
|
243
|
-
this._built = true;
|
|
244
|
-
|
|
245
|
-
return this;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Read file content (with caching)
|
|
250
|
-
* @param {string} abs - Absolute path
|
|
251
|
-
* @returns {string|null}
|
|
252
|
-
*/
|
|
253
|
-
_readFile(abs) {
|
|
254
|
-
if (this._contentCache.has(abs)) {
|
|
255
|
-
this.stats.contentCacheHits++;
|
|
256
|
-
return this._contentCache.get(abs);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
this.stats.contentCacheMisses++;
|
|
260
|
-
try {
|
|
261
|
-
const content = fs.readFileSync(abs, "utf8");
|
|
262
|
-
return content;
|
|
263
|
-
} catch {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Index tokens in a file
|
|
270
|
-
* @param {string} abs
|
|
271
|
-
* @param {string} content
|
|
272
|
-
*/
|
|
273
|
-
_indexTokens(abs, content) {
|
|
274
|
-
for (const token of PREFILTER_TOKENS) {
|
|
275
|
-
if (content.includes(token)) {
|
|
276
|
-
if (!this._tokenIndex.has(token)) {
|
|
277
|
-
this._tokenIndex.set(token, new Set());
|
|
278
|
-
}
|
|
279
|
-
this._tokenIndex.get(token).add(abs);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Detect framework signals from indexed files
|
|
286
|
-
*/
|
|
287
|
-
_detectSignals() {
|
|
288
|
-
// Check for manifest files
|
|
289
|
-
this.signals.hasPackageJson = fileExists(path.join(this.repoRoot, "package.json"));
|
|
290
|
-
this.signals.hasRequirementsTxt = fileExists(path.join(this.repoRoot, "requirements.txt"));
|
|
291
|
-
this.signals.hasPyprojectToml = fileExists(path.join(this.repoRoot, "pyproject.toml"));
|
|
292
|
-
this.signals.hasGoMod = fileExists(path.join(this.repoRoot, "go.mod"));
|
|
293
|
-
this.signals.hasGemfile = fileExists(path.join(this.repoRoot, "Gemfile"));
|
|
294
|
-
|
|
295
|
-
// Check for Next.js directories
|
|
296
|
-
this.signals.hasNextApp = dirExists(path.join(this.repoRoot, "app")) ||
|
|
297
|
-
this.files.some(f => f.rel.includes("/app/") && f.rel.includes("route."));
|
|
298
|
-
this.signals.hasNextPages = dirExists(path.join(this.repoRoot, "pages")) ||
|
|
299
|
-
this.files.some(f => f.rel.includes("/pages/api/"));
|
|
300
|
-
|
|
301
|
-
// Check for OpenAPI/GraphQL
|
|
302
|
-
this.signals.hasOpenAPI = this.files.some(f =>
|
|
303
|
-
/openapi\.(json|ya?ml)$/i.test(f.rel) || /swagger\.(json|ya?ml)$/i.test(f.rel)
|
|
304
|
-
);
|
|
305
|
-
this.signals.hasGraphQL = this.files.some(f =>
|
|
306
|
-
/\.graphql$/i.test(f.rel) || /schema\.gql$/i.test(f.rel)
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// Detect frameworks from package.json
|
|
310
|
-
if (this.signals.hasPackageJson) {
|
|
311
|
-
try {
|
|
312
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(this.repoRoot, "package.json"), "utf8"));
|
|
313
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
314
|
-
|
|
315
|
-
if (allDeps.fastify) this.signals.detectedFrameworks.add("fastify");
|
|
316
|
-
if (allDeps.express) this.signals.detectedFrameworks.add("express");
|
|
317
|
-
if (allDeps.koa) this.signals.detectedFrameworks.add("koa");
|
|
318
|
-
if (allDeps.hono) this.signals.detectedFrameworks.add("hono");
|
|
319
|
-
if (allDeps.next) this.signals.detectedFrameworks.add("next");
|
|
320
|
-
if (allDeps["@nestjs/core"]) this.signals.detectedFrameworks.add("nestjs");
|
|
321
|
-
} catch {}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Detect Python frameworks
|
|
325
|
-
if (this.signals.hasRequirementsTxt) {
|
|
326
|
-
try {
|
|
327
|
-
const req = fs.readFileSync(path.join(this.repoRoot, "requirements.txt"), "utf8").toLowerCase();
|
|
328
|
-
if (req.includes("flask")) this.signals.detectedFrameworks.add("flask");
|
|
329
|
-
if (req.includes("fastapi")) this.signals.detectedFrameworks.add("fastapi");
|
|
330
|
-
if (req.includes("django")) this.signals.detectedFrameworks.add("django");
|
|
331
|
-
} catch {}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Detect Go frameworks
|
|
335
|
-
if (this.signals.hasGoMod) {
|
|
336
|
-
try {
|
|
337
|
-
const gomod = fs.readFileSync(path.join(this.repoRoot, "go.mod"), "utf8").toLowerCase();
|
|
338
|
-
if (gomod.includes("gin-gonic")) this.signals.detectedFrameworks.add("gin");
|
|
339
|
-
if (gomod.includes("labstack/echo")) this.signals.detectedFrameworks.add("echo");
|
|
340
|
-
if (gomod.includes("gofiber/fiber")) this.signals.detectedFrameworks.add("fiber");
|
|
341
|
-
this.signals.detectedFrameworks.add("go");
|
|
342
|
-
} catch {}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Detect Ruby frameworks
|
|
346
|
-
if (this.signals.hasGemfile) {
|
|
347
|
-
try {
|
|
348
|
-
const gem = fs.readFileSync(path.join(this.repoRoot, "Gemfile"), "utf8").toLowerCase();
|
|
349
|
-
if (gem.includes("rails")) this.signals.detectedFrameworks.add("rails");
|
|
350
|
-
if (gem.includes("sinatra")) this.signals.detectedFrameworks.add("sinatra");
|
|
351
|
-
} catch {}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Add next if we have app/pages directories
|
|
355
|
-
if (this.signals.hasNextApp || this.signals.hasNextPages) {
|
|
356
|
-
this.signals.detectedFrameworks.add("next");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Get file content (cached)
|
|
362
|
-
* @param {string} abs - Absolute path
|
|
363
|
-
* @returns {string|null}
|
|
364
|
-
*/
|
|
365
|
-
getContent(abs) {
|
|
366
|
-
if (this._contentCache.has(abs)) {
|
|
367
|
-
this.stats.contentCacheHits++;
|
|
368
|
-
return this._contentCache.get(abs);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Lazy load if not in cache
|
|
372
|
-
this.stats.contentCacheMisses++;
|
|
373
|
-
try {
|
|
374
|
-
const content = fs.readFileSync(abs, "utf8");
|
|
375
|
-
this._contentCache.set(abs, content);
|
|
376
|
-
return content;
|
|
377
|
-
} catch {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Get files by extension
|
|
384
|
-
* @param {string} ext - Extension with dot (e.g., ".ts")
|
|
385
|
-
* @returns {import('./types').FileRecord[]}
|
|
386
|
-
*/
|
|
387
|
-
getByExtension(ext) {
|
|
388
|
-
return this._byExtension.get(ext.toLowerCase()) || [];
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Get files containing a token (prefilter)
|
|
393
|
-
* @param {string} token
|
|
394
|
-
* @returns {string[]} - Array of absolute paths
|
|
395
|
-
*/
|
|
396
|
-
getByToken(token) {
|
|
397
|
-
const set = this._tokenIndex.get(token);
|
|
398
|
-
return set ? Array.from(set) : [];
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Get files matching multiple tokens (OR)
|
|
403
|
-
* @param {string[]} tokens
|
|
404
|
-
* @returns {string[]} - Array of absolute paths
|
|
405
|
-
*/
|
|
406
|
-
getByAnyToken(tokens) {
|
|
407
|
-
const result = new Set();
|
|
408
|
-
for (const token of tokens) {
|
|
409
|
-
const set = this._tokenIndex.get(token);
|
|
410
|
-
if (set) {
|
|
411
|
-
for (const abs of set) {
|
|
412
|
-
result.add(abs);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return Array.from(result);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Get files matching all tokens (AND)
|
|
421
|
-
* @param {string[]} tokens
|
|
422
|
-
* @returns {string[]} - Array of absolute paths
|
|
423
|
-
*/
|
|
424
|
-
getByAllTokens(tokens) {
|
|
425
|
-
if (tokens.length === 0) return [];
|
|
426
|
-
|
|
427
|
-
const sets = tokens.map(t => this._tokenIndex.get(t)).filter(Boolean);
|
|
428
|
-
if (sets.length === 0) return [];
|
|
429
|
-
if (sets.length !== tokens.length) return []; // Some tokens not found
|
|
430
|
-
|
|
431
|
-
// Intersect all sets
|
|
432
|
-
let result = new Set(sets[0]);
|
|
433
|
-
for (let i = 1; i < sets.length; i++) {
|
|
434
|
-
const next = new Set();
|
|
435
|
-
for (const abs of result) {
|
|
436
|
-
if (sets[i].has(abs)) {
|
|
437
|
-
next.add(abs);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
result = next;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return Array.from(result);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Get files matching a glob pattern (from already-indexed files)
|
|
448
|
-
* @param {string} pattern - Glob pattern
|
|
449
|
-
* @returns {import('./types').FileRecord[]}
|
|
450
|
-
*/
|
|
451
|
-
getByPattern(pattern) {
|
|
452
|
-
const picomatch = require("picomatch");
|
|
453
|
-
const isMatch = picomatch(pattern);
|
|
454
|
-
return this.files.filter(f => isMatch(f.rel));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Check if a framework is detected
|
|
459
|
-
* @param {string} framework
|
|
460
|
-
* @returns {boolean}
|
|
461
|
-
*/
|
|
462
|
-
hasFramework(framework) {
|
|
463
|
-
return this.signals.detectedFrameworks.has(framework.toLowerCase());
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Get JS/TS files only
|
|
468
|
-
* @returns {import('./types').FileRecord[]}
|
|
469
|
-
*/
|
|
470
|
-
getJsFiles() {
|
|
471
|
-
return [
|
|
472
|
-
...this.getByExtension(".ts"),
|
|
473
|
-
...this.getByExtension(".tsx"),
|
|
474
|
-
...this.getByExtension(".js"),
|
|
475
|
-
...this.getByExtension(".jsx"),
|
|
476
|
-
...this.getByExtension(".mjs"),
|
|
477
|
-
...this.getByExtension(".cjs"),
|
|
478
|
-
];
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Get Python files only
|
|
483
|
-
* @returns {import('./types').FileRecord[]}
|
|
484
|
-
*/
|
|
485
|
-
getPyFiles() {
|
|
486
|
-
return this.getByExtension(".py");
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Get Go files only
|
|
491
|
-
* @returns {import('./types').FileRecord[]}
|
|
492
|
-
*/
|
|
493
|
-
getGoFiles() {
|
|
494
|
-
return this.getByExtension(".go");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Clear content cache to free memory
|
|
499
|
-
*/
|
|
500
|
-
clearContentCache() {
|
|
501
|
-
this._contentCache.clear();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Get relative path from absolute
|
|
506
|
-
* @param {string} abs
|
|
507
|
-
* @returns {string}
|
|
508
|
-
*/
|
|
509
|
-
relPath(abs) {
|
|
510
|
-
return path.relative(this.repoRoot, abs).replace(/\\/g, "/");
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
module.exports = { RepoIndex, DEFAULT_IGNORE, PREFILTER_TOKENS };
|
|
1
|
+
// bin/runners/lib/engine/repo-index.js
|
|
2
|
+
// Single-pass file indexer with content cache, token prefilter, and framework detection
|
|
3
|
+
|
|
4
|
+
const fg = require("fast-glob");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
|
|
9
|
+
// Default ignore patterns (performance-critical)
|
|
10
|
+
const DEFAULT_IGNORE = [
|
|
11
|
+
"**/node_modules/**",
|
|
12
|
+
"**/.next/**",
|
|
13
|
+
"**/dist/**",
|
|
14
|
+
"**/build/**",
|
|
15
|
+
"**/.turbo/**",
|
|
16
|
+
"**/.git/**",
|
|
17
|
+
"**/.vibecheck/**",
|
|
18
|
+
"**/.guardrail/**",
|
|
19
|
+
"**/coverage/**",
|
|
20
|
+
"**/.nyc_output/**",
|
|
21
|
+
"**/vendor/**",
|
|
22
|
+
"**/__pycache__/**",
|
|
23
|
+
"**/.venv/**",
|
|
24
|
+
"**/venv/**",
|
|
25
|
+
"**/env/**",
|
|
26
|
+
"**/site-packages/**",
|
|
27
|
+
"**/*.min.js",
|
|
28
|
+
"**/*.min.css",
|
|
29
|
+
"**/*.map",
|
|
30
|
+
"**/*.d.ts",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Tokens we search for to build a prefilter index
|
|
34
|
+
const PREFILTER_TOKENS = [
|
|
35
|
+
// Route definitions
|
|
36
|
+
"fastify", "Fastify", "register", "express", "Express", "app.get", "app.post",
|
|
37
|
+
"router.get", "router.post", "hono", "Hono", "koa", "Koa",
|
|
38
|
+
// Client calls
|
|
39
|
+
"fetch(", "axios", "useSWR", "useQuery",
|
|
40
|
+
// Python
|
|
41
|
+
"@app.route", "@router", "FastAPI", "Flask", "Django",
|
|
42
|
+
// Go
|
|
43
|
+
"gin.Default", "echo.New", "fiber.New",
|
|
44
|
+
// Auth
|
|
45
|
+
"requireAuth", "isAuthenticated", "checkPermission", "authorize",
|
|
46
|
+
// Env
|
|
47
|
+
"process.env", "import.meta.env", "os.environ", "getenv",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute SHA256 hash of content
|
|
52
|
+
* @param {string} content
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function sha256(content) {
|
|
56
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a file exists
|
|
61
|
+
* @param {string} p - Path to check
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function fileExists(p) {
|
|
65
|
+
try {
|
|
66
|
+
return fs.statSync(p).isFile();
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a directory exists
|
|
74
|
+
* @param {string} p - Path to check
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function dirExists(p) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.statSync(p).isDirectory();
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* RepoIndex - Single-pass file indexer
|
|
87
|
+
*
|
|
88
|
+
* Usage:
|
|
89
|
+
* const index = new RepoIndex(repoRoot, { ignorePatterns: [...] });
|
|
90
|
+
* await index.build();
|
|
91
|
+
*
|
|
92
|
+
* // Get all files
|
|
93
|
+
* const allFiles = index.files;
|
|
94
|
+
*
|
|
95
|
+
* // Get files by extension
|
|
96
|
+
* const tsFiles = index.getByExtension('.ts');
|
|
97
|
+
*
|
|
98
|
+
* // Get files containing a token
|
|
99
|
+
* const fastifyFiles = index.getByToken('fastify');
|
|
100
|
+
*
|
|
101
|
+
* // Get file content (cached)
|
|
102
|
+
* const content = index.getContent(fileAbs);
|
|
103
|
+
*
|
|
104
|
+
* // Check signals
|
|
105
|
+
* if (index.signals.detectedFrameworks.has('fastify')) { ... }
|
|
106
|
+
*/
|
|
107
|
+
class RepoIndex {
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} repoRoot - Absolute path to repo root
|
|
110
|
+
* @param {Object} [options]
|
|
111
|
+
* @param {string[]} [options.ignorePatterns] - Additional ignore patterns
|
|
112
|
+
* @param {string[]} [options.includePatterns] - File patterns to include (default: all code files)
|
|
113
|
+
* @param {boolean} [options.buildTokenIndex] - Whether to build token index (default: true)
|
|
114
|
+
* @param {number} [options.maxFileSize] - Max file size to index in bytes (default: 1MB)
|
|
115
|
+
*/
|
|
116
|
+
constructor(repoRoot, options = {}) {
|
|
117
|
+
this.repoRoot = repoRoot;
|
|
118
|
+
this.ignorePatterns = [...DEFAULT_IGNORE, ...(options.ignorePatterns || [])];
|
|
119
|
+
this.includePatterns = options.includePatterns || [
|
|
120
|
+
"**/*.{ts,tsx,js,jsx,mjs,cjs}",
|
|
121
|
+
"**/*.py",
|
|
122
|
+
"**/*.go",
|
|
123
|
+
"**/*.rb",
|
|
124
|
+
"**/*.json",
|
|
125
|
+
"**/*.yaml",
|
|
126
|
+
"**/*.yml",
|
|
127
|
+
];
|
|
128
|
+
this.buildTokenIndex = options.buildTokenIndex !== false;
|
|
129
|
+
this.maxFileSize = options.maxFileSize || 1024 * 1024; // 1MB
|
|
130
|
+
|
|
131
|
+
/** @type {import('./types').FileRecord[]} */
|
|
132
|
+
this.files = [];
|
|
133
|
+
|
|
134
|
+
/** @type {Map<string, string>} abs -> content */
|
|
135
|
+
this._contentCache = new Map();
|
|
136
|
+
|
|
137
|
+
/** @type {Map<string, Set<string>>} token -> Set<abs> */
|
|
138
|
+
this._tokenIndex = new Map();
|
|
139
|
+
|
|
140
|
+
/** @type {Map<string, import('./types').FileRecord[]>} ext -> FileRecord[] */
|
|
141
|
+
this._byExtension = new Map();
|
|
142
|
+
|
|
143
|
+
/** @type {import('./types').RepoSignals} */
|
|
144
|
+
this.signals = {
|
|
145
|
+
hasPackageJson: false,
|
|
146
|
+
hasRequirementsTxt: false,
|
|
147
|
+
hasPyprojectToml: false,
|
|
148
|
+
hasGoMod: false,
|
|
149
|
+
hasGemfile: false,
|
|
150
|
+
hasNextApp: false,
|
|
151
|
+
hasNextPages: false,
|
|
152
|
+
hasOpenAPI: false,
|
|
153
|
+
hasGraphQL: false,
|
|
154
|
+
detectedFrameworks: new Set(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.stats = {
|
|
158
|
+
totalFiles: 0,
|
|
159
|
+
totalSize: 0,
|
|
160
|
+
indexTimeMs: 0,
|
|
161
|
+
contentCacheHits: 0,
|
|
162
|
+
contentCacheMisses: 0,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
this._built = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the index (single glob pass)
|
|
170
|
+
* @returns {Promise<this>}
|
|
171
|
+
*/
|
|
172
|
+
async build() {
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
|
|
175
|
+
// Single glob pass
|
|
176
|
+
const filePaths = await fg(this.includePatterns, {
|
|
177
|
+
cwd: this.repoRoot,
|
|
178
|
+
absolute: true,
|
|
179
|
+
ignore: this.ignorePatterns,
|
|
180
|
+
stats: true,
|
|
181
|
+
onlyFiles: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Process files
|
|
185
|
+
for (const entry of filePaths) {
|
|
186
|
+
const abs = typeof entry === "string" ? entry : entry.path;
|
|
187
|
+
const rel = path.relative(this.repoRoot, abs).replace(/\\/g, "/");
|
|
188
|
+
const ext = path.extname(abs).toLowerCase();
|
|
189
|
+
|
|
190
|
+
let stat;
|
|
191
|
+
if (typeof entry === "object" && entry.stats) {
|
|
192
|
+
stat = entry.stats;
|
|
193
|
+
} else {
|
|
194
|
+
try {
|
|
195
|
+
stat = fs.statSync(abs);
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Skip files that are too large
|
|
202
|
+
if (stat.size > this.maxFileSize) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const content = this._readFile(abs);
|
|
207
|
+
if (content === null) continue;
|
|
208
|
+
|
|
209
|
+
const hash = sha256(content);
|
|
210
|
+
|
|
211
|
+
/** @type {import('./types').FileRecord} */
|
|
212
|
+
const record = {
|
|
213
|
+
abs,
|
|
214
|
+
rel,
|
|
215
|
+
size: stat.size,
|
|
216
|
+
mtime: stat.mtimeMs,
|
|
217
|
+
hash,
|
|
218
|
+
ext,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
this.files.push(record);
|
|
222
|
+
this._contentCache.set(abs, content);
|
|
223
|
+
this.stats.totalSize += stat.size;
|
|
224
|
+
|
|
225
|
+
// Index by extension
|
|
226
|
+
if (!this._byExtension.has(ext)) {
|
|
227
|
+
this._byExtension.set(ext, []);
|
|
228
|
+
}
|
|
229
|
+
this._byExtension.get(ext).push(record);
|
|
230
|
+
|
|
231
|
+
// Build token index
|
|
232
|
+
if (this.buildTokenIndex) {
|
|
233
|
+
this._indexTokens(abs, content);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.stats.totalFiles = this.files.length;
|
|
238
|
+
|
|
239
|
+
// Detect signals
|
|
240
|
+
this._detectSignals();
|
|
241
|
+
|
|
242
|
+
this.stats.indexTimeMs = Date.now() - startTime;
|
|
243
|
+
this._built = true;
|
|
244
|
+
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Read file content (with caching)
|
|
250
|
+
* @param {string} abs - Absolute path
|
|
251
|
+
* @returns {string|null}
|
|
252
|
+
*/
|
|
253
|
+
_readFile(abs) {
|
|
254
|
+
if (this._contentCache.has(abs)) {
|
|
255
|
+
this.stats.contentCacheHits++;
|
|
256
|
+
return this._contentCache.get(abs);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.stats.contentCacheMisses++;
|
|
260
|
+
try {
|
|
261
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
262
|
+
return content;
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Index tokens in a file
|
|
270
|
+
* @param {string} abs
|
|
271
|
+
* @param {string} content
|
|
272
|
+
*/
|
|
273
|
+
_indexTokens(abs, content) {
|
|
274
|
+
for (const token of PREFILTER_TOKENS) {
|
|
275
|
+
if (content.includes(token)) {
|
|
276
|
+
if (!this._tokenIndex.has(token)) {
|
|
277
|
+
this._tokenIndex.set(token, new Set());
|
|
278
|
+
}
|
|
279
|
+
this._tokenIndex.get(token).add(abs);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Detect framework signals from indexed files
|
|
286
|
+
*/
|
|
287
|
+
_detectSignals() {
|
|
288
|
+
// Check for manifest files
|
|
289
|
+
this.signals.hasPackageJson = fileExists(path.join(this.repoRoot, "package.json"));
|
|
290
|
+
this.signals.hasRequirementsTxt = fileExists(path.join(this.repoRoot, "requirements.txt"));
|
|
291
|
+
this.signals.hasPyprojectToml = fileExists(path.join(this.repoRoot, "pyproject.toml"));
|
|
292
|
+
this.signals.hasGoMod = fileExists(path.join(this.repoRoot, "go.mod"));
|
|
293
|
+
this.signals.hasGemfile = fileExists(path.join(this.repoRoot, "Gemfile"));
|
|
294
|
+
|
|
295
|
+
// Check for Next.js directories
|
|
296
|
+
this.signals.hasNextApp = dirExists(path.join(this.repoRoot, "app")) ||
|
|
297
|
+
this.files.some(f => f.rel.includes("/app/") && f.rel.includes("route."));
|
|
298
|
+
this.signals.hasNextPages = dirExists(path.join(this.repoRoot, "pages")) ||
|
|
299
|
+
this.files.some(f => f.rel.includes("/pages/api/"));
|
|
300
|
+
|
|
301
|
+
// Check for OpenAPI/GraphQL
|
|
302
|
+
this.signals.hasOpenAPI = this.files.some(f =>
|
|
303
|
+
/openapi\.(json|ya?ml)$/i.test(f.rel) || /swagger\.(json|ya?ml)$/i.test(f.rel)
|
|
304
|
+
);
|
|
305
|
+
this.signals.hasGraphQL = this.files.some(f =>
|
|
306
|
+
/\.graphql$/i.test(f.rel) || /schema\.gql$/i.test(f.rel)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Detect frameworks from package.json
|
|
310
|
+
if (this.signals.hasPackageJson) {
|
|
311
|
+
try {
|
|
312
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(this.repoRoot, "package.json"), "utf8"));
|
|
313
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
314
|
+
|
|
315
|
+
if (allDeps.fastify) this.signals.detectedFrameworks.add("fastify");
|
|
316
|
+
if (allDeps.express) this.signals.detectedFrameworks.add("express");
|
|
317
|
+
if (allDeps.koa) this.signals.detectedFrameworks.add("koa");
|
|
318
|
+
if (allDeps.hono) this.signals.detectedFrameworks.add("hono");
|
|
319
|
+
if (allDeps.next) this.signals.detectedFrameworks.add("next");
|
|
320
|
+
if (allDeps["@nestjs/core"]) this.signals.detectedFrameworks.add("nestjs");
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Detect Python frameworks
|
|
325
|
+
if (this.signals.hasRequirementsTxt) {
|
|
326
|
+
try {
|
|
327
|
+
const req = fs.readFileSync(path.join(this.repoRoot, "requirements.txt"), "utf8").toLowerCase();
|
|
328
|
+
if (req.includes("flask")) this.signals.detectedFrameworks.add("flask");
|
|
329
|
+
if (req.includes("fastapi")) this.signals.detectedFrameworks.add("fastapi");
|
|
330
|
+
if (req.includes("django")) this.signals.detectedFrameworks.add("django");
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Detect Go frameworks
|
|
335
|
+
if (this.signals.hasGoMod) {
|
|
336
|
+
try {
|
|
337
|
+
const gomod = fs.readFileSync(path.join(this.repoRoot, "go.mod"), "utf8").toLowerCase();
|
|
338
|
+
if (gomod.includes("gin-gonic")) this.signals.detectedFrameworks.add("gin");
|
|
339
|
+
if (gomod.includes("labstack/echo")) this.signals.detectedFrameworks.add("echo");
|
|
340
|
+
if (gomod.includes("gofiber/fiber")) this.signals.detectedFrameworks.add("fiber");
|
|
341
|
+
this.signals.detectedFrameworks.add("go");
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Detect Ruby frameworks
|
|
346
|
+
if (this.signals.hasGemfile) {
|
|
347
|
+
try {
|
|
348
|
+
const gem = fs.readFileSync(path.join(this.repoRoot, "Gemfile"), "utf8").toLowerCase();
|
|
349
|
+
if (gem.includes("rails")) this.signals.detectedFrameworks.add("rails");
|
|
350
|
+
if (gem.includes("sinatra")) this.signals.detectedFrameworks.add("sinatra");
|
|
351
|
+
} catch {}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Add next if we have app/pages directories
|
|
355
|
+
if (this.signals.hasNextApp || this.signals.hasNextPages) {
|
|
356
|
+
this.signals.detectedFrameworks.add("next");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get file content (cached)
|
|
362
|
+
* @param {string} abs - Absolute path
|
|
363
|
+
* @returns {string|null}
|
|
364
|
+
*/
|
|
365
|
+
getContent(abs) {
|
|
366
|
+
if (this._contentCache.has(abs)) {
|
|
367
|
+
this.stats.contentCacheHits++;
|
|
368
|
+
return this._contentCache.get(abs);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Lazy load if not in cache
|
|
372
|
+
this.stats.contentCacheMisses++;
|
|
373
|
+
try {
|
|
374
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
375
|
+
this._contentCache.set(abs, content);
|
|
376
|
+
return content;
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get files by extension
|
|
384
|
+
* @param {string} ext - Extension with dot (e.g., ".ts")
|
|
385
|
+
* @returns {import('./types').FileRecord[]}
|
|
386
|
+
*/
|
|
387
|
+
getByExtension(ext) {
|
|
388
|
+
return this._byExtension.get(ext.toLowerCase()) || [];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get files containing a token (prefilter)
|
|
393
|
+
* @param {string} token
|
|
394
|
+
* @returns {string[]} - Array of absolute paths
|
|
395
|
+
*/
|
|
396
|
+
getByToken(token) {
|
|
397
|
+
const set = this._tokenIndex.get(token);
|
|
398
|
+
return set ? Array.from(set) : [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get files matching multiple tokens (OR)
|
|
403
|
+
* @param {string[]} tokens
|
|
404
|
+
* @returns {string[]} - Array of absolute paths
|
|
405
|
+
*/
|
|
406
|
+
getByAnyToken(tokens) {
|
|
407
|
+
const result = new Set();
|
|
408
|
+
for (const token of tokens) {
|
|
409
|
+
const set = this._tokenIndex.get(token);
|
|
410
|
+
if (set) {
|
|
411
|
+
for (const abs of set) {
|
|
412
|
+
result.add(abs);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return Array.from(result);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get files matching all tokens (AND)
|
|
421
|
+
* @param {string[]} tokens
|
|
422
|
+
* @returns {string[]} - Array of absolute paths
|
|
423
|
+
*/
|
|
424
|
+
getByAllTokens(tokens) {
|
|
425
|
+
if (tokens.length === 0) return [];
|
|
426
|
+
|
|
427
|
+
const sets = tokens.map(t => this._tokenIndex.get(t)).filter(Boolean);
|
|
428
|
+
if (sets.length === 0) return [];
|
|
429
|
+
if (sets.length !== tokens.length) return []; // Some tokens not found
|
|
430
|
+
|
|
431
|
+
// Intersect all sets
|
|
432
|
+
let result = new Set(sets[0]);
|
|
433
|
+
for (let i = 1; i < sets.length; i++) {
|
|
434
|
+
const next = new Set();
|
|
435
|
+
for (const abs of result) {
|
|
436
|
+
if (sets[i].has(abs)) {
|
|
437
|
+
next.add(abs);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
result = next;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return Array.from(result);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get files matching a glob pattern (from already-indexed files)
|
|
448
|
+
* @param {string} pattern - Glob pattern
|
|
449
|
+
* @returns {import('./types').FileRecord[]}
|
|
450
|
+
*/
|
|
451
|
+
getByPattern(pattern) {
|
|
452
|
+
const picomatch = require("picomatch");
|
|
453
|
+
const isMatch = picomatch(pattern);
|
|
454
|
+
return this.files.filter(f => isMatch(f.rel));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Check if a framework is detected
|
|
459
|
+
* @param {string} framework
|
|
460
|
+
* @returns {boolean}
|
|
461
|
+
*/
|
|
462
|
+
hasFramework(framework) {
|
|
463
|
+
return this.signals.detectedFrameworks.has(framework.toLowerCase());
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get JS/TS files only
|
|
468
|
+
* @returns {import('./types').FileRecord[]}
|
|
469
|
+
*/
|
|
470
|
+
getJsFiles() {
|
|
471
|
+
return [
|
|
472
|
+
...this.getByExtension(".ts"),
|
|
473
|
+
...this.getByExtension(".tsx"),
|
|
474
|
+
...this.getByExtension(".js"),
|
|
475
|
+
...this.getByExtension(".jsx"),
|
|
476
|
+
...this.getByExtension(".mjs"),
|
|
477
|
+
...this.getByExtension(".cjs"),
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get Python files only
|
|
483
|
+
* @returns {import('./types').FileRecord[]}
|
|
484
|
+
*/
|
|
485
|
+
getPyFiles() {
|
|
486
|
+
return this.getByExtension(".py");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get Go files only
|
|
491
|
+
* @returns {import('./types').FileRecord[]}
|
|
492
|
+
*/
|
|
493
|
+
getGoFiles() {
|
|
494
|
+
return this.getByExtension(".go");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Clear content cache to free memory
|
|
499
|
+
*/
|
|
500
|
+
clearContentCache() {
|
|
501
|
+
this._contentCache.clear();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get relative path from absolute
|
|
506
|
+
* @param {string} abs
|
|
507
|
+
* @returns {string}
|
|
508
|
+
*/
|
|
509
|
+
relPath(abs) {
|
|
510
|
+
return path.relative(this.repoRoot, abs).replace(/\\/g, "/");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
module.exports = { RepoIndex, DEFAULT_IGNORE, PREFILTER_TOKENS };
|