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.
Files changed (64) hide show
  1. package/README.md +2 -4
  2. package/bin/cli.js +164 -145
  3. package/config/constitution.yaml +2 -0
  4. package/dashboard/dist/assets/{index-DNOHYBhy.css → index-BaGY7kJI.css} +1 -1
  5. package/dashboard/dist/assets/{index-6itPuGFl.js → index-DfHY_3ln.js} +25 -25
  6. package/dashboard/dist/index.html +2 -2
  7. package/lib/cli/CliLogger.js +78 -0
  8. package/lib/cli/SetupService.js +9 -718
  9. package/lib/cli/UpgradeService.js +23 -398
  10. package/lib/cli/deploy/FileDeployer.js +562 -0
  11. package/lib/cli/deploy/FileManifest.js +272 -0
  12. package/lib/external/mcp/McpServer.js +22 -26
  13. package/lib/external/mcp/autoApproveInjector.js +1 -0
  14. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +5 -5
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +25 -3
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +6 -6
  17. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +4 -0
  18. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +5 -5
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +89 -44
  20. package/lib/external/mcp/handlers/consolidated.js +8 -9
  21. package/lib/external/mcp/handlers/dimension-complete-external.js +4 -4
  22. package/lib/external/mcp/handlers/guard.js +283 -5
  23. package/lib/external/mcp/handlers/task.js +183 -9
  24. package/lib/external/mcp/tools.js +32 -81
  25. package/lib/http/routes/task.js +55 -0
  26. package/lib/service/chat/AnalystAgent.js +12 -12
  27. package/lib/service/chat/ChatAgent.js +227 -545
  28. package/lib/service/chat/ChatAgentPrompts.js +9 -11
  29. package/lib/service/chat/ContextWindow.js +2 -296
  30. package/lib/service/chat/EpisodicConsolidator.js +15 -15
  31. package/lib/service/chat/ExplorationTracker.js +1262 -0
  32. package/lib/service/chat/HandoffProtocol.js +8 -9
  33. package/lib/service/chat/Memory.js +4 -0
  34. package/lib/service/chat/ProducerAgent.js +9 -6
  35. package/lib/service/chat/ProjectSemanticMemory.js +4 -0
  36. package/lib/service/chat/ReasoningTrace.js +182 -0
  37. package/lib/service/chat/WorkingMemory.js +4 -0
  38. package/lib/service/chat/memory/ActiveContext.js +910 -0
  39. package/lib/service/chat/memory/MemoryCoordinator.js +662 -0
  40. package/lib/service/chat/memory/PersistentMemory.js +450 -0
  41. package/lib/service/chat/memory/SessionStore.js +896 -0
  42. package/lib/service/chat/memory/index.js +13 -0
  43. package/lib/service/chat/tools/ast-graph.js +17 -16
  44. package/lib/service/cursor/AgentInstructionsGenerator.js +76 -47
  45. package/lib/service/cursor/FileProtection.js +4 -1
  46. package/lib/service/guard/GuardCheckEngine.js +10 -3
  47. package/lib/service/task/TaskGraphService.js +3 -3
  48. package/lib/shared/LanguageService.js +2 -1
  49. package/package.json +1 -1
  50. package/skills/autosnippet-intent/SKILL.md +1 -3
  51. package/skills/autosnippet-recipes/SKILL.md +1 -3
  52. package/templates/claude-code/commands/prime.md +19 -0
  53. package/templates/claude-code/hooks/autosnippet-session.sh +63 -0
  54. package/templates/claude-code/settings.json +21 -0
  55. package/templates/copilot-instructions.md +64 -177
  56. package/templates/cursor-hooks/commands/prime.md +12 -0
  57. package/templates/cursor-hooks/hooks/session-start.sh +10 -0
  58. package/templates/cursor-hooks/hooks.json +11 -0
  59. package/templates/cursor-rules/autosnippet-conventions.mdc +52 -3
  60. package/templates/cursor-rules/autosnippet-workflow.mdc +51 -27
  61. package/lib/external/mcp/handlers/decide.js +0 -109
  62. package/lib/external/mcp/handlers/ready.js +0 -42
  63. package/lib/service/chat/ReasoningLayer.js +0 -888
  64. package/templates/claude-hooks.yaml +0 -19
@@ -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 { checkWriteSafety } from '../service/cursor/FileProtection.js';
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 configured = [];
437
-
438
- // MCP 配置(总是写入/合并,不会跳过)
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 (existsSync(filePath)) {
773
- const existing = readFileSync(filePath, 'utf8');
774
- if (existing.includes(BEGIN_MARKER)) {
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
- writeFileSync(filePath, `${snippetContent}\n`);
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 {