autosnippet 3.0.0 → 3.0.2

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 (290) hide show
  1. package/README.md +230 -324
  2. package/bin/api-server.js +1 -1
  3. package/bin/cli.js +204 -244
  4. package/bin/mcp-server.js +5 -3
  5. package/config/knowledge-base.config.js +132 -132
  6. package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
  7. package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
  8. package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
  9. package/dashboard/dist/index.html +3 -3
  10. package/lib/bootstrap.js +8 -8
  11. package/lib/cli/AiScanService.js +86 -40
  12. package/lib/cli/KnowledgeSyncService.js +113 -74
  13. package/lib/cli/SetupService.js +439 -277
  14. package/lib/cli/UpgradeService.js +63 -100
  15. package/lib/core/AstAnalyzer.js +276 -597
  16. package/lib/core/ast/ProjectGraph.js +101 -40
  17. package/lib/core/ast/ensure-grammars.js +232 -0
  18. package/lib/core/ast/index.js +115 -0
  19. package/lib/core/ast/lang-dart.js +661 -0
  20. package/lib/core/ast/lang-go.js +530 -0
  21. package/lib/core/ast/lang-java.js +435 -0
  22. package/lib/core/ast/lang-javascript.js +272 -0
  23. package/lib/core/ast/lang-kotlin.js +423 -0
  24. package/lib/core/ast/lang-objc.js +388 -0
  25. package/lib/core/ast/lang-python.js +371 -0
  26. package/lib/core/ast/lang-swift.js +337 -0
  27. package/lib/core/ast/lang-typescript.js +503 -0
  28. package/lib/core/capability/CapabilityProbe.js +18 -9
  29. package/lib/core/constitution/Constitution.js +2 -3
  30. package/lib/core/constitution/ConstitutionValidator.js +65 -24
  31. package/lib/core/discovery/DartDiscoverer.js +534 -0
  32. package/lib/core/discovery/DiscovererRegistry.js +83 -0
  33. package/lib/core/discovery/GenericDiscoverer.js +225 -0
  34. package/lib/core/discovery/GoDiscoverer.js +541 -0
  35. package/lib/core/discovery/JvmDiscoverer.js +506 -0
  36. package/lib/core/discovery/NodeDiscoverer.js +466 -0
  37. package/lib/core/discovery/ProjectDiscoverer.js +93 -0
  38. package/lib/core/discovery/PythonDiscoverer.js +338 -0
  39. package/lib/core/discovery/SpmDiscoverer.js +5 -0
  40. package/lib/core/discovery/index.js +53 -0
  41. package/lib/core/enhancement/EnhancementPack.js +71 -0
  42. package/lib/core/enhancement/EnhancementRegistry.js +47 -0
  43. package/lib/core/enhancement/android-enhancement.js +102 -0
  44. package/lib/core/enhancement/django-enhancement.js +70 -0
  45. package/lib/core/enhancement/fastapi-enhancement.js +63 -0
  46. package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
  47. package/lib/core/enhancement/go-web-enhancement.js +201 -0
  48. package/lib/core/enhancement/index.js +65 -0
  49. package/lib/core/enhancement/node-server-enhancement.js +88 -0
  50. package/lib/core/enhancement/react-enhancement.js +86 -0
  51. package/lib/core/enhancement/spring-enhancement.js +112 -0
  52. package/lib/core/enhancement/vue-enhancement.js +96 -0
  53. package/lib/core/gateway/Gateway.js +8 -9
  54. package/lib/core/gateway/GatewayActionRegistry.js +1 -1
  55. package/lib/core/permission/PermissionManager.js +12 -8
  56. package/lib/domain/index.js +13 -9
  57. package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
  58. package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
  59. package/lib/domain/knowledge/Lifecycle.js +22 -22
  60. package/lib/domain/knowledge/index.js +9 -12
  61. package/lib/domain/knowledge/values/Constraints.js +31 -21
  62. package/lib/domain/knowledge/values/Content.js +21 -13
  63. package/lib/domain/knowledge/values/Quality.js +31 -18
  64. package/lib/domain/knowledge/values/Reasoning.js +20 -12
  65. package/lib/domain/knowledge/values/Relations.js +37 -25
  66. package/lib/domain/knowledge/values/Stats.js +18 -12
  67. package/lib/domain/knowledge/values/index.js +4 -3
  68. package/lib/domain/snippet/Snippet.js +35 -10
  69. package/lib/external/ai/AiFactory.js +48 -16
  70. package/lib/external/ai/AiProvider.js +184 -90
  71. package/lib/external/ai/providers/ClaudeProvider.js +25 -12
  72. package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
  73. package/lib/external/ai/providers/MockProvider.js +9 -3
  74. package/lib/external/ai/providers/OpenAiProvider.js +51 -29
  75. package/lib/external/mcp/McpServer.js +66 -36
  76. package/lib/external/mcp/errorHandler.js +23 -11
  77. package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
  78. package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
  79. package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
  80. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
  81. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
  82. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
  83. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
  84. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
  85. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
  86. package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
  87. package/lib/external/mcp/handlers/bootstrap.js +691 -168
  88. package/lib/external/mcp/handlers/browse.js +66 -22
  89. package/lib/external/mcp/handlers/candidate.js +118 -35
  90. package/lib/external/mcp/handlers/consolidated.js +49 -17
  91. package/lib/external/mcp/handlers/guard.js +104 -39
  92. package/lib/external/mcp/handlers/knowledge.js +60 -36
  93. package/lib/external/mcp/handlers/search.js +43 -14
  94. package/lib/external/mcp/handlers/skill.js +120 -45
  95. package/lib/external/mcp/handlers/structure.js +240 -86
  96. package/lib/external/mcp/handlers/system.js +42 -12
  97. package/lib/external/mcp/handlers/wiki.js +58 -33
  98. package/lib/external/mcp/tools.js +306 -123
  99. package/lib/http/HttpServer.js +72 -47
  100. package/lib/http/middleware/RateLimiter.js +5 -3
  101. package/lib/http/middleware/errorHandler.js +6 -1
  102. package/lib/http/middleware/requestLogger.js +14 -3
  103. package/lib/http/middleware/roleResolver.js +30 -23
  104. package/lib/http/routes/ai.js +387 -265
  105. package/lib/http/routes/auth.js +81 -61
  106. package/lib/http/routes/candidates.js +430 -320
  107. package/lib/http/routes/commands.js +289 -189
  108. package/lib/http/routes/extract.js +158 -125
  109. package/lib/http/routes/guardRules.js +309 -217
  110. package/lib/http/routes/knowledge.js +213 -154
  111. package/lib/http/routes/modules.js +578 -0
  112. package/lib/http/routes/monitoring.js +6 -6
  113. package/lib/http/routes/recipes.js +104 -93
  114. package/lib/http/routes/search.js +361 -305
  115. package/lib/http/routes/skills.js +145 -98
  116. package/lib/http/routes/snippets.js +42 -30
  117. package/lib/http/routes/spm.js +3 -405
  118. package/lib/http/routes/violations.js +113 -93
  119. package/lib/http/routes/wiki.js +211 -170
  120. package/lib/http/utils/routeHelpers.js +3 -1
  121. package/lib/http/utils/sse-sessions.js +16 -6
  122. package/lib/http/utils/sse.js +15 -5
  123. package/lib/infrastructure/audit/AuditLogger.js +5 -2
  124. package/lib/infrastructure/audit/AuditStore.js +10 -7
  125. package/lib/infrastructure/cache/CacheService.js +3 -1
  126. package/lib/infrastructure/cache/GraphCache.js +8 -4
  127. package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
  128. package/lib/infrastructure/config/ConfigLoader.js +9 -5
  129. package/lib/infrastructure/config/Defaults.js +30 -10
  130. package/lib/infrastructure/config/Paths.js +28 -8
  131. package/lib/infrastructure/config/TriggerSymbol.js +22 -10
  132. package/lib/infrastructure/database/DatabaseConnection.js +15 -10
  133. package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
  134. package/lib/infrastructure/external/ClipboardManager.js +6 -2
  135. package/lib/infrastructure/external/NativeUi.js +50 -43
  136. package/lib/infrastructure/external/OpenBrowser.js +14 -17
  137. package/lib/infrastructure/external/XcodeAutomation.js +14 -258
  138. package/lib/infrastructure/logging/Logger.js +46 -30
  139. package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
  140. package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
  141. package/lib/infrastructure/paths/HeaderResolver.js +25 -9
  142. package/lib/infrastructure/paths/PathFinder.js +34 -12
  143. package/lib/infrastructure/plugin/PluginManager.js +26 -8
  144. package/lib/infrastructure/realtime/RealtimeService.js +2 -2
  145. package/lib/infrastructure/vector/Chunker.js +22 -7
  146. package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
  147. package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
  148. package/lib/infrastructure/vector/VectorStore.js +28 -10
  149. package/lib/injection/ServiceContainer.js +247 -93
  150. package/lib/platform/ios/index.js +63 -0
  151. package/lib/platform/ios/routes/spm.js +437 -0
  152. package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
  153. package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
  154. package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
  155. package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
  156. package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
  157. package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
  158. package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
  159. package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
  160. package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
  161. package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
  162. package/lib/repository/base/BaseRepository.js +7 -9
  163. package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
  164. package/lib/repository/token/TokenUsageStore.js +4 -2
  165. package/lib/service/automation/ActionPipeline.js +1 -1
  166. package/lib/service/automation/AutomationOrchestrator.js +8 -4
  167. package/lib/service/automation/ContextCollector.js +7 -5
  168. package/lib/service/automation/DirectiveDetector.js +23 -16
  169. package/lib/service/automation/FileWatcher.js +112 -56
  170. package/lib/service/automation/TriggerResolver.js +6 -4
  171. package/lib/service/automation/handlers/AlinkHandler.js +24 -12
  172. package/lib/service/automation/handlers/CreateHandler.js +19 -20
  173. package/lib/service/automation/handlers/DraftHandler.js +14 -8
  174. package/lib/service/automation/handlers/GuardHandler.js +93 -63
  175. package/lib/service/automation/handlers/HeaderHandler.js +1 -6
  176. package/lib/service/automation/handlers/SearchHandler.js +155 -88
  177. package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
  178. package/lib/service/candidate/SimilarityService.js +25 -9
  179. package/lib/service/chat/AnalystAgent.js +50 -24
  180. package/lib/service/chat/CandidateGuardrail.js +143 -17
  181. package/lib/service/chat/ChatAgent.js +759 -243
  182. package/lib/service/chat/ContextWindow.js +116 -71
  183. package/lib/service/chat/ConversationStore.js +77 -36
  184. package/lib/service/chat/EpisodicConsolidator.js +47 -23
  185. package/lib/service/chat/HandoffProtocol.js +98 -22
  186. package/lib/service/chat/Memory.js +34 -14
  187. package/lib/service/chat/ProducerAgent.js +40 -20
  188. package/lib/service/chat/ProjectSemanticMemory.js +109 -78
  189. package/lib/service/chat/ReasoningLayer.js +148 -70
  190. package/lib/service/chat/ReasoningTrace.js +44 -32
  191. package/lib/service/chat/TaskPipeline.js +39 -19
  192. package/lib/service/chat/ToolRegistry.js +48 -29
  193. package/lib/service/chat/WorkingMemory.js +44 -18
  194. package/lib/service/chat/tools.js +1096 -494
  195. package/lib/service/context/RecipeExtractor.js +132 -51
  196. package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
  197. package/lib/service/cursor/KnowledgeCompressor.js +25 -22
  198. package/lib/service/cursor/RulesGenerator.js +13 -7
  199. package/lib/service/cursor/SkillsSyncer.js +77 -27
  200. package/lib/service/cursor/TokenBudget.js +2 -2
  201. package/lib/service/cursor/TopicClassifier.js +54 -20
  202. package/lib/service/guard/ComplianceReporter.js +55 -43
  203. package/lib/service/guard/ExclusionManager.js +67 -29
  204. package/lib/service/guard/GuardCheckEngine.js +381 -86
  205. package/lib/service/guard/GuardFeedbackLoop.js +22 -10
  206. package/lib/service/guard/GuardService.js +29 -19
  207. package/lib/service/guard/RuleLearner.js +55 -23
  208. package/lib/service/guard/SourceFileCollector.js +27 -20
  209. package/lib/service/guard/ViolationsStore.js +43 -38
  210. package/lib/service/knowledge/CodeEntityGraph.js +147 -82
  211. package/lib/service/knowledge/ConfidenceRouter.js +12 -10
  212. package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
  213. package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
  214. package/lib/service/knowledge/KnowledgeService.js +222 -112
  215. package/lib/service/module/ModuleService.js +969 -0
  216. package/lib/service/quality/FeedbackCollector.js +27 -15
  217. package/lib/service/quality/QualityScorer.js +78 -24
  218. package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
  219. package/lib/service/recipe/RecipeParser.js +78 -45
  220. package/lib/service/search/CoarseRanker.js +43 -28
  221. package/lib/service/search/CrossEncoderReranker.js +32 -21
  222. package/lib/service/search/InvertedIndex.js +21 -7
  223. package/lib/service/search/MultiSignalRanker.js +90 -28
  224. package/lib/service/search/RetrievalFunnel.js +45 -24
  225. package/lib/service/search/SearchEngine.js +255 -103
  226. package/lib/service/skills/EventAggregator.js +32 -15
  227. package/lib/service/skills/SignalCollector.js +140 -64
  228. package/lib/service/skills/SkillAdvisor.js +79 -42
  229. package/lib/service/skills/SkillHooks.js +16 -14
  230. package/lib/service/snippet/PlaceholderConverter.js +5 -0
  231. package/lib/service/snippet/SnippetFactory.js +116 -99
  232. package/lib/service/snippet/SnippetInstaller.js +234 -62
  233. package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
  234. package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
  235. package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
  236. package/lib/service/wiki/WikiGenerator.js +637 -263
  237. package/lib/shared/DimensionCopyRegistry.js +472 -0
  238. package/lib/shared/LanguageService.js +399 -0
  239. package/lib/shared/PathGuard.js +45 -28
  240. package/lib/shared/RecipeReadinessChecker.js +72 -12
  241. package/lib/shared/constants.js +41 -41
  242. package/lib/shared/errors/BaseError.js +2 -2
  243. package/lib/shared/errors/index.js +4 -4
  244. package/lib/shared/similarity.js +25 -8
  245. package/lib/shared/token-utils.js +6 -2
  246. package/lib/shared/utils/common.js +12 -4
  247. package/package.json +49 -13
  248. package/scripts/bench-real-projects.mjs +256 -0
  249. package/scripts/build-native-ui.js +30 -30
  250. package/scripts/clear-old-vector-index.js +5 -35
  251. package/scripts/clear-vector-cache.js +7 -37
  252. package/scripts/collect-test-project-stats.mjs +160 -0
  253. package/scripts/diagnose-mcp.js +41 -32
  254. package/scripts/ensure-parse-package.js +6 -9
  255. package/scripts/generate-recipe-drafts.js +116 -77
  256. package/scripts/init-db.js +3 -20
  257. package/scripts/init-snippets.js +305 -0
  258. package/scripts/init-vector-db.js +173 -170
  259. package/scripts/install-cursor-skill.js +148 -104
  260. package/scripts/install-full.js +8 -21
  261. package/scripts/install-vscode-copilot.js +146 -145
  262. package/scripts/migrate-md-to-knowledge.mjs +139 -151
  263. package/scripts/postinstall-safe.js +5 -17
  264. package/scripts/recipe-audit.js +106 -82
  265. package/scripts/release.js +283 -323
  266. package/scripts/setup-mcp-config.js +60 -52
  267. package/scripts/verify-context-api.js +20 -20
  268. package/skills/autosnippet-analysis/SKILL.md +10 -6
  269. package/skills/autosnippet-candidates/SKILL.md +27 -26
  270. package/skills/autosnippet-coldstart/SKILL.md +555 -38
  271. package/skills/autosnippet-concepts/SKILL.md +349 -337
  272. package/skills/autosnippet-create/SKILL.md +5 -5
  273. package/skills/autosnippet-reference-dart/SKILL.md +543 -0
  274. package/skills/autosnippet-reference-go/SKILL.md +539 -0
  275. package/skills/autosnippet-reference-java/SKILL.md +534 -0
  276. package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
  277. package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
  278. package/skills/autosnippet-reference-objc/SKILL.md +29 -6
  279. package/skills/autosnippet-reference-python/SKILL.md +800 -0
  280. package/skills/autosnippet-reference-swift/SKILL.md +70 -14
  281. package/skills/autosnippet-structure/SKILL.md +4 -4
  282. package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
  283. package/templates/recipes-setup/README.md +2 -2
  284. package/templates/recipes-setup/_template.md +1 -1
  285. package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
  286. package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
  287. package/resources/asd-entry/main.swift +0 -159
  288. package/scripts/build-asd-entry.js +0 -51
  289. package/scripts/init-xcode-snippets.js +0 -311
  290. package/template.json +0 -39
