minovative-mind-cli 1.0.0
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/README.md +418 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.js +30 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +18 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/agent-tools.d.ts +36 -0
- package/dist/services/agent-tools.js +764 -0
- package/dist/services/agent.d.ts +21 -0
- package/dist/services/agent.js +648 -0
- package/dist/services/ai.d.ts +60 -0
- package/dist/services/ai.js +331 -0
- package/dist/services/auth.d.ts +3 -0
- package/dist/services/auth.js +183 -0
- package/dist/services/changeLogger.d.ts +23 -0
- package/dist/services/changeLogger.js +57 -0
- package/dist/services/contextAgent.d.ts +20 -0
- package/dist/services/contextAgent.js +440 -0
- package/dist/services/proxyClient.d.ts +21 -0
- package/dist/services/proxyClient.js +119 -0
- package/dist/services/verificationService.d.ts +10 -0
- package/dist/services/verificationService.js +148 -0
- package/dist/utils/atomicWrite.d.ts +6 -0
- package/dist/utils/atomicWrite.js +29 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/contextPrompts.d.ts +3 -0
- package/dist/utils/contextPrompts.js +34 -0
- package/dist/utils/dependencyTracer.d.ts +48 -0
- package/dist/utils/dependencyTracer.js +647 -0
- package/dist/utils/excludedExtensions.d.ts +8 -0
- package/dist/utils/excludedExtensions.js +125 -0
- package/dist/utils/fuzzyMatch.d.ts +21 -0
- package/dist/utils/fuzzyMatch.js +121 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +17 -0
- package/dist/utils/pathSecurity.d.ts +10 -0
- package/dist/utils/pathSecurity.js +26 -0
- package/dist/utils/symbolExtractor.d.ts +6 -0
- package/dist/utils/symbolExtractor.js +249 -0
- package/dist/utils/syntaxValidator.d.ts +5 -0
- package/dist/utils/syntaxValidator.js +81 -0
- package/dist/utils/systemPrompts.d.ts +5 -0
- package/dist/utils/systemPrompts.js +119 -0
- package/oclif.manifest.json +69 -0
- package/package.json +81 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { EXCLUDED_EXTENSIONS } from './excludedExtensions.js';
|
|
4
|
+
// ─── Language Profile Registry ───────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Each profile defines regex patterns that extract import specifiers for
|
|
7
|
+
// a family of languages. Patterns use named capture groups for uniform
|
|
8
|
+
// extraction. The registry is intentionally exhaustive — covering edge
|
|
9
|
+
// cases like dynamic imports, re-exports, CSS @use, Go multi-import
|
|
10
|
+
// blocks, and Rust `mod` declarations.
|
|
11
|
+
const LANGUAGE_PROFILES = [
|
|
12
|
+
// ── JavaScript / TypeScript ────────────────────────────────────────
|
|
13
|
+
{
|
|
14
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'],
|
|
15
|
+
patterns: [
|
|
16
|
+
// Static imports: import X from 'specifier' / import 'specifier'
|
|
17
|
+
// Covers: import Foo from '...', import { a, b } from '...', import * as X from '...'
|
|
18
|
+
// Also covers: import '...' (side-effect only)
|
|
19
|
+
/import\s+(?:[\s\S]*?\s+from\s+)?['"](?<specifier>[^'"]+)['"]/gm,
|
|
20
|
+
// Dynamic imports: import('specifier') / await import('specifier')
|
|
21
|
+
/import\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
22
|
+
// CommonJS require: require('specifier')
|
|
23
|
+
/require\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
24
|
+
// Re-exports: export { X } from 'specifier' / export * from 'specifier'
|
|
25
|
+
/export\s+(?:[\s\S]*?\s+from\s+)['"](?<specifier>[^'"]+)['"]/gm,
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
// ── Python ─────────────────────────────────────────────────────────
|
|
29
|
+
{
|
|
30
|
+
extensions: ['.py'],
|
|
31
|
+
patterns: [
|
|
32
|
+
// from package.module import X → captures 'package.module'
|
|
33
|
+
/^from\s+(?<specifier>[^\s]+)\s+import\s/gm,
|
|
34
|
+
// import package.module → captures 'package.module'
|
|
35
|
+
// Negative lookahead prevents matching 'from X import Y' (handled above)
|
|
36
|
+
/^import\s+(?!.*\sfrom\s)(?<specifier>[^\s,]+)/gm,
|
|
37
|
+
],
|
|
38
|
+
normalizeSpecifier: (spec) => {
|
|
39
|
+
// Convert dotted module path to filesystem path: foo.bar.baz → foo/bar/baz.py
|
|
40
|
+
if (spec.startsWith('.')) {
|
|
41
|
+
// Relative import: .foo → ./foo.py, ..foo → ../foo.py
|
|
42
|
+
const dots = spec.match(/^\.+/)[0];
|
|
43
|
+
const rest = spec.slice(dots.length);
|
|
44
|
+
const prefix = dots.length === 1 ? './' : '../'.repeat(dots.length - 1);
|
|
45
|
+
return rest ? `${prefix}${rest.replace(/\./g, '/')}.py` : prefix.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
return spec.replace(/\./g, '/') + '.py';
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
// ── Rust ────────────────────────────────────────────────────────────
|
|
51
|
+
{
|
|
52
|
+
extensions: ['.rs'],
|
|
53
|
+
patterns: [
|
|
54
|
+
// use crate::module::item → captures 'crate::module::item'
|
|
55
|
+
/use\s+(?<specifier>(?:crate|self|super)(?:::[a-zA-Z_][a-zA-Z0-9_]*)+)/gm,
|
|
56
|
+
// mod module_name; → captures 'module_name' (file-level module declaration)
|
|
57
|
+
/mod\s+(?<specifier>[a-zA-Z_][a-zA-Z0-9_]*)\s*;/gm,
|
|
58
|
+
],
|
|
59
|
+
normalizeSpecifier: (spec, sourceFile) => {
|
|
60
|
+
if (!spec.includes('::')) {
|
|
61
|
+
// It's a `mod foo;` — resolves to either foo.rs or foo/mod.rs
|
|
62
|
+
return spec + '.rs';
|
|
63
|
+
}
|
|
64
|
+
// crate::foo::bar → src/foo/bar.rs (strip 'crate::' prefix)
|
|
65
|
+
const parts = spec.replace(/^crate::/, '').replace(/^(self|super)::/, '').split('::');
|
|
66
|
+
return parts.join('/') + '.rs';
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// ── Go ─────────────────────────────────────────────────────────────
|
|
70
|
+
{
|
|
71
|
+
extensions: ['.go'],
|
|
72
|
+
patterns: [
|
|
73
|
+
// Single import: import "fmt" or import alias "pkg/path"
|
|
74
|
+
/import\s+(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']/gm,
|
|
75
|
+
// Multi-line import block: import ( "fmt" \n "os" )
|
|
76
|
+
// This captures individual lines inside the block
|
|
77
|
+
/^\s*(?:[a-zA-Z_][a-zA-Z0-9_]*\s+)?["'](?<specifier>[^"']+)["']\s*$/gm,
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
// ── C / C++ ────────────────────────────────────────────────────────
|
|
81
|
+
{
|
|
82
|
+
extensions: ['.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'],
|
|
83
|
+
patterns: [
|
|
84
|
+
// #include "local_header.h" → captures local includes (quoted)
|
|
85
|
+
/^\s*#\s*include\s+"(?<specifier>[^"]+)"/gm,
|
|
86
|
+
// #include <system_header> → captures system includes (angled)
|
|
87
|
+
// We still track these — the resolver will skip if not found locally
|
|
88
|
+
/^\s*#\s*include\s+<(?<specifier>[^>]+)>/gm,
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
// ── Java / Kotlin ──────────────────────────────────────────────────
|
|
92
|
+
{
|
|
93
|
+
extensions: ['.java', '.kt', '.kts'],
|
|
94
|
+
patterns: [
|
|
95
|
+
// import com.example.package.ClassName
|
|
96
|
+
/^import\s+(?:static\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*\.[a-zA-Z_][a-zA-Z0-9_]*)/gm,
|
|
97
|
+
],
|
|
98
|
+
normalizeSpecifier: (spec) => {
|
|
99
|
+
// com.example.Foo → com/example/Foo.java (best-effort)
|
|
100
|
+
return spec.replace(/\./g, '/') + '.java';
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
// ── CSS / SCSS / SASS / LESS ───────────────────────────────────────
|
|
104
|
+
{
|
|
105
|
+
extensions: ['.css', '.scss', '.sass', '.less', '.styl'],
|
|
106
|
+
patterns: [
|
|
107
|
+
// @import 'file' or @import url('file')
|
|
108
|
+
/@import\s+(?:url\s*\(\s*)?['"](?<specifier>[^'"]+)['"]/gm,
|
|
109
|
+
// @use 'file' (SCSS)
|
|
110
|
+
/@use\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
111
|
+
// @forward 'file' (SCSS)
|
|
112
|
+
/@forward\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
// ── Ruby ───────────────────────────────────────────────────────────
|
|
116
|
+
{
|
|
117
|
+
extensions: ['.rb'],
|
|
118
|
+
patterns: [
|
|
119
|
+
// require 'file' or require "file"
|
|
120
|
+
/require\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
121
|
+
// require_relative 'file'
|
|
122
|
+
/require_relative\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
123
|
+
// load 'file'
|
|
124
|
+
/load\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
// ── PHP ────────────────────────────────────────────────────────────
|
|
128
|
+
{
|
|
129
|
+
extensions: ['.php'],
|
|
130
|
+
patterns: [
|
|
131
|
+
// use Namespace\ClassName
|
|
132
|
+
/use\s+(?<specifier>[A-Z][a-zA-Z0-9_\\]+)/gm,
|
|
133
|
+
// include/require 'file.php' or include_once/require_once 'file.php'
|
|
134
|
+
/(?:include|require)(?:_once)?\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
135
|
+
// include/require with parentheses
|
|
136
|
+
/(?:include|require)(?:_once)?\s*\(\s*['"](?<specifier>[^'"]+)['"]\s*\)/gm,
|
|
137
|
+
],
|
|
138
|
+
normalizeSpecifier: (spec) => {
|
|
139
|
+
// Convert PHP namespace to path: App\Models\User → App/Models/User.php
|
|
140
|
+
if (spec.includes('\\')) {
|
|
141
|
+
return spec.replace(/\\/g, '/') + '.php';
|
|
142
|
+
}
|
|
143
|
+
return spec;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
// ── Swift ──────────────────────────────────────────────────────────
|
|
147
|
+
{
|
|
148
|
+
extensions: ['.swift'],
|
|
149
|
+
patterns: [
|
|
150
|
+
// import ModuleName or import class Module.Type
|
|
151
|
+
/^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|typealias\s+|func\s+|var\s+|let\s+)?(?<specifier>[a-zA-Z_][a-zA-Z0-9_.]*)/gm,
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
// ── Dart ───────────────────────────────────────────────────────────
|
|
155
|
+
{
|
|
156
|
+
extensions: ['.dart'],
|
|
157
|
+
patterns: [
|
|
158
|
+
// import 'package:...' or import 'relative/path.dart'
|
|
159
|
+
/import\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
160
|
+
// export 'file.dart'
|
|
161
|
+
/export\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
162
|
+
// part 'file.dart'
|
|
163
|
+
/part\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
164
|
+
// part of 'file.dart'
|
|
165
|
+
/part\s+of\s+['"](?<specifier>[^'"]+)['"]/gm,
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
// ─── Extension → Profile Lookup ──────────────────────────────────────
|
|
170
|
+
//
|
|
171
|
+
// Pre-computed for O(1) lookups during the workspace walk.
|
|
172
|
+
const PROFILE_BY_EXT = new Map();
|
|
173
|
+
for (const profile of LANGUAGE_PROFILES) {
|
|
174
|
+
for (const ext of profile.extensions) {
|
|
175
|
+
const existing = PROFILE_BY_EXT.get(ext) || [];
|
|
176
|
+
existing.push(profile);
|
|
177
|
+
PROFILE_BY_EXT.set(ext, existing);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ─── Ignored Directories ─────────────────────────────────────────────
|
|
181
|
+
//
|
|
182
|
+
// Mirrors the ignore lists from agent-tools.ts to stay consistent.
|
|
183
|
+
const WALK_IGNORED_DIRS = new Set([
|
|
184
|
+
'node_modules',
|
|
185
|
+
'.git',
|
|
186
|
+
'dist',
|
|
187
|
+
'.next',
|
|
188
|
+
'.nuxt',
|
|
189
|
+
'__pycache__',
|
|
190
|
+
'.venv',
|
|
191
|
+
'venv',
|
|
192
|
+
'.cache',
|
|
193
|
+
'coverage',
|
|
194
|
+
'.turbo',
|
|
195
|
+
'build',
|
|
196
|
+
'out',
|
|
197
|
+
'target', // Rust/Java build output
|
|
198
|
+
'.gradle',
|
|
199
|
+
'.idea',
|
|
200
|
+
]);
|
|
201
|
+
const WALK_IGNORED_FILES = new Set([
|
|
202
|
+
'package-lock.json',
|
|
203
|
+
'yarn.lock',
|
|
204
|
+
'pnpm-lock.yaml',
|
|
205
|
+
'.DS_Store',
|
|
206
|
+
]);
|
|
207
|
+
// Build a set of bare extensions from EXCLUDED_EXTENSIONS for fast lookup
|
|
208
|
+
const EXCLUDED_EXT_SET = new Set(EXCLUDED_EXTENSIONS.map((glob) => glob.replace('*', '')));
|
|
209
|
+
let cachedAliases = null;
|
|
210
|
+
let cachedAliasRoot = null;
|
|
211
|
+
/**
|
|
212
|
+
* Reads and caches `tsconfig.json` or `jsconfig.json` path aliases.
|
|
213
|
+
* Returns an array of alias mappings, or an empty array if none found.
|
|
214
|
+
*/
|
|
215
|
+
async function loadPathAliases(workspaceRoot) {
|
|
216
|
+
if (cachedAliasRoot === workspaceRoot && cachedAliases !== null) {
|
|
217
|
+
return cachedAliases;
|
|
218
|
+
}
|
|
219
|
+
const aliases = [];
|
|
220
|
+
for (const configName of ['tsconfig.json', 'jsconfig.json']) {
|
|
221
|
+
try {
|
|
222
|
+
const configPath = path.join(workspaceRoot, configName);
|
|
223
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
224
|
+
// Strip single-line comments (// ...) and trailing commas for lenient JSON parsing
|
|
225
|
+
const cleaned = raw
|
|
226
|
+
.replace(/\/\/.*$/gm, '')
|
|
227
|
+
.replace(/,\s*([\]}])/g, '$1');
|
|
228
|
+
const config = JSON.parse(cleaned);
|
|
229
|
+
const paths = config?.compilerOptions?.paths;
|
|
230
|
+
const baseUrl = config?.compilerOptions?.baseUrl || '.';
|
|
231
|
+
if (paths && typeof paths === 'object') {
|
|
232
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
233
|
+
if (!Array.isArray(targets))
|
|
234
|
+
continue;
|
|
235
|
+
// Convert tsconfig paths pattern to prefix matching
|
|
236
|
+
// e.g., "@/*" → prefix "@/", targets ["./src/*"] → ["./src/"]
|
|
237
|
+
const prefix = pattern.replace(/\*$/, '');
|
|
238
|
+
const resolvedTargets = targets.map((t) => {
|
|
239
|
+
const targetBase = t.replace(/\*$/, '');
|
|
240
|
+
return path.join(baseUrl, targetBase);
|
|
241
|
+
});
|
|
242
|
+
aliases.push({ prefix, targets: resolvedTargets });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break; // Found a config, stop looking
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Config doesn't exist or is invalid — try the next one
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
cachedAliases = aliases;
|
|
252
|
+
cachedAliasRoot = workspaceRoot;
|
|
253
|
+
return aliases;
|
|
254
|
+
}
|
|
255
|
+
// ─── Import Extraction ───────────────────────────────────────────────
|
|
256
|
+
/**
|
|
257
|
+
* Extracts all import specifiers from a file's content based on its extension.
|
|
258
|
+
* Returns an array of raw specifier strings (not yet resolved to paths).
|
|
259
|
+
*/
|
|
260
|
+
function extractImports(content, ext) {
|
|
261
|
+
const profiles = PROFILE_BY_EXT.get(ext);
|
|
262
|
+
if (!profiles || profiles.length === 0)
|
|
263
|
+
return [];
|
|
264
|
+
const specifiers = new Set();
|
|
265
|
+
for (const profile of profiles) {
|
|
266
|
+
for (const pattern of profile.patterns) {
|
|
267
|
+
// Reset lastIndex for safety (patterns are reused across files)
|
|
268
|
+
pattern.lastIndex = 0;
|
|
269
|
+
for (const match of content.matchAll(pattern)) {
|
|
270
|
+
const raw = match.groups?.specifier;
|
|
271
|
+
if (raw && raw.trim().length > 0) {
|
|
272
|
+
const normalized = profile.normalizeSpecifier
|
|
273
|
+
? profile.normalizeSpecifier(raw.trim(), '')
|
|
274
|
+
: raw.trim();
|
|
275
|
+
specifiers.add(normalized);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return Array.from(specifiers);
|
|
281
|
+
}
|
|
282
|
+
// ─── Path Resolution ─────────────────────────────────────────────────
|
|
283
|
+
/** Extensions to probe when the import specifier has no extension */
|
|
284
|
+
const PROBE_EXTENSIONS = [
|
|
285
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
|
|
286
|
+
'.py', '.rs', '.go', '.rb', '.php', '.swift', '.dart',
|
|
287
|
+
'.css', '.scss', '.sass', '.less',
|
|
288
|
+
'.json',
|
|
289
|
+
];
|
|
290
|
+
/** Index file basenames to probe for directory imports */
|
|
291
|
+
const INDEX_FILES = [
|
|
292
|
+
'index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs',
|
|
293
|
+
'mod.rs', // Rust convention
|
|
294
|
+
'__init__.py', // Python convention
|
|
295
|
+
];
|
|
296
|
+
/**
|
|
297
|
+
* Resolves a raw import specifier to a workspace-relative file path.
|
|
298
|
+
* Returns `null` if the specifier is external (node_modules, system import)
|
|
299
|
+
* or if the target file doesn't exist.
|
|
300
|
+
*
|
|
301
|
+
* Resolution cascade:
|
|
302
|
+
* 1. Path alias expansion (tsconfig/jsconfig `compilerOptions.paths`)
|
|
303
|
+
* 2. Relative path resolution
|
|
304
|
+
* 3. Extension probing (try appending .ts, .js, etc.)
|
|
305
|
+
* 4. Index/barrel file probing (try /index.ts, /index.js, etc.)
|
|
306
|
+
* 5. Existence verification
|
|
307
|
+
*/
|
|
308
|
+
async function resolveImportPath(workspaceRoot, sourceFile, specifier) {
|
|
309
|
+
// ── Skip clearly external / unresolvable specifiers ──
|
|
310
|
+
// Node built-ins (fs, path, etc.), npm packages, protocol imports
|
|
311
|
+
if (specifier.startsWith('node:') ||
|
|
312
|
+
specifier.startsWith('bun:') ||
|
|
313
|
+
specifier.startsWith('deno:') ||
|
|
314
|
+
specifier.startsWith('package:') || // Dart packages
|
|
315
|
+
specifier.startsWith('dart:') || // Dart SDK
|
|
316
|
+
specifier.startsWith('http://') ||
|
|
317
|
+
specifier.startsWith('https://')) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const isRelative = specifier.startsWith('.') || specifier.startsWith('/');
|
|
321
|
+
// ── Try alias expansion first ──
|
|
322
|
+
if (!isRelative) {
|
|
323
|
+
const aliases = await loadPathAliases(workspaceRoot);
|
|
324
|
+
let expanded = null;
|
|
325
|
+
for (const alias of aliases) {
|
|
326
|
+
if (specifier.startsWith(alias.prefix)) {
|
|
327
|
+
const remainder = specifier.slice(alias.prefix.length);
|
|
328
|
+
// Try each target mapping (usually just one)
|
|
329
|
+
for (const target of alias.targets) {
|
|
330
|
+
const candidate = path.join(workspaceRoot, target, remainder);
|
|
331
|
+
const resolved = await probeFilePath(candidate);
|
|
332
|
+
if (resolved) {
|
|
333
|
+
expanded = path.relative(workspaceRoot, resolved);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (expanded)
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (expanded)
|
|
342
|
+
return normalizeSlashes(expanded);
|
|
343
|
+
// If it's not relative and no alias matched, it's likely an external package
|
|
344
|
+
// But still try to resolve it from workspace root as a last resort
|
|
345
|
+
// (handles cases like monorepo internal packages)
|
|
346
|
+
const fromRoot = path.join(workspaceRoot, specifier);
|
|
347
|
+
const rootResolved = await probeFilePath(fromRoot);
|
|
348
|
+
if (rootResolved) {
|
|
349
|
+
return normalizeSlashes(path.relative(workspaceRoot, rootResolved));
|
|
350
|
+
}
|
|
351
|
+
return null; // External package
|
|
352
|
+
}
|
|
353
|
+
// ── Resolve relative path ──
|
|
354
|
+
const sourceDir = path.dirname(path.join(workspaceRoot, sourceFile));
|
|
355
|
+
const absoluteCandidate = path.resolve(sourceDir, specifier);
|
|
356
|
+
// Security: ensure it's still within workspace
|
|
357
|
+
if (!absoluteCandidate.startsWith(workspaceRoot)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const resolved = await probeFilePath(absoluteCandidate);
|
|
361
|
+
if (resolved) {
|
|
362
|
+
return normalizeSlashes(path.relative(workspaceRoot, resolved));
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Given an absolute path (possibly without extension), probes for
|
|
368
|
+
* the actual file by trying the path as-is, then with common extensions,
|
|
369
|
+
* then as a directory with index files.
|
|
370
|
+
*/
|
|
371
|
+
async function probeFilePath(candidate) {
|
|
372
|
+
// 1. Try exact path
|
|
373
|
+
if (await fileExists(candidate)) {
|
|
374
|
+
const stat = await fs.stat(candidate);
|
|
375
|
+
if (stat.isFile())
|
|
376
|
+
return candidate;
|
|
377
|
+
// It's a directory — fall through to index probing
|
|
378
|
+
}
|
|
379
|
+
// 2. Try appending extensions
|
|
380
|
+
for (const ext of PROBE_EXTENSIONS) {
|
|
381
|
+
const withExt = candidate + ext;
|
|
382
|
+
if (await fileExists(withExt)) {
|
|
383
|
+
return withExt;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// 3. Try as directory with index files
|
|
387
|
+
for (const indexFile of INDEX_FILES) {
|
|
388
|
+
const withIndex = path.join(candidate, indexFile);
|
|
389
|
+
if (await fileExists(withIndex)) {
|
|
390
|
+
return withIndex;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// 4. For CSS imports, try prepending underscore (SCSS partials: _file.scss)
|
|
394
|
+
const basename = path.basename(candidate);
|
|
395
|
+
if (!basename.startsWith('_')) {
|
|
396
|
+
const dir = path.dirname(candidate);
|
|
397
|
+
for (const ext of ['.scss', '.sass', '.less', '.css']) {
|
|
398
|
+
const partial = path.join(dir, `_${basename}${ext}`);
|
|
399
|
+
if (await fileExists(partial)) {
|
|
400
|
+
return partial;
|
|
401
|
+
}
|
|
402
|
+
// Also try if basename already has extension
|
|
403
|
+
if (basename.endsWith(ext)) {
|
|
404
|
+
const partialWithExt = path.join(dir, `_${basename}`);
|
|
405
|
+
if (await fileExists(partialWithExt)) {
|
|
406
|
+
return partialWithExt;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
async function fileExists(filePath) {
|
|
414
|
+
try {
|
|
415
|
+
await fs.access(filePath);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function normalizeSlashes(p) {
|
|
423
|
+
return p.replace(/\\/g, '/');
|
|
424
|
+
}
|
|
425
|
+
// ─── Workspace Walking ───────────────────────────────────────────────
|
|
426
|
+
/**
|
|
427
|
+
* Walks the workspace recursively, collecting all source files.
|
|
428
|
+
* Respects the same ignore rules as agent-tools.ts.
|
|
429
|
+
*/
|
|
430
|
+
async function walkWorkspace(workspaceRoot) {
|
|
431
|
+
const files = [];
|
|
432
|
+
// Parse .gitignore for supplemental ignore rules
|
|
433
|
+
const extraIgnored = new Set();
|
|
434
|
+
try {
|
|
435
|
+
const gitignoreContent = await fs.readFile(path.join(workspaceRoot, '.gitignore'), 'utf-8');
|
|
436
|
+
const lines = gitignoreContent
|
|
437
|
+
.split('\n')
|
|
438
|
+
.map((l) => l.trim())
|
|
439
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
const clean = line.replace(/^\//, '').replace(/\/$/, '');
|
|
442
|
+
if (!clean.includes('*')) {
|
|
443
|
+
extraIgnored.add(clean);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// No .gitignore
|
|
449
|
+
}
|
|
450
|
+
async function walk(dir) {
|
|
451
|
+
let entries;
|
|
452
|
+
try {
|
|
453
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return; // Permission denied or other error — skip silently
|
|
457
|
+
}
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
const name = entry.name;
|
|
460
|
+
// Skip hidden entries
|
|
461
|
+
if (name.startsWith('.'))
|
|
462
|
+
continue;
|
|
463
|
+
if (entry.isDirectory()) {
|
|
464
|
+
if (WALK_IGNORED_DIRS.has(name) || extraIgnored.has(name))
|
|
465
|
+
continue;
|
|
466
|
+
await walk(path.join(dir, name));
|
|
467
|
+
}
|
|
468
|
+
else if (entry.isFile()) {
|
|
469
|
+
if (WALK_IGNORED_FILES.has(name) || extraIgnored.has(name))
|
|
470
|
+
continue;
|
|
471
|
+
// Skip binary/generated extensions
|
|
472
|
+
const ext = path.extname(name).toLowerCase();
|
|
473
|
+
if (EXCLUDED_EXT_SET.has(ext))
|
|
474
|
+
continue;
|
|
475
|
+
// Only process files we have profiles for (source code)
|
|
476
|
+
if (!PROFILE_BY_EXT.has(ext))
|
|
477
|
+
continue;
|
|
478
|
+
const relPath = normalizeSlashes(path.relative(workspaceRoot, path.join(dir, name)));
|
|
479
|
+
files.push(relPath);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
await walk(workspaceRoot);
|
|
484
|
+
return files;
|
|
485
|
+
}
|
|
486
|
+
// ─── Graph Builder ───────────────────────────────────────────────────
|
|
487
|
+
/**
|
|
488
|
+
* Builds a complete bidirectional dependency graph of the workspace.
|
|
489
|
+
*
|
|
490
|
+
* Performance: This is pure regex + filesystem walking — no AST parsing.
|
|
491
|
+
* For a typical project (<5,000 source files), this completes in <1s.
|
|
492
|
+
* The graph is ephemeral: built once per `gatherContext` call and discarded.
|
|
493
|
+
*/
|
|
494
|
+
export async function buildDependencyGraph(workspaceRoot) {
|
|
495
|
+
const nodes = new Map();
|
|
496
|
+
function getOrCreate(filePath) {
|
|
497
|
+
let node = nodes.get(filePath);
|
|
498
|
+
if (!node) {
|
|
499
|
+
node = { imports: new Set(), importedBy: new Set() };
|
|
500
|
+
nodes.set(filePath, node);
|
|
501
|
+
}
|
|
502
|
+
return node;
|
|
503
|
+
}
|
|
504
|
+
// 1. Walk workspace to find all source files
|
|
505
|
+
const sourceFiles = await walkWorkspace(workspaceRoot);
|
|
506
|
+
// 2. Process each file: extract imports → resolve paths → populate graph
|
|
507
|
+
await Promise.all(sourceFiles.map(async (relPath) => {
|
|
508
|
+
const absPath = path.join(workspaceRoot, relPath);
|
|
509
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
510
|
+
let content;
|
|
511
|
+
try {
|
|
512
|
+
content = await fs.readFile(absPath, 'utf-8');
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return; // Unreadable file — skip
|
|
516
|
+
}
|
|
517
|
+
const rawImports = extractImports(content, ext);
|
|
518
|
+
const sourceNode = getOrCreate(relPath);
|
|
519
|
+
for (const specifier of rawImports) {
|
|
520
|
+
const resolved = await resolveImportPath(workspaceRoot, relPath, specifier);
|
|
521
|
+
if (resolved) {
|
|
522
|
+
sourceNode.imports.add(resolved);
|
|
523
|
+
const targetNode = getOrCreate(resolved);
|
|
524
|
+
targetNode.importedBy.add(relPath);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}));
|
|
528
|
+
// 3. Return the graph with query methods
|
|
529
|
+
return {
|
|
530
|
+
nodes,
|
|
531
|
+
getImports(filePath) {
|
|
532
|
+
return Array.from(nodes.get(filePath)?.imports || []);
|
|
533
|
+
},
|
|
534
|
+
getImportedBy(filePath) {
|
|
535
|
+
return Array.from(nodes.get(filePath)?.importedBy || []);
|
|
536
|
+
},
|
|
537
|
+
getReverseDependencyTree(filePath, maxDepth = 3) {
|
|
538
|
+
return bfsTraverse(nodes, filePath, 'importedBy', maxDepth);
|
|
539
|
+
},
|
|
540
|
+
getForwardDependencyTree(filePath, maxDepth = 3) {
|
|
541
|
+
return bfsTraverse(nodes, filePath, 'imports', maxDepth);
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* BFS traversal of the dependency graph in a given direction.
|
|
547
|
+
* Returns all unique files reachable from `startFile` within `maxDepth` hops.
|
|
548
|
+
* The start file itself is NOT included in the result.
|
|
549
|
+
*/
|
|
550
|
+
function bfsTraverse(nodes, startFile, direction, maxDepth) {
|
|
551
|
+
const visited = new Set();
|
|
552
|
+
visited.add(startFile); // Exclude the starting file from results
|
|
553
|
+
const queue = [{ file: startFile, depth: 0 }];
|
|
554
|
+
const result = [];
|
|
555
|
+
while (queue.length > 0) {
|
|
556
|
+
const { file, depth } = queue.shift();
|
|
557
|
+
if (depth >= maxDepth)
|
|
558
|
+
continue;
|
|
559
|
+
const node = nodes.get(file);
|
|
560
|
+
if (!node)
|
|
561
|
+
continue;
|
|
562
|
+
const neighbors = node[direction];
|
|
563
|
+
for (const neighbor of neighbors) {
|
|
564
|
+
if (!visited.has(neighbor)) {
|
|
565
|
+
visited.add(neighbor);
|
|
566
|
+
result.push(neighbor);
|
|
567
|
+
queue.push({ file: neighbor, depth: depth + 1 });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* High-level function exposed as a tool to the agents.
|
|
575
|
+
* Builds the dependency graph (or reuses if already built in this invocation),
|
|
576
|
+
* then queries it for the specified file.
|
|
577
|
+
*/
|
|
578
|
+
export async function findDependencies(workspaceRoot, filePath, direction = 'both', maxDepth = 3) {
|
|
579
|
+
const graph = await buildDependencyGraph(workspaceRoot);
|
|
580
|
+
const forwardDeps = direction !== 'reverse' ? graph.getImports(filePath) : [];
|
|
581
|
+
const reverseDeps = direction !== 'forward' ? graph.getImportedBy(filePath) : [];
|
|
582
|
+
const forwardTree = direction !== 'reverse' ? graph.getForwardDependencyTree(filePath, maxDepth) : [];
|
|
583
|
+
const reverseTree = direction !== 'forward' ? graph.getReverseDependencyTree(filePath, maxDepth) : [];
|
|
584
|
+
return { filePath, forwardDeps, reverseDeps, forwardTree, reverseTree };
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Formats a FindDependenciesResult into a human-readable string
|
|
588
|
+
* suitable for feeding back to the LLM.
|
|
589
|
+
*/
|
|
590
|
+
export function formatDependencyResult(result) {
|
|
591
|
+
const lines = [];
|
|
592
|
+
lines.push(`Dependency analysis for: ${result.filePath}`);
|
|
593
|
+
lines.push('');
|
|
594
|
+
if (result.forwardDeps.length > 0 || result.forwardTree.length > 0) {
|
|
595
|
+
lines.push('─── Direct Imports (what this file imports) ───');
|
|
596
|
+
if (result.forwardDeps.length > 0) {
|
|
597
|
+
for (const dep of result.forwardDeps) {
|
|
598
|
+
lines.push(` → ${dep}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
lines.push(' (none)');
|
|
603
|
+
}
|
|
604
|
+
lines.push('');
|
|
605
|
+
if (result.forwardTree.length > result.forwardDeps.length) {
|
|
606
|
+
lines.push('─── Transitive Import Tree ───');
|
|
607
|
+
for (const dep of result.forwardTree) {
|
|
608
|
+
const isDirect = result.forwardDeps.includes(dep);
|
|
609
|
+
lines.push(` ${isDirect ? '→' : '↳'} ${dep}${isDirect ? '' : ' (transitive)'}`);
|
|
610
|
+
}
|
|
611
|
+
lines.push('');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (result.reverseDeps.length > 0 || result.reverseTree.length > 0) {
|
|
615
|
+
lines.push('─── Direct Dependents (files that import this file) ───');
|
|
616
|
+
if (result.reverseDeps.length > 0) {
|
|
617
|
+
for (const dep of result.reverseDeps) {
|
|
618
|
+
lines.push(` ← ${dep}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
lines.push(' (none)');
|
|
623
|
+
}
|
|
624
|
+
lines.push('');
|
|
625
|
+
if (result.reverseTree.length > result.reverseDeps.length) {
|
|
626
|
+
lines.push('─── Transitive Dependent Tree ───');
|
|
627
|
+
for (const dep of result.reverseTree) {
|
|
628
|
+
const isDirect = result.reverseDeps.includes(dep);
|
|
629
|
+
lines.push(` ${isDirect ? '←' : '↰'} ${dep}${isDirect ? '' : ' (transitive)'}`);
|
|
630
|
+
}
|
|
631
|
+
lines.push('');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (result.forwardDeps.length === 0 && result.reverseDeps.length === 0) {
|
|
635
|
+
lines.push('No dependencies found. This file appears to be standalone (no imports and nothing imports it).');
|
|
636
|
+
}
|
|
637
|
+
const totalImpact = new Set([...result.forwardTree, ...result.reverseTree]).size;
|
|
638
|
+
lines.push(`Total impact radius: ${totalImpact} file(s)`);
|
|
639
|
+
return lines.join('\n');
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Resets the tsconfig alias cache (useful between workspace changes).
|
|
643
|
+
*/
|
|
644
|
+
export function resetAliasCache() {
|
|
645
|
+
cachedAliases = null;
|
|
646
|
+
cachedAliasRoot = null;
|
|
647
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive list of binary/generated file extensions that grep should skip.
|
|
3
|
+
* Covers images, video, audio, fonts, archives, documents, compiled artifacts,
|
|
4
|
+
* IDE files, and tool-specific generated files across all major languages.
|
|
5
|
+
* These are used to prevent commands like `grep` from searching through
|
|
6
|
+
* non-textual or compiled files, which can lead to incorrect results or errors.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EXCLUDED_EXTENSIONS: string[];
|