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.
Files changed (100) hide show
  1. package/bin/api-server.js +2 -0
  2. package/bin/cli.js +24 -19
  3. package/config/default.json +1 -1
  4. package/lib/bootstrap.js +4 -4
  5. package/lib/cli/SetupService.js +29 -29
  6. package/lib/cli/UpgradeService.js +3 -2
  7. package/lib/core/AstAnalyzer.js +1 -1
  8. package/lib/core/ast/ensure-grammars.js +1 -1
  9. package/lib/core/ast/index.js +62 -11
  10. package/lib/core/ast/lang-dart.js +27 -21
  11. package/lib/core/ast/lang-go.js +6 -20
  12. package/lib/core/ast/lang-rust.js +53 -28
  13. package/lib/core/ast/parser-init.js +9 -5
  14. package/lib/core/discovery/DartDiscoverer.js +4 -10
  15. package/lib/core/discovery/GoDiscoverer.js +45 -25
  16. package/lib/core/discovery/NodeDiscoverer.js +1 -3
  17. package/lib/core/discovery/PythonDiscoverer.js +7 -1
  18. package/lib/core/discovery/RustDiscoverer.js +111 -38
  19. package/lib/core/discovery/index.js +2 -2
  20. package/lib/core/enhancement/django-enhancement.js +10 -4
  21. package/lib/core/enhancement/fastapi-enhancement.js +16 -9
  22. package/lib/core/enhancement/go-grpc-enhancement.js +2 -1
  23. package/lib/core/enhancement/go-web-enhancement.js +3 -6
  24. package/lib/core/enhancement/ml-enhancement.js +6 -3
  25. package/lib/core/enhancement/nextjs-enhancement.js +17 -7
  26. package/lib/core/enhancement/node-server-enhancement.js +4 -2
  27. package/lib/core/enhancement/react-enhancement.js +6 -3
  28. package/lib/core/enhancement/rust-tokio-enhancement.js +6 -2
  29. package/lib/core/enhancement/rust-web-enhancement.js +13 -7
  30. package/lib/core/enhancement/vue-enhancement.js +10 -5
  31. package/lib/external/ai/AiFactory.js +3 -1
  32. package/lib/external/ai/AiProvider.js +3 -1
  33. package/lib/external/mcp/McpServer.js +2 -0
  34. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +1 -2
  35. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +7 -1
  36. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +55 -26
  37. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +8 -8
  38. package/lib/external/mcp/handlers/bootstrap/refine.js +3 -1
  39. package/lib/external/mcp/handlers/bootstrap.js +4 -10
  40. package/lib/external/mcp/handlers/browse.js +6 -2
  41. package/lib/external/mcp/handlers/guard.js +6 -2
  42. package/lib/external/mcp/handlers/skill.js +6 -2
  43. package/lib/http/HttpServer.js +1 -1
  44. package/lib/http/routes/candidates.js +3 -1
  45. package/lib/http/routes/extract.js +4 -5
  46. package/lib/http/routes/guardRules.js +1 -1
  47. package/lib/http/routes/modules.js +9 -3
  48. package/lib/http/routes/skills.js +54 -6
  49. package/lib/http/routes/violations.js +4 -3
  50. package/lib/infrastructure/external/ClipboardManager.js +24 -7
  51. package/lib/infrastructure/external/NativeUi.js +3 -1
  52. package/lib/infrastructure/external/OpenBrowser.js +1 -0
  53. package/lib/infrastructure/external/XcodeAutomation.js +5 -5
  54. package/lib/infrastructure/vector/IndexingPipeline.js +14 -5
  55. package/lib/injection/ServiceContainer.js +34 -11
  56. package/lib/platform/ios/index.js +20 -25
  57. package/lib/platform/ios/routes/spm.js +6 -3
  58. package/lib/platform/ios/snippet/PlaceholderConverter.js +6 -2
  59. package/lib/platform/ios/snippet/XcodeCodec.js +4 -2
  60. package/lib/platform/ios/spm/SpmDiscoverer.js +1 -1
  61. package/lib/platform/ios/spm/SpmService.js +3 -1
  62. package/lib/platform/ios/xcode/XcodeIntegration.js +10 -12
  63. package/lib/platform/ios/xcode/XcodeWriteUtils.js +6 -1
  64. package/lib/service/automation/FileWatcher.js +1 -3
  65. package/lib/service/automation/handlers/CreateHandler.js +3 -5
  66. package/lib/service/automation/handlers/GuardHandler.js +11 -32
  67. package/lib/service/automation/handlers/SearchHandler.js +9 -9
  68. package/lib/service/chat/CandidateGuardrail.js +11 -6
  69. package/lib/service/chat/ChatAgent.js +31 -22
  70. package/lib/service/chat/HandoffProtocol.js +5 -2
  71. package/lib/service/chat/tools/composite.js +3 -2
  72. package/lib/service/chat/tools/index.js +60 -71
  73. package/lib/service/chat/tools/infrastructure.js +9 -4
  74. package/lib/service/chat/tools/lifecycle.js +22 -5
  75. package/lib/service/chat/tools/project-access.js +5 -9
  76. package/lib/service/chat/tools.js +1 -2
  77. package/lib/service/cursor/AgentInstructionsGenerator.js +33 -15
  78. package/lib/service/cursor/CursorDeliveryPipeline.js +2 -1
  79. package/lib/service/cursor/KnowledgeCompressor.js +16 -7
  80. package/lib/service/guard/ComplianceReporter.js +5 -2
  81. package/lib/service/guard/GuardCheckEngine.js +53 -26
  82. package/lib/service/guard/GuardCodeChecks.js +217 -188
  83. package/lib/service/guard/GuardCrossFileChecks.js +203 -184
  84. package/lib/service/guard/GuardPatternUtils.js +17 -10
  85. package/lib/service/module/ModuleService.js +180 -56
  86. package/lib/service/recipe/RecipeCandidateValidator.js +11 -8
  87. package/lib/service/snippet/SnippetFactory.js +3 -3
  88. package/lib/service/snippet/SnippetInstaller.js +35 -11
  89. package/lib/service/snippet/codecs/VSCodeCodec.js +2 -2
  90. package/lib/service/wiki/WikiGenerator.js +54 -36
  91. package/lib/service/wiki/WikiRenderers.js +105 -80
  92. package/lib/service/wiki/WikiUtils.js +217 -80
  93. package/lib/shared/LanguageService.js +111 -53
  94. package/lib/shared/PathGuard.js +0 -8
  95. package/package.json +3 -9
  96. package/scripts/bench-real-projects.mjs +29 -29
  97. package/scripts/generate-recipe-drafts.js +17 -27
  98. package/scripts/init-snippets.js +43 -24
  99. package/scripts/install-vscode-copilot.js +3 -19
  100. package/scripts/setup-mcp-config.js +0 -4