@@ -1,43 +1,76 @@
1
1
  /**
2
- * SnippetInstaller — Xcode .codesnippet 安装器
3
- * 将生成的 snippet 文件写入 Xcode CodeSnippets 目录
2
+ * SnippetInstaller — Codec 驱动的 Snippet 安装器
3
+ *
4
+ * 支持:
5
+ * - Xcode: 每个 snippet 一个 .codesnippet 文件
6
+ * - VSCode: 所有 snippets 合并为单个 .code-snippets JSON 文件
7
+ *
8
+ * 行为由注入的 SnippetCodec 决定。
4
9
  */
5
10
 
6
- import { writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
7
12
  import { join } from 'node:path';
8
- import { homedir } from 'node:os';
9
-
10
- const XCODE_SNIPPETS_DIR = join(
11
- homedir(),
12
- 'Library/Developer/Xcode/UserData/CodeSnippets'
13
- );
14
13
 
15
14
  export class SnippetInstaller {
16
- #snippetsDir;
15
+ #codec;
17
16
  #snippetFactory;
17
+ #snippetsDirOverride;
18
18
 
19
+ /**
20
+ * @param {object} options
21
+ * @param {import('./codecs/SnippetCodec.js').SnippetCodec} options.codec — IDE codec
22
+ * @param {import('./SnippetFactory.js').SnippetFactory} [options.snippetFactory]
23
+ * @param {string} [options.snippetsDir] — 覆盖 codec 默认目录
24
+ */
19
25
  constructor(options = {}) {
20
- this.#snippetsDir = options.snippetsDir || XCODE_SNIPPETS_DIR;
26
+ this.#codec = options.codec || null;
21
27
  this.#snippetFactory = options.snippetFactory || null;
28
+ this.#snippetsDirOverride = options.snippetsDir || null;
29
+ }
30
+
31
+ /** codec ID ('xcode' | 'vscode') */
32
+ get target() {
33
+ return this.#codec?.id || 'unknown';
34
+ }
35
+
36
+ /** 当前安装目录 */
37
+ get snippetsDir() {
38
+ return this.#snippetsDirOverride || this.#codec?.getInstallDir(process.cwd()) || '';
39
+ }
40
+
41
+ setSnippetFactory(factory) {
42
+ this.#snippetFactory = factory;
22
43
  }
23
44
 
24
- setSnippetFactory(factory) { this.#snippetFactory = factory; }
45
+ setCodec(codec) {
46
+ this.#codec = codec;
47
+ }
48
+
49
+ // ─────────────── 安装 ───────────────
25
50
 
26
51
  /**
27
- * 安装单个 snippet
28
- * @param {object} spec — { identifier, title, completion, summary, code, language }
52
+ * 安装单个 snippet spec
53
+ * @param {object} spec — SnippetSpec
54
+ * @param {string} [projectRoot] — VSCode 需要 projectRoot 确定 .vscode/ 路径
29
55
  * @returns {{ success: boolean, path: string, message: string }}
30
56
  */
31
- install(spec) {
32
- if (!this.#snippetFactory) throw new Error('SnippetFactory not set');
33
-
57
+ install(spec, projectRoot) {
58
+ this.#assertCodec();
34
59
  try {
35
- this.#ensureDir();
36
- const xml = this.#snippetFactory.generate(spec);
37
- const filename = `${spec.identifier}.codesnippet`;
38
- const filePath = join(this.#snippetsDir, filename);
60
+ const dir = this.#resolveDir(projectRoot);
61
+ this.#ensureDir(dir);
39
62
 
40
- writeFileSync(filePath, xml);
63
+ const bundleFilename = this.#codec.getBundleFilename();
64
+ if (bundleFilename) {
65
+ // Bundle 模式 (VSCode): merge into single JSON file
66
+ return this.#installToBundle(spec, dir, bundleFilename);
67
+ }
68
+
69
+ // Per-file 模式 (Xcode): 每个 snippet 一个文件
70
+ const content = this.#codec.generate(spec);
71
+ const filename = `${spec.identifier}${this.#codec.fileExtension}`;
72
+ const filePath = join(dir, filename);
73
+ writeFileSync(filePath, content);
41
74
  return { success: true, path: filePath, message: `Installed: ${filename}` };
42
75
  } catch (error) {
43
76
  return { success: false, path: '', message: error.message };
@@ -46,88 +79,227 @@ export class SnippetInstaller {
46
79
 
47
80
  /**
48
81
  * 从 Recipe 批量安装
49
- * @param {Array} recipes — [{ id, title, trigger, code, description, language }]
82
+ * @param {Array} recipes
83
+ * @param {string} [projectRoot]
50
84
  * @returns {{ success: boolean, count: number, successCount: number, errorCount: number, details: Array }}
51
85
  */
52
- installFromRecipes(recipes) {
53
- if (!this.#snippetFactory) throw new Error('SnippetFactory not set');
86
+ installFromRecipes(recipes, projectRoot) {
87
+ this.#assertCodec();
88
+ this.#assertFactory();
89
+
90
+ const dir = this.#resolveDir(projectRoot);
91
+ this.#ensureDir(dir);
92
+
93
+ const specs = recipes.map((r) => this.#snippetFactory.fromRecipe(r));
94
+ const bundleFilename = this.#codec.getBundleFilename();
54
95
 
55
- this.#ensureDir();
96
+ if (bundleFilename) {
97
+ // Bundle 模式: 一次性写入整个 bundle
98
+ return this.#installBundleFromSpecs(specs, dir, bundleFilename, recipes.length);
99
+ }
100
+
101
+ // Per-file 模式
56
102
  const details = [];
57
103
  let successCount = 0;
58
104
  let errorCount = 0;
59
105
 
60
- for (const recipe of recipes) {
61
- try {
62
- const spec = this.#snippetFactory.fromRecipe(recipe);
63
- const result = this.install(spec);
64
- details.push(result);
65
- if (result.success) successCount++;
66
- else errorCount++;
67
- } catch (error) {
68
- details.push({ success: false, path: '', message: error.message });
69
- errorCount++;
70
- }
106
+ for (const spec of specs) {
107
+ const result = this.install(spec, projectRoot);
108
+ details.push(result);
109
+ if (result.success) successCount++;
110
+ else errorCount++;
71
111
  }
72
112
 
73
- return {
74
- success: errorCount === 0,
75
- count: recipes.length,
76
- successCount,
77
- errorCount,
78
- details,
79
- };
113
+ return { success: errorCount === 0, count: recipes.length, successCount, errorCount, details };
80
114
  }
81
115
 
116
+ // ─────────────── 查询 ───────────────
117
+
82
118
  /**
83
119
  * 列出已安装的 AutoSnippet 管理的 snippet
120
+ * @param {string} [projectRoot]
84
121
  * @returns {Array<{ filename: string, path: string }>}
85
122
  */
86
- listInstalled() {
87
- if (!existsSync(this.#snippetsDir)) return [];
123
+ listInstalled(projectRoot) {
124
+ const dir = this.#resolveDir(projectRoot);
125
+ if (!existsSync(dir)) return [];
126
+
127
+ const bundleFilename = this.#codec?.getBundleFilename();
128
+ if (bundleFilename) {
129
+ // VSCode: 检查 bundle 文件是否存在
130
+ const bundlePath = join(dir, bundleFilename);
131
+ if (!existsSync(bundlePath)) return [];
132
+ try {
133
+ const content = JSON.parse(readFileSync(bundlePath, 'utf-8'));
134
+ return Object.keys(content).map((key) => ({
135
+ filename: key,
136
+ path: bundlePath,
137
+ }));
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
88
142
 
89
- return readdirSync(this.#snippetsDir)
90
- .filter(f => f.startsWith('com.autosnippet.') && f.endsWith('.codesnippet'))
91
- .map(f => ({ filename: f, path: join(this.#snippetsDir, f) }));
143
+ // Xcode: 列出 com.autosnippet.*.codesnippet 文件
144
+ return readdirSync(dir)
145
+ .filter((f) => f.startsWith('com.autosnippet.') && f.endsWith(this.#codec?.fileExtension || '.codesnippet'))
146
+ .map((f) => ({ filename: f, path: join(dir, f) }));
92
147
  }
93
148
 
149
+ // ─────────────── 卸载 ───────────────
150
+
94
151
  /**
95
152
  * 卸载指定 snippet
96
153
  * @param {string} identifier
154
+ * @param {string} [projectRoot]
97
155
  * @returns {{ success: boolean, message: string }}
98
156
  */
99
- uninstall(identifier) {
100
- const filename = identifier.endsWith('.codesnippet') ? identifier : `${identifier}.codesnippet`;
101
- const filePath = join(this.#snippetsDir, filename);
157
+ uninstall(identifier, projectRoot) {
158
+ const dir = this.#resolveDir(projectRoot);
159
+ const bundleFilename = this.#codec?.getBundleFilename();
160
+
161
+ if (bundleFilename) {
162
+ // VSCode: 从 bundle JSON 中移除对应 key
163
+ return this.#removeFromBundle(identifier, dir, bundleFilename);
164
+ }
102
165
 
166
+ // Xcode: 删除单个文件
167
+ const ext = this.#codec?.fileExtension || '.codesnippet';
168
+ const filename = identifier.endsWith(ext) ? identifier : `${identifier}${ext}`;
169
+ const filePath = join(dir, filename);
103
170
  if (!existsSync(filePath)) {
104
171
  return { success: false, message: `Not found: ${filename}` };
105
172
  }
106
-
107
173
  unlinkSync(filePath);
108
174
  return { success: true, message: `Uninstalled: ${filename}` };
109
175
  }
110
176
 
111
177
  /**
112
178
  * 清除所有 AutoSnippet 管理的 snippet
179
+ * @param {string} [projectRoot]
113
180
  * @returns {{ success: boolean, removed: number }}
114
181
  */
115
- cleanAll() {
116
- const installed = this.listInstalled();
117
- let removed = 0;
182
+ cleanAll(projectRoot) {
183
+ const dir = this.#resolveDir(projectRoot);
184
+ const bundleFilename = this.#codec?.getBundleFilename();
118
185
 
119
- for (const { path } of installed) {
120
- try { unlinkSync(path); removed++; } catch { /* ignore */ }
186
+ if (bundleFilename) {
187
+ // VSCode: 删除整个 bundle 文件
188
+ const bundlePath = join(dir, bundleFilename);
189
+ if (existsSync(bundlePath)) {
190
+ unlinkSync(bundlePath);
191
+ return { success: true, removed: 1 };
192
+ }
193
+ return { success: true, removed: 0 };
121
194
  }
122
195
 
196
+ // Xcode: 删除所有 com.autosnippet.* 文件
197
+ const installed = this.listInstalled(projectRoot);
198
+ let removed = 0;
199
+ for (const { path: filePath } of installed) {
200
+ try {
201
+ unlinkSync(filePath);
202
+ removed++;
203
+ } catch {
204
+ /* ignore */
205
+ }
206
+ }
123
207
  return { success: true, removed };
124
208
  }
125
209
 
126
- get snippetsDir() { return this.#snippetsDir; }
210
+ // ─────────────── Private ───────────────
211
+
212
+ #assertCodec() {
213
+ if (!this.#codec) throw new Error('SnippetCodec not set');
214
+ }
215
+
216
+ #assertFactory() {
217
+ if (!this.#snippetFactory) throw new Error('SnippetFactory not set');
218
+ }
219
+
220
+ #resolveDir(projectRoot) {
221
+ if (this.#snippetsDirOverride) return this.#snippetsDirOverride;
222
+ return this.#codec?.getInstallDir(projectRoot || process.cwd()) || '';
223
+ }
224
+
225
+ #ensureDir(dir) {
226
+ if (!existsSync(dir)) {
227
+ mkdirSync(dir, { recursive: true });
228
+ }
229
+ }
230
+
231
+ /**
232
+ * VSCode bundle: 将单个 spec 追加/更新到 bundle JSON
233
+ */
234
+ #installToBundle(spec, dir, bundleFilename) {
235
+ const bundlePath = join(dir, bundleFilename);
236
+ let bundle = {};
237
+ if (existsSync(bundlePath)) {
238
+ try {
239
+ bundle = JSON.parse(readFileSync(bundlePath, 'utf-8'));
240
+ } catch {
241
+ bundle = {};
242
+ }
243
+ }
127
244
 
128
- #ensureDir() {
129
- if (!existsSync(this.#snippetsDir)) {
130
- mkdirSync(this.#snippetsDir, { recursive: true });
245
+ const key = `Recipe: ${spec.title || spec.identifier}`;
246
+ const content = JSON.parse(this.#codec.generate(spec));
247
+ const entryKey = Object.keys(content)[0];
248
+ bundle[key] = content[entryKey];
249
+
250
+ writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + '\n');
251
+ return { success: true, path: bundlePath, message: `Installed: ${key}` };
252
+ }
253
+
254
+ /**
255
+ * VSCode bundle: 一次性写入完整 bundle
256
+ */
257
+ #installBundleFromSpecs(specs, dir, bundleFilename, totalCount) {
258
+ try {
259
+ const content = this.#codec.generateBundle(specs);
260
+ const bundlePath = join(dir, bundleFilename);
261
+ writeFileSync(bundlePath, content);
262
+ return {
263
+ success: true,
264
+ count: totalCount,
265
+ successCount: totalCount,
266
+ errorCount: 0,
267
+ details: [{ success: true, path: bundlePath, message: `Bundle: ${totalCount} snippets` }],
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ success: false,
272
+ count: totalCount,
273
+ successCount: 0,
274
+ errorCount: totalCount,
275
+ details: [{ success: false, path: '', message: error.message }],
276
+ };
277
+ }
278
+ }
279
+
280
+ /**
281
+ * VSCode bundle: 从 JSON 中移除一个 snippet
282
+ */
283
+ #removeFromBundle(identifier, dir, bundleFilename) {
284
+ const bundlePath = join(dir, bundleFilename);
285
+ if (!existsSync(bundlePath)) {
286
+ return { success: false, message: `Bundle not found: ${bundleFilename}` };
287
+ }
288
+
289
+ try {
290
+ const bundle = JSON.parse(readFileSync(bundlePath, 'utf-8'));
291
+ // 按 identifier 或 title 匹配
292
+ const keyToRemove = Object.keys(bundle).find(
293
+ (k) => k.includes(identifier) || k === `Recipe: ${identifier}`
294
+ );
295
+ if (!keyToRemove) {
296
+ return { success: false, message: `Snippet not found in bundle: ${identifier}` };
297
+ }
298
+ delete bundle[keyToRemove];
299
+ writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + '\n');
300
+ return { success: true, message: `Uninstalled: ${keyToRemove}` };
301
+ } catch (error) {
302
+ return { success: false, message: error.message };
131
303
  }
132
304
  }
133
305
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SnippetCodec — IDE Snippet 格式抽象接口
3
+ *
4
+ * 子类实现:
5
+ * - XcodeCodec (.codesnippet plist XML)
6
+ * - VSCodeCodec (.code-snippets JSON, 兼容 Cursor)
7
+ *
8
+ * SnippetSpec 结构 (IDE 无关中间表示):
9
+ * { identifier, title, completion, summary, code, language }
10
+ */
11
+ export class SnippetCodec {
12
+ /** @returns {string} codec 标识 ('xcode' | 'vscode') */
13
+ get id() {
14
+ throw new Error('SnippetCodec.id is abstract');
15
+ }
16
+
17
+ /** @returns {string} 输出文件的扩展名 ('.codesnippet' | '.code-snippets') */
18
+ get fileExtension() {
19
+ throw new Error('SnippetCodec.fileExtension is abstract');
20
+ }
21
+
22
+ /**
23
+ * 单个 SnippetSpec → IDE 格式字符串
24
+ * @param {object} spec
25
+ * @returns {string}
26
+ */
27
+ generate(spec) {
28
+ throw new Error('SnippetCodec.generate() is abstract');
29
+ }
30
+
31
+ /**
32
+ * 批量 specs → IDE 格式输出
33
+ * Xcode: 返回 Array<{ filename, content }>(每个 snippet 一个文件)
34
+ * VSCode: 返回 string(单个 JSON bundle 文件内容)
35
+ * @param {object[]} specs
36
+ * @returns {string | Array<{ filename: string, content: string }>}
37
+ */
38
+ generateBundle(specs) {
39
+ throw new Error('SnippetCodec.generateBundle() is abstract');
40
+ }
41
+
42
+ /**
43
+ * 目标 snippet 安装目录路径
44
+ * @param {string} [projectRoot]
45
+ * @returns {string}
46
+ */
47
+ getInstallDir(projectRoot) {
48
+ throw new Error('SnippetCodec.getInstallDir() is abstract');
49
+ }
50
+
51
+ /**
52
+ * AutoSnippet 语言标识 → IDE 语言标识映射
53
+ * @param {string} lang
54
+ * @returns {string}
55
+ */
56
+ mapLanguage(lang) {
57
+ throw new Error('SnippetCodec.mapLanguage() is abstract');
58
+ }
59
+
60
+ /**
61
+ * 获取 bundle 文件名 (VSCode = 'autosnippet.code-snippets', Xcode = per-file)
62
+ * @returns {string|null}
63
+ */
64
+ getBundleFilename() {
65
+ return null;
66
+ }
67
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * VSCodeCodec — VSCode .code-snippets (JSON) 生成器
3
+ *
4
+ * 特性:
5
+ * - 项目级 .vscode/autosnippet.code-snippets 单 bundle 文件
6
+ * - 兼容 Cursor (100% VSCode snippet 格式)
7
+ * - Xcode 占位符 <#…#> 自动转换为 VSCode ${N:…}
8
+ * - 多语言 scope 自动映射
9
+ */
10
+
11
+ import { join } from 'node:path';
12
+ import { SnippetCodec } from './SnippetCodec.js';
13
+ import { PlaceholderConverter } from '../PlaceholderConverter.js';
14
+
15
+ /** AutoSnippet language → VSCode snippet scope */
16
+ const VSCODE_LANGUAGE_MAP = {
17
+ swift: 'swift',
18
+ 'objective-c': 'objective-c',
19
+ objc: 'objective-c',
20
+ c: 'c',
21
+ 'c++': 'cpp',
22
+ go: 'go',
23
+ python: 'python',
24
+ java: 'java',
25
+ kotlin: 'kotlin',
26
+ javascript: 'javascript,javascriptreact',
27
+ typescript: 'typescript,typescriptreact',
28
+ rust: 'rust',
29
+ ruby: 'ruby',
30
+ };
31
+
32
+ const BUNDLE_FILENAME = 'autosnippet.code-snippets';
33
+
34
+ export class VSCodeCodec extends SnippetCodec {
35
+ get id() {
36
+ return 'vscode';
37
+ }
38
+
39
+ get fileExtension() {
40
+ return '.code-snippets';
41
+ }
42
+
43
+ /**
44
+ * 单个 SnippetSpec → JSON 字符串
45
+ */
46
+ generate(spec) {
47
+ const entry = this.#specToEntry(spec);
48
+ return JSON.stringify({ [spec.title || spec.identifier]: entry }, null, 2);
49
+ }
50
+
51
+ /**
52
+ * VSCode: 所有 snippets 合并为单个 JSON bundle 文件
53
+ * @returns {string} JSON 字符串
54
+ */
55
+ generateBundle(specs) {
56
+ const bundle = {};
57
+ for (const spec of specs) {
58
+ const key = `Recipe: ${spec.title || spec.identifier}`;
59
+ bundle[key] = this.#specToEntry(spec);
60
+ }
61
+ return JSON.stringify(bundle, null, 2) + '\n';
62
+ }
63
+
64
+ /**
65
+ * VSCode snippets 安装目录 = 项目级 .vscode/
66
+ */
67
+ getInstallDir(projectRoot) {
68
+ return join(projectRoot, '.vscode');
69
+ }
70
+
71
+ mapLanguage(lang) {
72
+ return VSCODE_LANGUAGE_MAP[lang?.toLowerCase()] || '';
73
+ }
74
+
75
+ getBundleFilename() {
76
+ return BUNDLE_FILENAME;
77
+ }
78
+
79
+ /**
80
+ * @private SnippetSpec → VSCode snippet entry
81
+ */
82
+ #specToEntry(spec) {
83
+ const code = Array.isArray(spec.code) ? spec.code.join('\n') : spec.code || '';
84
+ // 自动将 Xcode 占位符转为 VSCode 格式
85
+ const converted = PlaceholderConverter.xcodeToVSCode(code);
86
+ const body = converted.split('\n');
87
+
88
+ const entry = {
89
+ prefix: spec.completion || spec.trigger || spec.identifier,
90
+ body,
91
+ description: spec.summary || '',
92
+ };
93
+
94
+ // 添加语言 scope (空字符串 = 所有语言)
95
+ const scope = this.mapLanguage(spec.language);
96
+ if (scope) {
97
+ entry.scope = scope;
98
+ }
99
+
100
+ return entry;
101
+ }
102
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @deprecated Moved to lib/platform/ios/snippet/XcodeCodec.js
3
+ * This re-export shim maintains backward compatibility.
4
+ */
5
+ export { XcodeCodec } from '../../../platform/ios/snippet/XcodeCodec.js';