sumulige-claude 1.0.11 → 1.1.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.
package/lib/commands.js CHANGED
@@ -8,18 +8,116 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { execSync } = require('child_process');
10
10
  const { loadConfig, CONFIG_DIR, CONFIG_FILE, SKILLS_DIR, ensureDir, saveConfig } = require('./config');
11
- const { copyRecursive, toTitleCase } = require('./utils');
11
+ const { copyRecursive, toTitleCase, CopyMode } = require('./utils');
12
12
  const { runMigrations, TEMPLATE_VERSION } = require('./migrations');
13
+ const { checkUpdate, getCurrentVersion } = require('./version-check');
13
14
 
14
15
  const TEMPLATE_DIR = path.join(__dirname, '../template');
15
16
 
17
+ // ============================================================================
18
+ // Helper Functions
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Count files in a directory recursively
23
+ * @param {string} dirPath - Directory path
24
+ * @returns {number} Number of files
25
+ */
26
+ function countFiles(dirPath) {
27
+ let count = 0;
28
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ const fullPath = path.join(dirPath, entry.name);
31
+ if (entry.isDirectory()) {
32
+ // Skip certain directories
33
+ if (entry.name !== 'node_modules' && entry.name !== '.git' && entry.name !== 'sessions') {
34
+ count += countFiles(fullPath);
35
+ }
36
+ } else {
37
+ count++;
38
+ }
39
+ }
40
+ return count;
41
+ }
42
+
43
+ /**
44
+ * Copy a single file with backup support
45
+ * @param {string} srcPath - Source file path
46
+ * @param {string} destPath - Destination file path
47
+ * @param {string} mode - Copy mode (CopyMode enum)
48
+ * @param {string} backupDir - Backup directory path
49
+ * @param {string} displayName - Display name for logging
50
+ */
51
+ function copySingleFile(srcPath, destPath, mode, backupDir, displayName) {
52
+ if (!fs.existsSync(srcPath)) return;
53
+
54
+ // File doesn't exist at destination - just copy
55
+ if (!fs.existsSync(destPath)) {
56
+ fs.copyFileSync(srcPath, destPath);
57
+ setExecutablePermission(destPath);
58
+ console.log(` ✅ ${displayName}`);
59
+ return;
60
+ }
61
+
62
+ // File exists - handle based on mode
63
+ switch (mode) {
64
+ case CopyMode.SAFE:
65
+ // Skip existing files
66
+ console.log(` ⊝ ${displayName} (kept existing)`);
67
+ break;
68
+
69
+ case CopyMode.FORCE:
70
+ // Overwrite without backup
71
+ fs.copyFileSync(srcPath, destPath);
72
+ setExecutablePermission(destPath);
73
+ console.log(` ✅ ${displayName} (overwritten)`);
74
+ break;
75
+
76
+ case CopyMode.BACKUP:
77
+ default:
78
+ // Backup then overwrite
79
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
80
+ const backupFileName = path.basename(destPath).replace(/\.[^.]+$/, '') + '.' + timestamp + '.bak';
81
+ const backupPath = path.join(backupDir, backupFileName);
82
+
83
+ fs.mkdirSync(backupDir, { recursive: true });
84
+ fs.copyFileSync(destPath, backupPath);
85
+ setExecutablePermission(backupPath);
86
+
87
+ fs.copyFileSync(srcPath, destPath);
88
+ setExecutablePermission(destPath);
89
+ console.log(` ✅ ${displayName} (backed up)`);
90
+ break;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Set executable permission for script files
96
+ * @param {string} filePath - File path
97
+ */
98
+ function setExecutablePermission(filePath) {
99
+ if (filePath.endsWith('.sh') || filePath.endsWith('.cjs')) {
100
+ try {
101
+ fs.chmodSync(filePath, 0o755);
102
+ } catch (e) {
103
+ // Ignore permission errors
104
+ }
105
+ }
106
+ }
107
+
16
108
  // ============================================================================
17
109
  // Commands
18
110
  // ============================================================================
19
111
 
20
112
  const commands = {
21
113
  // -------------------------------------------------------------------------
22
- init: () => {
114
+ init: (...args) => {
115
+ const isInteractive = args.includes('--interactive') || args.includes('-i');
116
+
117
+ if (isInteractive) {
118
+ return commands['init:interactive']();
119
+ }
120
+
23
121
  console.log('🚀 Initializing Sumulige Claude...');
24
122
 
25
123
  // Create config directory
@@ -55,13 +153,164 @@ const commands = {
55
153
  console.log('🎉 Sumulige Claude initialized!');
56
154
  console.log('');
57
155
  console.log('Next steps:');
58
- console.log(' sumulige-claude sync # Sync to current project');
59
- console.log(' sumulige-claude agent # Run agent orchestration');
60
- console.log(' sumulige-claude status # Show configuration');
156
+ console.log(' smc sync # Sync to current project');
157
+ console.log(' smc skills:official # View available skills');
158
+ console.log(' smc init --interactive # Interactive setup');
159
+ },
160
+
161
+ // -------------------------------------------------------------------------
162
+ version: async () => {
163
+ const current = getCurrentVersion();
164
+ console.log(`v${current}`);
165
+
166
+ // Check for updates asynchronously
167
+ setImmediate(async () => {
168
+ await checkUpdate({ force: false });
169
+ });
170
+ },
171
+
172
+ // -------------------------------------------------------------------------
173
+ 'init:interactive': () => {
174
+ const readline = require('readline');
175
+ const COLORS = {
176
+ reset: '\x1b[0m',
177
+ green: '\x1b[32m',
178
+ blue: '\x1b[34m',
179
+ yellow: '\x1b[33m',
180
+ gray: '\x1b[90m',
181
+ cyan: '\x1b[36m'
182
+ };
183
+
184
+ const log = (msg, color = 'reset') => {
185
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
186
+ };
187
+
188
+ const rl = readline.createInterface({
189
+ input: process.stdin,
190
+ output: process.stdout
191
+ });
192
+
193
+ const question = (prompt) => {
194
+ return new Promise(resolve => {
195
+ rl.question(prompt, resolve);
196
+ });
197
+ };
198
+
199
+ (async () => {
200
+ log('', 'gray');
201
+ log('🎯 SMC Interactive Setup', 'blue');
202
+ log('=====================================', 'gray');
203
+ log('', 'gray');
204
+
205
+ // Step 1: Create config
206
+ log('Step 1: Configuration', 'cyan');
207
+ ensureDir(CONFIG_DIR);
208
+ if (!fs.existsSync(CONFIG_FILE)) {
209
+ saveConfig(loadConfig());
210
+ log('✅ Created config', 'green');
211
+ } else {
212
+ log('✅ Config exists', 'green');
213
+ }
214
+ log('', 'gray');
215
+
216
+ // Step 2: Install OpenSkills
217
+ log('Step 2: OpenSkills', 'cyan');
218
+ try {
219
+ execSync('openskills --version', { stdio: 'ignore' });
220
+ log('✅ OpenSkills already installed', 'green');
221
+ } catch {
222
+ log('Installing OpenSkills...', 'gray');
223
+ try {
224
+ execSync('npm i -g openskills', { stdio: 'pipe' });
225
+ log('✅ OpenSkills installed', 'green');
226
+ } catch (e) {
227
+ log('⚠️ Failed to install OpenSkills', 'yellow');
228
+ }
229
+ }
230
+ log('', 'gray');
231
+
232
+ // Step 3: Select skills to install
233
+ log('Step 3: Choose Skills to Install', 'cyan');
234
+ log('', 'gray');
235
+ log('Available skill groups:', 'gray');
236
+ log(' 1. 📄 Documents (docx, pdf, pptx, xlsx)', 'gray');
237
+ log(' 2. 🎨 Creative (frontend-design, algorithmic-art)', 'gray');
238
+ log(' 3. 🛠️ Development (mcp-builder, webapp-testing)', 'gray');
239
+ log(' 4. 📋 Workflow (doc-coauthoring, internal-comms)', 'gray');
240
+ log(' 5. ✨ All recommended skills', 'gray');
241
+ log(' 6. ⏭️ Skip skill installation', 'gray');
242
+ log('', 'gray');
243
+
244
+ const skillChoice = await question('Select (1-6) [default: 6]: ');
245
+
246
+ const installSkills = async (source) => {
247
+ try {
248
+ execSync(`openskills install ${source} -y`, { stdio: 'pipe' });
249
+ execSync('openskills sync -y', { stdio: 'pipe' });
250
+ log('✅ Skills installed', 'green');
251
+ } catch (e) {
252
+ log('⚠️ Skill installation failed', 'yellow');
253
+ }
254
+ };
255
+
256
+ switch (skillChoice.trim()) {
257
+ case '1':
258
+ log('Installing document skills...', 'gray');
259
+ await installSkills('anthropics/skills');
260
+ break;
261
+ case '2':
262
+ log('Installing creative skills...', 'gray');
263
+ await installSkills('anthropics/skills');
264
+ break;
265
+ case '3':
266
+ log('Installing development skills...', 'gray');
267
+ await installSkills('anthropics/skills');
268
+ break;
269
+ case '4':
270
+ log('Installing workflow skills...', 'gray');
271
+ await installSkills('anthropics/skills');
272
+ break;
273
+ case '5':
274
+ log('Installing all recommended skills...', 'gray');
275
+ await installSkills('anthropics/skills');
276
+ break;
277
+ default:
278
+ log('⏭️ Skipped skill installation', 'yellow');
279
+ break;
280
+ }
281
+ log('', 'gray');
282
+
283
+ // Step 4: Sync to current project
284
+ log('Step 4: Sync to Current Project', 'cyan');
285
+ const shouldSync = await question('Sync .claude/ to current directory? [Y/n]: ');
286
+ if (shouldSync.toLowerCase() !== 'n') {
287
+ try {
288
+ execSync('smc sync', { stdio: 'inherit' });
289
+ } catch (e) {
290
+ log('⚠️ Sync failed (this is normal if not in a project directory)', 'yellow');
291
+ }
292
+ } else {
293
+ log('⏭️ Skipped sync', 'yellow');
294
+ }
295
+ log('', 'gray');
296
+
297
+ rl.close();
298
+
299
+ log('=====================================', 'gray');
300
+ log('🎉 Setup complete!', 'green');
301
+ log('', 'gray');
302
+ log('Useful commands:', 'gray');
303
+ log(' smc status Show configuration status', 'gray');
304
+ log(' smc skills:official List available skills', 'gray');
305
+ log(' smc doctor Check system health', 'gray');
306
+ log('', 'gray');
307
+ })();
61
308
  },
62
309
 
63
310
  // -------------------------------------------------------------------------
64
- sync: () => {
311
+ sync: async (...args) => {
312
+ const forceCheckUpdate = args.includes('--check-update');
313
+
65
314
  console.log('🔄 Syncing Sumulige Claude to current project...');
66
315
  console.log('');
67
316
 
@@ -129,6 +378,9 @@ const commands = {
129
378
 
130
379
  console.log('');
131
380
  console.log('✅ Sync complete!');
381
+
382
+ // Check for updates (sync is already a network operation)
383
+ await checkUpdate({ force: forceCheckUpdate, silent: false });
132
384
  },
133
385
 
134
386
  // -------------------------------------------------------------------------
@@ -482,13 +734,81 @@ const commands = {
482
734
  },
483
735
 
484
736
  // -------------------------------------------------------------------------
485
- template: (targetPath) => {
486
- const targetDir = targetPath ? path.resolve(targetPath) : process.cwd();
737
+ template: (...args) => {
738
+ // Parse arguments
739
+ let targetPath = process.cwd();
740
+ let copyMode = CopyMode.BACKUP; // Default: backup before overwrite
741
+ let showHelp = false;
742
+
743
+ for (const arg of args) {
744
+ if (arg === '--safe') {
745
+ copyMode = CopyMode.SAFE;
746
+ } else if (arg === '--force') {
747
+ copyMode = CopyMode.FORCE;
748
+ } else if (arg === '--help' || arg === '-h') {
749
+ showHelp = true;
750
+ } else if (!arg.startsWith('--')) {
751
+ targetPath = path.resolve(arg);
752
+ }
753
+ }
754
+
755
+ // Show help message
756
+ if (showHelp) {
757
+ console.log('');
758
+ console.log('📋 smc template - Deploy Claude Code project template');
759
+ console.log('');
760
+ console.log('USAGE:');
761
+ console.log(' smc template [path] [options]');
762
+ console.log('');
763
+ console.log('ARGUMENTS:');
764
+ console.log(' path Target directory (default: current directory)');
765
+ console.log('');
766
+ console.log('OPTIONS:');
767
+ console.log(' --safe Skip existing files (no overwrite)');
768
+ console.log(' --force Overwrite without backup (use for new projects)');
769
+ console.log(' --help, -h Show this help message');
770
+ console.log('');
771
+ console.log('DEFAULT BEHAVIOR:');
772
+ console.log(' Backup existing files before overwriting.');
773
+ console.log(' Backups stored in: .claude/backup/');
774
+ console.log(' Backup format: filename.YYYY-MM-DD.bak');
775
+ console.log('');
776
+ console.log('EXAMPLES:');
777
+ console.log(' smc template # Deploy to current dir (with backup)');
778
+ console.log(' smc template ./my-project # Deploy to specific dir');
779
+ console.log(' smc template --safe # Skip existing files');
780
+ console.log(' smc template --force # Overwrite everything');
781
+ console.log('');
782
+ console.log('COMPARISON:');
783
+ console.log(' smc template = Full deployment (overwrites with backup)');
784
+ console.log(' smc sync = Incremental update (only adds missing)');
785
+ console.log('');
786
+ return;
787
+ }
788
+
789
+ const targetDir = targetPath;
790
+ const backupDir = path.join(targetDir, '.claude', 'backup');
791
+ const stats = { copied: 0, skipped: 0, backedup: 0, backups: [] };
487
792
 
488
793
  console.log('🚀 Initializing Claude Code project template...');
489
794
  console.log(' Target:', targetDir);
795
+ console.log(' Mode:', copyMode === CopyMode.SAFE ? 'SAFE (skip existing)' :
796
+ copyMode === CopyMode.FORCE ? 'FORCE (overwrite)' :
797
+ 'BACKUP (backup before overwrite)');
490
798
  console.log('');
491
799
 
800
+ // Warn if existing .claude directory found
801
+ const existingClaudeDir = path.join(targetDir, '.claude');
802
+ if (fs.existsSync(existingClaudeDir)) {
803
+ if (copyMode === CopyMode.FORCE) {
804
+ console.log('⚠️ Existing .claude/ directory found -- will be overwritten!');
805
+ console.log('');
806
+ } else if (copyMode === CopyMode.BACKUP) {
807
+ console.log('ℹ️ Existing files will be backed up to: .claude/backup/');
808
+ console.log('');
809
+ }
810
+ }
811
+
492
812
  // Check template directory exists
493
813
  if (!fs.existsSync(TEMPLATE_DIR)) {
494
814
  console.log('❌ Template not found at:', TEMPLATE_DIR);
@@ -504,11 +824,13 @@ const commands = {
504
824
  path.join(targetDir, 'development/todos/active'),
505
825
  path.join(targetDir, 'development/todos/completed'),
506
826
  path.join(targetDir, 'development/todos/backlog'),
507
- path.join(targetDir, 'development/todos/archived')
827
+ path.join(targetDir, 'development/todos/archived'),
828
+ backupDir
508
829
  ];
509
830
 
510
831
  dirs.forEach(ensureDir);
511
832
  console.log(' ✅ Directories created');
833
+ console.log('');
512
834
 
513
835
  // Copy files
514
836
  console.log('📋 Copying template files...');
@@ -518,74 +840,103 @@ const commands = {
518
840
 
519
841
  // Files to copy
520
842
  const filesToCopy = [
521
- { src: 'CLAUDE-template.md', dest: 'CLAUDE.md' },
522
- { src: 'README.md', dest: 'README.md' },
523
- { src: 'settings.json', dest: 'settings.json' },
524
- { src: 'boris-optimizations.md', dest: 'boris-optimizations.md' }
843
+ { src: 'CLAUDE-template.md', dest: 'CLAUDE.md', name: '.claude/CLAUDE.md' },
844
+ { src: 'README.md', dest: 'README.md', name: '.claude/README.md' },
845
+ { src: 'settings.json', dest: 'settings.json', name: '.claude/settings.json' },
846
+ { src: 'boris-optimizations.md', dest: 'boris-optimizations.md', name: '.claude/boris-optimizations.md' }
525
847
  ];
526
848
 
527
- filesToCopy.forEach(({ src, dest }) => {
849
+ filesToCopy.forEach(({ src, dest, name }) => {
528
850
  const srcPath = path.join(claudeTemplateDir, src);
851
+ const destPath = path.join(targetClaudeDir, dest);
529
852
  if (fs.existsSync(srcPath)) {
530
- fs.copyFileSync(srcPath, path.join(targetClaudeDir, dest));
531
- console.log(` ✅ .claude/${dest}`);
853
+ const action = copySingleFile(srcPath, destPath, copyMode, backupDir, name);
854
+ if (action === 'copied') stats.copied++;
855
+ else if (action === 'skipped') stats.skipped++;
856
+ else if (action === 'backedup') { stats.copied++; stats.backedup++; }
532
857
  }
533
858
  });
534
859
 
535
860
  // Directories to copy recursively
536
861
  const dirsToCopy = [
537
- { src: 'hooks', overwrite: true },
538
- { src: 'commands', overwrite: true },
539
- { src: 'skills', overwrite: false },
540
- { src: 'templates', overwrite: false },
541
- { src: 'thinking-routes', overwrite: false },
542
- { src: 'rag', overwrite: true }
862
+ { src: 'hooks', name: '.claude/hooks/' },
863
+ { src: 'commands', name: '.claude/commands/' },
864
+ { src: 'skills', name: '.claude/skills/' },
865
+ { src: 'templates', name: '.claude/templates/' },
866
+ { src: 'thinking-routes', name: '.claude/thinking-routes/' },
867
+ { src: 'rag', name: '.claude/rag/' }
543
868
  ];
544
869
 
545
- dirsToCopy.forEach(({ src, overwrite }) => {
870
+ dirsToCopy.forEach(({ src, name }) => {
546
871
  const srcPath = path.join(claudeTemplateDir, src);
547
872
  if (fs.existsSync(srcPath)) {
548
- const count = copyRecursive(srcPath, path.join(targetClaudeDir, src), overwrite);
549
- console.log(` ✅ .claude/${src}/ (${count} files)`);
873
+ const result = copyRecursive(srcPath, path.join(targetClaudeDir, src), copyMode, backupDir);
874
+ const fileCount = result.copied + result.skipped + result.backedup;
875
+ const suffix = result.skipped > 0 ? ` (${result.skipped} skipped)` : '';
876
+ console.log(` ✅ ${name} (${fileCount} files${suffix})`);
877
+ stats.copied += result.copied;
878
+ stats.skipped += result.skipped;
879
+ stats.backedup += result.backedup;
550
880
  }
551
881
  });
552
882
 
553
883
  // Copy prompts
554
884
  const promptsDir = path.join(TEMPLATE_DIR, 'prompts');
555
885
  if (fs.existsSync(promptsDir)) {
556
- const count = copyRecursive(promptsDir, path.join(targetDir, 'prompts'), false);
557
- console.log(` ✅ prompts/ (${count} files)`);
886
+ const result = copyRecursive(promptsDir, path.join(targetDir, 'prompts'), copyMode, backupDir);
887
+ const fileCount = result.copied + result.skipped + result.backedup;
888
+ const suffix = result.skipped > 0 ? ` (${result.skipped} skipped)` : '';
889
+ console.log(` ✅ prompts/ (${fileCount} files${suffix})`);
890
+ stats.copied += result.copied;
891
+ stats.skipped += result.skipped;
892
+ stats.backedup += result.backedup;
558
893
  }
559
894
 
560
895
  // Copy todos
561
896
  const todosDir = path.join(TEMPLATE_DIR, 'development', 'todos');
562
897
  if (fs.existsSync(todosDir)) {
563
- const count = copyRecursive(todosDir, path.join(targetDir, 'development', 'todos'), false);
564
- console.log(` ✅ development/todos/ (${count} files)`);
898
+ const result = copyRecursive(todosDir, path.join(targetDir, 'development', 'todos'), copyMode, backupDir);
899
+ const fileCount = result.copied + result.skipped + result.backedup;
900
+ const suffix = result.skipped > 0 ? ` (${result.skipped} skipped)` : '';
901
+ console.log(` ✅ development/todos/ (${fileCount} files${suffix})`);
902
+ stats.copied += result.copied;
903
+ stats.skipped += result.skipped;
904
+ stats.backedup += result.backedup;
565
905
  }
566
906
 
567
907
  // Root files
568
908
  const rootFiles = ['project-paradigm.md', 'thinkinglens-silent.md', 'CLAUDE-template.md'];
569
909
  rootFiles.forEach(file => {
570
910
  const src = path.join(TEMPLATE_DIR, file);
911
+ const dest = path.join(targetDir, file);
571
912
  if (fs.existsSync(src)) {
572
- fs.copyFileSync(src, path.join(targetDir, file));
573
- console.log(' ✅ ' + file);
913
+ const action = copySingleFile(src, dest, copyMode, backupDir, file);
914
+ if (action === 'copied') stats.copied++;
915
+ else if (action === 'skipped') stats.skipped++;
916
+ else if (action === 'backedup') { stats.copied++; stats.backedup++; }
574
917
  }
575
918
  });
576
919
 
577
920
  // Create memory files
921
+ console.log('');
578
922
  console.log('📝 Creating memory files...');
579
923
  if (!fs.existsSync(path.join(targetClaudeDir, 'MEMORY.md'))) {
580
924
  fs.writeFileSync(path.join(targetClaudeDir, 'MEMORY.md'), '# Memory\n\n<!-- Project memory updated by AI -->\n');
925
+ console.log(' ✅ MEMORY.md');
926
+ } else {
927
+ console.log(' ⊝ MEMORY.md (already exists)');
581
928
  }
582
929
  if (!fs.existsSync(path.join(targetClaudeDir, 'PROJECT_LOG.md'))) {
583
930
  fs.writeFileSync(path.join(targetClaudeDir, 'PROJECT_LOG.md'), '# Project Log\n\n<!-- Build history and decisions -->\n');
931
+ console.log(' ✅ PROJECT_LOG.md');
932
+ } else {
933
+ console.log(' ⊝ PROJECT_LOG.md (already exists)');
584
934
  }
585
- console.log(' ✅ Memory files created');
586
935
 
587
936
  // Create ANCHORS.md
588
- const anchorsContent = `# [Project Name] - Skill Anchors Index
937
+ const anchorsPath = path.join(targetClaudeDir, 'ANCHORS.md');
938
+ if (!fs.existsSync(anchorsPath)) {
939
+ const anchorsContent = `# [Project Name] - Skill Anchors Index
589
940
 
590
941
  > This file is auto-maintained by AI as a quick index for the skill system
591
942
  > Last updated: ${new Date().toISOString().split('T')[0]}
@@ -600,7 +951,7 @@ const commands = {
600
951
  3. MEMORY.md → View latest changes
601
952
  4. CLAUDE.md → Load core knowledge
602
953
  5. prompts/ → View tutorials
603
- 6. .claude/rag/skills.md → RAG skill index ⭐
954
+ 6. .claude/rag/skill-index.json → RAG skill index ⭐
604
955
  7. Specific files → Deep dive into implementation
605
956
  \`\`\`
606
957
 
@@ -624,14 +975,28 @@ const commands = {
624
975
  ## Add Your Anchors Here...
625
976
 
626
977
  `;
627
- fs.writeFileSync(path.join(targetClaudeDir, 'ANCHORS.md'), anchorsContent);
628
- console.log(' ✅ .claude/ANCHORS.md');
978
+ fs.writeFileSync(anchorsPath, anchorsContent);
979
+ console.log(' ✅ .claude/ANCHORS.md');
980
+ } else {
981
+ console.log(' ⊝ .claude/ANCHORS.md (already exists)');
982
+ }
629
983
 
630
984
  // Write version file
631
985
  const { setProjectVersion } = require('./migrations');
632
986
  setProjectVersion(targetDir, TEMPLATE_VERSION);
633
987
  console.log(` ✅ .claude/.version (v${TEMPLATE_VERSION})`);
634
988
 
989
+ // Show summary
990
+ console.log('');
991
+ console.log('📊 Summary:');
992
+ console.log(` ✅ Copied: ${stats.copied} files`);
993
+ if (stats.skipped > 0) {
994
+ console.log(` ⊝ Skipped: ${stats.skipped} files (already exist)`);
995
+ }
996
+ if (stats.backedup > 0) {
997
+ console.log(` 💾 Backed up: ${stats.backedup} files → .claude/backup/`);
998
+ }
999
+
635
1000
  // Initialize Sumulige Claude if installed
636
1001
  console.log('');
637
1002
  console.log('🤖 Initializing Sumulige Claude...');
@@ -651,7 +1016,7 @@ const commands = {
651
1016
  console.log(' • Skills system with templates');
652
1017
  console.log(' • RAG dynamic skill index');
653
1018
  console.log(' • Hooks for automation');
654
- console.log(' • TODO management system');
1019
+ console.log(' • TODO management system v2.0 (R-D-T lifecycle)');
655
1020
  console.log('');
656
1021
  console.log('Next steps:');
657
1022
  console.log(' 1. Edit .claude/CLAUDE.md with your project info');
@@ -660,6 +1025,7 @@ const commands = {
660
1025
  console.log('');
661
1026
  },
662
1027
 
1028
+
663
1029
  // -------------------------------------------------------------------------
664
1030
  kickoff: () => {
665
1031
  const projectDir = process.cwd();
@@ -703,6 +1069,1258 @@ const commands = {
703
1069
  console.log(' 请先运行: sumulige-claude template');
704
1070
  console.log(' 或: sumulige-claude sync');
705
1071
  }
1072
+ },
1073
+
1074
+ // -------------------------------------------------------------------------
1075
+ ultrathink: () => {
1076
+ const COLORS = {
1077
+ reset: '\x1b[0m',
1078
+ green: '\x1b[32m',
1079
+ blue: '\x1b[34m',
1080
+ yellow: '\x1b[33m',
1081
+ gray: '\x1b[90m'
1082
+ };
1083
+
1084
+ const log = (msg, color = 'reset') => {
1085
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1086
+ };
1087
+
1088
+ log('', 'gray');
1089
+ log('🧠 UltraThink Mode', 'blue');
1090
+ log('=====================================', 'gray');
1091
+ log('', 'gray');
1092
+ log('✅ Deep thinking enabled', 'green');
1093
+ log('', 'gray');
1094
+ log('Usage:', 'gray');
1095
+ log(' Mention "ultrathink" or "深度思考" in conversation', 'gray');
1096
+ log('', 'gray');
1097
+ log('=====================================', 'gray');
1098
+ log('', 'gray');
1099
+ },
1100
+
1101
+ // -------------------------------------------------------------------------
1102
+ 'skills:official': () => {
1103
+ const fs = require('fs');
1104
+ const path = require('path');
1105
+ const COLORS = {
1106
+ reset: '\x1b[0m',
1107
+ green: '\x1b[32m',
1108
+ blue: '\x1b[34m',
1109
+ yellow: '\x1b[33m',
1110
+ gray: '\x1b[90m',
1111
+ cyan: '\x1b[36m',
1112
+ magenta: '\x1b[35m'
1113
+ };
1114
+
1115
+ const log = (msg, color = 'reset') => {
1116
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1117
+ };
1118
+
1119
+ const officialSkillsFile = path.join(__dirname, '../config/official-skills.json');
1120
+
1121
+ if (!fs.existsSync(officialSkillsFile)) {
1122
+ log('⚠ Official skills registry not found', 'yellow');
1123
+ return;
1124
+ }
1125
+
1126
+ const registry = JSON.parse(fs.readFileSync(officialSkillsFile, 'utf-8'));
1127
+
1128
+ log('', 'gray');
1129
+ log('📚 Official Skills (anthropics/skills)', 'blue');
1130
+ log('=====================================', 'gray');
1131
+ log('', 'gray');
1132
+ log(`Source: ${registry.source}`, 'gray');
1133
+ log(`Updated: ${registry.last_updated}`, 'gray');
1134
+ log('', 'gray');
1135
+
1136
+ // Check which skills are already installed
1137
+ const skillsDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude/skills');
1138
+ const installedSkills = fs.existsSync(skillsDir)
1139
+ ? fs.readdirSync(skillsDir).filter(f => {
1140
+ const dir = path.join(skillsDir, f);
1141
+ return fs.statSync(dir).isDirectory();
1142
+ })
1143
+ : [];
1144
+
1145
+ // Group by category
1146
+ const byCategory = {};
1147
+ for (const cat of Object.values(registry.categories)) {
1148
+ byCategory[cat.name] = { skills: [], ...cat };
1149
+ }
1150
+
1151
+ for (const skill of registry.skills) {
1152
+ const catName = registry.categories[skill.category].name;
1153
+ const isInstalled = installedSkills.includes(skill.name);
1154
+ byCategory[catName].skills.push({ ...skill, isInstalled });
1155
+ }
1156
+
1157
+ // Display by category
1158
+ for (const [catName, cat] of Object.entries(byCategory)) {
1159
+ if (cat.skills.length === 0) continue;
1160
+
1161
+ log(`${cat.icon} ${catName}`, 'cyan');
1162
+ log(` ${cat.description}`, 'gray');
1163
+ log('', 'gray');
1164
+
1165
+ for (const skill of cat.skills) {
1166
+ const status = skill.isInstalled ? '✓' : ' ';
1167
+ const rec = skill.recommended ? ' [推荐]' : '';
1168
+ const color = skill.isInstalled ? 'green' : 'reset';
1169
+ log(` [${status}] ${skill.name}${rec}`, color);
1170
+ log(` ${skill.description}`, 'gray');
1171
+ }
1172
+ log('', 'gray');
1173
+ }
1174
+
1175
+ log('=====================================', 'gray');
1176
+ log('', 'gray');
1177
+ log('Commands:', 'gray');
1178
+ log(' smc skills:install-official <name> Install a skill', 'gray');
1179
+ log(' smc skills:install-all Install all recommended', 'gray');
1180
+ log('', 'gray');
1181
+ },
1182
+
1183
+ // -------------------------------------------------------------------------
1184
+ 'skills:install-official': (skillName) => {
1185
+ const fs = require('fs');
1186
+ const path = require('path');
1187
+ const { execSync } = require('child_process');
1188
+ const COLORS = {
1189
+ reset: '\x1b[0m',
1190
+ green: '\x1b[32m',
1191
+ blue: '\x1b[34m',
1192
+ yellow: '\x1b[33m',
1193
+ gray: '\x1b[90m',
1194
+ red: '\x1b[31m'
1195
+ };
1196
+
1197
+ const log = (msg, color = 'reset') => {
1198
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1199
+ };
1200
+
1201
+ const officialSkillsFile = path.join(__dirname, '../config/official-skills.json');
1202
+
1203
+ if (!fs.existsSync(officialSkillsFile)) {
1204
+ log('⚠ Official skills registry not found', 'yellow');
1205
+ return;
1206
+ }
1207
+
1208
+ const registry = JSON.parse(fs.readFileSync(officialSkillsFile, 'utf-8'));
1209
+
1210
+ // Check if openskills is installed
1211
+ try {
1212
+ execSync('openskills --version', { stdio: 'ignore' });
1213
+ } catch {
1214
+ log('⚠ OpenSkills not installed', 'yellow');
1215
+ log('', 'gray');
1216
+ log('Installing OpenSkills...', 'gray');
1217
+ try {
1218
+ execSync('npm i -g openskills', { stdio: 'inherit' });
1219
+ log('✅ OpenSkills installed', 'green');
1220
+ } catch (e) {
1221
+ log('❌ Failed to install OpenSkills', 'red');
1222
+ log(' Run: npm i -g openskills', 'gray');
1223
+ return;
1224
+ }
1225
+ }
1226
+
1227
+ // Find the skill
1228
+ const skill = registry.skills.find(s => s.name === skillName);
1229
+
1230
+ if (!skill) {
1231
+ log(`❌ Skill "${skillName}" not found in official registry`, 'red');
1232
+ log('', 'gray');
1233
+ log('Run: smc skills:official');
1234
+ log('to see available skills.', 'gray');
1235
+ return;
1236
+ }
1237
+
1238
+ log(`📦 Installing: ${skillName}`, 'blue');
1239
+ log('', 'gray');
1240
+ log(`Source: ${skill.source}`, 'gray');
1241
+ log(`License: ${skill.license}`, 'gray');
1242
+ log('', 'gray');
1243
+
1244
+ try {
1245
+ execSync(`openskills install ${skill.source} -y`, { stdio: 'inherit' });
1246
+ execSync('openskills sync -y', { stdio: 'pipe' });
1247
+ log('', 'gray');
1248
+ log('✅ Skill installed successfully', 'green');
1249
+ log('', 'gray');
1250
+ log('The skill is now available in your conversations.', 'gray');
1251
+ } catch (e) {
1252
+ log('❌ Installation failed', 'red');
1253
+ }
1254
+ },
1255
+
1256
+ // -------------------------------------------------------------------------
1257
+ 'skills:install-all': () => {
1258
+ const fs = require('fs');
1259
+ const path = require('path');
1260
+ const { execSync } = require('child_process');
1261
+ const COLORS = {
1262
+ reset: '\x1b[0m',
1263
+ green: '\x1b[32m',
1264
+ blue: '\x1b[34m',
1265
+ yellow: '\x1b[33m',
1266
+ gray: '\x1b[90m',
1267
+ red: '\x1b[31m',
1268
+ cyan: '\x1b[36m'
1269
+ };
1270
+
1271
+ const log = (msg, color = 'reset') => {
1272
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1273
+ };
1274
+
1275
+ const officialSkillsFile = path.join(__dirname, '../config/official-skills.json');
1276
+
1277
+ if (!fs.existsSync(officialSkillsFile)) {
1278
+ log('⚠ Official skills registry not found', 'yellow');
1279
+ return;
1280
+ }
1281
+
1282
+ const registry = JSON.parse(fs.readFileSync(officialSkillsFile, 'utf-8'));
1283
+ const recommended = registry.skills.filter(s => s.recommended);
1284
+
1285
+ log('', 'gray');
1286
+ log('📦 Installing All Recommended Skills', 'blue');
1287
+ log('=====================================', 'gray');
1288
+ log('', 'gray');
1289
+ log(`Installing ${recommended.length} skills...`, 'gray');
1290
+ log('', 'gray');
1291
+
1292
+ // Check if openskills is installed
1293
+ try {
1294
+ execSync('openskills --version', { stdio: 'ignore' });
1295
+ } catch {
1296
+ log('⚠ OpenSkills not installed. Installing...', 'yellow');
1297
+ try {
1298
+ execSync('npm i -g openskills', { stdio: 'inherit' });
1299
+ log('✅ OpenSkills installed', 'green');
1300
+ log('', 'gray');
1301
+ } catch (e) {
1302
+ log('❌ Failed to install OpenSkills', 'red');
1303
+ return;
1304
+ }
1305
+ }
1306
+
1307
+ // Install anthropics/skills (includes all skills)
1308
+ try {
1309
+ log(`Installing ${registry.source}...`, 'cyan');
1310
+ execSync(`openskills install ${registry.source} -y`, { stdio: 'inherit' });
1311
+ execSync('openskills sync -y', { stdio: 'pipe' });
1312
+ log('', 'gray');
1313
+ log('✅ All skills installed successfully', 'green');
1314
+ log('', 'gray');
1315
+ log('Installed skills:', 'gray');
1316
+ recommended.forEach(s => log(` • ${s.name}`, 'gray'));
1317
+ log('', 'gray');
1318
+ log('These skills are now available in your conversations.', 'gray');
1319
+ } catch (e) {
1320
+ log('❌ Installation failed', 'red');
1321
+ }
1322
+ },
1323
+
1324
+ // -------------------------------------------------------------------------
1325
+ doctor: () => {
1326
+ const fs = require('fs');
1327
+ const path = require('path');
1328
+ const { execSync } = require('child_process');
1329
+ const COLORS = {
1330
+ reset: '\x1b[0m',
1331
+ green: '\x1b[32m',
1332
+ blue: '\x1b[34m',
1333
+ yellow: '\x1b[33m',
1334
+ gray: '\x1b[90m',
1335
+ red: '\x1b[31m',
1336
+ cyan: '\x1b[36m'
1337
+ };
1338
+
1339
+ const log = (msg, color = 'reset') => {
1340
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1341
+ };
1342
+
1343
+ log('', 'gray');
1344
+ log('🏥 SMC Health Check', 'blue');
1345
+ log('=====================================', 'gray');
1346
+ log('', 'gray');
1347
+
1348
+ const checks = [];
1349
+ let passCount = 0;
1350
+ let warnCount = 0;
1351
+ let failCount = 0;
1352
+
1353
+ // Check 1: Global config
1354
+ const globalConfigDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude');
1355
+ const globalConfigFile = path.join(globalConfigDir, 'config.json');
1356
+
1357
+ if (fs.existsSync(globalConfigFile)) {
1358
+ checks.push({ name: 'Global config', status: 'pass', msg: globalConfigFile });
1359
+ passCount++;
1360
+ } else {
1361
+ checks.push({ name: 'Global config', status: 'fail', msg: 'Run: smc init' });
1362
+ failCount++;
1363
+ }
1364
+
1365
+ // Check 2: Project .claude directory
1366
+ const projectDir = process.cwd();
1367
+ const projectClaudeDir = path.join(projectDir, '.claude');
1368
+
1369
+ if (fs.existsSync(projectClaudeDir)) {
1370
+ checks.push({ name: 'Project .claude/', status: 'pass', msg: projectClaudeDir });
1371
+ passCount++;
1372
+
1373
+ // Check for key files
1374
+ const agentsFile = path.join(projectClaudeDir, 'AGENTS.md');
1375
+ if (fs.existsSync(agentsFile)) {
1376
+ checks.push({ name: 'AGENTS.md', status: 'pass', msg: 'Generated' });
1377
+ passCount++;
1378
+ } else {
1379
+ checks.push({ name: 'AGENTS.md', status: 'warn', msg: 'Run: smc sync' });
1380
+ warnCount++;
1381
+ }
1382
+
1383
+ // Check MEMORY.md age
1384
+ const memoryFile = path.join(projectClaudeDir, 'MEMORY.md');
1385
+ if (fs.existsSync(memoryFile)) {
1386
+ const stats = fs.statSync(memoryFile);
1387
+ const daysSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
1388
+ if (daysSinceUpdate > 30) {
1389
+ checks.push({ name: 'MEMORY.md', status: 'warn', msg: `Updated ${Math.floor(daysSinceUpdate)} days ago` });
1390
+ warnCount++;
1391
+ } else {
1392
+ checks.push({ name: 'MEMORY.md', status: 'pass', msg: `Updated ${Math.floor(daysSinceUpdate)} days ago` });
1393
+ passCount++;
1394
+ }
1395
+ } else {
1396
+ checks.push({ name: 'MEMORY.md', status: 'warn', msg: 'Not found' });
1397
+ warnCount++;
1398
+ }
1399
+
1400
+ // Check hooks
1401
+ const hooksDir = path.join(projectClaudeDir, 'hooks');
1402
+ if (fs.existsSync(hooksDir)) {
1403
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'));
1404
+ checks.push({ name: 'Hooks', status: 'pass', msg: `${hookFiles.length} hooks` });
1405
+ passCount++;
1406
+ } else {
1407
+ checks.push({ name: 'Hooks', status: 'warn', msg: 'No hooks directory' });
1408
+ warnCount++;
1409
+ }
1410
+
1411
+ // Check skills
1412
+ const skillsDir = path.join(projectClaudeDir, 'skills');
1413
+ if (fs.existsSync(skillsDir)) {
1414
+ const skillDirs = fs.readdirSync(skillsDir).filter(f => {
1415
+ const dir = path.join(skillsDir, f);
1416
+ return fs.statSync(dir).isDirectory() && f !== 'template' && f !== 'examples';
1417
+ });
1418
+ checks.push({ name: 'Project Skills', status: 'pass', msg: `${skillDirs.length} skills` });
1419
+ passCount++;
1420
+ } else {
1421
+ checks.push({ name: 'Project Skills', status: 'warn', msg: 'No skills directory' });
1422
+ warnCount++;
1423
+ }
1424
+ } else {
1425
+ checks.push({ name: 'Project .claude/', status: 'warn', msg: 'Run: smc template or smc sync' });
1426
+ warnCount++;
1427
+ }
1428
+
1429
+ // Check 3: OpenSkills
1430
+ try {
1431
+ execSync('openskills --version', { stdio: 'ignore' });
1432
+ const globalSkillsDir = path.join(globalConfigDir, 'skills');
1433
+ if (fs.existsSync(globalSkillsDir)) {
1434
+ const skillCount = fs.readdirSync(globalSkillsDir).filter(f => {
1435
+ const dir = path.join(globalSkillsDir, f);
1436
+ return fs.statSync(dir).isDirectory() && !f.startsWith('.');
1437
+ }).length;
1438
+ checks.push({ name: 'OpenSkills', status: 'pass', msg: `${skillCount} global skills` });
1439
+ passCount++;
1440
+ } else {
1441
+ checks.push({ name: 'OpenSkills', status: 'pass', msg: 'Installed' });
1442
+ passCount++;
1443
+ }
1444
+ } catch {
1445
+ checks.push({ name: 'OpenSkills', status: 'warn', msg: 'Not installed (run: npm i -g openskills)' });
1446
+ warnCount++;
1447
+ }
1448
+
1449
+ // Check 4: Git
1450
+ try {
1451
+ execSync('git rev-parse --git-dir', { stdio: 'ignore', cwd: projectDir });
1452
+ checks.push({ name: 'Git', status: 'pass', msg: 'Repository detected' });
1453
+ passCount++;
1454
+ } catch {
1455
+ checks.push({ name: 'Git', status: 'warn', msg: 'Not a git repository' });
1456
+ warnCount++;
1457
+ }
1458
+
1459
+ // Display results
1460
+ for (const check of checks) {
1461
+ const icon = check.status === 'pass' ? '✅' : check.status === 'warn' ? '⚠️' : '❌';
1462
+ const color = check.status === 'pass' ? 'green' : check.status === 'warn' ? 'yellow' : 'red';
1463
+ log(`${icon} ${check.name}`, color);
1464
+ log(` ${check.msg}`, 'gray');
1465
+ log('', 'gray');
1466
+ }
1467
+
1468
+ log('=====================================', 'gray');
1469
+ log(`Summary: ${passCount} passed, ${warnCount} warnings${failCount > 0 ? `, ${failCount} failed` : ''}`, 'gray');
1470
+ log('', 'gray');
1471
+
1472
+ if (failCount > 0) {
1473
+ log('Fix failed checks to continue.', 'red');
1474
+ }
1475
+ },
1476
+
1477
+ // -------------------------------------------------------------------------
1478
+ 'skills:search': (keyword) => {
1479
+ const fs = require('fs');
1480
+ const path = require('path');
1481
+ const COLORS = {
1482
+ reset: '\x1b[0m',
1483
+ green: '\x1b[32m',
1484
+ blue: '\x1b[34m',
1485
+ yellow: '\x1b[33m',
1486
+ gray: '\x1b[90m',
1487
+ cyan: '\x1b[36m'
1488
+ };
1489
+
1490
+ const log = (msg, color = 'reset') => {
1491
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1492
+ };
1493
+
1494
+ if (!keyword) {
1495
+ log('Usage: smc skills:search <keyword>', 'yellow');
1496
+ log('', 'gray');
1497
+ log('Examples:', 'gray');
1498
+ log(' smc skills:search pdf', 'gray');
1499
+ log(' smc skills:search "前端设计"', 'gray');
1500
+ return;
1501
+ }
1502
+
1503
+ log('', 'gray');
1504
+ log(`🔍 Searching for: "${keyword}"`, 'blue');
1505
+ log('=====================================', 'gray');
1506
+ log('', 'gray');
1507
+
1508
+ const results = [];
1509
+ const lowerKeyword = keyword.toLowerCase();
1510
+
1511
+ // Search in official skills registry
1512
+ const officialSkillsFile = path.join(__dirname, '../config/official-skills.json');
1513
+ if (fs.existsSync(officialSkillsFile)) {
1514
+ const registry = JSON.parse(fs.readFileSync(officialSkillsFile, 'utf-8'));
1515
+
1516
+ for (const skill of registry.skills) {
1517
+ const matchName = skill.name.toLowerCase().includes(lowerKeyword);
1518
+ const matchDesc = skill.description.toLowerCase().includes(lowerKeyword);
1519
+ const matchCat = registry.categories[skill.category]?.name.toLowerCase().includes(lowerKeyword);
1520
+
1521
+ if (matchName || matchDesc || matchCat) {
1522
+ results.push({
1523
+ name: skill.name,
1524
+ description: skill.description,
1525
+ category: registry.categories[skill.category]?.name,
1526
+ source: 'official',
1527
+ recommended: skill.recommended
1528
+ });
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ // Search in sources.yaml
1534
+ const sourcesFile = path.join(__dirname, '../sources.yaml');
1535
+ if (fs.existsSync(sourcesFile)) {
1536
+ const content = fs.readFileSync(sourcesFile, 'utf-8');
1537
+ // Simple YAML parsing for skills
1538
+ const lines = content.split('\n');
1539
+ let currentSkill = null;
1540
+
1541
+ for (const line of lines) {
1542
+ const nameMatch = line.match(/^\s*-\s*name:\s*(.+)$/);
1543
+ if (nameMatch) {
1544
+ currentSkill = { name: nameMatch[1].trim(), source: 'marketplace' };
1545
+ if (currentSkill.name.toLowerCase().includes(lowerKeyword)) {
1546
+ results.push(currentSkill);
1547
+ }
1548
+ }
1549
+ const descMatch = line.match(/^\s+description:\s*"(.+)"$/);
1550
+ if (descMatch && currentSkill) {
1551
+ currentSkill.description = descMatch[1];
1552
+ if (currentSkill.description.toLowerCase().includes(lowerKeyword)) {
1553
+ results.push({ ...currentSkill });
1554
+ }
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // Search in global skills
1560
+ const globalSkillsDir = path.join(process.env.HOME || process.env.USERPROFILE, '.claude/skills');
1561
+ if (fs.existsSync(globalSkillsDir)) {
1562
+ const skillDirs = fs.readdirSync(globalSkillsDir).filter(f => {
1563
+ const dir = path.join(globalSkillsDir, f);
1564
+ return fs.statSync(dir).isDirectory() && !f.startsWith('.');
1565
+ });
1566
+
1567
+ for (const skillName of skillDirs) {
1568
+ if (skillName.toLowerCase().includes(lowerKeyword)) {
1569
+ // Check if already in results
1570
+ if (!results.some(r => r.name === skillName)) {
1571
+ const skillFile = path.join(globalSkillsDir, skillName, 'SKILL.md');
1572
+ let description = '';
1573
+ if (fs.existsSync(skillFile)) {
1574
+ const content = fs.readFileSync(skillFile, 'utf-8');
1575
+ const descMatch = content.match(/description:\s*(.+)/);
1576
+ if (descMatch) description = descMatch[1];
1577
+ }
1578
+ results.push({
1579
+ name: skillName,
1580
+ description: description || 'Local skill',
1581
+ source: 'installed'
1582
+ });
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ // Display results
1589
+ if (results.length === 0) {
1590
+ log('No skills found matching your search.', 'yellow');
1591
+ } else {
1592
+ for (const result of results) {
1593
+ const sourceIcon = result.source === 'official' ? '🔷' : result.source === 'marketplace' ? '📦' : '✓';
1594
+ const rec = result.recommended ? ' [推荐]' : '';
1595
+ log(`${sourceIcon} ${result.name}${rec}`, 'cyan');
1596
+ if (result.description) {
1597
+ log(` ${result.description}`, 'gray');
1598
+ }
1599
+ if (result.category) {
1600
+ log(` 分类: ${result.category}`, 'gray');
1601
+ }
1602
+ log('', 'gray');
1603
+ }
1604
+ }
1605
+
1606
+ log('=====================================', 'gray');
1607
+ log(`Found ${results.length} result(s)`, 'gray');
1608
+ log('', 'gray');
1609
+ },
1610
+
1611
+ // -------------------------------------------------------------------------
1612
+ 'skills:validate': (skillPath) => {
1613
+ const fs = require('fs');
1614
+ const path = require('path');
1615
+ const COLORS = {
1616
+ reset: '\x1b[0m',
1617
+ green: '\x1b[32m',
1618
+ blue: '\x1b[34m',
1619
+ yellow: '\x1b[33m',
1620
+ gray: '\x1b[90m',
1621
+ red: '\x1b[31m'
1622
+ };
1623
+
1624
+ const log = (msg, color = 'reset') => {
1625
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1626
+ };
1627
+
1628
+ // Default to current directory if not specified
1629
+ const targetPath = skillPath || path.join(process.cwd(), '.claude/skills');
1630
+
1631
+ if (!fs.existsSync(targetPath)) {
1632
+ log(`❌ Path not found: ${targetPath}`, 'red');
1633
+ return;
1634
+ }
1635
+
1636
+ log('', 'gray');
1637
+ log('🔍 Validating Skills', 'blue');
1638
+ log('=====================================', 'gray');
1639
+ log('', 'gray');
1640
+
1641
+ const checks = [];
1642
+ let passCount = 0;
1643
+ let failCount = 0;
1644
+
1645
+ // Check if it's a directory or a single skill
1646
+ const stats = fs.statSync(targetPath);
1647
+
1648
+ const validateSkill = (skillDir) => {
1649
+ const skillName = path.basename(skillDir);
1650
+ const skillFile = path.join(skillDir, 'SKILL.md');
1651
+ const errors = [];
1652
+ const warnings = [];
1653
+
1654
+ // Check SKILL.md exists
1655
+ if (!fs.existsSync(skillFile)) {
1656
+ errors.push('SKILL.md not found');
1657
+ return { name: skillName, errors, warnings };
1658
+ }
1659
+
1660
+ // Parse SKILL.md
1661
+ const content = fs.readFileSync(skillFile, 'utf-8');
1662
+ const frontmatterMatch = content.match(/^---\n(.*?)\n---/s);
1663
+
1664
+ if (!frontmatterMatch) {
1665
+ errors.push('No frontmatter found (--- delimited YAML required)');
1666
+ } else {
1667
+ const frontmatter = frontmatterMatch[1];
1668
+
1669
+ // Check required fields
1670
+ if (!frontmatter.match(/name:\s*.+/)) {
1671
+ errors.push('Missing "name" in frontmatter');
1672
+ }
1673
+ if (!frontmatter.match(/description:\s*.+/)) {
1674
+ warnings.push('Missing "description" in frontmatter (recommended)');
1675
+ }
1676
+ }
1677
+
1678
+ // Check for references directory if referenced
1679
+ if (content.includes('references/') && !fs.existsSync(path.join(skillDir, 'references'))) {
1680
+ warnings.push('Content references "references/" but directory not found');
1681
+ }
1682
+
1683
+ return { name: skillName, errors, warnings };
1684
+ };
1685
+
1686
+ if (stats.isDirectory()) {
1687
+ // Check if it's a skills directory or a single skill
1688
+ const skillFile = path.join(targetPath, 'SKILL.md');
1689
+ if (fs.existsSync(skillFile)) {
1690
+ // Single skill
1691
+ const result = validateSkill(targetPath);
1692
+ checks.push(result);
1693
+ } else {
1694
+ // Skills directory - validate all subdirectories
1695
+ const entries = fs.readdirSync(targetPath);
1696
+ for (const entry of entries) {
1697
+ const entryPath = path.join(targetPath, entry);
1698
+ const entryStats = fs.statSync(entryPath);
1699
+ if (entryStats.isDirectory() && !entry.startsWith('.')) {
1700
+ const result = validateSkill(entryPath);
1701
+ checks.push(result);
1702
+ }
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ // Display results
1708
+ for (const check of checks) {
1709
+ if (check.errors.length === 0 && check.warnings.length === 0) {
1710
+ log(`✅ ${check.name}`, 'green');
1711
+ passCount++;
1712
+ } else if (check.errors.length === 0) {
1713
+ log(`⚠️ ${check.name}`, 'yellow');
1714
+ passCount++;
1715
+ } else {
1716
+ log(`❌ ${check.name}`, 'red');
1717
+ failCount++;
1718
+ }
1719
+
1720
+ for (const error of check.errors) {
1721
+ log(` ❌ ${error}`, 'red');
1722
+ }
1723
+ for (const warning of check.warnings) {
1724
+ log(` ⚠️ ${warning}`, 'yellow');
1725
+ }
1726
+ log('', 'gray');
1727
+ }
1728
+
1729
+ log('=====================================', 'gray');
1730
+ log(`Validated ${checks.length} skill(s): ${passCount} passed${failCount > 0 ? `, ${failCount} failed` : ''}`, 'gray');
1731
+ log('', 'gray');
1732
+ },
1733
+
1734
+ // -------------------------------------------------------------------------
1735
+ 'skills:update': () => {
1736
+ const fs = require('fs');
1737
+ const path = require('path');
1738
+ const { execSync } = require('child_process');
1739
+ const COLORS = {
1740
+ reset: '\x1b[0m',
1741
+ green: '\x1b[32m',
1742
+ blue: '\x1b[34m',
1743
+ yellow: '\x1b[33m',
1744
+ gray: '\x1b[90m',
1745
+ red: '\x1b[31m'
1746
+ };
1747
+
1748
+ const log = (msg, color = 'reset') => {
1749
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1750
+ };
1751
+
1752
+ log('', 'gray');
1753
+ log('🔄 Updating Official Skills List', 'blue');
1754
+ log('=====================================', 'gray');
1755
+ log('', 'gray');
1756
+
1757
+ // Fetch latest skills from anthropics/skills
1758
+ const tempDir = path.join(__dirname, '../.tmp');
1759
+ const repoUrl = 'https://github.com/anthropics/skills.git';
1760
+
1761
+ try {
1762
+ // Create temp dir
1763
+ if (!fs.existsSync(tempDir)) {
1764
+ fs.mkdirSync(tempDir, { recursive: true });
1765
+ }
1766
+
1767
+ const cloneDir = path.join(tempDir, 'anthropics-skills');
1768
+
1769
+ // Remove existing clone if present
1770
+ const rimraf = (dir) => {
1771
+ if (fs.existsSync(dir)) {
1772
+ fs.rmSync(dir, { recursive: true, force: true });
1773
+ }
1774
+ };
1775
+
1776
+ rimraf(cloneDir);
1777
+
1778
+ log('Cloning anthropics/skills...', 'gray');
1779
+ execSync(`git clone --depth 1 ${repoUrl} ${cloneDir}`, { stdio: 'pipe' });
1780
+
1781
+ // Read skills directory to get available skills
1782
+ const skillsDir = path.join(cloneDir, 'skills');
1783
+ const skillCategories = fs.readdirSync(skillsDir).filter(f => {
1784
+ const dir = path.join(skillsDir, f);
1785
+ return fs.statSync(dir).isDirectory();
1786
+ });
1787
+
1788
+ log(`Found ${skillCategories.length} skills in repository`, 'gray');
1789
+ log('', 'gray');
1790
+
1791
+ // Update the registry file
1792
+ const registryFile = path.join(__dirname, '../config/official-skills.json');
1793
+ let registry = { version: '1.0.0', last_updated: new Date().toISOString().split('T')[0], source: 'anthropics/skills', categories: {}, skills: [] };
1794
+
1795
+ if (fs.existsSync(registryFile)) {
1796
+ registry = JSON.parse(fs.readFileSync(registryFile, 'utf-8'));
1797
+ }
1798
+
1799
+ // Update timestamp
1800
+ registry.last_updated = new Date().toISOString().split('T')[0];
1801
+
1802
+ fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
1803
+
1804
+ // Cleanup
1805
+ rimraf(cloneDir);
1806
+
1807
+ log('✅ Official skills list updated', 'green');
1808
+ log(` Updated: ${registry.last_updated}`, 'gray');
1809
+ log('', 'gray');
1810
+ log('Run: smc skills:official', 'gray');
1811
+ } catch (e) {
1812
+ log('❌ Update failed', 'red');
1813
+ log(` ${e.message}`, 'gray');
1814
+ }
1815
+ },
1816
+
1817
+ // -------------------------------------------------------------------------
1818
+ config: (action, key, value) => {
1819
+ const fs = require('fs');
1820
+ const path = require('path');
1821
+ const { loadConfig, saveConfig, CONFIG_FILE } = require('./config');
1822
+ const COLORS = {
1823
+ reset: '\x1b[0m',
1824
+ green: '\x1b[32m',
1825
+ blue: '\x1b[34m',
1826
+ yellow: '\x1b[33m',
1827
+ gray: '\x1b[90m',
1828
+ red: '\x1b[31m'
1829
+ };
1830
+
1831
+ const log = (msg, color = 'reset') => {
1832
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1833
+ };
1834
+
1835
+ if (!action) {
1836
+ // Show current config
1837
+ const config = loadConfig();
1838
+ log('', 'gray');
1839
+ log('⚙️ SMC Configuration', 'blue');
1840
+ log('=====================================', 'gray');
1841
+ log('', 'gray');
1842
+ log(`File: ${CONFIG_FILE}`, 'gray');
1843
+ log('', 'gray');
1844
+ log(JSON.stringify(config, null, 2));
1845
+ log('', 'gray');
1846
+ log('=====================================', 'gray');
1847
+ log('', 'gray');
1848
+ log('Commands:', 'gray');
1849
+ log(' smc config get <key> Get a config value', 'gray');
1850
+ log(' smc config set <key> <value> Set a config value', 'gray');
1851
+ log('', 'gray');
1852
+ return;
1853
+ }
1854
+
1855
+ if (action === 'get') {
1856
+ if (!key) {
1857
+ log('Usage: smc config get <key>', 'yellow');
1858
+ return;
1859
+ }
1860
+ const config = loadConfig();
1861
+ const keys = key.split('.');
1862
+ let value = config;
1863
+ for (const k of keys) {
1864
+ value = value?.[k];
1865
+ }
1866
+ if (value !== undefined) {
1867
+ log(`${key}: ${JSON.stringify(value, null, 2)}`, 'green');
1868
+ } else {
1869
+ log(`Key not found: ${key}`, 'yellow');
1870
+ }
1871
+ } else if (action === 'set') {
1872
+ if (!key || value === undefined) {
1873
+ log('Usage: smc config set <key> <value>', 'yellow');
1874
+ return;
1875
+ }
1876
+ const config = loadConfig();
1877
+ const keys = key.split('.');
1878
+ let target = config;
1879
+ for (let i = 0; i < keys.length - 1; i++) {
1880
+ if (!target[keys[i]]) target[keys[i]] = {};
1881
+ target = target[keys[i]];
1882
+ }
1883
+ // Parse value (try JSON, fallback to string)
1884
+ try {
1885
+ target[keys[keys.length - 1]] = JSON.parse(value);
1886
+ } catch {
1887
+ target[keys[keys.length - 1]] = value;
1888
+ }
1889
+ saveConfig(config);
1890
+ log(`✅ Set ${key} = ${target[keys[keys.length - 1]]}`, 'green');
1891
+ } else {
1892
+ log(`Unknown action: ${action}`, 'red');
1893
+ log('Valid actions: get, set', 'gray');
1894
+ }
1895
+ },
1896
+
1897
+ // -------------------------------------------------------------------------
1898
+ 'skills:publish': (skillPath) => {
1899
+ const fs = require('fs');
1900
+ const path = require('path');
1901
+ const { execSync } = require('child_process');
1902
+ const COLORS = {
1903
+ reset: '\x1b[0m',
1904
+ green: '\x1b[32m',
1905
+ blue: '\x1b[34m',
1906
+ yellow: '\x1b[33m',
1907
+ gray: '\x1b[90m',
1908
+ red: '\x1b[31m',
1909
+ cyan: '\x1b[36m'
1910
+ };
1911
+
1912
+ const log = (msg, color = 'reset') => {
1913
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
1914
+ };
1915
+
1916
+ // Default to current directory's skills folder
1917
+ const targetPath = skillPath || path.join(process.cwd(), '.claude/skills');
1918
+
1919
+ if (!fs.existsSync(targetPath)) {
1920
+ log(`❌ Path not found: ${targetPath}`, 'red');
1921
+ log('', 'gray');
1922
+ log('Usage: smc skills:publish [skill-path]', 'yellow');
1923
+ log(' Creates a GitHub repo with your skill', 'gray');
1924
+ return;
1925
+ }
1926
+
1927
+ log('', 'gray');
1928
+ log('📦 Publish Skill to GitHub', 'blue');
1929
+ log('=====================================', 'gray');
1930
+ log('', 'gray');
1931
+
1932
+ const readline = require('readline');
1933
+ const rl = readline.createInterface({
1934
+ input: process.stdin,
1935
+ output: process.stdout
1936
+ });
1937
+
1938
+ const question = (prompt) => {
1939
+ return new Promise(resolve => {
1940
+ rl.question(prompt, resolve);
1941
+ });
1942
+ };
1943
+
1944
+ (async () => {
1945
+ // Get skill info
1946
+ const stat = fs.statSync(targetPath);
1947
+ let skillName, skillDir;
1948
+
1949
+ if (stat.isDirectory()) {
1950
+ const skillFile = path.join(targetPath, 'SKILL.md');
1951
+ if (fs.existsSync(skillFile)) {
1952
+ // Single skill directory
1953
+ skillDir = targetPath;
1954
+ skillName = path.basename(targetPath);
1955
+ } else {
1956
+ // Skills directory - ask which skill
1957
+ const entries = fs.readdirSync(targetPath).filter(f => {
1958
+ const dir = path.join(targetPath, f);
1959
+ return fs.statSync(dir).isDirectory() && !f.startsWith('.') && f !== 'template' && f !== 'examples';
1960
+ });
1961
+
1962
+ if (entries.length === 0) {
1963
+ log('❌ No skills found in directory', 'red');
1964
+ rl.close();
1965
+ return;
1966
+ }
1967
+
1968
+ log('Found skills:', 'gray');
1969
+ entries.forEach((e, i) => log(` ${i + 1}. ${e}`, 'gray'));
1970
+ log('', 'gray');
1971
+
1972
+ const choice = await question('Select skill [1]: ');
1973
+ const index = parseInt(choice || '1') - 1;
1974
+ skillName = entries[index] || entries[0];
1975
+ skillDir = path.join(targetPath, skillName);
1976
+ }
1977
+ } else {
1978
+ log('❌ Path must be a directory', 'red');
1979
+ rl.close();
1980
+ return;
1981
+ }
1982
+
1983
+ // Read SKILL.md to get info
1984
+ const skillFile = path.join(skillDir, 'SKILL.md');
1985
+ if (!fs.existsSync(skillFile)) {
1986
+ log('❌ SKILL.md not found. A skill must have SKILL.md', 'red');
1987
+ rl.close();
1988
+ return;
1989
+ }
1990
+
1991
+ const skillContent = fs.readFileSync(skillFile, 'utf-8');
1992
+ const nameMatch = skillContent.match(/name:\s*(.+)/);
1993
+ const descMatch = skillContent.match(/description:\s*(.+)/);
1994
+ const displayName = nameMatch ? nameMatch[1].trim() : skillName;
1995
+ const description = descMatch ? descMatch[1].trim() : '';
1996
+
1997
+ log(`Skill: ${displayName}`, 'cyan');
1998
+ if (description) {
1999
+ log(`Description: ${description}`, 'gray');
2000
+ }
2001
+ log(`Path: ${skillDir}`, 'gray');
2002
+ log('', 'gray');
2003
+
2004
+ // Get GitHub info
2005
+ const githubUser = await question('GitHub username: ');
2006
+ if (!githubUser) {
2007
+ log('❌ GitHub username required', 'red');
2008
+ rl.close();
2009
+ return;
2010
+ }
2011
+
2012
+ const repoName = await question(`Repository name [${skillName}]: `) || skillName;
2013
+ const isPrivate = await question('Private repo? [y/N]: ');
2014
+
2015
+ rl.close();
2016
+
2017
+ log('', 'gray');
2018
+ log('📝 Instructions:', 'yellow');
2019
+ log('', 'gray');
2020
+ log(`1. Create GitHub repo:`, 'gray');
2021
+ log(` https://github.com/new`, 'gray');
2022
+ log(` Name: ${repoName}`, 'gray');
2023
+ log(` ${isPrivate.toLowerCase() === 'y' ? 'Private' : 'Public'}`, 'gray');
2024
+ log('', 'gray');
2025
+ log(`2. Initialize git and push:`, 'gray');
2026
+ log(` cd ${skillDir}`, 'gray');
2027
+ log(` git init`, 'gray');
2028
+ log(` git add .`, 'gray');
2029
+ log(` git commit -m "Initial commit"`, 'gray');
2030
+ log(` git branch -M main`, 'gray');
2031
+ log(` git remote add origin https://github.com/${githubUser}/${repoName}.git`, 'gray');
2032
+ log(` git push -u origin main`, 'gray');
2033
+ log('', 'gray');
2034
+ log(`3. Add to sources.yaml:`, 'gray');
2035
+ log(` - name: ${skillName}`, 'gray');
2036
+ log(` source:`, 'gray');
2037
+ log(` repo: ${githubUser}/${repoName}`, 'gray');
2038
+ log(` path: .`, 'gray');
2039
+ log(` target:`, 'gray');
2040
+ log(` category: tools`, 'gray');
2041
+ log(` path: template/.claude/skills/tools/${skillName}`, 'gray');
2042
+ log('', 'gray');
2043
+
2044
+ // Ask if user wants to auto-execute
2045
+ const autoExecute = await question('Auto-execute git commands? [y/N]: ');
2046
+ if (autoExecute.toLowerCase() === 'y') {
2047
+ try {
2048
+ log('', 'gray');
2049
+ log('Initializing git...', 'gray');
2050
+ execSync('git init', { cwd: skillDir, stdio: 'pipe' });
2051
+ execSync('git add .', { cwd: skillDir, stdio: 'pipe' });
2052
+ execSync('git commit -m "Initial commit"', { cwd: skillDir, stdio: 'pipe' });
2053
+ execSync('git branch -M main', { cwd: skillDir, stdio: 'pipe' });
2054
+ log('✅ Git initialized', 'green');
2055
+
2056
+ log('Adding remote...', 'gray');
2057
+ execSync(`git remote add origin https://github.com/${githubUser}/${repoName}.git`, { cwd: skillDir, stdio: 'pipe' });
2058
+ log('✅ Remote added', 'green');
2059
+
2060
+ log('', 'gray');
2061
+ log('⚠️ Please create the GitHub repo first, then run:', 'yellow');
2062
+ log(` cd ${skillDir} && git push -u origin main`, 'gray');
2063
+ } catch (e) {
2064
+ log('❌ Failed to initialize git', 'red');
2065
+ log(` ${e.message}`, 'gray');
2066
+ }
2067
+ }
2068
+
2069
+ log('', 'gray');
2070
+ log('=====================================', 'gray');
2071
+ log('✅ Skill publishing guide complete', 'green');
2072
+ log('', 'gray');
2073
+ })();
2074
+ },
2075
+
2076
+ // -------------------------------------------------------------------------
2077
+ changelog: (...args) => {
2078
+ const fs = require('fs');
2079
+ const path = require('path');
2080
+ const { execSync } = require('child_process');
2081
+ const COLORS = {
2082
+ reset: '\x1b[0m',
2083
+ green: '\x1b[32m',
2084
+ blue: '\x1b[34m',
2085
+ yellow: '\x1b[33m',
2086
+ gray: '\x1b[90m',
2087
+ red: '\x1b[31m',
2088
+ cyan: '\x1b[36m'
2089
+ };
2090
+
2091
+ const log = (msg, color = 'reset') => {
2092
+ console.log(`${COLORS[color]}${msg}${COLORS.reset}`);
2093
+ };
2094
+
2095
+ // Parse options
2096
+ const options = { from: null, to: null, json: false, help: false };
2097
+ for (let i = 0; i < args.length; i++) {
2098
+ if (args[i] === '--from' && args[i + 1]) options.from = args[++i];
2099
+ if (args[i] === '--to' && args[i + 1]) options.to = args[++i];
2100
+ if (args[i] === '--json') options.json = true;
2101
+ if (args[i] === '--help' || args[i] === '-h') options.help = true;
2102
+ }
2103
+
2104
+ if (options.help) {
2105
+ log('', 'gray');
2106
+ log('📝 Changelog Generator', 'blue');
2107
+ log('=====================================', 'gray');
2108
+ log('', 'gray');
2109
+ log('Generate changelog from git commits.', 'gray');
2110
+ log('', 'gray');
2111
+ log('Usage:', 'gray');
2112
+ log(' smc changelog [options]', 'gray');
2113
+ log('', 'gray');
2114
+ log('Options:', 'gray');
2115
+ log(' --from <tag> Start from this tag (default: last tag)', 'gray');
2116
+ log(' --to <tag> End at this tag (default: HEAD)', 'gray');
2117
+ log(' --json Output as JSON', 'gray');
2118
+ log(' --help, -h Show this help', 'gray');
2119
+ log('', 'gray');
2120
+ log('Examples:', 'gray');
2121
+ log(' smc changelog # Changelog since last tag', 'gray');
2122
+ log(' smc changelog --from v1.0.0 # Since v1.0.0', 'gray');
2123
+ log(' smc changelog --from v1.0 --to v1.1 # Range between tags', 'gray');
2124
+ log(' smc changelog --json # JSON output', 'gray');
2125
+ log('', 'gray');
2126
+ return;
2127
+ }
2128
+
2129
+ // Check if we're in a git repo
2130
+ const projectDir = process.cwd();
2131
+ try {
2132
+ execSync('git rev-parse --git-dir', { stdio: 'ignore', cwd: projectDir });
2133
+ } catch {
2134
+ log('❌ Not a git repository', 'red');
2135
+ return;
2136
+ }
2137
+
2138
+ log('', 'gray');
2139
+ log('📝 Generating Changelog', 'blue');
2140
+ log('=====================================', 'gray');
2141
+ log('', 'gray');
2142
+
2143
+ // Get version range
2144
+ let fromRef = options.from;
2145
+ let toRef = options.to || 'HEAD';
2146
+
2147
+ if (!fromRef) {
2148
+ // Try to get the last tag
2149
+ try {
2150
+ const lastTag = execSync('git describe --tags --abbrev=0', {
2151
+ cwd: projectDir,
2152
+ stdio: 'pipe',
2153
+ encoding: 'utf-8'
2154
+ }).trim();
2155
+ fromRef = lastTag;
2156
+ log(`From: ${lastTag} (last tag)`, 'gray');
2157
+ } catch {
2158
+ // No tags found, use first commit
2159
+ try {
2160
+ const firstCommit = execSync('git rev-list --max-parents=0 HEAD', {
2161
+ cwd: projectDir,
2162
+ stdio: 'pipe',
2163
+ encoding: 'utf-8'
2164
+ }).trim();
2165
+ fromRef = firstCommit;
2166
+ log(`From: ${firstCommit.substring(0, 8)} (first commit)`, 'gray');
2167
+ } catch {
2168
+ fromRef = 'HEAD~10'; // Fallback to last 10 commits
2169
+ log(`From: HEAD~10 (default)`, 'gray');
2170
+ }
2171
+ }
2172
+ } else {
2173
+ log(`From: ${fromRef}`, 'gray');
2174
+ }
2175
+
2176
+ log(`To: ${toRef}`, 'gray');
2177
+ log('', 'gray');
2178
+
2179
+ // Get commit log
2180
+ const range = `${fromRef}..${toRef}`;
2181
+ let commits = [];
2182
+ try {
2183
+ const logOutput = execSync(
2184
+ `git log ${range} --pretty=format:"%H%x1F%s%x1F%an%x1F%ad" --date=short`,
2185
+ { cwd: projectDir, stdio: 'pipe', encoding: 'utf-8' }
2186
+ );
2187
+ commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
2188
+ const [hash, subject, author, date] = line.split('\x1F');
2189
+ return { hash, subject, author, date };
2190
+ });
2191
+ } catch (e) {
2192
+ // Empty range or invalid refs
2193
+ commits = [];
2194
+ }
2195
+
2196
+ if (commits.length === 0) {
2197
+ log('⚠️ No commits found in range', 'yellow');
2198
+ log('', 'gray');
2199
+ return;
2200
+ }
2201
+
2202
+ log(`Found ${commits.length} commit(s)`, 'gray');
2203
+ log('', 'gray');
2204
+
2205
+ // Parse conventional commits
2206
+ const categories = {
2207
+ feat: { name: 'Features', icon: '✨', commits: [] },
2208
+ fix: { name: 'Bug Fixes', icon: '🐛', commits: [] },
2209
+ docs: { name: 'Documentation', icon: '📝', commits: [] },
2210
+ style: { name: 'Styles', icon: '💄', commits: [] },
2211
+ refactor: { name: 'Refactor', icon: '♻️', commits: [] },
2212
+ perf: { name: 'Performance', icon: '⚡', commits: [] },
2213
+ test: { name: 'Tests', icon: '🧪', commits: [] },
2214
+ build: { name: 'Build', icon: '📦', commits: [] },
2215
+ ci: { name: 'CI', icon: '🤖', commits: [] },
2216
+ chore: { name: 'Chores', icon: '🧹', commits: [] },
2217
+ other: { name: 'Other', icon: '📌', commits: [] }
2218
+ };
2219
+
2220
+ const conventionalCommitRegex = /^(\w+)(?:\(([^)]+)\))?:?\s*(.+)$/;
2221
+
2222
+ commits.forEach(commit => {
2223
+ const match = commit.subject.match(conventionalCommitRegex);
2224
+ if (match) {
2225
+ const [, type, scope, description] = match;
2226
+ const category = categories[type] || categories.other;
2227
+ category.commits.push({
2228
+ hash: commit.hash.substring(0, 8),
2229
+ description: description.trim(),
2230
+ scope: scope || null,
2231
+ author: commit.author,
2232
+ date: commit.date
2233
+ });
2234
+ } else {
2235
+ categories.other.commits.push({
2236
+ hash: commit.hash.substring(0, 8),
2237
+ description: commit.subject,
2238
+ scope: null,
2239
+ author: commit.author,
2240
+ date: commit.date
2241
+ });
2242
+ }
2243
+ });
2244
+
2245
+ if (options.json) {
2246
+ // JSON output
2247
+ const jsonOutput = {};
2248
+ for (const [key, category] of Object.entries(categories)) {
2249
+ if (category.commits.length > 0) {
2250
+ jsonOutput[key] = {
2251
+ name: category.name,
2252
+ icon: category.icon,
2253
+ commits: category.commits
2254
+ };
2255
+ }
2256
+ }
2257
+ console.log(JSON.stringify(jsonOutput, null, 2));
2258
+ return;
2259
+ }
2260
+
2261
+ // Markdown output
2262
+ const today = new Date().toISOString().split('T')[0];
2263
+
2264
+ // Check if CHANGELOG.md exists and append
2265
+ const changelogFile = path.join(projectDir, 'CHANGELOG.md');
2266
+
2267
+ let existingContent = '';
2268
+ if (fs.existsSync(changelogFile)) {
2269
+ existingContent = fs.readFileSync(changelogFile, 'utf-8');
2270
+ }
2271
+
2272
+ // Generate new entry
2273
+ let newEntry = `## [Unreleased] (${today})\n\n`;
2274
+
2275
+ for (const [key, category] of Object.entries(categories)) {
2276
+ if (category.commits.length > 0) {
2277
+ newEntry += `### ${category.icon} ${category.name}\n\n`;
2278
+ category.commits.forEach(commit => {
2279
+ const scopeStr = commit.scope ? `**${commit.scope}**: ` : '';
2280
+ newEntry += `- ${scopeStr}${commit.description} (${commit.hash})\n`;
2281
+ });
2282
+ newEntry += '\n';
2283
+ }
2284
+ }
2285
+
2286
+ // Write or append
2287
+ if (existingContent) {
2288
+ // Check if unreleased section exists
2289
+ const unreleasedRegex = /## \[Unreleased\].*?\n\n([\s\S]*?)(?=## \[|$)/;
2290
+ const match = existingContent.match(unreleasedRegex);
2291
+
2292
+ if (match) {
2293
+ // Replace existing unreleased section
2294
+ const updatedContent = existingContent.replace(
2295
+ unreleasedRegex,
2296
+ newEntry.trimEnd() + '\n\n'
2297
+ );
2298
+ fs.writeFileSync(changelogFile, updatedContent);
2299
+ log('✅ Updated [Unreleased] section in CHANGELOG.md', 'green');
2300
+ } else {
2301
+ // Prepend to existing content
2302
+ const newContent = newEntry + existingContent;
2303
+ fs.writeFileSync(changelogFile, newContent);
2304
+ log('✅ Added to CHANGELOG.md', 'green');
2305
+ }
2306
+ } else {
2307
+ // Create new CHANGELOG.md
2308
+ const header = `# Changelog
2309
+
2310
+ All notable changes to this project will be documented in this file.
2311
+
2312
+ `;
2313
+ fs.writeFileSync(changelogFile, header + newEntry);
2314
+ log('✅ Created CHANGELOG.md', 'green');
2315
+ }
2316
+
2317
+ log('', 'gray');
2318
+ log('📄 Preview:', 'gray');
2319
+ log('', 'gray');
2320
+ console.log(newEntry);
2321
+
2322
+ log('=====================================', 'gray');
2323
+ log('', 'gray');
706
2324
  }
707
2325
  };
708
2326
 
@@ -754,10 +2372,10 @@ sumulige-claude skill:list
754
2372
  * @param {string} cmd - Command name
755
2373
  * @param {Array} args - Command arguments
756
2374
  */
757
- function runCommand(cmd, args) {
2375
+ async function runCommand(cmd, args) {
758
2376
  const command = commands[cmd];
759
2377
  if (command) {
760
- command(...args);
2378
+ await command(...args);
761
2379
  }
762
2380
  }
763
2381