@@ -6,26 +6,69 @@
6
6
  * 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。
7
7
  */
8
8
 
9
- import { basename as _pathBasename, relative, join as _pathJoin, extname as _pathExtname, isAbsolute as _pathIsAbsolute } from 'node:path';
10
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
11
- import Logger from '../../infrastructure/logging/Logger.js';
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', '.git', 'dist', 'build', '.next', 'Pods',
18
- 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__',
19
- '.venv', 'venv', 'target', '.gradle', '.idea', 'out', 'coverage',
20
- '.cache', '.tox', '.mypy_cache', '.pytest_cache', 'AutoSnippet',
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', '.m', '.mm', '.h',
26
- '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs',
27
- '.py', '.java', '.kt', '.kts', '.go', '.rs', '.rb',
28
- '.vue', '.svelte', '.c', '.cpp', '.cs',
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) return;
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') continue;
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)) continue;
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') continue;
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)) continue;
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(`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`);
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)) continue;
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 { /* unreadable */ }
474
- if (allFiles.length >= MAX_FILES) break;
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) break;
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('[ModuleService] scanProject: No module targets, falling back to directory scan');
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) return [];
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 { /* Skills not available */ }
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) return;
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()) continue;
806
- if (entry.name.startsWith('.')) continue;
807
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
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 { /* skip */ }
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) return 0;
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) return count;
923
+ if (count >= 999) {
924
+ return count;
925
+ }
847
926
  }
848
- } catch { /* skip */ }
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) return;
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('.')) continue;
870
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
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 { /* skip */ }
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()) continue;
987
+ if (!entry.isFile()) {
988
+ continue;
989
+ }
899
990
  const ext = _pathExtname(entry.name).toLowerCase();
900
- if (!SOURCE_CODE_EXTS.has(ext)) continue;
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 { /* skip */ }
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', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages',
925
- 'cmd', 'internal', 'pkg',
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) return;
1033
+ if (allFiles.length >= maxFiles) {
1034
+ return;
1035
+ }
930
1036
  let entries;
931
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
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) break;
934
- if (ent.name.startsWith('.')) continue;
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)) continue;
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)) continue;
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) continue;
1062
+ if (st.size > 512 * 1024) {
1063
+ continue;
1064
+ }
945
1065
  const content = readFileSync(fp, 'utf8');
946
- if (content.split('\n').length < 5) continue;
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 { /* unreadable */ }
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', // @前缀 kebab-case
14
- 'category', // View/Service/Tool/Model/Network/Storage/UI/Utility
15
- 'language', // 编程语言标识
16
- 'kind', // rule / pattern / fact
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
- const REQUIRED_CONTENT_FIELDS = ['pattern', 'markdown', 'rationale'];
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(`category "${candidate.category}" 不在标准列表(View/Service/Tool/Model/Network/Storage/UI/Utility)`);
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(`No codec registered for target "${target}". Available: [${this.getRegisteredTargets().join(', ')}]`);
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 { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
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) successCount++;
110
- else errorCount++;
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)) return [];
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)) return [];
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((f) => f.startsWith('com.autosnippet.') && f.endsWith(this.#codec?.fileExtension || '.codesnippet'))
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) throw new Error('SnippetCodec not set');
231
+ if (!this.#codec) {
232
+ throw new Error('SnippetCodec not set');
233
+ }
214
234
  }
215
235
 
216
236
  #assertFactory() {
217
- if (!this.#snippetFactory) throw new Error('SnippetFactory not set');
237
+ if (!this.#snippetFactory) {
238
+ throw new Error('SnippetFactory not set');
239
+ }
218
240
  }
219
241
 
220
242
  #resolveDir(projectRoot) {
221
- if (this.#snippetsDirOverride) return 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) + '\n');
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) + '\n');
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) + '\n';
61
+ return `${JSON.stringify(bundle, null, 2)}\n`;
62
62
  }
63
63
 
64
64
  /**