autosnippet 3.0.3 → 3.0.7
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 +85 -240
- package/dashboard/dist/assets/{icons-Cdq22n2i.js → icons-eQ_rWCus.js} +97 -102
- package/dashboard/dist/assets/index-B3Nnkdxi.js +133 -0
- package/dashboard/dist/assets/index-BFNDAqh3.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/core/AstAnalyzer.js +2 -2
- package/lib/core/ast/ensure-grammars.js +2 -0
- package/lib/core/ast/index.js +8 -0
- package/lib/core/ast/lang-rust.js +695 -0
- package/lib/core/discovery/PythonDiscoverer.js +3 -0
- package/lib/core/discovery/RustDiscoverer.js +467 -0
- package/lib/core/discovery/index.js +3 -0
- package/lib/core/enhancement/django-enhancement.js +169 -3
- package/lib/core/enhancement/fastapi-enhancement.js +149 -3
- package/lib/core/enhancement/go-grpc-enhancement.js +4 -0
- package/lib/core/enhancement/go-web-enhancement.js +6 -0
- package/lib/core/enhancement/index.js +5 -0
- package/lib/core/enhancement/langchain-enhancement.js +233 -0
- package/lib/core/enhancement/ml-enhancement.js +265 -0
- package/lib/core/enhancement/nextjs-enhancement.js +219 -0
- package/lib/core/enhancement/node-server-enhancement.js +178 -4
- package/lib/core/enhancement/react-enhancement.js +165 -4
- package/lib/core/enhancement/rust-tokio-enhancement.js +231 -0
- package/lib/core/enhancement/rust-web-enhancement.js +256 -0
- package/lib/core/enhancement/spring-enhancement.js +2 -0
- package/lib/core/enhancement/vue-enhancement.js +143 -2
- package/lib/external/ai/AiProvider.js +45 -6
- package/lib/external/mcp/handlers/bootstrap/skills.js +2 -0
- package/lib/external/mcp/handlers/bootstrap.js +33 -9
- package/lib/external/mcp/handlers/guard.js +42 -0
- package/lib/http/routes/candidates.js +7 -1
- package/lib/service/chat/ChatAgent.js +1 -0
- package/lib/service/chat/tools.js +5 -1
- package/lib/service/guard/ComplianceReporter.js +20 -7
- package/lib/service/guard/GuardCheckEngine.js +156 -5
- package/lib/service/guard/SourceFileCollector.js +15 -0
- package/package.json +28 -6
- package/skills/autosnippet-coldstart/SKILL.md +4 -2
- package/skills/autosnippet-concepts/SKILL.md +5 -3
- package/skills/autosnippet-reference-rust/SKILL.md +401 -0
- package/skills/autosnippet-structure/SKILL.md +1 -1
- package/templates/recipes-setup/README.md +2 -2
- package/templates/recipes-setup/_template.md +1 -1
- package/dashboard/dist/assets/index-ClkyPkDX.js +0 -133
- package/dashboard/dist/assets/index-t4QrJwv1.css +0 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module RustDiscoverer
|
|
3
|
+
* @description Rust 项目结构发现器
|
|
4
|
+
*
|
|
5
|
+
* 检测信号: Cargo.toml, Cargo.lock, *.rs
|
|
6
|
+
* 支持: 单 crate 项目、Cargo workspace(多 crate)、标准目录布局 (src/ tests/ benches/ examples/)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { basename, extname, join, relative } from 'node:path';
|
|
11
|
+
import { ProjectDiscoverer } from './ProjectDiscoverer.js';
|
|
12
|
+
|
|
13
|
+
const SOURCE_EXTENSIONS = new Set(['.rs']);
|
|
14
|
+
|
|
15
|
+
const EXCLUDE_DIRS = new Set([
|
|
16
|
+
'.git',
|
|
17
|
+
'target',
|
|
18
|
+
'node_modules',
|
|
19
|
+
'.cargo',
|
|
20
|
+
'.idea',
|
|
21
|
+
'.vscode',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export class RustDiscoverer extends ProjectDiscoverer {
|
|
27
|
+
#projectRoot = null;
|
|
28
|
+
#targets = [];
|
|
29
|
+
#depGraph = { nodes: [], edges: [] };
|
|
30
|
+
#crateName = null;
|
|
31
|
+
|
|
32
|
+
get id() {
|
|
33
|
+
return 'rust';
|
|
34
|
+
}
|
|
35
|
+
get displayName() {
|
|
36
|
+
return 'Rust (Cargo)';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async detect(projectRoot) {
|
|
40
|
+
let confidence = 0;
|
|
41
|
+
const reasons = [];
|
|
42
|
+
|
|
43
|
+
if (existsSync(join(projectRoot, 'Cargo.toml'))) {
|
|
44
|
+
confidence = 0.92;
|
|
45
|
+
reasons.push('Cargo.toml exists');
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(join(projectRoot, 'Cargo.lock'))) {
|
|
48
|
+
confidence = Math.max(confidence, 0.7);
|
|
49
|
+
if (confidence < 0.92) {
|
|
50
|
+
confidence += 0.1;
|
|
51
|
+
}
|
|
52
|
+
reasons.push('Cargo.lock exists');
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(join(projectRoot, 'rust-toolchain.toml')) ||
|
|
55
|
+
existsSync(join(projectRoot, 'rust-toolchain'))) {
|
|
56
|
+
confidence = Math.max(confidence, 0.85);
|
|
57
|
+
reasons.push('rust-toolchain exists');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 兜底: 根目录有 .rs 文件
|
|
61
|
+
if (confidence === 0) {
|
|
62
|
+
try {
|
|
63
|
+
const entries = readdirSync(projectRoot);
|
|
64
|
+
if (entries.some((e) => e.endsWith('.rs'))) {
|
|
65
|
+
confidence = 0.5;
|
|
66
|
+
reasons.push('*.rs files found at root');
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
/* skip */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
match: confidence > 0,
|
|
75
|
+
confidence: Math.min(confidence, 1.0),
|
|
76
|
+
reason: reasons.join(', ') || 'No Rust markers found',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async load(projectRoot) {
|
|
81
|
+
this.#projectRoot = projectRoot;
|
|
82
|
+
this.#targets = [];
|
|
83
|
+
this.#depGraph = { nodes: [], edges: [] };
|
|
84
|
+
|
|
85
|
+
// 解析 Cargo.toml
|
|
86
|
+
const cargoInfo = this.#parseCargoToml(projectRoot);
|
|
87
|
+
this.#crateName = cargoInfo?.name || basename(projectRoot);
|
|
88
|
+
|
|
89
|
+
const framework = this.#detectFramework(projectRoot);
|
|
90
|
+
|
|
91
|
+
// 主 Target
|
|
92
|
+
this.#targets.push({
|
|
93
|
+
name: this.#crateName,
|
|
94
|
+
path: projectRoot,
|
|
95
|
+
type: cargoInfo?.isBin ? 'application' : 'library',
|
|
96
|
+
language: 'rust',
|
|
97
|
+
framework,
|
|
98
|
+
metadata: {
|
|
99
|
+
edition: cargoInfo?.edition || null,
|
|
100
|
+
crateName: this.#crateName,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
this.#depGraph.nodes.push(this.#crateName);
|
|
104
|
+
|
|
105
|
+
// Cargo workspace — 发现成员 crate
|
|
106
|
+
const workspaceMembers = this.#discoverWorkspaceMembers(projectRoot);
|
|
107
|
+
for (const member of workspaceMembers) {
|
|
108
|
+
this.#targets.push(member);
|
|
109
|
+
this.#depGraph.nodes.push(member.name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// examples/ 下的二进制示例
|
|
113
|
+
this.#discoverExamples(projectRoot, framework);
|
|
114
|
+
|
|
115
|
+
// benches/ 下的 benchmark
|
|
116
|
+
this.#discoverBenches(projectRoot);
|
|
117
|
+
|
|
118
|
+
// tests/ 集成测试
|
|
119
|
+
const testsDir = join(projectRoot, 'tests');
|
|
120
|
+
if (existsSync(testsDir)) {
|
|
121
|
+
this.#targets.push({
|
|
122
|
+
name: 'tests',
|
|
123
|
+
path: testsDir,
|
|
124
|
+
type: 'test',
|
|
125
|
+
language: 'rust',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 解析依赖
|
|
130
|
+
this.#parseDependencies(projectRoot);
|
|
131
|
+
|
|
132
|
+
// 发现内部模块
|
|
133
|
+
this.#discoverInternalModules(projectRoot);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listTargets() {
|
|
137
|
+
return this.#targets;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getTargetFiles(target) {
|
|
141
|
+
const targetPath =
|
|
142
|
+
typeof target === 'string'
|
|
143
|
+
? this.#targets.find((t) => t.name === target)?.path || this.#projectRoot
|
|
144
|
+
: target.path;
|
|
145
|
+
|
|
146
|
+
if (!targetPath || !existsSync(targetPath)) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const files = [];
|
|
151
|
+
this.#collectRsFiles(targetPath, targetPath, files);
|
|
152
|
+
return files;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getDependencyGraph() {
|
|
156
|
+
return this.#depGraph;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── 内部实现 ──
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 简易解析 Cargo.toml(无 TOML 解析器,使用正则)
|
|
163
|
+
*/
|
|
164
|
+
#parseCargoToml(projectRoot) {
|
|
165
|
+
const cargoPath = join(projectRoot, 'Cargo.toml');
|
|
166
|
+
if (!existsSync(cargoPath)) return null;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const content = readFileSync(cargoPath, 'utf8');
|
|
170
|
+
const name = content.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
171
|
+
const edition = content.match(/^\s*edition\s*=\s*"([^"]+)"/m)?.[1];
|
|
172
|
+
|
|
173
|
+
// 判断是 bin 还是 lib
|
|
174
|
+
const hasMainRs = existsSync(join(projectRoot, 'src', 'main.rs'));
|
|
175
|
+
const hasLibRs = existsSync(join(projectRoot, 'src', 'lib.rs'));
|
|
176
|
+
const hasBinSection = /\[\[bin\]\]/.test(content);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
name,
|
|
180
|
+
edition,
|
|
181
|
+
isBin: hasMainRs || hasBinSection,
|
|
182
|
+
isLib: hasLibRs,
|
|
183
|
+
};
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 发现 Cargo workspace 成员
|
|
191
|
+
*/
|
|
192
|
+
#discoverWorkspaceMembers(projectRoot) {
|
|
193
|
+
const cargoPath = join(projectRoot, 'Cargo.toml');
|
|
194
|
+
if (!existsSync(cargoPath)) return [];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const content = readFileSync(cargoPath, 'utf8');
|
|
198
|
+
|
|
199
|
+
// [workspace] members = ["crate_a", "crate_b", "crates/*"]
|
|
200
|
+
const workspaceBlock = content.match(/\[workspace\]([\s\S]*?)(?:\n\[|\s*$)/);
|
|
201
|
+
if (!workspaceBlock) return [];
|
|
202
|
+
|
|
203
|
+
const membersLine = workspaceBlock[1].match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
204
|
+
if (!membersLine) return [];
|
|
205
|
+
|
|
206
|
+
const memberPatterns = membersLine[1]
|
|
207
|
+
.split(',')
|
|
208
|
+
.map((s) => s.replace(/["\s]/g, ''))
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
|
|
211
|
+
const members = [];
|
|
212
|
+
for (const pattern of memberPatterns) {
|
|
213
|
+
if (pattern.includes('*')) {
|
|
214
|
+
// Glob — 展开
|
|
215
|
+
const prefix = pattern.replace('/*', '');
|
|
216
|
+
const parentDir = join(projectRoot, prefix);
|
|
217
|
+
if (!existsSync(parentDir)) continue;
|
|
218
|
+
try {
|
|
219
|
+
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
222
|
+
const memberPath = join(parentDir, entry.name);
|
|
223
|
+
if (existsSync(join(memberPath, 'Cargo.toml'))) {
|
|
224
|
+
const info = this.#parseCargoToml(memberPath);
|
|
225
|
+
members.push({
|
|
226
|
+
name: info?.name || entry.name,
|
|
227
|
+
path: memberPath,
|
|
228
|
+
type: info?.isBin ? 'application' : 'library',
|
|
229
|
+
language: 'rust',
|
|
230
|
+
metadata: {
|
|
231
|
+
edition: info?.edition,
|
|
232
|
+
isWorkspaceMember: true,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch { /* skip */ }
|
|
239
|
+
} else {
|
|
240
|
+
const memberPath = join(projectRoot, pattern);
|
|
241
|
+
if (existsSync(join(memberPath, 'Cargo.toml'))) {
|
|
242
|
+
const info = this.#parseCargoToml(memberPath);
|
|
243
|
+
members.push({
|
|
244
|
+
name: info?.name || basename(pattern),
|
|
245
|
+
path: memberPath,
|
|
246
|
+
type: info?.isBin ? 'application' : 'library',
|
|
247
|
+
language: 'rust',
|
|
248
|
+
metadata: {
|
|
249
|
+
edition: info?.edition,
|
|
250
|
+
isWorkspaceMember: true,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return members;
|
|
258
|
+
} catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 发现 examples/ 目录
|
|
265
|
+
*/
|
|
266
|
+
#discoverExamples(projectRoot, framework) {
|
|
267
|
+
const examplesDir = join(projectRoot, 'examples');
|
|
268
|
+
if (!existsSync(examplesDir)) return;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const entries = readdirSync(examplesDir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (entry.isFile() && entry.name.endsWith('.rs')) {
|
|
274
|
+
// 单文件示例不作为独立 target,只记录目录
|
|
275
|
+
} else if (entry.isDirectory()) {
|
|
276
|
+
const subDir = join(examplesDir, entry.name);
|
|
277
|
+
if (existsSync(join(subDir, 'main.rs'))) {
|
|
278
|
+
this.#targets.push({
|
|
279
|
+
name: `examples/${entry.name}`,
|
|
280
|
+
path: subDir,
|
|
281
|
+
type: 'example',
|
|
282
|
+
language: 'rust',
|
|
283
|
+
framework,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// 如果有任何 .rs 文件,添加整个 examples 目录
|
|
289
|
+
if (entries.some((e) => e.isFile() && e.name.endsWith('.rs'))) {
|
|
290
|
+
this.#targets.push({
|
|
291
|
+
name: 'examples',
|
|
292
|
+
path: examplesDir,
|
|
293
|
+
type: 'example',
|
|
294
|
+
language: 'rust',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} catch { /* skip */ }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 发现 benches/ 目录
|
|
302
|
+
*/
|
|
303
|
+
#discoverBenches(projectRoot) {
|
|
304
|
+
const benchDir = join(projectRoot, 'benches');
|
|
305
|
+
if (!existsSync(benchDir)) return;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const entries = readdirSync(benchDir);
|
|
309
|
+
if (entries.some((e) => e.endsWith('.rs'))) {
|
|
310
|
+
this.#targets.push({
|
|
311
|
+
name: 'benches',
|
|
312
|
+
path: benchDir,
|
|
313
|
+
type: 'benchmark',
|
|
314
|
+
language: 'rust',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
} catch { /* skip */ }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 检测 Rust Web/网络框架
|
|
322
|
+
*/
|
|
323
|
+
#detectFramework(projectRoot) {
|
|
324
|
+
const cargoPath = join(projectRoot, 'Cargo.toml');
|
|
325
|
+
if (!existsSync(cargoPath)) return null;
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const content = readFileSync(cargoPath, 'utf8');
|
|
329
|
+
|
|
330
|
+
if (/\bactix-web\b/.test(content)) return 'actix-web';
|
|
331
|
+
if (/\baxum\b/.test(content)) return 'axum';
|
|
332
|
+
if (/\brocket\b/.test(content)) return 'rocket';
|
|
333
|
+
if (/\bwarp\b/.test(content)) return 'warp';
|
|
334
|
+
if (/\btokio\b/.test(content) && /\bhyper\b/.test(content)) return 'hyper';
|
|
335
|
+
if (/\btokio\b/.test(content)) return 'tokio';
|
|
336
|
+
if (/\basync-std\b/.test(content)) return 'async-std';
|
|
337
|
+
if (/\btauri\b/.test(content)) return 'tauri';
|
|
338
|
+
if (/\bbevy\b/.test(content)) return 'bevy';
|
|
339
|
+
if (/\bclap\b/.test(content)) return 'clap-cli';
|
|
340
|
+
if (/\bserde\b/.test(content)) return 'serde';
|
|
341
|
+
} catch { /* skip */ }
|
|
342
|
+
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 解析 Cargo.toml 的 [dependencies] 到 depGraph
|
|
348
|
+
*/
|
|
349
|
+
#parseDependencies(projectRoot) {
|
|
350
|
+
const cargoPath = join(projectRoot, 'Cargo.toml');
|
|
351
|
+
if (!existsSync(cargoPath)) return;
|
|
352
|
+
|
|
353
|
+
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
|
|
354
|
+
const rootNode =
|
|
355
|
+
typeof this.#depGraph.nodes[0] === 'string'
|
|
356
|
+
? this.#depGraph.nodes[0]
|
|
357
|
+
: this.#depGraph.nodes[0]?.id || 'root';
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const content = readFileSync(cargoPath, 'utf8');
|
|
361
|
+
|
|
362
|
+
// 匹配 [dependencies] 和 [dev-dependencies] 块
|
|
363
|
+
const depSections = content.matchAll(
|
|
364
|
+
/\[((?:dev-|build-)?dependencies)\]([\s\S]*?)(?=\n\[|$)/g
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
for (const section of depSections) {
|
|
368
|
+
const sectionType = section[1];
|
|
369
|
+
const isDev = sectionType.startsWith('dev-');
|
|
370
|
+
const isBuild = sectionType.startsWith('build-');
|
|
371
|
+
const lines = section[2].split('\n');
|
|
372
|
+
|
|
373
|
+
for (const line of lines) {
|
|
374
|
+
const trimmed = line.trim();
|
|
375
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('[')) continue;
|
|
376
|
+
|
|
377
|
+
// dep = "version" 或 dep = { version = "...", ... }
|
|
378
|
+
const depMatch = trimmed.match(/^(\S+)\s*=/);
|
|
379
|
+
if (depMatch) {
|
|
380
|
+
const depName = depMatch[1].replace(/"/g, '');
|
|
381
|
+
if (!nodeSet.has(depName)) {
|
|
382
|
+
this.#depGraph.nodes.push({
|
|
383
|
+
id: depName,
|
|
384
|
+
label: depName,
|
|
385
|
+
type: 'external',
|
|
386
|
+
isDev,
|
|
387
|
+
isBuild,
|
|
388
|
+
});
|
|
389
|
+
nodeSet.add(depName);
|
|
390
|
+
}
|
|
391
|
+
this.#depGraph.edges.push({
|
|
392
|
+
from: rootNode,
|
|
393
|
+
to: depName,
|
|
394
|
+
type: isDev ? 'dev-dependency' : isBuild ? 'build-dependency' : 'dependency',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch { /* skip */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 发现内部模块(src/ 子目录)
|
|
404
|
+
*/
|
|
405
|
+
#discoverInternalModules(projectRoot) {
|
|
406
|
+
const srcDir = join(projectRoot, 'src');
|
|
407
|
+
if (!existsSync(srcDir)) return;
|
|
408
|
+
|
|
409
|
+
const nodeSet = new Set(this.#depGraph.nodes.map((n) => (typeof n === 'string' ? n : n.id)));
|
|
410
|
+
|
|
411
|
+
const walk = (dir, relPath, depth) => {
|
|
412
|
+
if (depth > 6) return;
|
|
413
|
+
try {
|
|
414
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
415
|
+
for (const entry of entries) {
|
|
416
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name))
|
|
417
|
+
continue;
|
|
418
|
+
const subDir = join(dir, entry.name);
|
|
419
|
+
const subRel = relPath ? `${relPath}/${entry.name}` : entry.name;
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const subEntries = readdirSync(subDir);
|
|
423
|
+
const hasRsFiles = subEntries.some((e) => e.endsWith('.rs'));
|
|
424
|
+
if (hasRsFiles && !nodeSet.has(subRel)) {
|
|
425
|
+
this.#depGraph.nodes.push({ id: subRel, label: subRel, type: 'internal' });
|
|
426
|
+
nodeSet.add(subRel);
|
|
427
|
+
}
|
|
428
|
+
} catch { /* skip */ }
|
|
429
|
+
|
|
430
|
+
walk(subDir, subRel, depth + 1);
|
|
431
|
+
}
|
|
432
|
+
} catch { /* skip */ }
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
walk(srcDir, '', 0);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 递归收集 .rs 文件
|
|
440
|
+
*/
|
|
441
|
+
#collectRsFiles(dir, rootDir, files, depth = 0) {
|
|
442
|
+
if (depth > 15) return;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
if (entry.name.startsWith('.')) continue;
|
|
448
|
+
|
|
449
|
+
if (entry.isDirectory()) {
|
|
450
|
+
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
451
|
+
this.#collectRsFiles(join(dir, entry.name), rootDir, files, depth + 1);
|
|
452
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
453
|
+
const fullPath = join(dir, entry.name);
|
|
454
|
+
try {
|
|
455
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
456
|
+
files.push({
|
|
457
|
+
name: entry.name,
|
|
458
|
+
path: fullPath,
|
|
459
|
+
relativePath: relative(rootDir, fullPath),
|
|
460
|
+
content,
|
|
461
|
+
});
|
|
462
|
+
} catch { /* unreadable */ }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch { /* permission error */ }
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -8,6 +8,7 @@ import { DiscovererRegistry } from './DiscovererRegistry.js';
|
|
|
8
8
|
import { GenericDiscoverer } from './GenericDiscoverer.js';
|
|
9
9
|
import { GoDiscoverer } from './GoDiscoverer.js';
|
|
10
10
|
import { JvmDiscoverer } from './JvmDiscoverer.js';
|
|
11
|
+
import { RustDiscoverer } from './RustDiscoverer.js';
|
|
11
12
|
import { NodeDiscoverer } from './NodeDiscoverer.js';
|
|
12
13
|
import { PythonDiscoverer } from './PythonDiscoverer.js';
|
|
13
14
|
import { SpmDiscoverer } from './SpmDiscoverer.js';
|
|
@@ -29,6 +30,7 @@ export function getDiscovererRegistry() {
|
|
|
29
30
|
.register(new JvmDiscoverer())
|
|
30
31
|
.register(new GoDiscoverer())
|
|
31
32
|
.register(new DartDiscoverer())
|
|
33
|
+
.register(new RustDiscoverer())
|
|
32
34
|
.register(new GenericDiscoverer());
|
|
33
35
|
}
|
|
34
36
|
return _registry;
|
|
@@ -45,6 +47,7 @@ export { DartDiscoverer } from './DartDiscoverer.js';
|
|
|
45
47
|
export { DiscovererRegistry } from './DiscovererRegistry.js';
|
|
46
48
|
export { GenericDiscoverer } from './GenericDiscoverer.js';
|
|
47
49
|
export { GoDiscoverer } from './GoDiscoverer.js';
|
|
50
|
+
export { RustDiscoverer } from './RustDiscoverer.js';
|
|
48
51
|
export { JvmDiscoverer } from './JvmDiscoverer.js';
|
|
49
52
|
export { NodeDiscoverer } from './NodeDiscoverer.js';
|
|
50
53
|
// Re-exports
|