ai-engineering-init 1.4.3 → 1.6.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 (131) hide show
  1. package/.cursor/skills/bug-detective/SKILL.md +19 -19
  2. package/.cursor/skills/project-navigator/SKILL.md +164 -258
  3. package/README.md +20 -236
  4. package/bin/index.js +437 -7
  5. package/package.json +7 -1
  6. package/scripts/build-skills.js +180 -0
  7. package/src/platform-map.json +56 -0
  8. package/src/skills/add-skill/SKILL.md +488 -0
  9. package/src/skills/add-todo/SKILL.md +269 -0
  10. package/src/skills/api-development/SKILL.md +266 -0
  11. package/src/skills/architecture-design/SKILL.md +262 -0
  12. package/src/skills/backend-annotations/SKILL.md +302 -0
  13. package/src/skills/banana-image/CHANGELOG.md +37 -0
  14. package/src/skills/banana-image/README.md +146 -0
  15. package/src/skills/banana-image/SKILL.md +171 -0
  16. package/src/skills/banana-image/assets/logo.png +0 -0
  17. package/src/skills/banana-image/references/advanced-usage.md +189 -0
  18. package/src/skills/banana-image/scripts/apply_template.py +125 -0
  19. package/src/skills/banana-image/scripts/banana_image_exec.ts +412 -0
  20. package/src/skills/banana-image/scripts/batch_prep.py +82 -0
  21. package/src/skills/banana-image/scripts/package-lock.json +1437 -0
  22. package/src/skills/banana-image/scripts/package.json +18 -0
  23. package/src/skills/banana-image/scripts/requirements.txt +10 -0
  24. package/src/skills/banana-image/templates/poster.json +22 -0
  25. package/src/skills/banana-image/templates/product.json +17 -0
  26. package/src/skills/banana-image/templates/social.json +22 -0
  27. package/src/skills/banana-image/templates/thumbnail.json +17 -0
  28. package/src/skills/brainstorm/SKILL.md +216 -0
  29. package/src/skills/bug-detective/SKILL.md +256 -0
  30. package/src/skills/bug-detective/references/error-patterns.md +242 -0
  31. package/src/skills/check/SKILL.md +367 -0
  32. package/src/skills/code-patterns/SKILL.md +280 -0
  33. package/src/skills/code-patterns/references/leniu-code-patterns.md +87 -0
  34. package/src/skills/codex-code-review/SKILL.md +135 -0
  35. package/src/skills/collaborating-with-codex/SKILL.md +174 -0
  36. package/src/skills/collaborating-with-codex/scripts/codex_bridge.py +275 -0
  37. package/src/skills/collaborating-with-gemini/SKILL.md +194 -0
  38. package/src/skills/collaborating-with-gemini/scripts/gemini_bridge.py +275 -0
  39. package/src/skills/crud/SKILL.md +265 -0
  40. package/src/skills/crud-development/SKILL.md +409 -0
  41. package/src/skills/data-permission/SKILL.md +292 -0
  42. package/src/skills/data-permission/references/custom-data-scope.md +90 -0
  43. package/src/skills/database-ops/SKILL.md +407 -0
  44. package/src/skills/dev/SKILL.md +187 -0
  45. package/src/skills/error-handler/SKILL.md +371 -0
  46. package/src/skills/file-oss-management/SKILL.md +255 -0
  47. package/src/skills/file-oss-management/references/entities.md +105 -0
  48. package/src/skills/file-oss-management/references/service-impl.md +104 -0
  49. package/src/skills/git-workflow/SKILL.md +397 -0
  50. package/src/skills/init-docs/SKILL.md +194 -0
  51. package/src/skills/json-serialization/SKILL.md +357 -0
  52. package/src/skills/leniu-api-development/SKILL.md +319 -0
  53. package/src/skills/leniu-api-development/references/real-examples.md +273 -0
  54. package/src/skills/leniu-architecture-design/SKILL.md +383 -0
  55. package/src/skills/leniu-backend-annotations/SKILL.md +277 -0
  56. package/src/skills/leniu-brainstorm/SKILL.md +242 -0
  57. package/src/skills/leniu-brainstorm/references/business-scenarios.md +162 -0
  58. package/src/skills/leniu-code-patterns/SKILL.md +411 -0
  59. package/src/skills/leniu-crud-development/SKILL.md +404 -0
  60. package/src/skills/leniu-crud-development/references/templates.md +597 -0
  61. package/src/skills/leniu-customization-location/SKILL.md +410 -0
  62. package/src/skills/leniu-data-permission/SKILL.md +341 -0
  63. package/src/skills/leniu-database-ops/SKILL.md +426 -0
  64. package/src/skills/leniu-error-handler/SKILL.md +462 -0
  65. package/src/skills/leniu-java-amount-handling/SKILL.md +461 -0
  66. package/src/skills/leniu-java-code-style/SKILL.md +510 -0
  67. package/src/skills/leniu-java-concurrent/SKILL.md +400 -0
  68. package/src/skills/leniu-java-entity/SKILL.md +237 -0
  69. package/src/skills/leniu-java-entity/references/templates.md +237 -0
  70. package/src/skills/leniu-java-export/SKILL.md +570 -0
  71. package/src/skills/leniu-java-logging/SKILL.md +229 -0
  72. package/src/skills/leniu-java-logging/references/data-mask.md +46 -0
  73. package/src/skills/leniu-java-logging/references/logging-scenarios.md +113 -0
  74. package/src/skills/leniu-java-mq/SKILL.md +338 -0
  75. package/src/skills/leniu-java-mybatis/SKILL.md +267 -0
  76. package/src/skills/leniu-java-mybatis/references/report-mapper.md +88 -0
  77. package/src/skills/leniu-java-report-query-param/SKILL.md +291 -0
  78. package/src/skills/leniu-java-task/SKILL.md +367 -0
  79. package/src/skills/leniu-java-total-line/SKILL.md +196 -0
  80. package/src/skills/leniu-marketing-price-rule-customizer/SKILL.md +301 -0
  81. package/src/skills/leniu-marketing-recharge-rule-customizer/SKILL.md +285 -0
  82. package/src/skills/leniu-mealtime/SKILL.md +215 -0
  83. package/src/skills/leniu-redis-cache/SKILL.md +331 -0
  84. package/src/skills/leniu-report-customization/SKILL.md +335 -0
  85. package/src/skills/leniu-report-customization/references/table-fields.md +93 -0
  86. package/src/skills/leniu-report-standard-customization/SKILL.md +328 -0
  87. package/src/skills/leniu-report-standard-customization/references/analysis-module.md +64 -0
  88. package/src/skills/leniu-report-standard-customization/references/table-fields.md +113 -0
  89. package/src/skills/leniu-security-guard/SKILL.md +306 -0
  90. package/src/skills/leniu-utils-toolkit/SKILL.md +380 -0
  91. package/src/skills/mysql-debug/SKILL.md +364 -0
  92. package/src/skills/next/SKILL.md +137 -0
  93. package/src/skills/openspec-apply-change/SKILL.md +165 -0
  94. package/src/skills/openspec-archive-change/SKILL.md +122 -0
  95. package/src/skills/openspec-bulk-archive-change/SKILL.md +254 -0
  96. package/src/skills/openspec-continue-change/SKILL.md +126 -0
  97. package/src/skills/openspec-explore/SKILL.md +299 -0
  98. package/src/skills/openspec-ff-change/SKILL.md +109 -0
  99. package/src/skills/openspec-new-change/SKILL.md +82 -0
  100. package/src/skills/openspec-onboard/SKILL.md +414 -0
  101. package/src/skills/openspec-sync-specs/SKILL.md +146 -0
  102. package/src/skills/openspec-verify-change/SKILL.md +176 -0
  103. package/src/skills/performance-doctor/SKILL.md +303 -0
  104. package/src/skills/progress/SKILL.md +193 -0
  105. package/src/skills/project-navigator/SKILL.md +211 -0
  106. package/src/skills/redis-cache/SKILL.md +333 -0
  107. package/src/skills/redis-cache/references/listeners.md +23 -0
  108. package/src/skills/scheduled-jobs/SKILL.md +314 -0
  109. package/src/skills/security-guard/SKILL.md +353 -0
  110. package/src/skills/security-guard/references/encrypt-config.md +103 -0
  111. package/src/skills/security-guard/references/sensitive-strategies.md +42 -0
  112. package/src/skills/sms-mail/SKILL.md +308 -0
  113. package/src/skills/sms-mail/references/mail-config.md +88 -0
  114. package/src/skills/sms-mail/references/sms-config.md +74 -0
  115. package/src/skills/social-login/SKILL.md +266 -0
  116. package/src/skills/social-login/references/provider-configs.md +118 -0
  117. package/src/skills/start/SKILL.md +154 -0
  118. package/src/skills/store-pc/SKILL.md +366 -0
  119. package/src/skills/sync/SKILL.md +149 -0
  120. package/src/skills/task-tracker/SKILL.md +307 -0
  121. package/src/skills/tech-decision/SKILL.md +393 -0
  122. package/src/skills/tenant-management/SKILL.md +288 -0
  123. package/src/skills/tenant-management/references/tenant-scenarios.md +91 -0
  124. package/src/skills/test-development/SKILL.md +301 -0
  125. package/src/skills/test-development/references/parameterized-examples.md +119 -0
  126. package/src/skills/ui-pc/SKILL.md +438 -0
  127. package/src/skills/update-status/SKILL.md +159 -0
  128. package/src/skills/utils-toolkit/SKILL.md +362 -0
  129. package/src/skills/utils-toolkit/references/redis-utils-api.md +56 -0
  130. package/src/skills/websocket-sse/SKILL.md +271 -0
  131. package/src/skills/workflow-engine/SKILL.md +321 -0
