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
package/lib/cli/SetupService.js
CHANGED
|
@@ -48,13 +48,11 @@ import {
|
|
|
48
48
|
existsSync,
|
|
49
49
|
mkdirSync,
|
|
50
50
|
readdirSync,
|
|
51
|
-
readFileSync,
|
|
52
|
-
realpathSync,
|
|
53
51
|
writeFileSync,
|
|
54
52
|
} from 'node:fs';
|
|
55
53
|
import { dirname, join, resolve } from 'node:path';
|
|
56
54
|
import { fileURLToPath } from 'node:url';
|
|
57
|
-
import {
|
|
55
|
+
import { FileDeployer } from './deploy/FileDeployer.js';
|
|
58
56
|
|
|
59
57
|
const __filename = fileURLToPath(import.meta.url);
|
|
60
58
|
const __dirname = dirname(__filename);
|
|
@@ -172,9 +170,6 @@ export class SetupService {
|
|
|
172
170
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
173
171
|
}
|
|
174
172
|
|
|
175
|
-
// 确保 .autosnippet/ 在主仓库 .gitignore 中
|
|
176
|
-
this._ensureGitignore();
|
|
177
|
-
|
|
178
173
|
// .env — AI 配置模板
|
|
179
174
|
this._ensureEnvFile();
|
|
180
175
|
|
|
@@ -433,632 +428,21 @@ export class SetupService {
|
|
|
433
428
|
/* ═══ Step 3: IDE 集成 ═══════════════════════════════ */
|
|
434
429
|
|
|
435
430
|
stepIDE() {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this._configureVSCodeMCP();
|
|
440
|
-
configured.push('vscode-mcp');
|
|
441
|
-
this._configureCursorMCP();
|
|
442
|
-
configured.push('cursor-mcp');
|
|
443
|
-
|
|
444
|
-
// 模板文件(可能因已存在/用户文件而跳过)
|
|
445
|
-
const copilotCopied = this._copyCopilotInstructions();
|
|
446
|
-
if (copilotCopied) {
|
|
447
|
-
configured.push('copilot-instructions');
|
|
448
|
-
} else {
|
|
449
|
-
// 模板未复制(用户已有文件)→ 注入 AutoSnippet 段落
|
|
450
|
-
this._ensureTaskGraphInstructions();
|
|
451
|
-
}
|
|
452
|
-
if (this._generateAgentInstructionFiles()) configured.push('agent-instructions');
|
|
453
|
-
if (this._copyCursorRules()) configured.push('cursor-rules');
|
|
454
|
-
if (this._copySkillsTemplate()) configured.push('skills-template');
|
|
455
|
-
if (this._copyCursorWorkflow()) configured.push('cursor-workflow');
|
|
456
|
-
if (this._copyClaudeHooks()) configured.push('claude-hooks');
|
|
457
|
-
if (this._copyGuardCI()) configured.push('guard-ci');
|
|
458
|
-
if (this._installPreCommitHook()) configured.push('pre-commit-hook');
|
|
459
|
-
// NOTE: .qoder/ .trae/ 不再自动创建,用户可通过 `asd mirror` 按需生成
|
|
460
|
-
|
|
461
|
-
const extResult = this._installVSCodeExtension();
|
|
462
|
-
if (extResult) {
|
|
463
|
-
configured.push(...extResult);
|
|
464
|
-
}
|
|
465
|
-
return { configured };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* @private 构建 + 安装 VSCode Extension (.vsix)
|
|
470
|
-
*
|
|
471
|
-
* 流程:
|
|
472
|
-
* 1. 编译 TypeScript(tsc)
|
|
473
|
-
* 2. 打包 .vsix(vsce package)
|
|
474
|
-
* 3. 探测所有可用的 VS Code 兼容 IDE CLI
|
|
475
|
-
* 4. 对每个 IDE 执行 --install-extension
|
|
476
|
-
*
|
|
477
|
-
* 支持:VS Code / Cursor / Codex 等基于 VS Code 的 IDE。
|
|
478
|
-
* 找不到任何 IDE CLI 时静默跳过,不阻断 setup 流程。
|
|
479
|
-
*
|
|
480
|
-
* @returns {string[]|null} 安装成功的 IDE 列表, 或 null
|
|
481
|
-
*/
|
|
482
|
-
_installVSCodeExtension() {
|
|
483
|
-
const extDir = join(REPO_ROOT, 'resources', 'vscode-ext');
|
|
484
|
-
const pkgJson = join(extDir, 'package.json');
|
|
485
|
-
|
|
486
|
-
if (!existsSync(pkgJson)) {
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// ── 1. 编译 TypeScript ──
|
|
491
|
-
try {
|
|
492
|
-
execSync('npx tsc -p ./tsconfig.json', { cwd: extDir, stdio: 'pipe' });
|
|
493
|
-
} catch (e) {
|
|
494
|
-
console.error(` ⚠ VSCode Extension 编译失败: ${e.stderr?.toString().trim() || e.message}`);
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// ── 2. 打包 .vsix ──
|
|
499
|
-
let vsixPath;
|
|
500
|
-
try {
|
|
501
|
-
const out = execSync('npx @vscode/vsce package --no-dependencies 2>&1', {
|
|
502
|
-
cwd: extDir,
|
|
503
|
-
stdio: 'pipe',
|
|
504
|
-
encoding: 'utf8',
|
|
505
|
-
});
|
|
506
|
-
// 从输出中提取 vsix 文件路径: "DONE Packaged: /path/to/autosnippet-0.1.0.vsix ..."
|
|
507
|
-
const m = out.match(/Packaged:\s*(.+\.vsix)/);
|
|
508
|
-
if (m) {
|
|
509
|
-
vsixPath = m[1].trim();
|
|
510
|
-
}
|
|
511
|
-
} catch (e) {
|
|
512
|
-
console.error(` ⚠ VSCode Extension 打包失败: ${e.message}`);
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// fallback: 扫描目录找 .vsix 文件
|
|
517
|
-
if (!vsixPath || !existsSync(vsixPath)) {
|
|
518
|
-
try {
|
|
519
|
-
const files = readdirSync(extDir).filter((f) => f.endsWith('.vsix'));
|
|
520
|
-
if (files.length > 0) {
|
|
521
|
-
// 取最新的
|
|
522
|
-
files.sort().reverse();
|
|
523
|
-
vsixPath = join(extDir, files[0]);
|
|
524
|
-
}
|
|
525
|
-
} catch {
|
|
526
|
-
/* ignore */
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!vsixPath || !existsSync(vsixPath)) {
|
|
531
|
-
console.error(' ⚠ 找不到 .vsix 文件,跳过 Extension 安装');
|
|
532
|
-
return null;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// ── 3. 探测可用的 IDE CLI ──
|
|
536
|
-
const cliCandidates = this._discoverIDEClis();
|
|
537
|
-
if (cliCandidates.length === 0) {
|
|
538
|
-
console.error(' ⚠ 未找到 VS Code / Cursor 等 IDE CLI,跳过 Extension 安装');
|
|
539
|
-
console.error(' 提示: 手动安装 → 在 IDE 中 Cmd+Shift+P → "Install from VSIX"');
|
|
540
|
-
console.error(` 文件: ${vsixPath}`);
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// ── 4. 逐个安装 ──
|
|
545
|
-
const installed = [];
|
|
546
|
-
for (const { name, cli } of cliCandidates) {
|
|
547
|
-
try {
|
|
548
|
-
execSync(`"${cli}" --install-extension "${vsixPath}" --force 2>&1`, {
|
|
549
|
-
stdio: 'pipe',
|
|
550
|
-
encoding: 'utf8',
|
|
551
|
-
timeout: 30_000,
|
|
552
|
-
});
|
|
553
|
-
installed.push(`vscode-ext:${name}`);
|
|
554
|
-
} catch (e) {
|
|
555
|
-
console.error(` ⚠ ${name} Extension 安装失败: ${e.message}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return installed.length > 0 ? installed : null;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* @private 探测系统中所有 VS Code 兼容 IDE 的 CLI 路径
|
|
563
|
-
* @returns {{ name: string, cli: string }[]}
|
|
564
|
-
*/
|
|
565
|
-
_discoverIDEClis() {
|
|
566
|
-
const candidates = [];
|
|
567
|
-
|
|
568
|
-
// 1. PATH 中的命令(跨平台: which → Unix, where → Windows)
|
|
569
|
-
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
570
|
-
for (const cmd of ['code', 'cursor', 'codex', 'code-insiders']) {
|
|
571
|
-
try {
|
|
572
|
-
const p = execSync(`${whichCmd} ${cmd}`, {
|
|
573
|
-
encoding: 'utf8',
|
|
574
|
-
stdio: ['pipe', 'pipe', 'ignore'],
|
|
575
|
-
})
|
|
576
|
-
.trim()
|
|
577
|
-
.split(/\r?\n/)[0];
|
|
578
|
-
if (p) {
|
|
579
|
-
candidates.push({ name: cmd, cli: p });
|
|
580
|
-
}
|
|
581
|
-
} catch {
|
|
582
|
-
/* not in PATH */
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// 2. 平台特定的 IDE 安装路径
|
|
587
|
-
if (process.platform === 'darwin') {
|
|
588
|
-
// macOS: /Applications/xxx.app/Contents/Resources/app/bin/
|
|
589
|
-
const appPaths = [
|
|
590
|
-
{ name: 'vscode', app: '/Applications/Visual Studio Code.app', bin: 'code' },
|
|
591
|
-
{
|
|
592
|
-
name: 'vscode-insiders',
|
|
593
|
-
app: '/Applications/Visual Studio Code - Insiders.app',
|
|
594
|
-
bin: 'code-insiders',
|
|
595
|
-
},
|
|
596
|
-
{ name: 'cursor', app: '/Applications/Cursor.app', bin: 'cursor' },
|
|
597
|
-
{ name: 'codex', app: '/Applications/Codex.app', bin: 'codex' },
|
|
598
|
-
];
|
|
599
|
-
for (const { name, app, bin } of appPaths) {
|
|
600
|
-
const cli = join(app, 'Contents', 'Resources', 'app', 'bin', bin);
|
|
601
|
-
if (existsSync(cli) && !candidates.some((c) => c.name === name)) {
|
|
602
|
-
candidates.push({ name, cli });
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
} else if (process.platform === 'win32') {
|
|
606
|
-
// Windows: %LOCALAPPDATA%\Programs\xxx
|
|
607
|
-
const localAppData =
|
|
608
|
-
process.env.LOCALAPPDATA || join(process.env.USERPROFILE || '', 'AppData', 'Local');
|
|
609
|
-
const winPaths = [
|
|
610
|
-
{ name: 'vscode', dir: 'Microsoft VS Code', bin: 'bin/code.cmd' },
|
|
611
|
-
{
|
|
612
|
-
name: 'vscode-insiders',
|
|
613
|
-
dir: 'Microsoft VS Code Insiders',
|
|
614
|
-
bin: 'bin/code-insiders.cmd',
|
|
615
|
-
},
|
|
616
|
-
{ name: 'cursor', dir: 'cursor', bin: 'cursor.exe' },
|
|
617
|
-
];
|
|
618
|
-
for (const { name, dir, bin } of winPaths) {
|
|
619
|
-
const cli = join(localAppData, 'Programs', dir, bin);
|
|
620
|
-
if (existsSync(cli) && !candidates.some((c) => c.name === name)) {
|
|
621
|
-
candidates.push({ name, cli });
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
} else {
|
|
625
|
-
// Linux: 常见安装路径
|
|
626
|
-
const linuxPaths = [
|
|
627
|
-
{ name: 'vscode', bin: '/usr/share/code/bin/code' },
|
|
628
|
-
{ name: 'vscode', bin: '/usr/bin/code' },
|
|
629
|
-
{ name: 'cursor', bin: '/usr/bin/cursor' },
|
|
630
|
-
{ name: 'cursor', bin: `${process.env.HOME || ''}/.local/bin/cursor` },
|
|
631
|
-
];
|
|
632
|
-
for (const { name, bin } of linuxPaths) {
|
|
633
|
-
if (existsSync(bin) && !candidates.some((c) => c.name === name)) {
|
|
634
|
-
candidates.push({ name, cli: bin });
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// 3. 去重(同一个二进制不重复安装)— 使用 Node.js 原生 realpathSync 替代 readlink -f
|
|
640
|
-
const seen = new Set();
|
|
641
|
-
return candidates.filter((c) => {
|
|
642
|
-
let realPath = c.cli;
|
|
643
|
-
try {
|
|
644
|
-
realPath = realpathSync(c.cli);
|
|
645
|
-
} catch {
|
|
646
|
-
/* use as-is */
|
|
647
|
-
}
|
|
648
|
-
if (seen.has(realPath)) {
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
seen.add(realPath);
|
|
652
|
-
return true;
|
|
431
|
+
const deployer = new FileDeployer({
|
|
432
|
+
projectRoot: this.projectRoot,
|
|
433
|
+
force: this.force,
|
|
653
434
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
/** @private .vscode/mcp.json → VSCode MCP (新标准格式) */
|
|
657
|
-
_configureVSCodeMCP() {
|
|
658
|
-
const vscodeDir = join(this.projectRoot, '.vscode');
|
|
659
|
-
const mcpConfigPath = join(vscodeDir, 'mcp.json');
|
|
660
|
-
mkdirSync(vscodeDir, { recursive: true });
|
|
661
|
-
|
|
662
|
-
let config = {};
|
|
663
|
-
if (existsSync(mcpConfigPath)) {
|
|
664
|
-
try {
|
|
665
|
-
config = JSON.parse(readFileSync(mcpConfigPath, 'utf8'));
|
|
666
|
-
} catch {
|
|
667
|
-
/* ignore */
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (!config.servers) {
|
|
672
|
-
config.servers = {};
|
|
673
|
-
}
|
|
674
|
-
config.servers.autosnippet = {
|
|
675
|
-
type: 'stdio',
|
|
676
|
-
command: 'asd-mcp',
|
|
677
|
-
env: {
|
|
678
|
-
ASD_PROJECT_DIR: this.projectRoot,
|
|
679
|
-
},
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* @private .cursor/mcp.json
|
|
687
|
-
*
|
|
688
|
-
* 初始 setup 时不写入 autoApprove — 让用户首次使用时亲眼看到并手动授权每个工具。
|
|
689
|
-
* 首次 `autosnippet_bootstrap` 成功后,bootstrap-external.js 会自动注入 autoApprove,
|
|
690
|
-
* 后续调用无需再点击授权。升级时 UpgradeService 也会注入。
|
|
691
|
-
*/
|
|
692
|
-
_configureCursorMCP() {
|
|
693
|
-
const cursorDir = join(this.projectRoot, '.cursor');
|
|
694
|
-
const configPath = join(cursorDir, 'mcp.json');
|
|
695
|
-
mkdirSync(cursorDir, { recursive: true });
|
|
696
|
-
|
|
697
|
-
let existing = {};
|
|
698
|
-
if (existsSync(configPath)) {
|
|
699
|
-
try {
|
|
700
|
-
existing = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
701
|
-
} catch {
|
|
702
|
-
/* ignore */
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (!existing.mcpServers) {
|
|
707
|
-
existing.mcpServers = {};
|
|
708
|
-
}
|
|
709
|
-
existing.mcpServers.autosnippet = {
|
|
710
|
-
command: 'asd-mcp',
|
|
711
|
-
env: {
|
|
712
|
-
ASD_PROJECT_DIR: this.projectRoot,
|
|
713
|
-
},
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/** @private .github/copilot-instructions.md (static template fallback) */
|
|
720
|
-
_copyCopilotInstructions() {
|
|
721
|
-
const src = join(REPO_ROOT, 'templates', 'copilot-instructions.md');
|
|
722
|
-
if (!existsSync(src)) {
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const destDir = join(this.projectRoot, '.github');
|
|
727
|
-
const dest = join(destDir, 'copilot-instructions.md');
|
|
728
|
-
if (existsSync(dest) && !this.force) {
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 即使 --force,也不覆盖用户原有的非 AutoSnippet 文件
|
|
733
|
-
const { canWrite } = checkWriteSafety(dest);
|
|
734
|
-
if (!canWrite) {
|
|
735
|
-
return false;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
mkdirSync(destDir, { recursive: true });
|
|
739
|
-
copyFileSync(src, dest);
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* @private TaskGraph 决策记忆段落注入
|
|
745
|
-
* 从 templates/copilot-instructions.md 提取 <!-- autosnippet:begin/end --> 间的内容注入。
|
|
746
|
-
* 单一数据源:模板即注入内容,无需维护第二份文件。
|
|
747
|
-
*/
|
|
748
|
-
_ensureTaskGraphInstructions() {
|
|
749
|
-
const githubDir = join(this.projectRoot, '.github');
|
|
750
|
-
const filePath = join(githubDir, 'copilot-instructions.md');
|
|
751
|
-
|
|
752
|
-
const BEGIN_MARKER = '<!-- autosnippet:begin -->';
|
|
753
|
-
const END_MARKER = '<!-- autosnippet:end -->';
|
|
754
|
-
|
|
755
|
-
// 从模板提取标记间内容
|
|
756
|
-
const templateSrc = join(REPO_ROOT, 'templates', 'copilot-instructions.md');
|
|
757
|
-
if (!existsSync(templateSrc)) {
|
|
758
|
-
return { action: 'skipped' };
|
|
759
|
-
}
|
|
760
|
-
const templateContent = readFileSync(templateSrc, 'utf8');
|
|
761
|
-
const beginIdx = templateContent.indexOf(BEGIN_MARKER);
|
|
762
|
-
const endIdx = templateContent.indexOf(END_MARKER);
|
|
763
|
-
if (beginIdx === -1 || endIdx === -1) {
|
|
764
|
-
return { action: 'skipped' };
|
|
765
|
-
}
|
|
766
|
-
const snippetContent = templateContent.slice(beginIdx, endIdx + END_MARKER.length);
|
|
767
|
-
|
|
768
|
-
if (!existsSync(githubDir)) {
|
|
769
|
-
mkdirSync(githubDir, { recursive: true });
|
|
770
|
-
}
|
|
435
|
+
const { deployed, skipped, errors } = deployer.deployAll('setup');
|
|
771
436
|
|
|
772
|
-
if (
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
// 替换现有段落(支持升级)
|
|
776
|
-
const updated = existing.replace(
|
|
777
|
-
new RegExp(`${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`),
|
|
778
|
-
snippetContent
|
|
779
|
-
);
|
|
780
|
-
writeFileSync(filePath, updated);
|
|
781
|
-
return { action: 'updated' };
|
|
437
|
+
if (errors.length > 0) {
|
|
438
|
+
for (const { id, error } of errors) {
|
|
439
|
+
console.error(` ⚠ ${id}: ${error}`);
|
|
782
440
|
}
|
|
783
|
-
// 追加到末尾
|
|
784
|
-
writeFileSync(filePath, `${existing}\n\n${snippetContent}\n`);
|
|
785
|
-
return { action: 'appended' };
|
|
786
441
|
}
|
|
787
442
|
|
|
788
|
-
|
|
789
|
-
return { action: 'created' };
|
|
443
|
+
return { configured: deployed };
|
|
790
444
|
}
|
|
791
445
|
|
|
792
|
-
/**
|
|
793
|
-
* @private 生成 AGENTS.md + CLAUDE.md 静态骨架
|
|
794
|
-
* setup 阶段没有知识库数据,所以只生成指向 MCP 的基础版本。
|
|
795
|
-
* bootstrap / delivery pipeline 完成后会用动态版本覆盖。
|
|
796
|
-
*/
|
|
797
|
-
_generateAgentInstructionFiles() {
|
|
798
|
-
const projectName = this.projectRoot.split('/').pop();
|
|
799
|
-
let wrote = false;
|
|
800
|
-
|
|
801
|
-
// AGENTS.md
|
|
802
|
-
const agentsPath = join(this.projectRoot, 'AGENTS.md');
|
|
803
|
-
if (!existsSync(agentsPath) || this.force) {
|
|
804
|
-
const { canWrite } = checkWriteSafety(agentsPath);
|
|
805
|
-
if (canWrite) {
|
|
806
|
-
const agentsContent = [
|
|
807
|
-
`# ${projectName} — Agent Instructions`,
|
|
808
|
-
'',
|
|
809
|
-
'> Auto-generated by AutoSnippet.',
|
|
810
|
-
'',
|
|
811
|
-
'## AutoSnippet Integration',
|
|
812
|
-
'',
|
|
813
|
-
'This project uses **AutoSnippet** for knowledge management and decision tracking.',
|
|
814
|
-
'',
|
|
815
|
-
'### MCP Tools',
|
|
816
|
-
'',
|
|
817
|
-
'- `autosnippet_search` — Search knowledge',
|
|
818
|
-
'- `autosnippet_knowledge` — Browse/get recipes',
|
|
819
|
-
'- `autosnippet_submit_knowledge` — Submit candidate',
|
|
820
|
-
'- `autosnippet_guard` — Code compliance check',
|
|
821
|
-
'- `autosnippet_health` — Service health & KB stats',
|
|
822
|
-
'- `autosnippet_ready` — Session entry point (loads decisions + tasks)',
|
|
823
|
-
'- `autosnippet_decide` — Decision management (record/revise/unpin)',
|
|
824
|
-
'- `autosnippet_task` — Task CRUD (create/claim/close/fail/defer)',
|
|
825
|
-
'',
|
|
826
|
-
'### VS Code Agent Mode',
|
|
827
|
-
'',
|
|
828
|
-
'Type `#asd` before your message in Agent Mode to activate project memory.',
|
|
829
|
-
'',
|
|
830
|
-
'### Constraints',
|
|
831
|
-
'',
|
|
832
|
-
'1. Do NOT modify knowledge base files directly.',
|
|
833
|
-
'2. Create or update knowledge only through MCP tools.',
|
|
834
|
-
'',
|
|
835
|
-
].join('\n');
|
|
836
|
-
writeFileSync(agentsPath, agentsContent);
|
|
837
|
-
wrote = true;
|
|
838
|
-
} else {
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// CLAUDE.md
|
|
843
|
-
const claudePath = join(this.projectRoot, 'CLAUDE.md');
|
|
844
|
-
if (!existsSync(claudePath) || this.force) {
|
|
845
|
-
const { canWrite } = checkWriteSafety(claudePath);
|
|
846
|
-
if (canWrite) {
|
|
847
|
-
const claudeContent = [
|
|
848
|
-
`# ${projectName} — Claude Code Instructions`,
|
|
849
|
-
'',
|
|
850
|
-
'> Auto-generated by AutoSnippet.',
|
|
851
|
-
'',
|
|
852
|
-
'## AutoSnippet Integration',
|
|
853
|
-
'',
|
|
854
|
-
'This project uses AutoSnippet for knowledge management and decision tracking.',
|
|
855
|
-
'',
|
|
856
|
-
'### MCP Tools',
|
|
857
|
-
'',
|
|
858
|
-
'- `autosnippet_search` — Search knowledge',
|
|
859
|
-
'- `autosnippet_knowledge` — Browse/get recipes',
|
|
860
|
-
'- `autosnippet_submit_knowledge` — Submit candidate',
|
|
861
|
-
'- `autosnippet_guard` — Code compliance check',
|
|
862
|
-
'- `autosnippet_health` — Service health & KB stats',
|
|
863
|
-
'- `autosnippet_ready` — Session entry point (loads decisions + tasks)',
|
|
864
|
-
'- `autosnippet_decide` — Decision management (record/revise/unpin)',
|
|
865
|
-
'- `autosnippet_task` — Task CRUD (create/claim/close/fail/defer)',
|
|
866
|
-
'',
|
|
867
|
-
'### Claude + VS Code',
|
|
868
|
-
'',
|
|
869
|
-
'Claude Code uses MCP tools directly. Run `asd server` to start the API server.',
|
|
870
|
-
'In VS Code Agent Mode, type `#asd` before your message to activate project memory.',
|
|
871
|
-
'',
|
|
872
|
-
'### Constraints',
|
|
873
|
-
'',
|
|
874
|
-
'1. Do NOT modify knowledge base files directly.',
|
|
875
|
-
'2. Create or update knowledge only through MCP tools.',
|
|
876
|
-
'',
|
|
877
|
-
].join('\n');
|
|
878
|
-
writeFileSync(claudePath, claudeContent);
|
|
879
|
-
wrote = true;
|
|
880
|
-
} else {
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return wrote;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/** @private .cursor/rules/autosnippet-conventions.mdc */
|
|
887
|
-
_copyCursorRules() {
|
|
888
|
-
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-conventions.mdc');
|
|
889
|
-
if (!existsSync(src)) {
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
894
|
-
const dest = join(destDir, 'autosnippet-conventions.mdc');
|
|
895
|
-
if (existsSync(dest) && !this.force) {
|
|
896
|
-
return false;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
mkdirSync(destDir, { recursive: true });
|
|
900
|
-
copyFileSync(src, dest);
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/** @private .cursor/rules/autosnippet-skills.mdc — Project Skills 索引模板 */
|
|
905
|
-
_copySkillsTemplate() {
|
|
906
|
-
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-skills.mdc');
|
|
907
|
-
if (!existsSync(src)) {
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
912
|
-
const dest = join(destDir, 'autosnippet-skills.mdc');
|
|
913
|
-
if (existsSync(dest) && !this.force) {
|
|
914
|
-
return false;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
mkdirSync(destDir, { recursive: true });
|
|
918
|
-
copyFileSync(src, dest);
|
|
919
|
-
return true;
|
|
920
|
-
}
|
|
921
|
-
/** @private .cursor/rules/autosnippet-workflow.mdc — TaskGraph & Guard 工作流 */
|
|
922
|
-
_copyCursorWorkflow() {
|
|
923
|
-
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-workflow.mdc');
|
|
924
|
-
if (!existsSync(src)) {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
929
|
-
const dest = join(destDir, 'autosnippet-workflow.mdc');
|
|
930
|
-
if (existsSync(dest) && !this.force) {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
mkdirSync(destDir, { recursive: true });
|
|
935
|
-
copyFileSync(src, dest);
|
|
936
|
-
return true;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* @private .claude/hooks.yaml — Claude Code SessionStart hook
|
|
941
|
-
* 会话开始时自动 prime TaskGraph 上下文
|
|
942
|
-
*/
|
|
943
|
-
_copyClaudeHooks() {
|
|
944
|
-
const src = join(REPO_ROOT, 'templates', 'claude-hooks.yaml');
|
|
945
|
-
if (!existsSync(src)) {
|
|
946
|
-
return false;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const claudeDir = join(this.projectRoot, '.claude');
|
|
950
|
-
const dest = join(claudeDir, 'hooks.yaml');
|
|
951
|
-
if (existsSync(dest) && !this.force) {
|
|
952
|
-
return false;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// 即使 --force,也不覆盖用户自己的 hooks 文件
|
|
956
|
-
const { canWrite } = checkWriteSafety(dest);
|
|
957
|
-
if (!canWrite) {
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
962
|
-
copyFileSync(src, dest);
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
/**
|
|
967
|
-
* @private .github/workflows/autosnippet-guard.yml — Guard CI/CD workflow
|
|
968
|
-
* 在 push/PR 时自动运行 Guard 合规检查
|
|
969
|
-
*/
|
|
970
|
-
_copyGuardCI() {
|
|
971
|
-
const src = join(REPO_ROOT, 'templates', 'guard-ci.yml');
|
|
972
|
-
if (!existsSync(src)) {
|
|
973
|
-
return false;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const destDir = join(this.projectRoot, '.github', 'workflows');
|
|
977
|
-
const dest = join(destDir, 'autosnippet-guard.yml');
|
|
978
|
-
if (existsSync(dest) && !this.force) {
|
|
979
|
-
return false;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const { canWrite } = checkWriteSafety(dest);
|
|
983
|
-
if (!canWrite) {
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
mkdirSync(destDir, { recursive: true });
|
|
988
|
-
copyFileSync(src, dest);
|
|
989
|
-
return true;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/**
|
|
993
|
-
* @private .git/hooks/pre-commit — Guard pre-commit hook
|
|
994
|
-
* 如果项目使用 husky,则安装到 .husky/pre-commit
|
|
995
|
-
* 否则安装到 .git/hooks/pre-commit
|
|
996
|
-
* 已存在 pre-commit hook 时不覆盖(避免破坏用户现有 hook)
|
|
997
|
-
*/
|
|
998
|
-
_installPreCommitHook() {
|
|
999
|
-
const src = join(REPO_ROOT, 'templates', 'pre-commit-guard.sh');
|
|
1000
|
-
if (!existsSync(src)) {
|
|
1001
|
-
return false;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// 优先检查 husky
|
|
1005
|
-
const huskyDir = join(this.projectRoot, '.husky');
|
|
1006
|
-
const gitHooksDir = join(this.projectRoot, '.git', 'hooks');
|
|
1007
|
-
|
|
1008
|
-
let dest;
|
|
1009
|
-
if (existsSync(huskyDir)) {
|
|
1010
|
-
// 项目使用 husky — 安装到 .husky/pre-commit
|
|
1011
|
-
dest = join(huskyDir, 'pre-commit');
|
|
1012
|
-
} else if (existsSync(join(this.projectRoot, '.git'))) {
|
|
1013
|
-
// 普通 git 项目 — 安装到 .git/hooks/pre-commit
|
|
1014
|
-
dest = join(gitHooksDir, 'pre-commit');
|
|
1015
|
-
mkdirSync(gitHooksDir, { recursive: true });
|
|
1016
|
-
} else {
|
|
1017
|
-
// 非 git 项目,跳过
|
|
1018
|
-
return false;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// 已存在 pre-commit hook 时不覆盖(可能是用户自己的 hook)
|
|
1022
|
-
if (existsSync(dest) && !this.force) {
|
|
1023
|
-
return false;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// 即使 --force,也不覆盖用户自己的 pre-commit hook
|
|
1027
|
-
const { canWrite } = checkWriteSafety(dest);
|
|
1028
|
-
if (!canWrite) {
|
|
1029
|
-
return false;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
copyFileSync(src, dest);
|
|
1033
|
-
// 确保可执行
|
|
1034
|
-
try {
|
|
1035
|
-
execSync(`chmod +x "${dest}"`, { stdio: 'pipe' });
|
|
1036
|
-
} catch {
|
|
1037
|
-
/* Windows 不需要 chmod */
|
|
1038
|
-
}
|
|
1039
|
-
return true;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/** @private 镜像 .cursor/rules/ 中的 autosnippet-* 文件到目标 IDE 目录(Qoder / Trae 兼容)
|
|
1043
|
-
* 只复制 autosnippet- 前缀的文件,不触碰用户自己创建的规则 */
|
|
1044
|
-
_mirrorCursorToIDE(targetDirName) {
|
|
1045
|
-
const cursorRulesDir = join(this.projectRoot, '.cursor', 'rules');
|
|
1046
|
-
if (!existsSync(cursorRulesDir)) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const targetRulesDir = join(this.projectRoot, targetDirName, 'rules');
|
|
1051
|
-
mkdirSync(targetRulesDir, { recursive: true });
|
|
1052
|
-
|
|
1053
|
-
// 只镜像 autosnippet- 前缀的文件,保留目标目录中用户已有的其他文件
|
|
1054
|
-
const files = readdirSync(cursorRulesDir).filter(
|
|
1055
|
-
(f) => (f.endsWith('.mdc') || f.endsWith('.md')) && f.startsWith('autosnippet-')
|
|
1056
|
-
);
|
|
1057
|
-
for (const file of files) {
|
|
1058
|
-
const destName = file.endsWith('.mdc') ? file.replace(/\.mdc$/, '.md') : file;
|
|
1059
|
-
copyFileSync(join(cursorRulesDir, file), join(targetRulesDir, destName));
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
446
|
/* ═══ Step 4: 数据库初始化 ═══════════════════════════ */
|
|
1063
447
|
|
|
1064
448
|
async stepDatabase() {
|
|
@@ -1155,99 +539,6 @@ export class SetupService {
|
|
|
1155
539
|
}
|
|
1156
540
|
}
|
|
1157
541
|
|
|
1158
|
-
/** @private 确保项目 .gitignore 正确配置 AutoSnippet 相关规则 */
|
|
1159
|
-
_ensureGitignore() {
|
|
1160
|
-
const giPath = join(this.projectRoot, '.gitignore');
|
|
1161
|
-
let content = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
1162
|
-
let changed = false;
|
|
1163
|
-
|
|
1164
|
-
// ── v2.4.0 迁移:旧格式 ".autosnippet/" → 新格式 ".autosnippet/*" ──
|
|
1165
|
-
// 旧格式会忽略整个目录(git 不遍历内部),导致 skills/ 和 config.json 无法被 negation 恢复
|
|
1166
|
-
// 新格式忽略目录内所有文件,允许 negation 模式取消特定子路径
|
|
1167
|
-
if (content.includes('.autosnippet/') && !content.includes('.autosnippet/*')) {
|
|
1168
|
-
content = content.replace(/^\.autosnippet\/$/m, '.autosnippet/*');
|
|
1169
|
-
changed = true;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// ── 必须忽略:.autosnippet/*(运行时缓存、DB、向量索引、memory) ──
|
|
1173
|
-
if (!content.includes('.autosnippet/') && !content.includes('.autosnippet/*')) {
|
|
1174
|
-
content += `\n# AutoSnippet 运行时缓存(不入库)\n.autosnippet/*\n`;
|
|
1175
|
-
changed = true;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// ── 必须跟踪:.autosnippet/config.json(项目配置) ──
|
|
1179
|
-
if (!content.includes('!.autosnippet/config.json')) {
|
|
1180
|
-
content += `!.autosnippet/config.json\n`;
|
|
1181
|
-
changed = true;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// ── 必须忽略:.env(包含 API Key 等敏感信息) ──
|
|
1185
|
-
if (!content.includes('.env') || (!content.match(/^\.env$/m) && !content.match(/^\.env\s/m))) {
|
|
1186
|
-
content += `\n# AutoSnippet 环境变量(含 API Key,不入库)\n.env\n`;
|
|
1187
|
-
changed = true;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// ── 必须忽略:logs/(winston 运行日志,可达数十 MB) ──
|
|
1191
|
-
if (!content.match(/^logs\/?$/m)) {
|
|
1192
|
-
content += `\n# AutoSnippet 运行日志\nlogs/\n`;
|
|
1193
|
-
changed = true;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// ── 必须忽略:.autosnippet-drafts/(AI 草稿临时目录) ──
|
|
1197
|
-
if (!content.includes('.autosnippet-drafts')) {
|
|
1198
|
-
content += `\n# AutoSnippet AI 草稿(临时)\n.autosnippet-drafts/\n`;
|
|
1199
|
-
changed = true;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// ── 必须忽略:_draft_*.md(AI Agent 在项目根目录创建的草稿文件) ──
|
|
1203
|
-
if (!content.includes('_draft_*.md')) {
|
|
1204
|
-
content += `\n# AutoSnippet AI 草稿文件(项目根目录临时文件)\n_draft_*.md\n`;
|
|
1205
|
-
changed = true;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// ── 必须忽略:常见系统 / 编辑器临时文件 ──
|
|
1209
|
-
if (!content.includes('.DS_Store')) {
|
|
1210
|
-
content += `\n# macOS 元数据\n.DS_Store\n`;
|
|
1211
|
-
changed = true;
|
|
1212
|
-
}
|
|
1213
|
-
if (!content.includes('nohup.out')) {
|
|
1214
|
-
content += `nohup.out\n`;
|
|
1215
|
-
changed = true;
|
|
1216
|
-
}
|
|
1217
|
-
if (!content.match(/\*\.sw[a-p]/)) {
|
|
1218
|
-
content += `*.sw[a-p]\n`;
|
|
1219
|
-
changed = true;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// Skills 已迁移到 AutoSnippet/skills/(知识库目录内),自动跟随 Git
|
|
1223
|
-
|
|
1224
|
-
// ── 清理旧版本的 .autosnippet/skills/ negation(已迁移,不再需要)──
|
|
1225
|
-
if (content.includes('!.autosnippet/skills/')) {
|
|
1226
|
-
content = content.replace(/^!?\.autosnippet\/skills\/.*\n?/gm, '');
|
|
1227
|
-
changed = true;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// ── 必须跟踪:AutoSnippet/(知识库子仓库)──
|
|
1231
|
-
// 如果用户误将 AutoSnippet/ 加入忽略,追加 !AutoSnippet/ 取消忽略
|
|
1232
|
-
const lines = content.split('\n');
|
|
1233
|
-
const hasIgnoreAS = lines.some((l) => {
|
|
1234
|
-
const t = l.trim();
|
|
1235
|
-
return (
|
|
1236
|
-
(t === 'AutoSnippet/' || t === 'AutoSnippet') && !t.startsWith('#') && !t.startsWith('!')
|
|
1237
|
-
);
|
|
1238
|
-
});
|
|
1239
|
-
const hasNegation = lines.some((l) => l.trim() === '!AutoSnippet/');
|
|
1240
|
-
|
|
1241
|
-
if (hasIgnoreAS && !hasNegation) {
|
|
1242
|
-
content += `\n# AutoSnippet 知识库必须入库(取消上方忽略)\n!AutoSnippet/\n`;
|
|
1243
|
-
changed = true;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
if (changed) {
|
|
1247
|
-
writeFileSync(giPath, content);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
542
|
/** @private 在指定目录执行 git 命令 */
|
|
1252
543
|
_git(args, cwd) {
|
|
1253
544
|
try {
|