autosnippet 2.8.3 → 2.10.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 (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -1,42 +1,230 @@
1
1
  /**
2
- * XcodeIntegration — Xcode IDE 自动化工具方法
3
- * 头文件插入、代码插入、行号查找等
2
+ * XcodeIntegration — Xcode IDE 代码自动插入服务
3
+ *
4
+ * 核心能力:
5
+ * §1 import 语句解析 — 支持 ObjC (#import/#include/@import) 和 Swift (import)
6
+ * §2 头文件搜索 — 在 target 源目录中递归查找头文件并计算相对路径
7
+ * §3 import 格式化 — 根据 同target/跨target 关系生成正确的引号/尖括号格式
8
+ * §4 三级去重 — 精确匹配 → 模块匹配 → 相似头文件名匹配
9
+ * §5 SPM 依赖决策 — block(循环依赖) / review(缺失可补) / continue(已存在)
10
+ * §6 Xcode 自动插入 — osascript 跳转+粘贴,保持 Undo 可用
11
+ * §7 文件写入回退 — Xcode 失败时直接写文件,Xcode 自动 reload
12
+ * §8 粘贴行号偏移 — headers 插入后自动修正代码粘贴位置
13
+ * §9 完整插入流程 — cut 触发行 → preflight → headers → offset → paste
4
14
  */
5
15
 
6
- import { readFileSync, writeFileSync } from 'node:fs';
16
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
17
+ import { basename, dirname, relative, resolve as pathResolve, sep } from 'node:path';
18
+ import { saveEventFilter } from './SaveEventFilter.js';
19
+
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // §1 常量与工具函数
22
+ // ═══════════════════════════════════════════════════════════════
7
23
 
8
24
  function _sleep(ms) {
9
25
  return new Promise(resolve => setTimeout(resolve, ms));
10
26
  }
11
27
 
12
28
  /**
13
- * import 语句中推断模块名
14
- * #import <Module/Header.h> → Module
15
- * @import Module; → Module
16
- * import Module → Module
17
- * #import "Local.h" → null(本地头文件不检查)
29
+ * import 行末尾附加来源标记注释
18
30
  */
19
- function _inferModulesFromHeaders(headers) {
20
- const modules = new Set();
21
- for (const h of headers) {
22
- const t = h.trim();
23
- let m;
24
- // #import <Module/xxx.h> or #import <Module.h>
25
- m = t.match(/^#import\s+<([^/> ]+)/);
26
- if (m) { modules.add(m[1]); continue; }
27
- // @import Module;
28
- m = t.match(/^@import\s+(\w+)/);
29
- if (m) { modules.add(m[1]); continue; }
30
- // import Module (Swift)
31
- m = t.match(/^import\s+(\w+)/);
32
- if (m && m[1] !== 'class' && m[1] !== 'struct' && m[1] !== 'enum' && m[1] !== 'protocol') {
33
- modules.add(m[1]);
31
+ function _withAutoSnippetNote(importLine) {
32
+ if (!importLine) return importLine;
33
+ const note = '// AutoSnippet: 自动插入';
34
+ if (importLine.includes(note)) return importLine;
35
+ return `${importLine} ${note}`;
36
+ }
37
+
38
+ /**
39
+ * 解析原始 header 字符串,提取 moduleName 和 headerName
40
+ *
41
+ * 支持格式:
42
+ * #import <Module/Header.h> → { moduleName: 'Module', headerName: 'Header.h', isAngle: true }
43
+ * #import "Header.h" → { moduleName: '', headerName: 'Header.h', isAngle: false }
44
+ * @import Module; → { moduleName: 'Module', headerName: '', isAngle: false, isAtImport: true }
45
+ * import Module (Swift) → { moduleName: 'Module', headerName: '', isAngle: false, isSwiftImport: true }
46
+ * Header.h → { moduleName: '', headerName: 'Header.h', isAngle: false, isRaw: true }
47
+ */
48
+ function _parseHeaderString(header) {
49
+ const t = header.trim();
50
+ // #import <Module/Header.h>
51
+ let m = t.match(/^#(?:import|include)\s+<([^/> ]+)\/([^>]+)>/);
52
+ if (m) return { moduleName: m[1], headerName: m[2], isAngle: true };
53
+ // #import <Module> (framework umbrella)
54
+ m = t.match(/^#(?:import|include)\s+<([^>]+)>/);
55
+ if (m) return { moduleName: m[1], headerName: '', isAngle: true };
56
+ // #import "Header.h" or #import "Dir/Header.h"
57
+ m = t.match(/^#(?:import|include)\s+"([^"]+)"/);
58
+ if (m) {
59
+ const parts = m[1].split('/');
60
+ return { moduleName: '', headerName: parts[parts.length - 1], isAngle: false, quotedPath: m[1] };
61
+ }
62
+ // @import Module;
63
+ m = t.match(/^@import\s+(\w+)/);
64
+ if (m) return { moduleName: m[1], headerName: '', isAngle: false, isAtImport: true };
65
+ // import Module (Swift)
66
+ m = t.match(/^import\s+(\w+)/);
67
+ if (m && !['class', 'struct', 'enum', 'protocol', 'func', 'var', 'let'].includes(m[1])) {
68
+ return { moduleName: m[1], headerName: '', isAngle: false, isSwiftImport: true };
69
+ }
70
+ // 裸 header 名: Header.h
71
+ if (/\.(h|hpp|hh)$/i.test(t)) {
72
+ return { moduleName: '', headerName: t, isAngle: false, isRaw: true };
73
+ }
74
+ return { moduleName: '', headerName: t, isAngle: false, isRaw: true };
75
+ }
76
+
77
+ /**
78
+ * 在 target 源目录中搜索头文件,返回相对于当前文件的路径
79
+ *
80
+ * 搜索策略:
81
+ * 1. 当前文件同目录
82
+ * 2. 从项目根目录递归查找(最多深度 6 层,优先 Sources/ 下)
83
+ * 3. 找到后计算相对于当前文件目录的路径
84
+ *
85
+ * @param {string} headerName - 头文件名 (如 "Foo.h")
86
+ * @param {string} currentFilePath - 当前正在编辑的文件绝对路径
87
+ * @param {string} [projectRoot] - 项目根目录
88
+ * @returns {string|null} 相对路径 (如 "Foo.h" 或 "../SubDir/Foo.h"),null 表示未找到
89
+ */
90
+ function _findHeaderRelativePath(headerName, currentFilePath, projectRoot) {
91
+ if (!headerName || !currentFilePath) return null;
92
+ try {
93
+ const currentDir = dirname(currentFilePath);
94
+
95
+ // 1. 同目录检查
96
+ const sameDir = pathResolve(currentDir, headerName);
97
+ if (existsSync(sameDir)) return headerName;
98
+
99
+ // 2. 向上找 Sources/ 或 target 根目录,在其下递归搜索
100
+ const searchRoots = [];
101
+ if (projectRoot) {
102
+ const sourcesDir = pathResolve(projectRoot, 'Sources');
103
+ if (existsSync(sourcesDir)) searchRoots.push(sourcesDir);
104
+ searchRoots.push(projectRoot);
105
+ }
106
+ // 也从当前文件向上找 Sources 目录
107
+ let dir = currentDir;
108
+ for (let i = 0; i < 8; i++) {
109
+ const base = basename(dir);
110
+ if (base === 'Sources' || base === 'Source' || base === 'src') {
111
+ searchRoots.unshift(dir);
112
+ break;
113
+ }
114
+ const parent = dirname(dir);
115
+ if (parent === dir) break;
116
+ dir = parent;
34
117
  }
118
+
119
+ // 在 searchRoots 中递归查找 headerName(限深度 6)
120
+ for (const root of searchRoots) {
121
+ const found = _findFileRecursive(root, headerName, 6);
122
+ if (found) {
123
+ let rel = relative(currentDir, found);
124
+ // 统一用 / 分隔
125
+ rel = rel.split(sep).join('/');
126
+ return rel;
127
+ }
128
+ }
129
+
130
+ return null;
131
+ } catch {
132
+ return null;
35
133
  }
36
- return [...modules];
37
134
  }
38
135
 
39
- // 常见 Apple 系统框架,不需要依赖检查
136
+ /**
137
+ * 递归查找文件(限最大深度)
138
+ */
139
+ function _findFileRecursive(dir, fileName, maxDepth) {
140
+ if (maxDepth <= 0) return null;
141
+ try {
142
+ const entries = readdirSync(dir);
143
+ // 先在当前层查找
144
+ for (const e of entries) {
145
+ if (e === fileName) return pathResolve(dir, e);
146
+ }
147
+ // 再递归子目录(跳过隐藏目录和常见无关目录)
148
+ for (const e of entries) {
149
+ if (e.startsWith('.') || e === 'node_modules' || e === 'build' || e === 'DerivedData') continue;
150
+ const full = pathResolve(dir, e);
151
+ try {
152
+ if (statSync(full).isDirectory()) {
153
+ const found = _findFileRecursive(full, fileName, maxDepth - 1);
154
+ if (found) return found;
155
+ }
156
+ } catch { /* 跳过不可访问的目录 */ }
157
+ }
158
+ } catch { /* 跳过不可读目录 */ }
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * 根据当前文件 target 和 header 的 module 关系,生成正确格式的 import 行
164
+ *
165
+ * 规则:
166
+ * Swift: 始终 `import Module`(无 quote/angle 区别)
167
+ * ObjC 同 target: `#import "Header.h"` (引号格式)
168
+ * ObjC 跨 target: `#import <Module/Header.h>` (尖括号格式)
169
+ * @import 格式保持原样(已经模块级)
170
+ *
171
+ * @param {string} rawHeader 原始 header 字符串
172
+ * @param {object} ctx { currentTarget, headerModuleName, isSwift, fullPath, projectRoot }
173
+ * - currentTarget: 当前文件所属的 target 名
174
+ * - headerModuleName: header 所属的 module/target 名(来自 recipe.moduleName 或推断)
175
+ * - isSwift: 目标文件是否是 Swift
176
+ * - fullPath: 当前编辑文件的绝对路径(用于计算同 target 相对路径)
177
+ * - projectRoot: 项目根目录(用于搜索头文件物理位置)
178
+ * @returns {string} 格式化后的完整 import 行
179
+ */
180
+ function _resolveHeaderFormat(rawHeader, ctx) {
181
+ const { currentTarget, headerModuleName, isSwift, fullPath, projectRoot } = ctx;
182
+ const parsed = _parseHeaderString(rawHeader);
183
+
184
+ // Swift: 始终 `import Module`
185
+ if (isSwift || parsed.isSwiftImport) {
186
+ // 已经是完整 swift import 语句
187
+ if (parsed.isSwiftImport) return rawHeader.trim();
188
+ // 从 ObjC 格式推断 swift import
189
+ const mod = parsed.moduleName || headerModuleName || '';
190
+ if (mod) return `import ${mod}`;
191
+ return rawHeader.trim(); // 无法推断,原样返回
192
+ }
193
+
194
+ // @import 保持原样(模块级引用不受 target 影响)
195
+ if (parsed.isAtImport) return rawHeader.trim();
196
+
197
+ // 已经是尖括号格式 → 保持(明确的跨模块引用)
198
+ if (parsed.isAngle) return rawHeader.trim();
199
+
200
+ // ── ObjC: 判断同 target vs 跨 target ──
201
+ const effectiveModule = parsed.moduleName || headerModuleName || '';
202
+
203
+ // 如果没有 target 信息,无法判断,保持原样
204
+ if (!currentTarget || !effectiveModule) return rawHeader.trim();
205
+
206
+ const isSameTarget = currentTarget === effectiveModule;
207
+
208
+ if (isSameTarget) {
209
+ // 同 target → 引号格式,计算相对路径
210
+ if (parsed.headerName && fullPath) {
211
+ const relPath = _findHeaderRelativePath(parsed.headerName, fullPath, projectRoot);
212
+ if (relPath) return `#import "${relPath}"`;
213
+ }
214
+ if (parsed.quotedPath) return `#import "${parsed.quotedPath}"`;
215
+ if (parsed.headerName) return `#import "${parsed.headerName}"`;
216
+ return rawHeader.trim();
217
+ }
218
+
219
+ // 跨 target → 尖括号格式 <Module/Header.h>
220
+ if (parsed.headerName) {
221
+ return `#import <${effectiveModule}/${parsed.headerName}>`;
222
+ }
223
+ // 没有 headerName(裸模块名),用 @import
224
+ return `@import ${effectiveModule};`;
225
+ }
226
+
227
+ /** 常见 Apple 系统框架(无需 SPM 依赖检查) */
40
228
  const _SYSTEM_FRAMEWORKS = new Set([
41
229
  'Foundation', 'UIKit', 'AppKit', 'SwiftUI', 'Combine', 'CoreFoundation',
42
230
  'CoreGraphics', 'CoreData', 'CoreAnimation', 'CoreLocation', 'CoreMedia',
@@ -50,19 +238,366 @@ const _SYSTEM_FRAMEWORKS = new Set([
50
238
  'Dispatch', 'XCTest',
51
239
  ]);
52
240
 
241
+ // ═══════════════════════════════════════════════════════════════
242
+ // §4 三级 import 去重
243
+ // ═══════════════════════════════════════════════════════════════
244
+
245
+ /**
246
+ * 从文件中收集已有的 import 语句
247
+ */
248
+ function _collectImportsFromFile(filePath, isSwift) {
249
+ try {
250
+ if (!existsSync(filePath)) return [];
251
+ const content = readFileSync(filePath, 'utf8');
252
+ const lines = content.split(/\r?\n/);
253
+ const imports = [];
254
+ for (const line of lines) {
255
+ const t = line.trim();
256
+ if (isSwift) {
257
+ if (t.startsWith('import ')) imports.push(t);
258
+ } else {
259
+ if (t.startsWith('#import ') || t.startsWith('@import ') || t.startsWith('#include ')) {
260
+ imports.push(t);
261
+ }
262
+ }
263
+ }
264
+ return imports;
265
+ } catch {
266
+ return [];
267
+ }
268
+ }
269
+
53
270
  /**
54
- * 统一的头文件插入方法(所有场景共用)
271
+ * 收集 .m 文件对应 .h 文件中的 imports(ObjC 接口/实现配对去重)
272
+ */
273
+ function _collectImportsFromHeaderFile(sourcePath, importArray) {
274
+ const dotIndex = sourcePath.lastIndexOf('.');
275
+ if (dotIndex <= 0) return;
276
+ const headerPath = sourcePath.substring(0, dotIndex) + '.h';
277
+ const importReg = /^#import\s*<[A-Za-z0-9_]+\/[A-Za-z0-9_+.-]+\.h>$/;
278
+ try {
279
+ if (!existsSync(headerPath)) return;
280
+ const data = readFileSync(headerPath, 'utf8');
281
+ for (const line of data.split('\n')) {
282
+ const t = line.trim();
283
+ if (importReg.test(t) && !importArray.includes(t)) {
284
+ importArray.push(t);
285
+ }
286
+ }
287
+ } catch { /* ignore */ }
288
+ }
289
+
290
+ /**
291
+ * 三级 import 去重检查
55
292
  *
56
- * 流程:
57
- * 1. 去重(跳过已存在的 import)
58
- * 2. 依赖检查 SPM 模块可达性检查 + NativeUI 弹窗确认
59
- * 3. Xcode 自动化优先 — 跳转到 import 区域 + 剪贴板写入 + 自动粘贴
60
- * 4. 文件级回退 Xcode 不可用或 AppleScript 失败时直接写文件
293
+ * hasHeader 精确匹配(同一 import 行)
294
+ * hasModule 模块级匹配(同模块不同头文件,或 @import)
295
+ * hasSimilarHeader 文件名 case-insensitive 匹配
296
+ *
297
+ * @param {string[]} importArray 已有的 import
298
+ * @param {string} headerLine 待插入的 import 行
299
+ * @param {boolean} isSwift
300
+ */
301
+ function _checkImportStatus(importArray, headerLine, isSwift) {
302
+ const trimmed = headerLine.trim();
303
+
304
+ // 提取 module / headerFileName
305
+ let moduleName = '';
306
+ let headerFileName = '';
307
+
308
+ if (isSwift) {
309
+ const m = trimmed.match(/^import\s+(\w+)/);
310
+ if (m) moduleName = m[1];
311
+ headerFileName = moduleName;
312
+ } else {
313
+ const angle = trimmed.match(/<([^/]+)\/([^>]+)>/);
314
+ if (angle) {
315
+ moduleName = angle[1];
316
+ headerFileName = angle[2];
317
+ }
318
+ const quote = trimmed.match(/"([^"]+)"/);
319
+ if (quote) {
320
+ headerFileName = basename(quote[1]);
321
+ }
322
+ }
323
+
324
+ const headerFileNameLower = headerFileName.toLowerCase();
325
+
326
+ for (const imp of importArray) {
327
+ const impT = imp.trim();
328
+
329
+ // ── 级别 1: 精确匹配 ──
330
+ if (impT === trimmed) {
331
+ return { hasHeader: true, hasModule: false, hasSimilarHeader: false };
332
+ }
333
+ // 去掉可能的 AutoSnippet 注释后缀再比较
334
+ const impTClean = impT.replace(/\s*\/\/\s*AutoSnippet.*$/, '').trim();
335
+ if (impTClean === trimmed) {
336
+ return { hasHeader: true, hasModule: false, hasSimilarHeader: false };
337
+ }
338
+
339
+ if (isSwift) {
340
+ // ── 级别 2: Swift 模块匹配 ──
341
+ const m2 = impT.match(/^import\s+(\w+)/);
342
+ if (m2 && m2[1] === moduleName) {
343
+ return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
344
+ }
345
+ } else {
346
+ // ── 级别 2: ObjC 模块匹配(<Module/xxx> 或 @import Module) ──
347
+ if (moduleName) {
348
+ const impAngle = impT.match(/<([^/]+)\//);
349
+ if (impAngle && impAngle[1] === moduleName) {
350
+ return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
351
+ }
352
+ const impAt = impT.match(/@import\s+(\w+)/);
353
+ if (impAt && impAt[1] === moduleName) {
354
+ return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
355
+ }
356
+ }
357
+
358
+ // ── 级别 3: 相似头文件名匹配(case-insensitive) ──
359
+ if (headerFileNameLower) {
360
+ let importedFileName = null;
361
+ const a = impT.match(/<[^/]+\/([^>]+)>/);
362
+ if (a) importedFileName = a[1].toLowerCase();
363
+ const q = impT.match(/"([^"]+)"/);
364
+ if (q) importedFileName = basename(q[1]).toLowerCase();
365
+ if (importedFileName && importedFileName === headerFileNameLower) {
366
+ return { hasHeader: false, hasModule: false, hasSimilarHeader: true };
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ return { hasHeader: false, hasModule: false, hasSimilarHeader: false };
373
+ }
374
+
375
+ // ═══════════════════════════════════════════════════════════════
376
+ // §5 模块名推断
377
+ // ═══════════════════════════════════════════════════════════════
378
+
379
+ /**
380
+ * 从 import 语句推断模块名
381
+ *
382
+ * #import <Module/Header.h> → Module
383
+ * @import Module; → Module
384
+ * import Module (Swift) → Module
385
+ * #import "Local.h" → null
386
+ */
387
+ function _inferModulesFromHeaders(headers) {
388
+ const modules = new Set();
389
+ for (const h of headers) {
390
+ const t = h.trim();
391
+ let m;
392
+ m = t.match(/^#import\s+<([^/> ]+)/);
393
+ if (m) { modules.add(m[1]); continue; }
394
+ m = t.match(/^@import\s+(\w+)/);
395
+ if (m) { modules.add(m[1]); continue; }
396
+ m = t.match(/^import\s+(\w+)/);
397
+ if (m && !['class', 'struct', 'enum', 'protocol'].includes(m[1])) {
398
+ modules.add(m[1]);
399
+ }
400
+ }
401
+ return [...modules];
402
+ }
403
+
404
+ // ═══════════════════════════════════════════════════════════════
405
+ // §6 SPM 依赖检查决策引擎
406
+ // ═══════════════════════════════════════════════════════════════
407
+
408
+ /**
409
+ * 将 SpmService.ensureDependency 返回值映射为三种动作:
410
+ * continue — 依赖已存在
411
+ * block — 循环/反向依赖,禁止插入
412
+ * review — 依赖缺失但可添加,需用户确认
413
+ */
414
+ function _evaluateDepResult(ensureResult, from, to) {
415
+ if (ensureResult.exists) return { action: 'continue' };
416
+ if (!ensureResult.canAdd) {
417
+ return { action: 'block', reason: ensureResult.reason || 'cycleBlocked', from, to };
418
+ }
419
+ return { action: 'review', reason: ensureResult.reason || 'missingDependency', from, to };
420
+ }
421
+
422
+ /**
423
+ * 公共依赖审查弹窗逻辑(insertHeaders 和 _preflightDeps 共享)
424
+ *
425
+ * @param {object} ctx - { spmService, currentTarget, mod, ensureResult, NU, depWarnings, label }
426
+ * @returns {{ blocked: boolean }}
427
+ */
428
+ function _handleDepReview(ctx) {
429
+ const { spmService, currentTarget, mod, ensureResult, NU, depWarnings, label = '' } = ctx;
430
+
431
+ const fixMode = spmService.getFixMode();
432
+ const buttons = fixMode === 'fix'
433
+ ? ['直接插入(信任架构)', '提示操作插入', '自动修复依赖', '取消操作']
434
+ : ['直接插入(信任架构)', '提示操作插入', '取消操作'];
435
+
436
+ const crossTag = ensureResult.crossPackage ? ' (跨包)' : '';
437
+ const prefix = label ? `[${label}] ` : '';
438
+ console.log(` ⚠️ ${prefix}依赖缺失: ${currentTarget} -> ${mod}`);
439
+
440
+ const userChoice = NU.promptWithButtons(
441
+ `检测到依赖缺失:${currentTarget} -> ${mod}${crossTag}\n\n请选择处理方式:`,
442
+ buttons,
443
+ 'AutoSnippet SPM 依赖决策',
444
+ );
445
+
446
+ if (userChoice === '取消操作' || (!userChoice && !['直接插入(信任架构)', '提示操作插入', '自动修复依赖'].includes(userChoice))) {
447
+ return { blocked: true };
448
+ }
449
+
450
+ if (userChoice === '提示操作插入') {
451
+ console.log(` 📋 ${prefix}提示操作:依赖缺失 ${currentTarget} -> ${mod}`);
452
+ depWarnings.set(mod, `${currentTarget} -> ${mod}`);
453
+ }
454
+
455
+ if (userChoice === '自动修复依赖') {
456
+ const fixResult = spmService.addDependency(currentTarget, mod);
457
+ if (fixResult.ok) {
458
+ console.log(` ✅ ${prefix}已自动补齐依赖: ${currentTarget} -> ${mod}${fixResult.crossPackage ? ' (跨包)' : ''} (${fixResult.file})`);
459
+ NU.notify(`已补齐依赖:${currentTarget} -> ${mod}`, 'AutoSnippet SPM');
460
+ } else {
461
+ console.warn(` ⚠️ ${prefix}自动修复失败: ${fixResult.error},继续插入`);
462
+ depWarnings.set(mod, `${currentTarget} -> ${mod}`);
463
+ }
464
+ }
465
+
466
+ return { blocked: false };
467
+ }
468
+
469
+ // ═══════════════════════════════════════════════════════════════
470
+ // §7 Xcode osascript 单条 import 写入
471
+ // ═══════════════════════════════════════════════════════════════
472
+
473
+ /**
474
+ * 通过 Xcode 自动化插入一条 import,保持 Xcode Undo 可用。
475
+ *
476
+ * 流程:保存剪贴板 → 写入 import 内容 → osascript 跳转+粘贴 → 恢复剪贴板
477
+ *
478
+ * @param {string} importLine 完整的 import 文本
479
+ * @param {number} insertLine 1-based 行号
480
+ * @param {object} XA XcodeAutomation 模块
481
+ * @param {object} CM ClipboardManager 模块
482
+ * @returns {boolean}
483
+ */
484
+ function _writeImportLineXcode(importLine, insertLine, XA, CM) {
485
+ if (!XA.isXcodeRunning()) return false;
486
+ try {
487
+ const contentToWrite = String(importLine).trim() + '\n';
488
+ const previousClipboard = CM.read();
489
+
490
+ CM.write(contentToWrite);
491
+ const ok = XA.insertAtLineStartInXcode(insertLine);
492
+
493
+ // 始终恢复剪贴板
494
+ if (typeof previousClipboard === 'string') {
495
+ CM.write(previousClipboard);
496
+ }
497
+ return ok;
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
502
+
503
+ // ═══════════════════════════════════════════════════════════════
504
+ // §8 文件写入回退
505
+ // ═══════════════════════════════════════════════════════════════
506
+
507
+ /**
508
+ * 纯文件写入插入单条 import。
509
+ * Xcode 会因文件变更而自动 reload。
510
+ */
511
+ function _writeImportLineFile(filePath, importLine, isSwift) {
512
+ try {
513
+ const content = readFileSync(filePath, 'utf8');
514
+ const lines = content.split('\n');
515
+ let lastImportIdx = -1;
516
+ for (let i = 0; i < lines.length; i++) {
517
+ const t = lines[i].trim();
518
+ if (isSwift) {
519
+ if (t.startsWith('import ') && !t.startsWith('import (')) lastImportIdx = i;
520
+ } else {
521
+ if (t.startsWith('#import ') || t.startsWith('#include ') || t.startsWith('@import ')) {
522
+ lastImportIdx = i;
523
+ }
524
+ }
525
+ }
526
+ const insertAt = lastImportIdx >= 0 ? lastImportIdx + 1 : 0;
527
+ lines.splice(insertAt, 0, importLine);
528
+ const newContent = lines.join('\n');
529
+ saveEventFilter.markWrite(filePath, newContent);
530
+ writeFileSync(filePath, newContent, 'utf8');
531
+ return true;
532
+ } catch {
533
+ return false;
534
+ }
535
+ }
536
+
537
+ // ═══════════════════════════════════════════════════════════════
538
+ // §9 粘贴行号偏移计算
539
+ // ═══════════════════════════════════════════════════════════════
540
+
541
+ /**
542
+ * 查找文件中最后一个 import 行的行号(1-based,0 表示无 import)
543
+ */
544
+ function _getLastImportLine(filePath) {
545
+ try {
546
+ if (!existsSync(filePath)) return 0;
547
+ const content = readFileSync(filePath, 'utf8');
548
+ const lines = content.split(/\r?\n/);
549
+ let lastIdx = -1;
550
+ for (let i = 0; i < lines.length; i++) {
551
+ const t = lines[i].trim();
552
+ if (t.startsWith('#import ') || t.startsWith('@import ')
553
+ || t.startsWith('#include ') || t.startsWith('import ')) {
554
+ lastIdx = i;
555
+ }
556
+ }
557
+ return lastIdx >= 0 ? lastIdx + 1 : 0;
558
+ } catch {
559
+ return 0;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * 计算代码粘贴行号
565
+ *
566
+ * 如果 headers 插入在 trigger 行之前(import 区),trigger 行号需要向下偏移。
567
+ */
568
+ function _computePasteLineNumber(triggerLineNumber, headerInsertCount, filePath, options = {}) {
569
+ const expectedCount = Number.isFinite(options.expectedHeaderCount)
570
+ ? options.expectedHeaderCount
571
+ : headerInsertCount;
572
+ if (expectedCount > 0) {
573
+ if (options.forceOffset) {
574
+ return triggerLineNumber + expectedCount;
575
+ }
576
+ const headerInsertPosition = _getLastImportLine(filePath);
577
+ if (headerInsertPosition > 0 && headerInsertPosition < triggerLineNumber) {
578
+ return triggerLineNumber + expectedCount;
579
+ }
580
+ }
581
+ return triggerLineNumber;
582
+ }
583
+
584
+ // ═══════════════════════════════════════════════════════════════
585
+ // §10 导出:insertHeaders
586
+ // ═══════════════════════════════════════════════════════════════
587
+
588
+ /**
589
+ * 统一的头文件插入方法
590
+ *
591
+ * 逐条处理:
592
+ * 1. 三级去重
593
+ * 2. SPM 依赖检查(block/review/continue 决策)
594
+ * 3. Xcode osascript 自动插入,失败则文件写入回退
595
+ * 4. 附加 AutoSnippet 注释后缀
61
596
  *
62
597
  * @param {import('./FileWatcher.js').FileWatcher} watcher
63
- * @param {string} fullPath 目标文件绝对路径
64
- * @param {string[]} headers 待插入的 import 行数组
65
- * @param {object} [opts]
598
+ * @param {string} fullPath 目标文件绝对路径
599
+ * @param {string[]} headers 待插入的 import 行数组
600
+ * @param {object} [opts]
66
601
  * @returns {Promise<{inserted: string[], skipped: string[], cancelled: boolean}>}
67
602
  */
68
603
  export async function insertHeaders(watcher, fullPath, headers, opts = {}) {
@@ -71,119 +606,172 @@ export async function insertHeaders(watcher, fullPath, headers, opts = {}) {
71
606
  const NU = await import('../../infrastructure/external/NativeUi.js');
72
607
 
73
608
  const result = { inserted: [], skipped: [], cancelled: false };
609
+ /** @type {Map<string, string>} 模块名 → 提示注释('提示操作插入'按钮选择时记录) */
610
+ const depWarnings = opts.depWarnings instanceof Map ? new Map(opts.depWarnings) : new Map();
74
611
  if (!headers || headers.length === 0) return result;
75
612
 
76
613
  const isSwift = opts.isSwift ?? fullPath.endsWith('.swift');
77
614
 
78
- // ── 1. 去重 ──
79
- let content;
80
- try {
81
- content = readFileSync(fullPath, 'utf8');
82
- } catch {
83
- return result;
615
+ // ── Step 1: 收集已有 imports ──
616
+ const importArray = _collectImportsFromFile(fullPath, isSwift);
617
+ // .m 文件还要收集对应 .h 的 imports
618
+ if (!isSwift && !fullPath.endsWith('.h')) {
619
+ _collectImportsFromHeaderFile(fullPath, importArray);
84
620
  }
85
621
 
86
- const existingImports = new Set();
87
- for (const line of content.split('\n')) {
88
- const t = line.trim();
89
- if (t.startsWith('#import') || t.startsWith('@import') || t.startsWith('import ')) {
90
- existingImports.add(t);
622
+ // ── Step 2: SPM 服务准备 ──
623
+ // 优先复用 opts 传入的 spmService/currentTarget(避免与 _preflightDeps 重复 load)
624
+ let spmService = opts._spmService || null;
625
+ let currentTarget = opts._currentTarget || null;
626
+ if (!spmService && !opts.skipDepCheck) {
627
+ const inferredModules = _inferModulesFromHeaders(headers);
628
+ if (opts.moduleName && !inferredModules.includes(opts.moduleName)) {
629
+ inferredModules.push(opts.moduleName);
91
630
  }
92
- }
93
- const newHeaders = headers.filter(h => !existingImports.has(h.trim()));
94
- if (newHeaders.length === 0) {
95
- result.skipped = [...headers];
96
- return result;
97
- }
98
-
99
- // ── 2. 依赖检查(自动推断模块名,过滤系统框架) ──
100
- if (!opts.skipDepCheck) {
101
- const inferredModules = opts.moduleName
102
- ? [opts.moduleName]
103
- : _inferModulesFromHeaders(newHeaders);
104
-
105
- // 过滤掉系统框架
106
631
  const thirdPartyModules = inferredModules.filter(m => !_SYSTEM_FRAMEWORKS.has(m));
107
-
108
632
  if (thirdPartyModules.length > 0) {
109
- const missingModules = [];
110
- let spmAvailable = false;
111
-
112
633
  try {
113
634
  const { ServiceContainer } = await import('../../injection/ServiceContainer.js');
114
635
  const container = ServiceContainer.getInstance();
115
- const spmService = container.get('spmService');
636
+ spmService = container.get('spmService');
116
637
  if (spmService) {
117
- const targets = await spmService.getTargets();
118
- if (targets && targets.length > 0) {
119
- spmAvailable = true;
120
- const targetNames = new Set(targets.map(t => t.name));
121
- for (const mod of thirdPartyModules) {
122
- if (!targetNames.has(mod)) {
123
- missingModules.push(mod);
124
- }
125
- }
638
+ if (spmService.getFixMode() === 'off') {
639
+ spmService = null;
640
+ } else {
641
+ try { await spmService.load(); } catch { /* Package.swift 不存在则跳过 */ }
642
+ currentTarget = spmService.resolveCurrentTarget(fullPath);
126
643
  }
127
644
  }
128
- } catch {
129
- // SPM 检查失败,静默跳过
130
- }
645
+ } catch { /* SPM 检查异常不阻断 */ }
646
+ }
647
+ }
131
648
 
132
- // 仅当 SPM 确认模块缺失时才弹窗提示
133
- if (spmAvailable && missingModules.length > 0) {
134
- const depWarning = missingModules.length === 1
135
- ? `模块 "${missingModules[0]}" 不在当前 SPM 依赖中`
136
- : `以下模块不在当前 SPM 依赖中:${missingModules.join('')}`;
137
- console.log(` ⚠️ ${depWarning}`);
138
- const decision = NU.promptWithButtons(
139
- `${depWarning}\n\n仍要添加 import 吗?`,
140
- ['继续添加', '取消'],
141
- 'AutoSnippet 依赖检查'
142
- );
143
- if (decision !== '继续添加') {
144
- console.log(` ⏹️ 用户取消`);
649
+ // ── Step 3: Xcode 自动化准备 ──
650
+ const xcodeReady = XA.isXcodeRunning();
651
+ // 从当前文件内容计算 import 插入基准行(1-based)
652
+ let content;
653
+ try { content = readFileSync(fullPath, 'utf8'); } catch { return result; }
654
+ const baseInsertLine = findImportInsertLine(content, isSwift) + 1; // 0-based → 1-based
655
+ let xcodeOffset = 0; // 每次 Xcode 插入成功后 +1(修正多条 header 行号偏移)
656
+ let fileWriteUsed = false; // 一旦使用文件写入,后续全部走文件写入(避免 Xcode reload 冲突)
657
+
658
+ // ── Step 4: 逐条处理 ──
659
+ for (const header of headers) {
660
+ const headerTrimmed = header.trim();
661
+ if (!headerTrimmed) continue;
662
+
663
+ // ── 三级去重 ──
664
+ // 先按原始格式检查,再按解析后格式检查(同一 header 可能格式不同)
665
+ const preResolvedHeader = _resolveHeaderFormat(headerTrimmed, {
666
+ currentTarget,
667
+ headerModuleName: opts.moduleName || null,
668
+ isSwift,
669
+ fullPath,
670
+ projectRoot: watcher?.projectRoot || null,
671
+ });
672
+ const status = _checkImportStatus(importArray, headerTrimmed, isSwift);
673
+ const statusResolved = (preResolvedHeader !== headerTrimmed)
674
+ ? _checkImportStatus(importArray, preResolvedHeader, isSwift)
675
+ : status;
676
+ if (status.hasHeader || statusResolved.hasHeader) {
677
+ console.log(` ⏭️ 已存在(精确匹配): ${preResolvedHeader}`);
678
+ result.skipped.push(preResolvedHeader);
679
+ continue;
680
+ }
681
+ if (status.hasModule || statusResolved.hasModule) {
682
+ console.log(` ⏭️ 已存在(模块匹配): ${preResolvedHeader}`);
683
+ result.skipped.push(preResolvedHeader);
684
+ continue;
685
+ }
686
+ if (status.hasSimilarHeader || statusResolved.hasSimilarHeader) {
687
+ console.log(` ⏭️ 已存在(相似头文件): ${preResolvedHeader}`);
688
+ result.skipped.push(preResolvedHeader);
689
+ continue;
690
+ }
691
+
692
+ // ── SPM 依赖检查 ──
693
+ const headerModules = _inferModulesFromHeaders([headerTrimmed]);
694
+ if (spmService && currentTarget && !opts.skipDepCheck) {
695
+ for (const mod of headerModules) {
696
+ if (_SYSTEM_FRAMEWORKS.has(mod) || mod === currentTarget) continue;
697
+
698
+ const ensureResult = spmService.ensureDependency(currentTarget, mod);
699
+ const decision = _evaluateDepResult(ensureResult, currentTarget, mod);
700
+
701
+ if (decision.action === 'block') {
702
+ console.warn(` ⛔ 依赖被阻止: ${currentTarget} -> ${mod} (${decision.reason})`);
703
+ NU.notify(
704
+ `已阻止依赖注入\n${currentTarget} -> ${mod}\n${decision.reason}`,
705
+ 'AutoSnippet SPM 依赖策略',
706
+ );
145
707
  result.cancelled = true;
146
708
  return result;
147
709
  }
710
+
711
+ if (decision.action === 'review') {
712
+ const reviewResult = _handleDepReview({
713
+ spmService, currentTarget, mod, ensureResult, NU, depWarnings,
714
+ });
715
+ if (reviewResult.blocked) {
716
+ console.log(` ⏹️ 用户取消`);
717
+ result.cancelled = true;
718
+ return result;
719
+ }
720
+ }
148
721
  }
149
722
  }
150
- }
151
723
 
152
- // ── 3. 写入文件(V1 策略:文件写入优先,Xcode 会自动 reload) ──
153
- try {
154
- content = readFileSync(fullPath, 'utf8');
155
- const insertPoint = findImportInsertLine(content, isSwift);
156
- const lines = content.split('\n');
724
+ // ── 构建带注释后缀的 import ──
725
+ // 复用 dedup 阶段已计算的 preResolvedHeader
726
+ const resolvedHeader = preResolvedHeader;
727
+ const depHint = headerModules.find(m => depWarnings.has(m));
728
+ const importLine = depHint
729
+ ? _withAutoSnippetNote(resolvedHeader) + ` // ⚠️ 依赖缺失: ${depWarnings.get(depHint)},需手动补齐 Package.swift`
730
+ : _withAutoSnippetNote(resolvedHeader);
157
731
 
158
- // 检查 import 区后面是否已有空行,没有则补一行
159
- const lineAfterInsert = lines[insertPoint] ?? '';
160
- const needsBlankLine = lineAfterInsert.trim().length > 0;
161
- const toInsert = needsBlankLine ? [...newHeaders, ''] : [...newHeaders];
732
+ // ── 写入:Xcode 自动化优先 → 文件写入回退 ──
733
+ let inserted = false;
162
734
 
163
- lines.splice(insertPoint, 0, ...toInsert);
164
- writeFileSync(fullPath, lines.join('\n'), 'utf8');
165
- result.inserted = [...newHeaders];
166
- console.log(` 📦 已添加 ${newHeaders.length} 个依赖(文件写入)`);
167
- } catch (err) {
168
- console.warn(` ⚠️ Header 写入失败: ${err.message}`);
735
+ if (xcodeReady && !fileWriteUsed) {
736
+ // 逐条 osascript 跳转 + 粘贴
737
+ inserted = _writeImportLineXcode(importLine, baseInsertLine + xcodeOffset, XA, CM);
738
+ if (inserted) {
739
+ xcodeOffset++;
740
+ }
741
+ }
742
+
743
+ if (!inserted) {
744
+ _writeImportLineFile(fullPath, importLine, isSwift);
745
+ fileWriteUsed = true;
746
+ }
747
+
748
+ result.inserted.push(resolvedHeader);
749
+ importArray.push(resolvedHeader); // 添加到去重列表(用解析后格式)
750
+ console.log(` + ${resolvedHeader}`);
169
751
  }
170
752
 
171
- for (const h of result.inserted) {
172
- console.log(` + ${h}`);
753
+ if (result.inserted.length > 0) {
754
+ console.log(` 📦 已添加 ${result.inserted.length} 个依赖`);
173
755
  }
174
756
  return result;
175
757
  }
176
758
 
759
+ // ═══════════════════════════════════════════════════════════════
760
+ // §11 导出:insertCodeToXcode
761
+ // ═══════════════════════════════════════════════════════════════
762
+
177
763
  /**
178
- * 将选中的搜索结果代码插入 Xcode(或回退到文件写入)
764
+ * 将选中的搜索结果代码插入 Xcode
179
765
  *
180
- * V1 兼容流程(Xcode 自动化模式):
766
+ * 流程:
181
767
  * 1. 找到触发行号
182
- * 2. Cut 触发行内容(Xcode 剪切,不写文件)
183
- * 3. 依赖检查 + Headers 写入文件(Xcode 自动 reload)
184
- * 4. 计算偏移后的粘贴行号
185
- * 5. Jump 到粘贴行 选中行内容 → Cmd+V 粘贴替换
186
- * 6. 任一步失败 → 降级到纯文件写入
768
+ * 2. Preflight 预检依赖决策(不实际写入)
769
+ * 3. Cut 触发行内容(Xcode 剪切,不写文件)
770
+ * 4. 构建带缩进 + 注释标记的代码块
771
+ * 5. 插入 Headers(Xcode osascript / 文件写入)
772
+ * 6. 计算偏移后的粘贴行号(computePasteLineNumber)
773
+ * 7. Jump 到粘贴行 → 选中行内容 → Cmd+V 粘贴替换
774
+ * 8. 任一步失败 → 降级到纯文件写入
187
775
  *
188
776
  * @param {import('./FileWatcher.js').FileWatcher} watcher
189
777
  */
@@ -199,67 +787,116 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
199
787
  }
200
788
 
201
789
  const headersToInsert = (selected.headers || []).filter(h => h && h.trim());
790
+ const isSwift = fullPath.endsWith('.swift');
202
791
 
203
792
  // ═══════════════════════════════════════════════════════
204
- // 主路径:Xcode 自动化(cut + paste,headers 写文件)
793
+ // 主路径:Xcode 自动化
205
794
  // ═══════════════════════════════════════════════════════
206
795
  if (XA.isXcodeRunning()) {
207
- // Step 1: 从磁盘找到触发行号
208
- let content = readFileSync(fullPath, 'utf8');
796
+ // ── 窗口上下文验证 ──
797
+ if (!XA.isXcodeFrontmost()) {
798
+ console.warn(` ⚠️ Xcode 不是前台应用,自动化操作可能不准确`);
799
+ // 宽松模式:仅警告,不阻断
800
+ // 如需严格模式,可设置 ASD_XCODE_STRICT_FOCUS=1
801
+ if (process.env.ASD_XCODE_STRICT_FOCUS === '1') {
802
+ console.warn(` ⏹️ ASD_XCODE_STRICT_FOCUS=1, 跳过自动化`);
803
+ return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
804
+ }
805
+ }
806
+ // ── Step 1: 找到触发行号 ──
807
+ let content;
808
+ try { content = readFileSync(fullPath, 'utf8'); } catch {
809
+ return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
810
+ }
209
811
  const triggerLineNumber = findTriggerLineNumber(content, triggerLine);
210
812
  if (triggerLineNumber < 0) {
211
813
  console.warn(` ⚠️ 未在文件中找到触发行,降级为文件写入`);
212
814
  return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
213
815
  }
214
816
 
215
- // Step 2: 剪切触发行内容(V1: _tryAutoCutXcode)
817
+ // 计算触发行缩进
818
+ const lines = content.split(/\r?\n/);
819
+ const triggerContent = lines[triggerLineNumber - 1] || '';
820
+ const indentMatch = triggerContent.match(/^(\s*)/);
821
+ const indent = indentMatch ? indentMatch[1] : '';
822
+
823
+ // ── Step 2: Preflight 预检依赖 ──
824
+ let preflightDepWarnings = null;
825
+ let _spmServiceCached = null;
826
+ let _currentTargetCached = null;
827
+ if (headersToInsert.length > 0) {
828
+ const preflight = await _preflightDeps(fullPath, headersToInsert, selected, NU);
829
+ if (preflight.blocked) {
830
+ console.log(` ⏹️ 依赖检查被阻止,跳过代码插入`);
831
+ return;
832
+ }
833
+ if (preflight.depWarnings && preflight.depWarnings.size > 0) {
834
+ preflightDepWarnings = preflight.depWarnings;
835
+ }
836
+ // 缓存 spmService/currentTarget 供 insertHeaders 复用(避免重复 load)
837
+ _spmServiceCached = preflight._spmService || null;
838
+ _currentTargetCached = preflight._currentTarget || null;
839
+ }
840
+
841
+ // ── Step 3: 剪切触发行内容 ──
216
842
  const cutOk = XA.cutLineInXcode(triggerLineNumber);
217
843
  if (!cutOk) {
218
844
  console.warn(` ⚠️ 自动剪切失败,降级为文件写入`);
219
- return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
845
+ // Preflight 已通过,skipDepCheck 避免重复弹窗
846
+ return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher, { skipDepCheck: true });
220
847
  }
221
848
  await _sleep(300);
222
849
 
223
- // Step 3: 依赖检查 + Headers 写入文件(Xcode 自动 reload)
850
+ // ── Step 4: 构建带缩进的代码块 ──
851
+ const codeLines = code.split(/\r?\n/);
852
+ // 移除末尾空行
853
+ while (codeLines.length > 0 && !codeLines[codeLines.length - 1].trim()) {
854
+ codeLines.pop();
855
+ }
856
+ const indentedLines = codeLines.map(line => line ? indent + line : line);
857
+ // 注释标记
858
+ const commentMarker = _generateInsertMarker(fullPath, selected);
859
+ const markedLines = commentMarker
860
+ ? [indent + commentMarker, ...indentedLines]
861
+ : indentedLines;
862
+ const indentedCode = markedLines.join('\n');
863
+
864
+ // ── Step 5: 插入 Headers ──
224
865
  let headerInsertCount = 0;
225
866
  if (headersToInsert.length > 0) {
226
867
  const headerResult = await insertHeaders(watcher, fullPath, headersToInsert, {
227
868
  moduleName: selected.moduleName || null,
869
+ isSwift,
870
+ skipDepCheck: true, // Preflight 已检查过
871
+ depWarnings: preflightDepWarnings,
872
+ _spmService: _spmServiceCached,
873
+ _currentTarget: _currentTargetCached,
228
874
  });
229
875
  if (headerResult.cancelled) {
230
- console.log(` ⏹️ 依赖检查被取消,跳过代码插入`);
876
+ console.log(` ⏹️ Headers 插入被取消`);
231
877
  return;
232
878
  }
233
879
  headerInsertCount = headerResult.inserted.length;
234
880
  }
235
881
 
236
- // Step 4: 计算偏移后的粘贴行号(V1: computePasteLineNumber)
237
- // headers 写在 import 区(触发行之前),所以触发行向下偏移
238
- let pasteLineNumber = triggerLineNumber;
239
- if (headerInsertCount > 0) {
240
- // 检查 headers 插入位置是否在触发行之前
241
- content = readFileSync(fullPath, 'utf8');
242
- const importInsertLine = findImportInsertLine(content, fullPath.endsWith('.swift'));
243
- if (importInsertLine <= triggerLineNumber) {
244
- pasteLineNumber = triggerLineNumber + headerInsertCount;
245
- // 如果补了空行,再加 1
246
- const lines = content.split('\n');
247
- const lineAfterHeaders = lines[importInsertLine + headerInsertCount - 1];
248
- if (lineAfterHeaders !== undefined && lineAfterHeaders.trim() === '') {
249
- // insertHeaders 补了空行
250
- pasteLineNumber += 1;
251
- }
252
- }
253
- }
882
+ // ── Step 6: 计算偏移后的粘贴行号 ──
883
+ // 使用实际插入的 header 数量计算偏移,而非期望数量
884
+ // headers 全部重复被跳过时,headerInsertCount = 0,不应偏移
885
+ const pasteLineNumber = _computePasteLineNumber(
886
+ triggerLineNumber,
887
+ headerInsertCount,
888
+ fullPath,
889
+ { forceOffset: headerInsertCount > 0, expectedHeaderCount: headerInsertCount },
890
+ );
254
891
 
255
- // 等待 Xcode 检测到文件变化并 reload
892
+ // 如果 headers 通过文件写入,等待 Xcode reload
256
893
  if (headerInsertCount > 0) {
257
894
  await _sleep(600);
258
895
  }
259
896
 
260
- // Step 5: Jump + 选中行内容 + 粘贴替换
897
+ // ── Step 7: Jump + 选中行内容 + 粘贴替换 ──
261
898
  await CM.withClipboardSave(async () => {
262
- const wrote = CM.write(code);
899
+ const wrote = CM.write(indentedCode);
263
900
  if (!wrote) {
264
901
  console.warn(` ⚠️ 剪贴板写入失败`);
265
902
  return;
@@ -272,7 +909,7 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
272
909
  });
273
910
 
274
911
  console.log(` ✅ 代码已粘贴到 Xcode(可 Cmd+Z 撤销)`);
275
- NU.notify(`已插入「${selected.title}」`, 'AutoSnippet');
912
+ NU.notify(`已插入「${selected.title || '代码片段'}」`, 'AutoSnippet');
276
913
  return;
277
914
  }
278
915
 
@@ -282,14 +919,90 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
282
919
  return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
283
920
  }
284
921
 
922
+ // ═══════════════════════════════════════════════════════════════
923
+ // §12 Preflight 依赖预检
924
+ // ═══════════════════════════════════════════════════════════════
925
+
285
926
  /**
286
- * 纯文件写入降级方案
927
+ * 预检所有 headers 的 SPM 依赖状态
928
+ *
929
+ * 不实际写入文件,只检查并弹窗确认。
930
+ * 返回 { blocked: true } 表示有依赖被阻止或用户取消。
287
931
  */
288
- async function _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher) {
932
+ async function _preflightDeps(fullPath, headers, selected, NU) {
933
+ const result = { blocked: false };
934
+
935
+ // 始终从所有 headers 推断模块(不仅依赖 selected.moduleName)
936
+ const inferredModules = _inferModulesFromHeaders(headers);
937
+ if (selected.moduleName && !inferredModules.includes(selected.moduleName)) {
938
+ inferredModules.push(selected.moduleName);
939
+ }
940
+ const thirdPartyModules = inferredModules.filter(m => !_SYSTEM_FRAMEWORKS.has(m));
941
+ if (thirdPartyModules.length === 0) return result;
942
+
943
+ try {
944
+ const { ServiceContainer } = await import('../../injection/ServiceContainer.js');
945
+ const container = ServiceContainer.getInstance();
946
+ const spmService = container.get('spmService');
947
+ if (!spmService) return result;
948
+
949
+ // Fix Mode 检查:off 模式完全跳过
950
+ if (spmService.getFixMode() === 'off') return result;
951
+
952
+ try { await spmService.load(); } catch { return result; }
953
+
954
+ const currentTarget = spmService.resolveCurrentTarget(fullPath);
955
+ if (!currentTarget) return result;
956
+
957
+ for (const mod of thirdPartyModules) {
958
+ if (mod === currentTarget) continue;
959
+
960
+ const ensureResult = spmService.ensureDependency(currentTarget, mod);
961
+ const decision = _evaluateDepResult(ensureResult, currentTarget, mod);
962
+
963
+ if (decision.action === 'block') {
964
+ console.warn(` ⛔ [Preflight] 依赖被阻止: ${currentTarget} -> ${mod} (${decision.reason})`);
965
+ NU.notify(
966
+ `已阻止依赖注入\n${currentTarget} -> ${mod}\n${decision.reason}`,
967
+ 'AutoSnippet SPM 依赖策略',
968
+ );
969
+ result.blocked = true;
970
+ return result;
971
+ }
972
+
973
+ if (decision.action === 'review') {
974
+ if (!result.depWarnings) result.depWarnings = new Map();
975
+ const reviewResult = _handleDepReview({
976
+ spmService, currentTarget, mod, ensureResult, NU,
977
+ depWarnings: result.depWarnings, label: 'Preflight',
978
+ });
979
+ if (reviewResult.blocked) {
980
+ result.blocked = true;
981
+ return result;
982
+ }
983
+ }
984
+ }
985
+
986
+ // 缓存 spmService/currentTarget 供下游 insertHeaders 复用
987
+ result._spmService = spmService;
988
+ result._currentTarget = currentTarget;
989
+ } catch (err) {
990
+ console.warn(` ⚠️ Preflight 依赖检查异常: ${err.message}`);
991
+ }
992
+
993
+ return result;
994
+ }
995
+
996
+ // ═══════════════════════════════════════════════════════════════
997
+ // §13 文件写入降级
998
+ // ═══════════════════════════════════════════════════════════════
999
+
1000
+ async function _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher, opts = {}) {
289
1001
  // 先写 headers
290
1002
  if (headersToInsert.length > 0) {
291
1003
  const headerResult = await insertHeaders(watcher, fullPath, headersToInsert, {
292
1004
  moduleName: selected.moduleName || null,
1005
+ skipDepCheck: opts.skipDepCheck || false, // Preflight 已通过时跳过重复检查
293
1006
  });
294
1007
  if (headerResult.cancelled) return;
295
1008
  }
@@ -298,12 +1011,48 @@ async function _fileInsertFallback(fullPath, selected, triggerLine, headersToIns
298
1011
  const code = selected.code || '';
299
1012
  try {
300
1013
  const content = readFileSync(fullPath, 'utf8');
301
- const newContent = content.replace(triggerLine.trim(), code);
302
- if (newContent !== content) {
1014
+ const lines = content.split(/\r?\n/);
1015
+ const triggerTrimmed = triggerLine.trim();
1016
+
1017
+ // 从后往前查找触发行
1018
+ let found = -1;
1019
+ for (let i = lines.length - 1; i >= 0; i--) {
1020
+ if (lines[i].trim() === triggerTrimmed) {
1021
+ found = i;
1022
+ break;
1023
+ }
1024
+ }
1025
+
1026
+ if (found >= 0) {
1027
+ // 计算缩进 → 对齐 → 替换
1028
+ const triggerContent = lines[found];
1029
+ const indentMatch = triggerContent.match(/^(\s*)/);
1030
+ const indent = indentMatch ? indentMatch[1] : '';
1031
+
1032
+ const codeLines = code.split(/\r?\n/);
1033
+ while (codeLines.length > 0 && !codeLines[codeLines.length - 1].trim()) {
1034
+ codeLines.pop();
1035
+ }
1036
+ const indentedLines = codeLines.map(line => line ? indent + line : line);
1037
+
1038
+ const commentMarker = _generateInsertMarker(fullPath, selected);
1039
+ const markedLines = commentMarker
1040
+ ? [indent + commentMarker, ...indentedLines]
1041
+ : indentedLines;
1042
+
1043
+ while (markedLines.length > 0 && !markedLines[markedLines.length - 1].trim()) {
1044
+ markedLines.pop();
1045
+ }
1046
+
1047
+ const newLines = [...lines.slice(0, found), ...markedLines, ...lines.slice(found + 1)];
1048
+ const newContent = newLines.join('\n');
1049
+ saveEventFilter.markWrite(fullPath, newContent);
303
1050
  writeFileSync(fullPath, newContent, 'utf8');
304
1051
  console.log(` ✅ 代码已写入文件(替换触发行)`);
305
1052
  } else {
306
- writeFileSync(fullPath, content + '\n' + code + '\n', 'utf8');
1053
+ const appendContent = content + '\n' + code + '\n';
1054
+ saveEventFilter.markWrite(fullPath, appendContent);
1055
+ writeFileSync(fullPath, appendContent, 'utf8');
307
1056
  console.log(` ✅ 代码已追加到文件末尾`);
308
1057
  }
309
1058
  } catch (err) {
@@ -311,8 +1060,38 @@ async function _fileInsertFallback(fullPath, selected, triggerLine, headersToIns
311
1060
  }
312
1061
  }
313
1062
 
1063
+ // ═══════════════════════════════════════════════════════════════
1064
+ // §14 注释标记生成
1065
+ // ═══════════════════════════════════════════════════════════════
1066
+
1067
+ function _generateInsertMarker(filePath, selected) {
1068
+ try {
1069
+ const ext = (filePath.match(/\.[^.]+$/) || [''])[0].toLowerCase();
1070
+ const trigger = selected.trigger ? `[${selected.trigger}]` : '';
1071
+ const recipeName = selected.name ? ` from ${selected.name}` : '';
1072
+ const timestamp = new Date().toLocaleString('zh-CN', {
1073
+ year: 'numeric', month: '2-digit', day: '2-digit',
1074
+ hour: '2-digit', minute: '2-digit',
1075
+ });
1076
+
1077
+ const marker = `🤖 AutoSnippet${trigger}${recipeName} @ ${timestamp}`;
1078
+
1079
+ if (['.py', '.rb'].includes(ext)) return `# ${marker}`;
1080
+ if (['.lua', '.sql'].includes(ext)) return `-- ${marker}`;
1081
+ if (['.html', '.xml', '.svg'].includes(ext)) return `<!-- ${marker} -->`;
1082
+ if (['.css', '.scss', '.less'].includes(ext)) return `/* ${marker} */`;
1083
+ return `// ${marker}`;
1084
+ } catch {
1085
+ return null;
1086
+ }
1087
+ }
1088
+
1089
+ // ═══════════════════════════════════════════════════════════════
1090
+ // §15 工具函数
1091
+ // ═══════════════════════════════════════════════════════════════
1092
+
314
1093
  /**
315
- * 查找任意触发行的行号 (1-based)
1094
+ * 查找触发行的行号(1-based,-1 表示未找到)
316
1095
  */
317
1096
  export function findTriggerLineNumber(content, triggerLine) {
318
1097
  if (!content || !triggerLine) return -1;
@@ -325,7 +1104,7 @@ export function findTriggerLineNumber(content, triggerLine) {
325
1104
  }
326
1105
 
327
1106
  /**
328
- * 查找 import 语句的插入位置(行号,0-based
1107
+ * 查找 import 语句的插入位置(0-based 行索引,在最后一个 import 之后)
329
1108
  */
330
1109
  export function findImportInsertLine(content, isSwift) {
331
1110
  const lines = content.split('\n');
@@ -333,13 +1112,9 @@ export function findImportInsertLine(content, isSwift) {
333
1112
  for (let i = 0; i < lines.length; i++) {
334
1113
  const t = lines[i].trim();
335
1114
  if (isSwift) {
336
- if (t.startsWith('import ') && !t.startsWith('import (')) {
337
- lastImportLine = i;
338
- }
1115
+ if (t.startsWith('import ') && !t.startsWith('import (')) lastImportLine = i;
339
1116
  } else {
340
- if (t.startsWith('#import') || t.startsWith('@import')) {
341
- lastImportLine = i;
342
- }
1117
+ if (t.startsWith('#import') || t.startsWith('@import')) lastImportLine = i;
343
1118
  }
344
1119
  }
345
1120
  return lastImportLine >= 0 ? lastImportLine + 1 : 0;