package/bin/index.js CHANGED
@@ -46,10 +46,12 @@ console.log('');
46
46
 
47
47
  // ── 参数解析 ───────────────────────────────────────────────────────────────
48
48
  const args = process.argv.slice(2);
49
- let command = ''; // 'update' | 'global' | ''
49
+ let command = ''; // 'update' | 'global' | 'sync-back' | ''
50
50
  let tool = '';
51
51
  let targetDir = process.cwd();
52
52
  let force = false;
53
+ let skillFilter = ''; // sync-back --skill <名称>
54
+ let submitIssue = false; // sync-back --submit
53
55
 
54
56
  for (let i = 0; i < args.length; i++) {
55
57
  const arg = args[i];
@@ -60,6 +62,9 @@ for (let i = 0; i < args.length; i++) {
60
62
  case 'global':
61
63
  command = 'global';
62
64
  break;
65
+ case 'sync-back':
66
+ command = 'sync-back';
67
+ break;
63
68
  case '--tool': case '-t':
64
69
  if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
65
70
  console.error(fmt('red', `错误:${arg} 需要一个值(claude | cursor | codex | all)`));
@@ -77,6 +82,16 @@ for (let i = 0; i < args.length; i++) {
77
82
  case '--force': case '-f':
78
83
  force = true;
79
84
  break;
85
+ case '--skill': case '-s':
86
+ if (i + 1 >= args.length || args[i + 1].startsWith('-')) {
87
+ console.error(fmt('red', `错误:${arg} 需要一个技能名称`));
88
+ process.exit(1);
89
+ }
90
+ skillFilter = args[++i];
91
+ break;
92
+ case '--submit':
93
+ submitIssue = true;
94
+ break;
80
95
  case '--help': case '-h':
81
96
  printHelp();
82
97
  process.exit(0);
@@ -96,12 +111,15 @@ function printHelp() {
96
111
  console.log('命令:');
97
112
  console.log(` ${fmt('bold', '(无)')} 交互式初始化(安装到当前项目目录)`);
98
113
  console.log(` ${fmt('bold', 'update')} 更新已安装的框架文件(跳过用户自定义文件)`);
99
- console.log(` ${fmt('bold', 'global')} 全局安装到 ~/.claude / ~/.cursor 等,对所有项目生效\n`);
114
+ console.log(` ${fmt('bold', 'global')} 全局安装到 ~/.claude / ~/.cursor 等,对所有项目生效`);
115
+ console.log(` ${fmt('bold', 'sync-back')} 对比本地技能修改,生成 diff 或提交 GitHub Issue\n`);
100
116
  console.log('选项:');
101
- console.log(' --tool, -t <工具> 指定工具: claude | cursor | codex | all');
102
- console.log(' --dir, -d <目录> 目标目录(默认:当前目录,仅 init/update 有效)');
103
- console.log(' --force,-f 强制覆盖(init 时覆盖已有文件;update/global 时同时更新保留文件)');
104
- console.log(' --help, -h 显示此帮助\n');
117
+ console.log(' --tool, -t <工具> 指定工具: claude | cursor | codex | all');
118
+ console.log(' --dir, -d <目录> 目标目录(默认:当前目录,仅 init/update 有效)');
119
+ console.log(' --force, -f 强制覆盖(init 时覆盖已有文件;update/global 时同时更新保留文件)');
120
+ console.log(' --skill, -s <技能> sync-back 时只对比指定技能');
121
+ console.log(' --submit sync-back 时自动创建 GitHub Issue(需要 gh CLI)');
122
+ console.log(' --help, -h 显示此帮助\n');
105
123
  console.log('示例:');
106
124
  console.log(' npx ai-engineering-init --tool claude');
107
125
  console.log(' npx ai-engineering-init --tool all --dir /path/to/project');
@@ -109,7 +127,11 @@ function printHelp() {
109
127
  console.log(' npx ai-engineering-init update --tool claude # 只更新 Claude');
110
128
  console.log(' npx ai-engineering-init update --force # 强制更新,包括保留文件');
111
129
  console.log(' npx ai-engineering-init global # 全局安装所有工具');
112
- console.log(' npx ai-engineering-init global --tool claude # 只全局安装 Claude\n');
130
+ console.log(' npx ai-engineering-init global --tool claude # 只全局安装 Claude');
131
+ console.log(' npx ai-engineering-init sync-back # 扫描所有已安装工具');
132
+ console.log(' npx ai-engineering-init sync-back --tool claude # 只扫描 Claude');
133
+ console.log(' npx ai-engineering-init sync-back --skill bug-detective # 只对比指定技能');
134
+ console.log(' npx ai-engineering-init sync-back --skill bug-detective --submit # 提交 Issue\n');
113
135
  }
114
136
 
115
137
  // ── 工具定义(init 用)────────────────────────────────────────────────────
@@ -675,11 +697,419 @@ function runUpdate(selectedTool) {
675
697
  if (totalFailed > 0) process.exitCode = 1;
676
698
  }
677
699
 
700
+ // ── SYNC-BACK 逻辑 ─────────────────────────────────────────────────────────
701
+
702
+ /** 技能目录名称到工具的映射 */
703
+ const SKILL_DIRS = {
704
+ claude: '.claude/skills',
705
+ cursor: '.cursor/skills',
706
+ codex: '.codex/skills',
707
+ };
708
+
709
+ /** 递归列出目录下所有文件(相对路径) */
710
+ function listFilesRecursive(dir, prefix) {
711
+ prefix = prefix || '';
712
+ let results = [];
713
+ let entries;
714
+ try { entries = fs.readdirSync(dir); } catch { return results; }
715
+ for (const entry of entries) {
716
+ const fullPath = path.join(dir, entry);
717
+ const relPath = prefix ? prefix + '/' + entry : entry;
718
+ try {
719
+ if (fs.statSync(fullPath).isDirectory()) {
720
+ results = results.concat(listFilesRecursive(fullPath, relPath));
721
+ } else {
722
+ results.push(relPath);
723
+ }
724
+ } catch { /* 跳过不可读文件 */ }
725
+ }
726
+ return results;
727
+ }
728
+
729
+ /**
730
+ * 简易 unified diff 生成器(纯 Node.js,零依赖)
731
+ * 使用贪心 LCS 简化算法,输出标准 unified diff 格式
732
+ */
733
+ function generateDiff(oldContent, newContent, oldLabel, newLabel) {
734
+ const oldLines = oldContent.split(/\r?\n/);
735
+ const newLines = newContent.split(/\r?\n/);
736
+
737
+ // 计算 LCS 表(简化版,O(n*m) 但对技能文件足够)
738
+ const m = oldLines.length;
739
+ const n = newLines.length;
740
+
741
+ // 对于大文件,跳过 LCS 直接标记全部替换
742
+ if (m * n > 1000000) {
743
+ const lines = [];
744
+ lines.push(`--- ${oldLabel}`);
745
+ lines.push(`+++ ${newLabel}`);
746
+ lines.push(`@@ -1,${m} +1,${n} @@`);
747
+ for (const line of oldLines) lines.push('-' + line);
748
+ for (const line of newLines) lines.push('+' + line);
749
+ return lines.join('\n');
750
+ }
751
+
752
+ // LCS 回溯表
753
+ const dp = Array.from({ length: m + 1 }, () => new Uint16Array(n + 1));
754
+ for (let i = 1; i <= m; i++) {
755
+ for (let j = 1; j <= n; j++) {
756
+ if (oldLines[i - 1] === newLines[j - 1]) {
757
+ dp[i][j] = dp[i - 1][j - 1] + 1;
758
+ } else {
759
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
760
+ }
761
+ }
762
+ }
763
+
764
+ // 回溯生成编辑操作序列
765
+ const ops = []; // { type: 'equal'|'delete'|'insert', line }
766
+ let i = m, j = n;
767
+ while (i > 0 || j > 0) {
768
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
769
+ ops.push({ type: 'equal', oldIdx: i, newIdx: j, line: oldLines[i - 1] });
770
+ i--; j--;
771
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
772
+ ops.push({ type: 'insert', newIdx: j, line: newLines[j - 1] });
773
+ j--;
774
+ } else {
775
+ ops.push({ type: 'delete', oldIdx: i, line: oldLines[i - 1] });
776
+ i--;
777
+ }
778
+ }
779
+ ops.reverse();
780
+
781
+ // 将操作序列组织为 hunks(上下文 3 行)
782
+ const CTX = 3;
783
+ const hunks = [];
784
+ let hunkOps = [];
785
+ let lastChangeIdx = -999;
786
+
787
+ for (let k = 0; k < ops.length; k++) {
788
+ if (ops[k].type !== 'equal') {
789
+ // 如果距离上次变更超过 2*CTX+1,开始新 hunk
790
+ if (k - lastChangeIdx > 2 * CTX + 1 && hunkOps.length > 0) {
791
+ hunks.push(hunkOps);
792
+ hunkOps = [];
793
+ // 回退加上下文
794
+ const start = Math.max(k - CTX, lastChangeIdx + CTX + 1);
795
+ for (let c = start; c < k; c++) {
796
+ if (ops[c]) hunkOps.push(ops[c]);
797
+ }
798
+ } else if (hunkOps.length === 0) {
799
+ // 新 hunk 加前上下文
800
+ const start = Math.max(0, k - CTX);
801
+ for (let c = start; c < k; c++) {
802
+ hunkOps.push(ops[c]);
803
+ }
804
+ } else {
805
+ // 补充中间的上下文行
806
+ for (let c = lastChangeIdx + 1; c < k; c++) {
807
+ if (!hunkOps.includes(ops[c])) hunkOps.push(ops[c]);
808
+ }
809
+ }
810
+ hunkOps.push(ops[k]);
811
+ lastChangeIdx = k;
812
+ }
813
+ }
814
+ // 最后一个 hunk 加后上下文
815
+ if (hunkOps.length > 0) {
816
+ const end = Math.min(ops.length, lastChangeIdx + CTX + 1);
817
+ for (let c = lastChangeIdx + 1; c < end; c++) {
818
+ hunkOps.push(ops[c]);
819
+ }
820
+ hunks.push(hunkOps);
821
+ }
822
+
823
+ if (hunks.length === 0) return ''; // 无差异
824
+
825
+ // 格式化输出
826
+ const lines = [];
827
+ lines.push(`--- ${oldLabel}`);
828
+ lines.push(`+++ ${newLabel}`);
829
+
830
+ for (const hunk of hunks) {
831
+ // 计算 hunk 头
832
+ let oldStart = Infinity, oldCount = 0, newStart = Infinity, newCount = 0;
833
+ for (const op of hunk) {
834
+ if (op.type === 'equal' || op.type === 'delete') {
835
+ if (op.oldIdx < oldStart) oldStart = op.oldIdx;
836
+ oldCount++;
837
+ }
838
+ if (op.type === 'equal' || op.type === 'insert') {
839
+ if (op.newIdx < newStart) newStart = op.newIdx;
840
+ newCount++;
841
+ }
842
+ }
843
+ lines.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
844
+ for (const op of hunk) {
845
+ if (op.type === 'equal') lines.push(' ' + op.line);
846
+ if (op.type === 'delete') lines.push('-' + op.line);
847
+ if (op.type === 'insert') lines.push('+' + op.line);
848
+ }
849
+ }
850
+
851
+ return lines.join('\n');
852
+ }
853
+
854
+ /** 统计 diff 中的增删行数 */
855
+ function countDiffLines(diffText) {
856
+ let added = 0, removed = 0;
857
+ for (const line of diffText.split('\n')) {
858
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
859
+ if (line.startsWith('-') && !line.startsWith('---')) removed++;
860
+ }
861
+ return { added, removed };
862
+ }
863
+
864
+ /** 检测 gh CLI 是否可用 */
865
+ function isGhAvailable() {
866
+ try {
867
+ const { execSync } = require('child_process');
868
+ execSync('gh --version', { stdio: 'pipe' });
869
+ return true;
870
+ } catch { return false; }
871
+ }
872
+
873
+ /** 通过 gh CLI 创建 GitHub Issue */
874
+ function submitGitHubIssue(changes, allDiffText) {
875
+ const { execSync } = require('child_process');
876
+ const skillNames = changes.map(c => c.skillName).join(', ');
877
+ const title = `[sync-back] 技能改进:${skillNames}`;
878
+ const body = [
879
+ '## 技能修改反馈',
880
+ '',
881
+ `> 由 \`npx ai-engineering-init sync-back --submit\` 自动生成`,
882
+ '',
883
+ '### 修改的技能',
884
+ '',
885
+ ...changes.map(c => {
886
+ const files = c.files.map(f => ` - \`${f.relPath}\` (+${f.added}, -${f.removed})`).join('\n');
887
+ return `- **${c.skillName}**\n${files}`;
888
+ }),
889
+ '',
890
+ '### Diff',
891
+ '',
892
+ '```diff',
893
+ allDiffText,
894
+ '```',
895
+ '',
896
+ '---',
897
+ `CLI 版本: v${PKG_VERSION}`,
898
+ ].join('\n');
899
+
900
+ try {
901
+ const result = execSync(
902
+ `gh issue create --repo xu-cell/ai-engineering-init --title "${title.replace(/"/g, '\\"')}" --body-file -`,
903
+ { input: body, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' }
904
+ );
905
+ return result.trim();
906
+ } catch (e) {
907
+ return null;
908
+ }
909
+ }
910
+
911
+ /** sync-back 命令主流程 */
912
+ function runSyncBack(selectedTool, selectedSkill, doSubmit) {
913
+ console.log(` 目标目录: ${fmt('bold', targetDir)}`);
914
+ console.log(` 本机版本: ${fmt('bold', `v${PKG_VERSION}`)}`);
915
+ console.log('');
916
+
917
+ // 1. 确定扫描范围
918
+ let toolsToScan = [];
919
+ if (selectedTool && selectedTool !== 'all') {
920
+ if (!SKILL_DIRS[selectedTool]) {
921
+ console.error(fmt('red', `无效工具: "${selectedTool}"。有效选项: claude | cursor | codex | all`));
922
+ process.exit(1);
923
+ }
924
+ toolsToScan = [selectedTool];
925
+ } else {
926
+ toolsToScan = detectInstalledTools();
927
+ }
928
+
929
+ if (toolsToScan.length === 0) {
930
+ console.log(fmt('yellow', '⚠ 当前目录未检测到已安装的 AI 工具配置。'));
931
+ console.log(` 请先运行: ${fmt('bold', hintCmd('--tool claude'))}\n`);
932
+ process.exit(1);
933
+ }
934
+
935
+ console.log(` 扫描工具: ${fmt('bold', toolsToScan.join(', '))}`);
936
+ console.log('');
937
+ console.log(fmt('bold', '🔍 正在对比技能文件...'));
938
+ console.log('');
939
+
940
+ // 2. 对比每个工具的 skills 目录
941
+ const allChanges = []; // { toolKey, skillName, files: [{ relPath, diff, added, removed }] }
942
+
943
+ for (const toolKey of toolsToScan) {
944
+ const skillDir = SKILL_DIRS[toolKey];
945
+ const userSkillsDir = path.join(targetDir, skillDir);
946
+ const srcSkillsDir = path.join(SOURCE_DIR, skillDir);
947
+
948
+ if (!isRealDir(userSkillsDir) || !isRealDir(srcSkillsDir)) continue;
949
+
950
+ // 列出用户目录中的技能
951
+ let skillNames;
952
+ try { skillNames = fs.readdirSync(userSkillsDir); } catch { continue; }
953
+
954
+ for (const name of skillNames) {
955
+ if (selectedSkill && name !== selectedSkill) continue;
956
+
957
+ const userSkillDir = path.join(userSkillsDir, name);
958
+ const srcSkillDir = path.join(srcSkillsDir, name);
959
+
960
+ if (!isRealDir(userSkillDir)) continue;
961
+
962
+ // 列出用户技能目录下所有文件
963
+ const userFiles = listFilesRecursive(userSkillDir);
964
+ const srcFiles = isRealDir(srcSkillDir) ? listFilesRecursive(srcSkillDir) : [];
965
+ const allFiles = [...new Set([...userFiles, ...srcFiles])].sort();
966
+
967
+ const changedFiles = [];
968
+
969
+ for (const relFile of allFiles) {
970
+ const userFile = path.join(userSkillDir, relFile);
971
+ const srcFile = path.join(srcSkillDir, relFile);
972
+
973
+ const userExists = fs.existsSync(userFile);
974
+ const srcExists = fs.existsSync(srcFile);
975
+
976
+ if (userExists && srcExists) {
977
+ // 两边都有,对比内容
978
+ const userContent = fs.readFileSync(userFile, 'utf8');
979
+ const srcContent = fs.readFileSync(srcFile, 'utf8');
980
+ if (userContent !== srcContent) {
981
+ const diff = generateDiff(srcContent, userContent,
982
+ `原版 (v${PKG_VERSION})`, '本地修改');
983
+ if (diff) {
984
+ const { added, removed } = countDiffLines(diff);
985
+ changedFiles.push({ relPath: relFile, diff, added, removed, status: 'modified' });
986
+ }
987
+ }
988
+ } else if (userExists && !srcExists) {
989
+ // 用户新增的文件
990
+ const content = fs.readFileSync(userFile, 'utf8');
991
+ const lineCount = content.split(/\r?\n/).length;
992
+ changedFiles.push({
993
+ relPath: relFile,
994
+ diff: `--- /dev/null\n+++ 本地新增\n@@ -0,0 +1,${lineCount} @@\n` +
995
+ content.split(/\r?\n/).map(l => '+' + l).join('\n'),
996
+ added: lineCount, removed: 0, status: 'added'
997
+ });
998
+ }
999
+ // srcExists && !userExists: 用户删除的文件(不报告,可能是有意删除)
1000
+ }
1001
+
1002
+ if (changedFiles.length > 0) {
1003
+ // 去重:只保留第一个工具的结果(多工具 skills 内容相同)
1004
+ const existing = allChanges.find(c => c.skillName === name);
1005
+ if (!existing) {
1006
+ allChanges.push({ toolKey, skillName: name, files: changedFiles });
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ // 3. 展示结果
1013
+ if (allChanges.length === 0) {
1014
+ console.log(fmt('green', ' ✓ 未检测到技能修改,所有技能与包版本一致。'));
1015
+ console.log('');
1016
+ return;
1017
+ }
1018
+
1019
+ console.log(` 检测到 ${fmt('bold', String(allChanges.length))} 个技能有修改:`);
1020
+ console.log('');
1021
+
1022
+ for (let idx = 0; idx < allChanges.length; idx++) {
1023
+ const change = allChanges[idx];
1024
+ console.log(` ${fmt('bold', String(idx + 1) + '.')} ${fmt('cyan', change.skillName)}`);
1025
+ for (const file of change.files) {
1026
+ const statusLabel = file.status === 'added' ? fmt('green', '新增文件') : fmt('yellow', '修改文件');
1027
+ console.log(` ${statusLabel}: ${file.relPath} (${fmt('green', '+' + file.added)} 行, ${fmt('red', '-' + file.removed)} 行)`);
1028
+ }
1029
+ console.log('');
1030
+ }
1031
+
1032
+ // 4. 展示 diff 详情
1033
+ const allDiffParts = [];
1034
+
1035
+ for (const change of allChanges) {
1036
+ for (const file of change.files) {
1037
+ const header = `${change.skillName}/${file.relPath}`;
1038
+ console.log(fmt('bold', '─'.repeat(Math.min(50, header.length + 10))));
1039
+ console.log(`📋 ${fmt('bold', header)} 的变更:`);
1040
+ console.log(fmt('bold', '─'.repeat(Math.min(50, header.length + 10))));
1041
+
1042
+ // 着色 diff 输出
1043
+ for (const line of file.diff.split('\n')) {
1044
+ if (line.startsWith('+++') || line.startsWith('---')) {
1045
+ console.log(fmt('bold', line));
1046
+ } else if (line.startsWith('+')) {
1047
+ console.log(fmt('green', line));
1048
+ } else if (line.startsWith('-')) {
1049
+ console.log(fmt('red', line));
1050
+ } else if (line.startsWith('@@')) {
1051
+ console.log(fmt('cyan', line));
1052
+ } else {
1053
+ console.log(line);
1054
+ }
1055
+ }
1056
+ console.log('');
1057
+
1058
+ allDiffParts.push(`# ${header}\n${file.diff}`);
1059
+ }
1060
+ }
1061
+
1062
+ const allDiffText = allDiffParts.join('\n\n');
1063
+
1064
+ // 5. 提交 Issue 或提示
1065
+ if (doSubmit) {
1066
+ console.log(fmt('bold', '📤 正在提交 GitHub Issue...'));
1067
+ console.log('');
1068
+
1069
+ if (!isGhAvailable()) {
1070
+ console.log(fmt('yellow', '⚠ 未检测到 gh CLI,无法自动提交 Issue。'));
1071
+ console.log(` 安装方法: ${fmt('bold', 'https://cli.github.com/')}`);
1072
+ console.log('');
1073
+ console.log(fmt('bold', '📋 请手动复制以下内容到 GitHub Issue:'));
1074
+ console.log('');
1075
+ console.log(fmt('cyan', '─'.repeat(50)));
1076
+ console.log(`标题: [sync-back] 技能改进:${allChanges.map(c => c.skillName).join(', ')}`);
1077
+ console.log(fmt('cyan', '─'.repeat(50)));
1078
+ console.log(allDiffText);
1079
+ console.log(fmt('cyan', '─'.repeat(50)));
1080
+ console.log('');
1081
+ console.log(`提交到: ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
1082
+ } else {
1083
+ const issueUrl = submitGitHubIssue(allChanges, allDiffText);
1084
+ if (issueUrl) {
1085
+ console.log(fmt('green', fmt('bold', '✅ Issue 已创建!')));
1086
+ console.log(` ${fmt('bold', issueUrl)}`);
1087
+ } else {
1088
+ console.log(fmt('red', '✗ Issue 创建失败,请检查 gh 认证状态(gh auth status)'));
1089
+ console.log('');
1090
+ console.log(fmt('bold', '📋 请手动复制上方 diff 到 GitHub Issue:'));
1091
+ console.log(` ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
1092
+ }
1093
+ }
1094
+ } else {
1095
+ console.log(fmt('cyan', '💡 提交方式:'));
1096
+ if (allChanges.length === 1) {
1097
+ console.log(` → 运行 ${fmt('bold', hintCmd(`sync-back --skill ${allChanges[0].skillName} --submit`))}`);
1098
+ } else {
1099
+ console.log(` → 运行 ${fmt('bold', hintCmd('sync-back --submit'))}`);
1100
+ }
1101
+ console.log(` → 或手动复制上方 diff 到 ${fmt('bold', 'https://github.com/xu-cell/ai-engineering-init/issues/new')}`);
1102
+ }
1103
+ console.log('');
1104
+ }
1105
+
678
1106
  // ── 主入口 ────────────────────────────────────────────────────────────────
679
1107
  if (command === 'update') {
680
1108
  runUpdate(tool);
681
1109
  } else if (command === 'global') {
682
1110
  runGlobal(tool);
1111
+ } else if (command === 'sync-back') {
1112
+ runSyncBack(tool, skillFilter, submitIssue);
683
1113
  } else if (tool) {
684
1114
  run(tool);
685
1115
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-engineering-init",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "AI 工程化配置初始化工具 — 一键为 Claude Code、OpenAI Codex 等 AI 工具初始化 Skills 和项目规范",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -16,11 +16,17 @@
16
16
  "url": "https://github.com/xu-cell/ai-engineering-init.git"
17
17
  },
18
18
  "license": "MIT",
19
+ "scripts": {
20
+ "build:skills": "node scripts/build-skills.js",
21
+ "check:skills": "node scripts/build-skills.js --check"
22
+ },
19
23
  "bin": {
20
24
  "ai-engineering-init": "./bin/index.js"
21
25
  },
22
26
  "files": [
23
27
  "bin/",
28
+ "src/",
29
+ "scripts/",
24
30
  ".claude/",
25
31
  ".cursor/",
26
32
  ".codex/",
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * build-skills.js
5
+ *
6
+ * 从 src/skills/ 单一源生成 .claude/skills/, .codex/skills/, .cursor/skills/ 三个平台目录。
7
+ * 根据 src/platform-map.json 决定每个技能分发到哪些平台。
8
+ *
9
+ * 用法:
10
+ * node scripts/build-skills.js # 构建
11
+ * node scripts/build-skills.js --check # 仅检查一致性(CI 用)
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ const ROOT = path.resolve(__dirname, '..');
19
+ const SRC_DIR = path.join(ROOT, 'src', 'skills');
20
+ const PLATFORM_MAP_PATH = path.join(ROOT, 'src', 'platform-map.json');
21
+
22
+ const PLATFORM_DIRS = {
23
+ claude: path.join(ROOT, '.claude', 'skills'),
24
+ codex: path.join(ROOT, '.codex', 'skills'),
25
+ cursor: path.join(ROOT, '.cursor', 'skills'),
26
+ };
27
+
28
+ const CHECK_MODE = process.argv.includes('--check');
29
+
30
+ // ── 工具函数 ────────────────────────────────────────────
31
+
32
+ function loadPlatformMap() {
33
+ const raw = fs.readFileSync(PLATFORM_MAP_PATH, 'utf-8');
34
+ return JSON.parse(raw);
35
+ }
36
+
37
+ function getSkillPlatforms(skillName, platformMap) {
38
+ if (platformMap.overrides[skillName]) {
39
+ return platformMap.overrides[skillName].platforms;
40
+ }
41
+ return platformMap.defaults.platforms;
42
+ }
43
+
44
+ function hashFile(filePath) {
45
+ const content = fs.readFileSync(filePath);
46
+ return crypto.createHash('md5').update(content).digest('hex').slice(0, 8);
47
+ }
48
+
49
+ function copyDirRecursive(src, dest) {
50
+ fs.mkdirSync(dest, { recursive: true });
51
+ const entries = fs.readdirSync(src, { withFileTypes: true });
52
+
53
+ for (const entry of entries) {
54
+ const srcPath = path.join(src, entry.name);
55
+ const destPath = path.join(dest, entry.name);
56
+
57
+ if (entry.isDirectory()) {
58
+ copyDirRecursive(srcPath, destPath);
59
+ } else {
60
+ fs.copyFileSync(srcPath, destPath);
61
+ }
62
+ }
63
+ }
64
+
65
+ function dirContentsEqual(dir1, dir2) {
66
+ if (!fs.existsSync(dir1) || !fs.existsSync(dir2)) return false;
67
+
68
+ const entries1 = getAllFiles(dir1).map(f => path.relative(dir1, f)).sort();
69
+ const entries2 = getAllFiles(dir2).map(f => path.relative(dir2, f)).sort();
70
+
71
+ if (entries1.length !== entries2.length) return false;
72
+
73
+ for (let i = 0; i < entries1.length; i++) {
74
+ if (entries1[i] !== entries2[i]) return false;
75
+
76
+ const content1 = fs.readFileSync(path.join(dir1, entries1[i]));
77
+ const content2 = fs.readFileSync(path.join(dir2, entries2[i]));
78
+
79
+ if (!content1.equals(content2)) return false;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ function getAllFiles(dir) {
85
+ const results = [];
86
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ const fullPath = path.join(dir, entry.name);
89
+ if (entry.isDirectory()) {
90
+ results.push(...getAllFiles(fullPath));
91
+ } else {
92
+ results.push(fullPath);
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+
98
+ function removeDirRecursive(dirPath) {
99
+ if (fs.existsSync(dirPath)) {
100
+ fs.rmSync(dirPath, { recursive: true, force: true });
101
+ }
102
+ }
103
+
104
+ // ── 主逻辑 ──────────────────────────────────────────────
105
+
106
+ function build() {
107
+ const platformMap = loadPlatformMap();
108
+ const skillDirs = fs.readdirSync(SRC_DIR, { withFileTypes: true })
109
+ .filter(d => d.isDirectory())
110
+ .map(d => d.name)
111
+ .sort();
112
+
113
+ const stats = { claude: 0, codex: 0, cursor: 0 };
114
+ const errors = [];
115
+
116
+ // 构建每个技能到目标平台
117
+ for (const skillName of skillDirs) {
118
+ const platforms = getSkillPlatforms(skillName, platformMap);
119
+ const srcSkillDir = path.join(SRC_DIR, skillName);
120
+
121
+ for (const platform of platforms) {
122
+ const destSkillDir = path.join(PLATFORM_DIRS[platform], skillName);
123
+
124
+ if (CHECK_MODE) {
125
+ // 检查模式:对比内容是否一致
126
+ if (!dirContentsEqual(srcSkillDir, destSkillDir)) {
127
+ errors.push(`${platform}/skills/${skillName} 与 src/skills/${skillName} 不一致`);
128
+ }
129
+ } else {
130
+ // 构建模式:删除旧的,复制新的
131
+ removeDirRecursive(destSkillDir);
132
+ copyDirRecursive(srcSkillDir, destSkillDir);
133
+ }
134
+ stats[platform]++;
135
+ }
136
+ }
137
+
138
+ if (CHECK_MODE) {
139
+ // 反向检查:平台目录中是否存在 src 中没有的技能
140
+ for (const [platform, platformDir] of Object.entries(PLATFORM_DIRS)) {
141
+ if (!fs.existsSync(platformDir)) continue;
142
+ const platformSkills = fs.readdirSync(platformDir, { withFileTypes: true })
143
+ .filter(d => d.isDirectory())
144
+ .map(d => d.name);
145
+
146
+ for (const skill of platformSkills) {
147
+ const platforms = getSkillPlatforms(skill, platformMap);
148
+ if (!platforms.includes(platform)) {
149
+ errors.push(`${platform}/skills/${skill} 不应存在(platform-map 未配置)`);
150
+ }
151
+ if (!fs.existsSync(path.join(SRC_DIR, skill))) {
152
+ errors.push(`${platform}/skills/${skill} 在 src/skills/ 中不存在`);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ return { stats, errors, skillCount: skillDirs.length };
159
+ }
160
+
161
+ // ── 执行 ────────────────────────────────────────────────
162
+
163
+ console.log(CHECK_MODE ? '🔍 检查三平台一致性...' : '🔨 从 src/skills/ 构建三平台目录...');
164
+ console.log();
165
+
166
+ const { stats, errors, skillCount } = build();
167
+
168
+ console.log(`📦 src/skills/ 源技能数: ${skillCount}`);
169
+ console.log(` → .claude/skills/: ${stats.claude} 个`);
170
+ console.log(` → .codex/skills/: ${stats.codex} 个`);
171
+ console.log(` → .cursor/skills/: ${stats.cursor} 个`);
172
+ console.log();
173
+
174
+ if (errors.length > 0) {
175
+ console.error(`❌ 发现 ${errors.length} 个不一致:`);
176
+ errors.forEach(e => console.error(` • ${e}`));
177
+ process.exit(1);
178
+ } else {
179
+ console.log(CHECK_MODE ? '✅ 三平台与 src/skills/ 完全一致' : '✅ 构建完成');
180
+ }