autosnippet 3.0.13 → 3.1.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/bin/api-server.js +2 -0
- package/bin/cli.js +24 -19
- package/config/default.json +1 -1
- package/lib/bootstrap.js +4 -4
- package/lib/cli/SetupService.js +29 -29
- package/lib/cli/UpgradeService.js +3 -2
- package/lib/core/AstAnalyzer.js +1 -1
- package/lib/core/ast/ensure-grammars.js +1 -1
- package/lib/core/ast/index.js +62 -11
- package/lib/core/ast/lang-dart.js +27 -21
- package/lib/core/ast/lang-go.js +6 -20
- package/lib/core/ast/lang-rust.js +53 -28
- package/lib/core/ast/parser-init.js +9 -5
- package/lib/core/discovery/DartDiscoverer.js +4 -10
- package/lib/core/discovery/GoDiscoverer.js +45 -25
- package/lib/core/discovery/NodeDiscoverer.js +1 -3
- package/lib/core/discovery/PythonDiscoverer.js +7 -1
- package/lib/core/discovery/RustDiscoverer.js +111 -38
- package/lib/core/discovery/index.js +2 -2
- package/lib/core/enhancement/django-enhancement.js +10 -4
- package/lib/core/enhancement/fastapi-enhancement.js +16 -9
- package/lib/core/enhancement/go-grpc-enhancement.js +2 -1
- package/lib/core/enhancement/go-web-enhancement.js +3 -6
- package/lib/core/enhancement/ml-enhancement.js +6 -3
- package/lib/core/enhancement/nextjs-enhancement.js +17 -7
- package/lib/core/enhancement/node-server-enhancement.js +4 -2
- package/lib/core/enhancement/react-enhancement.js +6 -3
- package/lib/core/enhancement/rust-tokio-enhancement.js +6 -2
- package/lib/core/enhancement/rust-web-enhancement.js +13 -7
- package/lib/core/enhancement/vue-enhancement.js +10 -5
- package/lib/external/ai/AiFactory.js +3 -1
- package/lib/external/ai/AiProvider.js +3 -1
- package/lib/external/mcp/McpServer.js +2 -0
- package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +1 -2
- package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +7 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +55 -26
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +8 -8
- package/lib/external/mcp/handlers/bootstrap/refine.js +3 -1
- package/lib/external/mcp/handlers/bootstrap.js +4 -10
- package/lib/external/mcp/handlers/browse.js +6 -2
- package/lib/external/mcp/handlers/guard.js +6 -2
- package/lib/external/mcp/handlers/skill.js +6 -2
- package/lib/http/HttpServer.js +1 -1
- package/lib/http/routes/candidates.js +3 -1
- package/lib/http/routes/extract.js +4 -5
- package/lib/http/routes/guardRules.js +1 -1
- package/lib/http/routes/modules.js +9 -3
- package/lib/http/routes/skills.js +54 -6
- package/lib/http/routes/violations.js +4 -3
- package/lib/infrastructure/external/ClipboardManager.js +24 -7
- package/lib/infrastructure/external/NativeUi.js +3 -1
- package/lib/infrastructure/external/OpenBrowser.js +1 -0
- package/lib/infrastructure/external/XcodeAutomation.js +5 -5
- package/lib/infrastructure/vector/IndexingPipeline.js +14 -5
- package/lib/injection/ServiceContainer.js +34 -11
- package/lib/platform/ios/index.js +20 -25
- package/lib/platform/ios/routes/spm.js +6 -3
- package/lib/platform/ios/snippet/PlaceholderConverter.js +6 -2
- package/lib/platform/ios/snippet/XcodeCodec.js +4 -2
- package/lib/platform/ios/spm/SpmDiscoverer.js +1 -1
- package/lib/platform/ios/spm/SpmService.js +3 -1
- package/lib/platform/ios/xcode/XcodeIntegration.js +10 -12
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +6 -1
- package/lib/service/automation/FileWatcher.js +1 -3
- package/lib/service/automation/handlers/CreateHandler.js +3 -5
- package/lib/service/automation/handlers/GuardHandler.js +11 -32
- package/lib/service/automation/handlers/SearchHandler.js +9 -9
- package/lib/service/chat/CandidateGuardrail.js +11 -6
- package/lib/service/chat/ChatAgent.js +31 -22
- package/lib/service/chat/HandoffProtocol.js +5 -2
- package/lib/service/chat/tools/composite.js +3 -2
- package/lib/service/chat/tools/index.js +60 -71
- package/lib/service/chat/tools/infrastructure.js +9 -4
- package/lib/service/chat/tools/lifecycle.js +22 -5
- package/lib/service/chat/tools/project-access.js +5 -9
- package/lib/service/chat/tools.js +1 -2
- package/lib/service/cursor/AgentInstructionsGenerator.js +33 -15
- package/lib/service/cursor/CursorDeliveryPipeline.js +2 -1
- package/lib/service/cursor/KnowledgeCompressor.js +16 -7
- package/lib/service/guard/ComplianceReporter.js +5 -2
- package/lib/service/guard/GuardCheckEngine.js +53 -26
- package/lib/service/guard/GuardCodeChecks.js +217 -188
- package/lib/service/guard/GuardCrossFileChecks.js +203 -184
- package/lib/service/guard/GuardPatternUtils.js +17 -10
- package/lib/service/module/ModuleService.js +180 -56
- package/lib/service/recipe/RecipeCandidateValidator.js +11 -8
- package/lib/service/snippet/SnippetFactory.js +3 -3
- package/lib/service/snippet/SnippetInstaller.js +35 -11
- package/lib/service/snippet/codecs/VSCodeCodec.js +2 -2
- package/lib/service/wiki/WikiGenerator.js +54 -36
- package/lib/service/wiki/WikiRenderers.js +105 -80
- package/lib/service/wiki/WikiUtils.js +217 -80
- package/lib/shared/LanguageService.js +111 -53
- package/lib/shared/PathGuard.js +0 -8
- package/package.json +3 -9
- package/scripts/bench-real-projects.mjs +29 -29
- package/scripts/generate-recipe-drafts.js +17 -27
- package/scripts/init-snippets.js +43 -24
- package/scripts/install-vscode-copilot.js +3 -19
- package/scripts/setup-mcp-config.js +0 -4
|
@@ -6,26 +6,69 @@
|
|
|
6
6
|
* 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import {
|
|
11
|
+
basename as _pathBasename,
|
|
12
|
+
extname as _pathExtname,
|
|
13
|
+
isAbsolute as _pathIsAbsolute,
|
|
14
|
+
join as _pathJoin,
|
|
15
|
+
relative,
|
|
16
|
+
} from 'node:path';
|
|
12
17
|
import { getDiscovererRegistry } from '../../core/discovery/index.js';
|
|
13
18
|
import { inferLang } from '../../external/mcp/handlers/LanguageExtensions.js';
|
|
19
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
14
20
|
|
|
15
21
|
/** 全局排除目录 */
|
|
16
22
|
const SCAN_EXCLUDE_DIRS = new Set([
|
|
17
|
-
'node_modules',
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
23
|
+
'node_modules',
|
|
24
|
+
'.git',
|
|
25
|
+
'dist',
|
|
26
|
+
'build',
|
|
27
|
+
'.next',
|
|
28
|
+
'Pods',
|
|
29
|
+
'Carthage',
|
|
30
|
+
'.build',
|
|
31
|
+
'DerivedData',
|
|
32
|
+
'vendor',
|
|
33
|
+
'__pycache__',
|
|
34
|
+
'.venv',
|
|
35
|
+
'venv',
|
|
36
|
+
'target',
|
|
37
|
+
'.gradle',
|
|
38
|
+
'.idea',
|
|
39
|
+
'out',
|
|
40
|
+
'coverage',
|
|
41
|
+
'.cache',
|
|
42
|
+
'.tox',
|
|
43
|
+
'.mypy_cache',
|
|
44
|
+
'.pytest_cache',
|
|
45
|
+
'AutoSnippet',
|
|
21
46
|
]);
|
|
22
47
|
|
|
23
48
|
/** 源码文件扩展名 */
|
|
24
49
|
const SOURCE_CODE_EXTS = new Set([
|
|
25
|
-
'.swift',
|
|
26
|
-
'.
|
|
27
|
-
'.
|
|
28
|
-
'.
|
|
50
|
+
'.swift',
|
|
51
|
+
'.m',
|
|
52
|
+
'.mm',
|
|
53
|
+
'.h',
|
|
54
|
+
'.js',
|
|
55
|
+
'.ts',
|
|
56
|
+
'.tsx',
|
|
57
|
+
'.jsx',
|
|
58
|
+
'.mjs',
|
|
59
|
+
'.cjs',
|
|
60
|
+
'.py',
|
|
61
|
+
'.java',
|
|
62
|
+
'.kt',
|
|
63
|
+
'.kts',
|
|
64
|
+
'.go',
|
|
65
|
+
'.rs',
|
|
66
|
+
'.rb',
|
|
67
|
+
'.vue',
|
|
68
|
+
'.svelte',
|
|
69
|
+
'.c',
|
|
70
|
+
'.cpp',
|
|
71
|
+
'.cs',
|
|
29
72
|
]);
|
|
30
73
|
|
|
31
74
|
export class ModuleService {
|
|
@@ -82,7 +125,9 @@ export class ModuleService {
|
|
|
82
125
|
* 自动检测项目类型并加载所有匹配的 Discoverer
|
|
83
126
|
*/
|
|
84
127
|
async load() {
|
|
85
|
-
if (this.#loaded)
|
|
128
|
+
if (this.#loaded) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
86
131
|
|
|
87
132
|
const matches = await this.#registry.detectAll(this.#projectRoot);
|
|
88
133
|
this.#activeDiscoverers = [];
|
|
@@ -143,12 +188,16 @@ export class ModuleService {
|
|
|
143
188
|
|
|
144
189
|
// 第一遍:加载非 generic 的 Discoverer(真实项目结构识别器)
|
|
145
190
|
for (const { discoverer } of this.#activeDiscoverers) {
|
|
146
|
-
if (discoverer.id === 'generic')
|
|
191
|
+
if (discoverer.id === 'generic') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
147
194
|
try {
|
|
148
195
|
const targets = await discoverer.listTargets();
|
|
149
196
|
for (const t of targets) {
|
|
150
197
|
const key = `${discoverer.id}::${t.name}`;
|
|
151
|
-
if (seenNames.has(key))
|
|
198
|
+
if (seenNames.has(key)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
152
201
|
seenNames.add(key);
|
|
153
202
|
allTargets.push(this.#normalizeTarget(t, discoverer));
|
|
154
203
|
hasRealDiscovererTargets = true;
|
|
@@ -163,12 +212,16 @@ export class ModuleService {
|
|
|
163
212
|
// 第二遍:仅当没有真实 Discoverer 产出 target 时,才加载 GenericDiscoverer 的结果(兜底)
|
|
164
213
|
if (!hasRealDiscovererTargets) {
|
|
165
214
|
for (const { discoverer } of this.#activeDiscoverers) {
|
|
166
|
-
if (discoverer.id !== 'generic')
|
|
215
|
+
if (discoverer.id !== 'generic') {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
167
218
|
try {
|
|
168
219
|
const targets = await discoverer.listTargets();
|
|
169
220
|
for (const t of targets) {
|
|
170
221
|
const key = `${discoverer.id}::${t.name}`;
|
|
171
|
-
if (seenNames.has(key))
|
|
222
|
+
if (seenNames.has(key)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
172
225
|
seenNames.add(key);
|
|
173
226
|
allTargets.push(this.#normalizeTarget(t, discoverer));
|
|
174
227
|
}
|
|
@@ -235,14 +288,14 @@ export class ModuleService {
|
|
|
235
288
|
if (targets.some((t) => t.name === targetObj.name)) {
|
|
236
289
|
return discoverer.getTargetFiles(targetObj);
|
|
237
290
|
}
|
|
238
|
-
} catch {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
291
|
+
} catch {}
|
|
241
292
|
}
|
|
242
293
|
|
|
243
294
|
// 兜底:如果 target 有 path 属性且目录存在,直接收集
|
|
244
295
|
if (targetObj.path && existsSync(targetObj.path)) {
|
|
245
|
-
this.#logger.info(
|
|
296
|
+
this.#logger.info(
|
|
297
|
+
`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`
|
|
298
|
+
);
|
|
246
299
|
return this.#collectFolderFiles(targetObj.path);
|
|
247
300
|
}
|
|
248
301
|
|
|
@@ -459,7 +512,9 @@ export class ModuleService {
|
|
|
459
512
|
const fileList = await this.getTargetFiles(t);
|
|
460
513
|
for (const f of fileList) {
|
|
461
514
|
const fp = typeof f === 'string' ? f : f.path;
|
|
462
|
-
if (seenPaths.has(fp))
|
|
515
|
+
if (seenPaths.has(fp)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
463
518
|
seenPaths.add(fp);
|
|
464
519
|
try {
|
|
465
520
|
const content = readFileSync(fp, 'utf8');
|
|
@@ -470,19 +525,27 @@ export class ModuleService {
|
|
|
470
525
|
content,
|
|
471
526
|
targetName: t.name,
|
|
472
527
|
});
|
|
473
|
-
} catch {
|
|
474
|
-
|
|
528
|
+
} catch {
|
|
529
|
+
/* unreadable */
|
|
530
|
+
}
|
|
531
|
+
if (allFiles.length >= MAX_FILES) {
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
475
534
|
}
|
|
476
535
|
} catch (e) {
|
|
477
536
|
this.#logger.warn(`[ModuleService] scanProject: skipping module ${t.name}: ${e.message}`);
|
|
478
537
|
}
|
|
479
|
-
if (allFiles.length >= MAX_FILES)
|
|
538
|
+
if (allFiles.length >= MAX_FILES) {
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
480
541
|
}
|
|
481
542
|
}
|
|
482
543
|
|
|
483
544
|
// 如果没有 target 收集到文件,回退到目录扫描
|
|
484
545
|
if (allFiles.length === 0) {
|
|
485
|
-
this.#logger.info(
|
|
546
|
+
this.#logger.info(
|
|
547
|
+
'[ModuleService] scanProject: No module targets, falling back to directory scan'
|
|
548
|
+
);
|
|
486
549
|
this.#walkProjectForFiles(allFiles, seenPaths, MAX_FILES);
|
|
487
550
|
}
|
|
488
551
|
|
|
@@ -611,9 +674,7 @@ export class ModuleService {
|
|
|
611
674
|
* @returns {Promise<Array<{ name: string, path: string, depth: number, language: string, sourceFileCount: number, hasSourceFiles: boolean }>>}
|
|
612
675
|
*/
|
|
613
676
|
async browseDirectories(basePath = '', maxDepth = 2) {
|
|
614
|
-
const root = basePath
|
|
615
|
-
? _pathJoin(this.#projectRoot, basePath)
|
|
616
|
-
: this.#projectRoot;
|
|
677
|
+
const root = basePath ? _pathJoin(this.#projectRoot, basePath) : this.#projectRoot;
|
|
617
678
|
|
|
618
679
|
if (!existsSync(root)) {
|
|
619
680
|
return [];
|
|
@@ -727,7 +788,9 @@ export class ModuleService {
|
|
|
727
788
|
createProvider,
|
|
728
789
|
} = this.#aiFactory;
|
|
729
790
|
let ai = await getProviderWithFallback();
|
|
730
|
-
if (!ai)
|
|
791
|
+
if (!ai) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
731
794
|
|
|
732
795
|
// 加载语言参考 Skill
|
|
733
796
|
let extractOpts = {};
|
|
@@ -741,7 +804,9 @@ export class ModuleService {
|
|
|
741
804
|
extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
|
|
742
805
|
}
|
|
743
806
|
}
|
|
744
|
-
} catch {
|
|
807
|
+
} catch {
|
|
808
|
+
/* Skills not available */
|
|
809
|
+
}
|
|
745
810
|
|
|
746
811
|
try {
|
|
747
812
|
return await ai.extractRecipes(targetName, files, extractOpts);
|
|
@@ -798,13 +863,21 @@ export class ModuleService {
|
|
|
798
863
|
* 目录遍历 — 浏览子目录结构
|
|
799
864
|
*/
|
|
800
865
|
#walkDirsForBrowse(dir, dirs, depth, maxDepth) {
|
|
801
|
-
if (depth >= maxDepth)
|
|
866
|
+
if (depth >= maxDepth) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
802
869
|
try {
|
|
803
870
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
804
871
|
for (const entry of entries) {
|
|
805
|
-
if (!entry.isDirectory())
|
|
806
|
-
|
|
807
|
-
|
|
872
|
+
if (!entry.isDirectory()) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (entry.name.startsWith('.')) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
808
881
|
|
|
809
882
|
const fullPath = _pathJoin(dir, entry.name);
|
|
810
883
|
const relativePath = relative(this.#projectRoot, fullPath);
|
|
@@ -826,14 +899,18 @@ export class ModuleService {
|
|
|
826
899
|
|
|
827
900
|
this.#walkDirsForBrowse(fullPath, dirs, depth + 1, maxDepth);
|
|
828
901
|
}
|
|
829
|
-
} catch {
|
|
902
|
+
} catch {
|
|
903
|
+
/* skip */
|
|
904
|
+
}
|
|
830
905
|
}
|
|
831
906
|
|
|
832
907
|
/**
|
|
833
908
|
* 递归统计目录下源码文件数(限深度 + 上限 999 防止超大目录卡顿)
|
|
834
909
|
*/
|
|
835
910
|
#countSourceFilesDeep(dir, maxDepth, depth = 0) {
|
|
836
|
-
if (depth >= maxDepth)
|
|
911
|
+
if (depth >= maxDepth) {
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
837
914
|
let count = 0;
|
|
838
915
|
try {
|
|
839
916
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -843,9 +920,13 @@ export class ModuleService {
|
|
|
843
920
|
} else if (e.isDirectory() && !e.name.startsWith('.') && !SCAN_EXCLUDE_DIRS.has(e.name)) {
|
|
844
921
|
count += this.#countSourceFilesDeep(_pathJoin(dir, e.name), maxDepth, depth + 1);
|
|
845
922
|
}
|
|
846
|
-
if (count >= 999)
|
|
923
|
+
if (count >= 999) {
|
|
924
|
+
return count;
|
|
925
|
+
}
|
|
847
926
|
}
|
|
848
|
-
} catch {
|
|
927
|
+
} catch {
|
|
928
|
+
/* skip */
|
|
929
|
+
}
|
|
849
930
|
return count;
|
|
850
931
|
}
|
|
851
932
|
|
|
@@ -862,12 +943,18 @@ export class ModuleService {
|
|
|
862
943
|
* 递归收集源码文件
|
|
863
944
|
*/
|
|
864
945
|
#walkCollectSourceFiles(dir, rootDir, files, depth, maxDepth) {
|
|
865
|
-
if (depth > maxDepth || files.length > 500)
|
|
946
|
+
if (depth > maxDepth || files.length > 500) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
866
949
|
try {
|
|
867
950
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
868
951
|
for (const entry of entries) {
|
|
869
|
-
if (entry.name.startsWith('.'))
|
|
870
|
-
|
|
952
|
+
if (entry.name.startsWith('.')) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
871
958
|
|
|
872
959
|
const fullPath = _pathJoin(dir, entry.name);
|
|
873
960
|
if (entry.isDirectory()) {
|
|
@@ -884,7 +971,9 @@ export class ModuleService {
|
|
|
884
971
|
}
|
|
885
972
|
}
|
|
886
973
|
}
|
|
887
|
-
} catch {
|
|
974
|
+
} catch {
|
|
975
|
+
/* skip */
|
|
976
|
+
}
|
|
888
977
|
}
|
|
889
978
|
|
|
890
979
|
/**
|
|
@@ -895,15 +984,21 @@ export class ModuleService {
|
|
|
895
984
|
try {
|
|
896
985
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
897
986
|
for (const entry of entries) {
|
|
898
|
-
if (!entry.isFile())
|
|
987
|
+
if (!entry.isFile()) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
899
990
|
const ext = _pathExtname(entry.name).toLowerCase();
|
|
900
|
-
if (!SOURCE_CODE_EXTS.has(ext))
|
|
991
|
+
if (!SOURCE_CODE_EXTS.has(ext)) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
901
994
|
const lang = inferLang(entry.name);
|
|
902
995
|
if (lang) {
|
|
903
996
|
langCount[lang] = (langCount[lang] || 0) + 1;
|
|
904
997
|
}
|
|
905
998
|
}
|
|
906
|
-
} catch {
|
|
999
|
+
} catch {
|
|
1000
|
+
/* skip */
|
|
1001
|
+
}
|
|
907
1002
|
|
|
908
1003
|
let maxLang = 'unknown';
|
|
909
1004
|
let maxCount = 0;
|
|
@@ -921,29 +1016,56 @@ export class ModuleService {
|
|
|
921
1016
|
*/
|
|
922
1017
|
#walkProjectForFiles(allFiles, seenPaths, maxFiles) {
|
|
923
1018
|
const srcDirs = [
|
|
924
|
-
'Sources',
|
|
925
|
-
'
|
|
1019
|
+
'Sources',
|
|
1020
|
+
'src',
|
|
1021
|
+
'lib',
|
|
1022
|
+
'app',
|
|
1023
|
+
'pages',
|
|
1024
|
+
'components',
|
|
1025
|
+
'modules',
|
|
1026
|
+
'packages',
|
|
1027
|
+
'cmd',
|
|
1028
|
+
'internal',
|
|
1029
|
+
'pkg',
|
|
926
1030
|
];
|
|
927
1031
|
|
|
928
1032
|
const walkDir = (dir, targetName) => {
|
|
929
|
-
if (allFiles.length >= maxFiles)
|
|
1033
|
+
if (allFiles.length >= maxFiles) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
930
1036
|
let entries;
|
|
931
|
-
try {
|
|
1037
|
+
try {
|
|
1038
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1039
|
+
} catch {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
932
1042
|
for (const ent of entries) {
|
|
933
|
-
if (allFiles.length >= maxFiles)
|
|
934
|
-
|
|
1043
|
+
if (allFiles.length >= maxFiles) {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
if (ent.name.startsWith('.')) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
935
1049
|
const fp = _pathJoin(dir, ent.name);
|
|
936
1050
|
if (ent.isDirectory()) {
|
|
937
|
-
if (SCAN_EXCLUDE_DIRS.has(ent.name))
|
|
1051
|
+
if (SCAN_EXCLUDE_DIRS.has(ent.name)) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
938
1054
|
walkDir(fp, targetName);
|
|
939
1055
|
} else if (ent.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(ent.name).toLowerCase())) {
|
|
940
|
-
if (seenPaths.has(fp))
|
|
1056
|
+
if (seenPaths.has(fp)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
941
1059
|
seenPaths.add(fp);
|
|
942
1060
|
try {
|
|
943
1061
|
const st = statSync(fp);
|
|
944
|
-
if (st.size > 512 * 1024)
|
|
1062
|
+
if (st.size > 512 * 1024) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
945
1065
|
const content = readFileSync(fp, 'utf8');
|
|
946
|
-
if (content.split('\n').length < 5)
|
|
1066
|
+
if (content.split('\n').length < 5) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
947
1069
|
allFiles.push({
|
|
948
1070
|
name: ent.name,
|
|
949
1071
|
path: fp,
|
|
@@ -951,7 +1073,9 @@ export class ModuleService {
|
|
|
951
1073
|
content,
|
|
952
1074
|
targetName,
|
|
953
1075
|
});
|
|
954
|
-
} catch {
|
|
1076
|
+
} catch {
|
|
1077
|
+
/* unreadable */
|
|
1078
|
+
}
|
|
955
1079
|
}
|
|
956
1080
|
}
|
|
957
1081
|
};
|
|
@@ -9,17 +9,18 @@ import { LanguageService } from '../../shared/LanguageService.js';
|
|
|
9
9
|
|
|
10
10
|
/* ── V3 必填字段 ── */
|
|
11
11
|
const REQUIRED_FIELDS = [
|
|
12
|
-
'title',
|
|
13
|
-
'trigger',
|
|
14
|
-
'category',
|
|
15
|
-
'language',
|
|
16
|
-
'kind',
|
|
17
|
-
'doClause',
|
|
12
|
+
'title', // 中文简短标题
|
|
13
|
+
'trigger', // @前缀 kebab-case
|
|
14
|
+
'category', // View/Service/Tool/Model/Network/Storage/UI/Utility
|
|
15
|
+
'language', // 编程语言标识
|
|
16
|
+
'kind', // rule / pattern / fact
|
|
17
|
+
'doClause', // 英文祈使句正向指令
|
|
18
18
|
'description', // 中文简述 ≤80字
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
/* ── 需要 content 子对象有内容 ── */
|
|
22
|
-
|
|
22
|
+
// NOTE: reserved for future content sub-field validation
|
|
23
|
+
// const REQUIRED_CONTENT_FIELDS = ['pattern', 'markdown', 'rationale'];
|
|
23
24
|
|
|
24
25
|
const VALID_CATEGORIES = new Set([
|
|
25
26
|
'view',
|
|
@@ -94,7 +95,9 @@ export class RecipeCandidateValidator {
|
|
|
94
95
|
|
|
95
96
|
// ── category 合法性 ──
|
|
96
97
|
if (candidate.category && !VALID_CATEGORIES.has(candidate.category.toLowerCase())) {
|
|
97
|
-
warnings.push(
|
|
98
|
+
warnings.push(
|
|
99
|
+
`category "${candidate.category}" 不在标准列表(View/Service/Tool/Model/Network/Storage/UI/Utility)`
|
|
100
|
+
);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
// ── language 合法性 ──
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* factory.generate(spec, 'xcode') — 按 target 生成
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { XcodeCodec } from './codecs/XcodeCodec.js';
|
|
15
|
-
|
|
16
14
|
export class SnippetFactory {
|
|
17
15
|
/** @type {Map<string, import('./codecs/SnippetCodec.js').SnippetCodec>} */
|
|
18
16
|
#codecs = new Map();
|
|
@@ -174,7 +172,9 @@ export class SnippetFactory {
|
|
|
174
172
|
#resolveCodec(target) {
|
|
175
173
|
const codec = this.#codecs.get(target);
|
|
176
174
|
if (!codec) {
|
|
177
|
-
throw new Error(
|
|
175
|
+
throw new Error(
|
|
176
|
+
`No codec registered for target "${target}". Available: [${this.getRegisteredTargets().join(', ')}]`
|
|
177
|
+
);
|
|
178
178
|
}
|
|
179
179
|
return codec;
|
|
180
180
|
}
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* 行为由注入的 SnippetCodec 决定。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from 'node:fs';
|
|
12
19
|
import { join } from 'node:path';
|
|
13
20
|
|
|
14
21
|
export class SnippetInstaller {
|
|
@@ -106,8 +113,11 @@ export class SnippetInstaller {
|
|
|
106
113
|
for (const spec of specs) {
|
|
107
114
|
const result = this.install(spec, projectRoot);
|
|
108
115
|
details.push(result);
|
|
109
|
-
if (result.success)
|
|
110
|
-
|
|
116
|
+
if (result.success) {
|
|
117
|
+
successCount++;
|
|
118
|
+
} else {
|
|
119
|
+
errorCount++;
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
return { success: errorCount === 0, count: recipes.length, successCount, errorCount, details };
|
|
@@ -122,13 +132,17 @@ export class SnippetInstaller {
|
|
|
122
132
|
*/
|
|
123
133
|
listInstalled(projectRoot) {
|
|
124
134
|
const dir = this.#resolveDir(projectRoot);
|
|
125
|
-
if (!existsSync(dir))
|
|
135
|
+
if (!existsSync(dir)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
126
138
|
|
|
127
139
|
const bundleFilename = this.#codec?.getBundleFilename();
|
|
128
140
|
if (bundleFilename) {
|
|
129
141
|
// VSCode: 检查 bundle 文件是否存在
|
|
130
142
|
const bundlePath = join(dir, bundleFilename);
|
|
131
|
-
if (!existsSync(bundlePath))
|
|
143
|
+
if (!existsSync(bundlePath)) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
132
146
|
try {
|
|
133
147
|
const content = JSON.parse(readFileSync(bundlePath, 'utf-8'));
|
|
134
148
|
return Object.keys(content).map((key) => ({
|
|
@@ -142,7 +156,11 @@ export class SnippetInstaller {
|
|
|
142
156
|
|
|
143
157
|
// Xcode: 列出 com.autosnippet.*.codesnippet 文件
|
|
144
158
|
return readdirSync(dir)
|
|
145
|
-
.filter(
|
|
159
|
+
.filter(
|
|
160
|
+
(f) =>
|
|
161
|
+
f.startsWith('com.autosnippet.') &&
|
|
162
|
+
f.endsWith(this.#codec?.fileExtension || '.codesnippet')
|
|
163
|
+
)
|
|
146
164
|
.map((f) => ({ filename: f, path: join(dir, f) }));
|
|
147
165
|
}
|
|
148
166
|
|
|
@@ -210,15 +228,21 @@ export class SnippetInstaller {
|
|
|
210
228
|
// ─────────────── Private ───────────────
|
|
211
229
|
|
|
212
230
|
#assertCodec() {
|
|
213
|
-
if (!this.#codec)
|
|
231
|
+
if (!this.#codec) {
|
|
232
|
+
throw new Error('SnippetCodec not set');
|
|
233
|
+
}
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
#assertFactory() {
|
|
217
|
-
if (!this.#snippetFactory)
|
|
237
|
+
if (!this.#snippetFactory) {
|
|
238
|
+
throw new Error('SnippetFactory not set');
|
|
239
|
+
}
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
#resolveDir(projectRoot) {
|
|
221
|
-
if (this.#snippetsDirOverride)
|
|
243
|
+
if (this.#snippetsDirOverride) {
|
|
244
|
+
return this.#snippetsDirOverride;
|
|
245
|
+
}
|
|
222
246
|
return this.#codec?.getInstallDir(projectRoot || process.cwd()) || '';
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -247,7 +271,7 @@ export class SnippetInstaller {
|
|
|
247
271
|
const entryKey = Object.keys(content)[0];
|
|
248
272
|
bundle[key] = content[entryKey];
|
|
249
273
|
|
|
250
|
-
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2)
|
|
274
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
251
275
|
return { success: true, path: bundlePath, message: `Installed: ${key}` };
|
|
252
276
|
}
|
|
253
277
|
|
|
@@ -296,7 +320,7 @@ export class SnippetInstaller {
|
|
|
296
320
|
return { success: false, message: `Snippet not found in bundle: ${identifier}` };
|
|
297
321
|
}
|
|
298
322
|
delete bundle[keyToRemove];
|
|
299
|
-
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2)
|
|
323
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
300
324
|
return { success: true, message: `Uninstalled: ${keyToRemove}` };
|
|
301
325
|
} catch (error) {
|
|
302
326
|
return { success: false, message: error.message };
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { SnippetCodec } from './SnippetCodec.js';
|
|
13
12
|
import { PlaceholderConverter } from '../PlaceholderConverter.js';
|
|
13
|
+
import { SnippetCodec } from './SnippetCodec.js';
|
|
14
14
|
|
|
15
15
|
/** AutoSnippet language → VSCode snippet scope */
|
|
16
16
|
const VSCODE_LANGUAGE_MAP = {
|
|
@@ -58,7 +58,7 @@ export class VSCodeCodec extends SnippetCodec {
|
|
|
58
58
|
const key = `Recipe: ${spec.title || spec.identifier}`;
|
|
59
59
|
bundle[key] = this.#specToEntry(spec);
|
|
60
60
|
}
|
|
61
|
-
return JSON.stringify(bundle, null, 2)
|
|
61
|
+
return `${JSON.stringify(bundle, null, 2)}\n`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|