autosnippet 3.2.4 → 3.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/bin/cli.js +164 -145
- package/config/constitution.yaml +2 -0
- package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
- package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/CliLogger.js +78 -0
- package/lib/cli/SetupService.js +9 -718
- package/lib/cli/UpgradeService.js +23 -398
- package/lib/cli/deploy/FileDeployer.js +562 -0
- package/lib/cli/deploy/FileManifest.js +272 -0
- package/lib/external/mcp/McpServer.js +22 -26
- package/lib/external/mcp/autoApproveInjector.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
- package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
- package/lib/external/mcp/handlers/consolidated.js +8 -9
- package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
- package/lib/external/mcp/handlers/guard.js +283 -5
- package/lib/external/mcp/handlers/task.js +183 -9
- package/lib/external/mcp/tools.js +32 -81
- package/lib/http/routes/task.js +55 -0
- package/lib/service/chat/AnalystAgent.js +12 -12
- package/lib/service/chat/ChatAgent.js +227 -545
- package/lib/service/chat/ChatAgentPrompts.js +9 -11
- package/lib/service/chat/ContextWindow.js +2 -296
- package/lib/service/chat/EpisodicConsolidator.js +15 -15
- package/lib/service/chat/ExplorationTracker.js +1262 -0
- package/lib/service/chat/HandoffProtocol.js +8 -9
- package/lib/service/chat/Memory.js +4 -0
- package/lib/service/chat/ProducerAgent.js +9 -6
- package/lib/service/chat/ProjectSemanticMemory.js +4 -0
- package/lib/service/chat/ReasoningTrace.js +182 -0
- package/lib/service/chat/WorkingMemory.js +4 -0
- package/lib/service/chat/memory/ActiveContext.js +910 -0
- package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
- package/lib/service/chat/memory/PersistentMemory.js +450 -0
- package/lib/service/chat/memory/SessionStore.js +896 -0
- package/lib/service/chat/memory/index.js +13 -0
- package/lib/service/chat/tools/ast-graph.js +17 -16
- package/lib/service/cursor/AgentInstructionsGenerator.js +76 -47
- package/lib/service/cursor/FileProtection.js +4 -1
- package/lib/service/guard/GuardCheckEngine.js +10 -3
- package/lib/service/task/TaskGraphService.js +3 -3
- package/lib/shared/LanguageService.js +2 -1
- package/package.json +1 -1
- package/skills/autosnippet-intent/SKILL.md +1 -3
- package/skills/autosnippet-recipes/SKILL.md +1 -3
- package/templates/claude-code/commands/prime.md +19 -0
- package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
- package/templates/claude-code/settings.json +21 -0
- package/templates/copilot-instructions.md +64 -177
- package/templates/cursor-hooks/commands/prime.md +12 -0
- package/templates/cursor-hooks/hooks/session-start.sh +10 -0
- package/templates/cursor-hooks/hooks.json +11 -0
- package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
- package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
- package/lib/external/mcp/handlers/decide.js +0 -109
- package/lib/external/mcp/handlers/ready.js +0 -42
- package/lib/service/chat/ReasoningLayer.js +0 -888
- package/templates/claude-hooks.yaml +0 -19
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileDeployer — 统一文件部署引擎
|
|
3
|
+
*
|
|
4
|
+
* 根据 FileManifest 中的策略,部署文件到用户项目。
|
|
5
|
+
* SetupService 和 UpgradeService 共享此引擎,消除所有重复代码。
|
|
6
|
+
*
|
|
7
|
+
* 策略实现:
|
|
8
|
+
* overwrite — mkdirSync + copyFileSync
|
|
9
|
+
* overwrite-dir — 递归复制目录中的所有文件
|
|
10
|
+
* signature-safe — safeCopyFile(签名匹配才覆盖)
|
|
11
|
+
* create-only — 仅在文件不存在时复制
|
|
12
|
+
* merge-json — 读取现有 JSON,合并 autosnippet 键,写回
|
|
13
|
+
* merge-gitignore — 增量追加缺失规则 + 迁移旧格式
|
|
14
|
+
* backup-overwrite — 备份旧文件再覆盖
|
|
15
|
+
* inject-marker — 在 <!-- autosnippet:begin/end --> 标记间注入
|
|
16
|
+
* generate — 调用自定义生成函数
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import {
|
|
21
|
+
copyFileSync,
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from 'node:fs';
|
|
29
|
+
import { dirname, join, resolve } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { checkWriteSafety, safeCopyFile } from '../../service/cursor/FileProtection.js';
|
|
32
|
+
import { injectAutoApprove } from '../../external/mcp/autoApproveInjector.js';
|
|
33
|
+
import {
|
|
34
|
+
MANIFEST,
|
|
35
|
+
GITIGNORE_RULES,
|
|
36
|
+
GITIGNORE_MIGRATIONS,
|
|
37
|
+
buildMcpServerEntry,
|
|
38
|
+
} from './FileManifest.js';
|
|
39
|
+
|
|
40
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
+
const __dirname = dirname(__filename);
|
|
42
|
+
|
|
43
|
+
/** AutoSnippet 源码仓库根目录 */
|
|
44
|
+
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
|
|
45
|
+
const TEMPLATES_DIR = join(REPO_ROOT, 'templates');
|
|
46
|
+
|
|
47
|
+
export class FileDeployer {
|
|
48
|
+
/**
|
|
49
|
+
* @param {{ projectRoot: string, force?: boolean }} options
|
|
50
|
+
*/
|
|
51
|
+
constructor({ projectRoot, force = false }) {
|
|
52
|
+
this.projectRoot = resolve(projectRoot);
|
|
53
|
+
this.projectName = this.projectRoot.split('/').pop();
|
|
54
|
+
this.force = force;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ═══ 公共入口 ═══════════════════════════════════════ */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 部署所有适用的文件
|
|
61
|
+
* @param {'setup'|'upgrade'} mode
|
|
62
|
+
* @param {{ filter?: string[] }} options — 可选过滤部署的 category
|
|
63
|
+
* @returns {{ deployed: string[], skipped: string[], errors: Array<{id: string, error: string}> }}
|
|
64
|
+
*/
|
|
65
|
+
deployAll(mode, { filter } = {}) {
|
|
66
|
+
const applicable = MANIFEST.filter((entry) => {
|
|
67
|
+
if (entry.on !== 'both' && entry.on !== mode) return false;
|
|
68
|
+
if (filter && !filter.includes(entry.category)) return false;
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const deployed = [];
|
|
73
|
+
const skipped = [];
|
|
74
|
+
const errors = [];
|
|
75
|
+
|
|
76
|
+
for (const entry of applicable) {
|
|
77
|
+
try {
|
|
78
|
+
const result = this._deployOne(entry, mode);
|
|
79
|
+
if (result) {
|
|
80
|
+
deployed.push(entry.id);
|
|
81
|
+
} else {
|
|
82
|
+
skipped.push(entry.id);
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
errors.push({ id: entry.id, error: err.message });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { deployed, skipped, errors };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 按 category 部署
|
|
94
|
+
* @param {string} category
|
|
95
|
+
* @param {'setup'|'upgrade'} mode
|
|
96
|
+
*/
|
|
97
|
+
deployCategory(category, mode) {
|
|
98
|
+
return this.deployAll(mode, { filter: [category] });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ═══ 单文件部署路由 ═════════════════════════════════ */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {object} entry — Manifest 条目
|
|
105
|
+
* @param {'setup'|'upgrade'} mode
|
|
106
|
+
* @returns {boolean} — 是否实际写入了文件
|
|
107
|
+
*/
|
|
108
|
+
_deployOne(entry, mode) {
|
|
109
|
+
switch (entry.strategy) {
|
|
110
|
+
case 'overwrite':
|
|
111
|
+
return this._strategyOverwrite(entry);
|
|
112
|
+
case 'overwrite-dir':
|
|
113
|
+
return this._strategyOverwriteDir(entry);
|
|
114
|
+
case 'signature-safe':
|
|
115
|
+
return this._strategySignatureSafe(entry, mode);
|
|
116
|
+
case 'create-only':
|
|
117
|
+
return this._strategyCreateOnly(entry);
|
|
118
|
+
case 'merge-json':
|
|
119
|
+
return this._strategyMergeJson(entry);
|
|
120
|
+
case 'merge-gitignore':
|
|
121
|
+
return this._strategyMergeGitignore(entry);
|
|
122
|
+
case 'backup-overwrite':
|
|
123
|
+
return this._strategyBackupOverwrite(entry);
|
|
124
|
+
case 'inject-marker':
|
|
125
|
+
return this._strategyInjectMarker(entry);
|
|
126
|
+
case 'generate':
|
|
127
|
+
return this._strategyGenerate(entry);
|
|
128
|
+
default:
|
|
129
|
+
throw new Error(`Unknown deploy strategy: ${entry.strategy}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ═══ 策略实现 ═══════════════════════════════════════ */
|
|
134
|
+
|
|
135
|
+
/** overwrite — AutoSnippet 完全拥有,始终覆盖 */
|
|
136
|
+
_strategyOverwrite(entry) {
|
|
137
|
+
const src = join(TEMPLATES_DIR, entry.src);
|
|
138
|
+
if (!existsSync(src)) return false;
|
|
139
|
+
|
|
140
|
+
const dest = join(this.projectRoot, entry.dest);
|
|
141
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
142
|
+
copyFileSync(src, dest);
|
|
143
|
+
if (entry.chmod) this._chmodExec(dest);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** overwrite-dir — 递归覆盖目录 */
|
|
148
|
+
_strategyOverwriteDir(entry) {
|
|
149
|
+
const srcDir = join(TEMPLATES_DIR, entry.src);
|
|
150
|
+
if (!existsSync(srcDir)) return false;
|
|
151
|
+
|
|
152
|
+
const destDir = join(this.projectRoot, entry.dest);
|
|
153
|
+
const copied = this._copyDirRecursive(srcDir, destDir, entry.chmod);
|
|
154
|
+
|
|
155
|
+
// 清理旧文件
|
|
156
|
+
if (entry.cleanup) {
|
|
157
|
+
for (const rel of entry.cleanup) {
|
|
158
|
+
const old = join(this.projectRoot, rel);
|
|
159
|
+
if (existsSync(old)) {
|
|
160
|
+
try { unlinkSync(old); } catch { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return copied;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** signature-safe — 有 AutoSnippet 签名才覆盖 */
|
|
169
|
+
_strategySignatureSafe(entry, mode) {
|
|
170
|
+
const src = join(TEMPLATES_DIR, entry.src);
|
|
171
|
+
if (!existsSync(src)) return false;
|
|
172
|
+
|
|
173
|
+
const dest = join(this.projectRoot, entry.dest);
|
|
174
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
175
|
+
|
|
176
|
+
// setup + 不存在 → 直接复制
|
|
177
|
+
if (mode === 'setup' && !existsSync(dest)) {
|
|
178
|
+
copyFileSync(src, dest);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// setup + 已存在 + 非 force → 尝试签名覆盖
|
|
183
|
+
if (mode === 'setup' && existsSync(dest) && !this.force) {
|
|
184
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
185
|
+
if (!canWrite) {
|
|
186
|
+
// 签名保护失败 → 尝试 fallback 策略
|
|
187
|
+
if (entry.fallback === 'inject-marker') {
|
|
188
|
+
return this._strategyInjectMarker(entry);
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
copyFileSync(src, dest);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// upgrade 或 force → safeCopyFile
|
|
197
|
+
const { written } = safeCopyFile(src, dest);
|
|
198
|
+
if (!written && entry.fallback === 'inject-marker') {
|
|
199
|
+
return this._strategyInjectMarker(entry);
|
|
200
|
+
}
|
|
201
|
+
return written;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** create-only — 仅在不存在时创建 */
|
|
205
|
+
_strategyCreateOnly(entry) {
|
|
206
|
+
let dest;
|
|
207
|
+
if (entry.resolveDest) {
|
|
208
|
+
dest = this._resolvers[entry.resolveDest]?.call(this);
|
|
209
|
+
if (!dest) return false;
|
|
210
|
+
} else {
|
|
211
|
+
dest = join(this.projectRoot, entry.dest);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (existsSync(dest) && !this.force) return false;
|
|
215
|
+
|
|
216
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
217
|
+
if (!canWrite) return false;
|
|
218
|
+
|
|
219
|
+
const src = join(TEMPLATES_DIR, entry.src);
|
|
220
|
+
if (!existsSync(src)) return false;
|
|
221
|
+
|
|
222
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
223
|
+
copyFileSync(src, dest);
|
|
224
|
+
if (entry.chmod) this._chmodExec(dest);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** merge-json — 读取现有 JSON,合并 autosnippet 键 */
|
|
229
|
+
_strategyMergeJson(entry) {
|
|
230
|
+
const dest = join(this.projectRoot, entry.dest);
|
|
231
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
232
|
+
|
|
233
|
+
let config = {};
|
|
234
|
+
if (existsSync(dest)) {
|
|
235
|
+
try { config = JSON.parse(readFileSync(dest, 'utf8')); } catch { /* */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const parentKey = entry.jsonKey;
|
|
239
|
+
if (!config[parentKey]) config[parentKey] = {};
|
|
240
|
+
|
|
241
|
+
const ide = entry.id === 'vscode-mcp' ? 'vscode' : 'cursor';
|
|
242
|
+
config[parentKey].autosnippet = buildMcpServerEntry(this.projectRoot, ide);
|
|
243
|
+
|
|
244
|
+
writeFileSync(dest, JSON.stringify(config, null, 2));
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** merge-gitignore — 增量追加规则 + 迁移旧格式 */
|
|
249
|
+
_strategyMergeGitignore(_entry) {
|
|
250
|
+
const giPath = join(this.projectRoot, '.gitignore');
|
|
251
|
+
let content = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
252
|
+
let changed = false;
|
|
253
|
+
|
|
254
|
+
// 1. 迁移旧格式
|
|
255
|
+
for (const migration of GITIGNORE_MIGRATIONS) {
|
|
256
|
+
if (migration.find.test(content)) {
|
|
257
|
+
content = content.replace(migration.find, migration.replace);
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 2. 追加缺失规则
|
|
263
|
+
for (const rule of GITIGNORE_RULES) {
|
|
264
|
+
const pattern = rule.pattern;
|
|
265
|
+
// 对 negation 规则 (!xxx) 检查原模式
|
|
266
|
+
const checkStr = rule.negation ? pattern : pattern.replace(/[[\]*?]/g, '\\$&');
|
|
267
|
+
if (!content.includes(pattern)) {
|
|
268
|
+
const prefix = rule.comment ? `\n# ${rule.comment}\n` : '';
|
|
269
|
+
content += `${prefix}${pattern}\n`;
|
|
270
|
+
changed = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 3. 确保 AutoSnippet/ 不被忽略
|
|
275
|
+
const lines = content.split('\n');
|
|
276
|
+
const hasIgnoreAS = lines.some((l) => {
|
|
277
|
+
const t = l.trim();
|
|
278
|
+
return (t === 'AutoSnippet/' || t === 'AutoSnippet') && !t.startsWith('#') && !t.startsWith('!');
|
|
279
|
+
});
|
|
280
|
+
if (hasIgnoreAS && !lines.some((l) => l.trim() === '!AutoSnippet/')) {
|
|
281
|
+
content += `\n# AutoSnippet 知识库必须入库(取消上方忽略)\n!AutoSnippet/\n`;
|
|
282
|
+
changed = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (changed) {
|
|
286
|
+
writeFileSync(giPath, content);
|
|
287
|
+
}
|
|
288
|
+
return changed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** backup-overwrite — 备份旧文件后覆盖 */
|
|
292
|
+
_strategyBackupOverwrite(entry) {
|
|
293
|
+
const src = join(TEMPLATES_DIR, entry.src);
|
|
294
|
+
if (!existsSync(src)) return false;
|
|
295
|
+
|
|
296
|
+
// 需要目标目录存在
|
|
297
|
+
if (entry.requireDir) {
|
|
298
|
+
const reqDir = join(this.projectRoot, entry.requireDir);
|
|
299
|
+
if (!existsSync(reqDir)) return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const dest = join(this.projectRoot, entry.dest);
|
|
303
|
+
|
|
304
|
+
if (existsSync(dest)) {
|
|
305
|
+
const oldContent = readFileSync(dest, 'utf8');
|
|
306
|
+
const newContent = readFileSync(src, 'utf8');
|
|
307
|
+
if (oldContent === newContent) return false; // 无变化
|
|
308
|
+
copyFileSync(dest, `${dest}.bak`); // 备份
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
312
|
+
copyFileSync(src, dest);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** inject-marker — 在 autosnippet:begin/end 标记间注入 */
|
|
317
|
+
_strategyInjectMarker(entry) {
|
|
318
|
+
const BEGIN_MARKER = '<!-- autosnippet:begin -->';
|
|
319
|
+
const END_MARKER = '<!-- autosnippet:end -->';
|
|
320
|
+
|
|
321
|
+
const src = join(TEMPLATES_DIR, entry.src);
|
|
322
|
+
if (!existsSync(src)) return false;
|
|
323
|
+
|
|
324
|
+
const templateContent = readFileSync(src, 'utf8');
|
|
325
|
+
const beginIdx = templateContent.indexOf(BEGIN_MARKER);
|
|
326
|
+
const endIdx = templateContent.indexOf(END_MARKER);
|
|
327
|
+
if (beginIdx === -1 || endIdx === -1) return false;
|
|
328
|
+
|
|
329
|
+
const snippet = templateContent.slice(beginIdx, endIdx + END_MARKER.length);
|
|
330
|
+
const dest = join(this.projectRoot, entry.dest);
|
|
331
|
+
const destDir = dirname(dest);
|
|
332
|
+
mkdirSync(destDir, { recursive: true });
|
|
333
|
+
|
|
334
|
+
if (existsSync(dest)) {
|
|
335
|
+
const existing = readFileSync(dest, 'utf8');
|
|
336
|
+
if (existing.includes(BEGIN_MARKER)) {
|
|
337
|
+
// 替换现有段落
|
|
338
|
+
const updated = existing.replace(
|
|
339
|
+
new RegExp(`${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`),
|
|
340
|
+
snippet,
|
|
341
|
+
);
|
|
342
|
+
writeFileSync(dest, updated);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
// 追加到末尾
|
|
346
|
+
writeFileSync(dest, `${existing}\n\n${snippet}\n`);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
writeFileSync(dest, `${snippet}\n`);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** generate — 自定义生成逻辑 */
|
|
355
|
+
_strategyGenerate(entry) {
|
|
356
|
+
const fn = this._generators[entry.generate];
|
|
357
|
+
if (!fn) {
|
|
358
|
+
throw new Error(`Unknown generator: ${entry.generate}`);
|
|
359
|
+
}
|
|
360
|
+
return fn.call(this);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ═══ 自定义生成器 ═══════════════════════════════════ */
|
|
364
|
+
|
|
365
|
+
_generators = {
|
|
366
|
+
/** AGENTS.md 静态骨架 */
|
|
367
|
+
generateAgentsMd() {
|
|
368
|
+
const claudePath = join(this.projectRoot, 'CLAUDE.md');
|
|
369
|
+
if (existsSync(claudePath)) return false; // 有 CLAUDE.md 时跳过
|
|
370
|
+
|
|
371
|
+
const agentsPath = join(this.projectRoot, 'AGENTS.md');
|
|
372
|
+
if (existsSync(agentsPath) && !this.force) return false;
|
|
373
|
+
|
|
374
|
+
const { canWrite } = checkWriteSafety(agentsPath);
|
|
375
|
+
if (!canWrite) return false;
|
|
376
|
+
|
|
377
|
+
const content = [
|
|
378
|
+
`# ${this.projectName} — Agent Instructions`,
|
|
379
|
+
'',
|
|
380
|
+
'> Auto-generated by AutoSnippet.',
|
|
381
|
+
'',
|
|
382
|
+
'## AutoSnippet Integration',
|
|
383
|
+
'',
|
|
384
|
+
'This project uses **AutoSnippet** for knowledge management and decision tracking.',
|
|
385
|
+
'',
|
|
386
|
+
'### MCP Tools',
|
|
387
|
+
'',
|
|
388
|
+
'- `autosnippet_search` — Search knowledge',
|
|
389
|
+
'- `autosnippet_knowledge` — Browse/get recipes',
|
|
390
|
+
'- `autosnippet_submit_knowledge` — Submit candidate',
|
|
391
|
+
'- `autosnippet_guard` — Code compliance check',
|
|
392
|
+
'- `autosnippet_health` — Service health & KB stats',
|
|
393
|
+
'- `autosnippet_task` — Unified task & decision management (prime/create/claim/close/record_decision/revise_decision/unpin_decision/list_decisions)',
|
|
394
|
+
'',
|
|
395
|
+
'### VS Code Agent Mode',
|
|
396
|
+
'',
|
|
397
|
+
'Type `#asd` before your message in Agent Mode to activate project memory.',
|
|
398
|
+
'',
|
|
399
|
+
'### Constraints',
|
|
400
|
+
'',
|
|
401
|
+
'1. Do NOT modify knowledge base files directly.',
|
|
402
|
+
'2. Create or update knowledge only through MCP tools.',
|
|
403
|
+
'',
|
|
404
|
+
].join('\n');
|
|
405
|
+
|
|
406
|
+
writeFileSync(agentsPath, content);
|
|
407
|
+
return true;
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
/** 安装 Cursor Skills */
|
|
411
|
+
installSkills() {
|
|
412
|
+
const installScript = join(REPO_ROOT, 'scripts', 'install-cursor-skill.js');
|
|
413
|
+
if (!existsSync(installScript)) return false;
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
execSync(`node "${installScript}"`, {
|
|
417
|
+
cwd: this.projectRoot,
|
|
418
|
+
stdio: 'pipe',
|
|
419
|
+
env: { ...process.env, NODE_PATH: join(REPO_ROOT, 'node_modules') },
|
|
420
|
+
});
|
|
421
|
+
return true;
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
/** 确保 AutoSnippet/skills/ 目录存在 */
|
|
428
|
+
ensureSkillsDir() {
|
|
429
|
+
const autoDir = join(this.projectRoot, 'AutoSnippet');
|
|
430
|
+
if (!existsSync(autoDir)) return false;
|
|
431
|
+
|
|
432
|
+
const skillsDir = join(autoDir, 'skills');
|
|
433
|
+
if (existsSync(skillsDir)) return false;
|
|
434
|
+
|
|
435
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
436
|
+
return true;
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
/** 触发 Cursor Delivery Pipeline 动态生成(fire-and-forget) */
|
|
440
|
+
triggerCursorDelivery() {
|
|
441
|
+
this._triggerCursorDeliveryAsync().catch(() => {});
|
|
442
|
+
return true;
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/** 注入 autoApprove */
|
|
446
|
+
injectAutoApprove() {
|
|
447
|
+
try {
|
|
448
|
+
injectAutoApprove(this.projectRoot);
|
|
449
|
+
return true;
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/** 构建并安装 VSCode Extension */
|
|
456
|
+
installVSCodeExtension() {
|
|
457
|
+
const extDir = join(REPO_ROOT, 'resources', 'vscode-ext');
|
|
458
|
+
const pkgJson = join(extDir, 'package.json');
|
|
459
|
+
if (!existsSync(pkgJson)) return false;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
// 编译 TypeScript
|
|
463
|
+
execSync('npx tsc -p ./tsconfig.json', { cwd: extDir, stdio: 'pipe' });
|
|
464
|
+
|
|
465
|
+
// 打包 .vsix
|
|
466
|
+
execSync('npx @vscode/vsce package --no-dependencies -o autosnippet.vsix', {
|
|
467
|
+
cwd: extDir,
|
|
468
|
+
stdio: 'pipe',
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const vsixPath = join(extDir, 'autosnippet.vsix');
|
|
472
|
+
if (!existsSync(vsixPath)) return false;
|
|
473
|
+
|
|
474
|
+
// 探测可用 IDE CLI
|
|
475
|
+
const cliCandidates = ['code', 'cursor', 'codex'];
|
|
476
|
+
const installed = [];
|
|
477
|
+
|
|
478
|
+
for (const cli of cliCandidates) {
|
|
479
|
+
try {
|
|
480
|
+
execSync(`which ${cli}`, { stdio: 'pipe' });
|
|
481
|
+
execSync(`${cli} --install-extension "${vsixPath}" --force`, { stdio: 'pipe' });
|
|
482
|
+
installed.push(cli);
|
|
483
|
+
} catch { /* CLI 不可用 */ }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return installed.length > 0;
|
|
487
|
+
} catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/* ═══ Destination Resolvers ══════════════════════════ */
|
|
494
|
+
|
|
495
|
+
_resolvers = {
|
|
496
|
+
/** 解析 pre-commit hook 的目标路径 */
|
|
497
|
+
resolvePreCommitDest() {
|
|
498
|
+
const huskyDir = join(this.projectRoot, '.husky');
|
|
499
|
+
if (existsSync(huskyDir)) {
|
|
500
|
+
return join(huskyDir, 'pre-commit');
|
|
501
|
+
}
|
|
502
|
+
if (existsSync(join(this.projectRoot, '.git'))) {
|
|
503
|
+
const hooksDir = join(this.projectRoot, '.git', 'hooks');
|
|
504
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
505
|
+
return join(hooksDir, 'pre-commit');
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/* ═══ Helpers ════════════════════════════════════════ */
|
|
512
|
+
|
|
513
|
+
/** 递归复制目录 */
|
|
514
|
+
_copyDirRecursive(srcDir, destDir, chmod = false) {
|
|
515
|
+
if (!existsSync(srcDir)) return false;
|
|
516
|
+
let copied = false;
|
|
517
|
+
|
|
518
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
519
|
+
for (const entry of entries) {
|
|
520
|
+
const srcPath = join(srcDir, entry.name);
|
|
521
|
+
const destPath = join(destDir, entry.name);
|
|
522
|
+
|
|
523
|
+
if (entry.isDirectory()) {
|
|
524
|
+
const sub = this._copyDirRecursive(srcPath, destPath, chmod);
|
|
525
|
+
copied = copied || sub;
|
|
526
|
+
} else {
|
|
527
|
+
mkdirSync(destDir, { recursive: true });
|
|
528
|
+
copyFileSync(srcPath, destPath);
|
|
529
|
+
if (chmod && entry.name.endsWith('.sh')) {
|
|
530
|
+
this._chmodExec(destPath);
|
|
531
|
+
}
|
|
532
|
+
copied = true;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return copied;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** chmod +x */
|
|
539
|
+
_chmodExec(filePath) {
|
|
540
|
+
try {
|
|
541
|
+
execSync(`chmod +x "${filePath}"`, { stdio: 'pipe' });
|
|
542
|
+
} catch { /* Windows — ignore */ }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** 异步触发 Cursor Delivery Pipeline */
|
|
546
|
+
async _triggerCursorDeliveryAsync() {
|
|
547
|
+
try {
|
|
548
|
+
const { getServiceContainer } = await import('../../injection/ServiceContainer.js');
|
|
549
|
+
const container = getServiceContainer();
|
|
550
|
+
const pipeline = container.services.cursorDeliveryPipeline
|
|
551
|
+
? container.get('cursorDeliveryPipeline')
|
|
552
|
+
: null;
|
|
553
|
+
if (pipeline) {
|
|
554
|
+
await pipeline.deliver();
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
// ServiceContainer 未初始化 — 正常(upgrade 可能在无 DB 环境执行)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export default FileDeployer;
|