sumulige-claude 1.0.11 → 1.1.1
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/.claude/commands/todos.md +41 -6
- package/.claude/hooks/pre-commit.cjs +86 -0
- package/.claude/hooks/pre-push.cjs +103 -0
- package/.claude/hooks/session-restore.cjs +102 -0
- package/.claude/hooks/session-save.cjs +164 -0
- package/.claude/hooks/todo-manager.cjs +262 -141
- package/.claude/quality-gate.json +61 -0
- package/.claude/settings.local.json +12 -1
- package/.claude/skills/api-tester/SKILL.md +52 -23
- package/.claude/skills/test-workflow/SKILL.md +191 -0
- package/.claude/templates/tasks/develop.md +69 -0
- package/.claude/templates/tasks/research.md +64 -0
- package/.claude/templates/tasks/test.md +96 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.versionrc +25 -0
- package/AGENTS.md +7 -1
- package/CHANGELOG.md +83 -4
- package/PROJECT_STRUCTURE.md +40 -3
- package/Q&A.md +184 -0
- package/README.md +52 -2
- package/cli.js +102 -5
- package/config/official-skills.json +183 -0
- package/config/quality-gate.json +61 -0
- package/development/todos/.state.json +4 -0
- package/development/todos/INDEX.md +64 -38
- package/docs/RELEASE.md +93 -0
- package/lib/commands.js +1865 -39
- package/lib/config-manager.js +441 -0
- package/lib/config-schema.js +408 -0
- package/lib/config-validator.js +330 -0
- package/lib/config.js +52 -1
- package/lib/errors.js +305 -0
- package/lib/quality-gate.js +431 -0
- package/lib/quality-rules.js +373 -0
- package/lib/utils.js +102 -14
- package/lib/version-check.js +169 -0
- package/package.json +11 -2
- package/template/.claude/hooks/project-kickoff.cjs +190 -1
- package/template/.claude/hooks/session-restore.cjs +102 -0
- package/template/.claude/hooks/session-save.cjs +164 -0
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('
|
|
59
|
-
console.log('
|
|
60
|
-
console.log('
|
|
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: (
|
|
486
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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',
|
|
538
|
-
{ src: 'commands',
|
|
539
|
-
{ src: 'skills',
|
|
540
|
-
{ src: 'templates',
|
|
541
|
-
{ src: 'thinking-routes',
|
|
542
|
-
{ src: 'rag',
|
|
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,
|
|
870
|
+
dirsToCopy.forEach(({ src, name }) => {
|
|
546
871
|
const srcPath = path.join(claudeTemplateDir, src);
|
|
547
872
|
if (fs.existsSync(srcPath)) {
|
|
548
|
-
const
|
|
549
|
-
|
|
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
|
|
557
|
-
|
|
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
|
|
564
|
-
|
|
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
|
-
|
|
573
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
628
|
-
|
|
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,1466 @@ 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');
|
|
2324
|
+
},
|
|
2325
|
+
|
|
2326
|
+
// ==========================================================================
|
|
2327
|
+
// Quality Gate Commands
|
|
2328
|
+
// ==========================================================================
|
|
2329
|
+
|
|
2330
|
+
'qg:check': async (severity = 'warn') => {
|
|
2331
|
+
const { QualityGate } = require('./quality-gate');
|
|
2332
|
+
const gate = new QualityGate({ projectDir: process.cwd() });
|
|
2333
|
+
const result = await gate.check({ severity });
|
|
2334
|
+
process.exit(result.passed ? 0 : 1);
|
|
2335
|
+
},
|
|
2336
|
+
|
|
2337
|
+
'qg:rules': () => {
|
|
2338
|
+
const registry = require('./quality-rules').registry;
|
|
2339
|
+
const rules = registry.getAll();
|
|
2340
|
+
|
|
2341
|
+
console.log('📋 Available Quality Rules');
|
|
2342
|
+
console.log('');
|
|
2343
|
+
console.log('Rules are checked when running quality gate:');
|
|
2344
|
+
console.log('');
|
|
2345
|
+
|
|
2346
|
+
const byCategory = {};
|
|
2347
|
+
for (const rule of rules) {
|
|
2348
|
+
if (!byCategory[rule.category]) byCategory[rule.category] = [];
|
|
2349
|
+
byCategory[rule.category].push(rule);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
for (const [category, rules] of Object.entries(byCategory)) {
|
|
2353
|
+
console.log(`${category.toUpperCase()}:`);
|
|
2354
|
+
for (const rule of rules) {
|
|
2355
|
+
const status = rule.enabled ? '✅' : '⊝';
|
|
2356
|
+
const sev = { info: 'I', warn: 'W', error: 'E', critical: 'X' }[rule.severity];
|
|
2357
|
+
console.log(` ${status} ${rule.id} [${sev}] - ${rule.name}`);
|
|
2358
|
+
}
|
|
2359
|
+
console.log('');
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
|
|
2363
|
+
'qg:init': () => {
|
|
2364
|
+
const projectDir = process.cwd();
|
|
2365
|
+
const configDir = path.join(projectDir, '.claude');
|
|
2366
|
+
const targetPath = path.join(configDir, 'quality-gate.json');
|
|
2367
|
+
const sourcePath = path.join(__dirname, '../config/quality-gate.json');
|
|
2368
|
+
|
|
2369
|
+
if (fs.existsSync(targetPath)) {
|
|
2370
|
+
console.log('⚠️ quality-gate.json already exists');
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
ensureDir(configDir);
|
|
2375
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
2376
|
+
console.log('✅ Created .claude/quality-gate.json');
|
|
2377
|
+
console.log('');
|
|
2378
|
+
console.log('To enable Git hooks:');
|
|
2379
|
+
console.log(' ln -s .claude/hooks/pre-commit.cjs .git/hooks/pre-commit');
|
|
2380
|
+
console.log(' ln -s .claude/hooks/pre-push.cjs .git/hooks/pre-push');
|
|
2381
|
+
},
|
|
2382
|
+
|
|
2383
|
+
// ==========================================================================
|
|
2384
|
+
// Config Commands
|
|
2385
|
+
// ==========================================================================
|
|
2386
|
+
|
|
2387
|
+
'config:validate': () => {
|
|
2388
|
+
const { ConfigValidator } = require('./config-validator');
|
|
2389
|
+
const validator = new ConfigValidator();
|
|
2390
|
+
|
|
2391
|
+
console.log('🔍 Validating configuration...');
|
|
2392
|
+
console.log('');
|
|
2393
|
+
|
|
2394
|
+
let hasErrors = false;
|
|
2395
|
+
let hasWarnings = false;
|
|
2396
|
+
|
|
2397
|
+
// Check global config
|
|
2398
|
+
const globalConfigPath = path.join(process.env.HOME, '.claude', 'config.json');
|
|
2399
|
+
console.log(`Global: ${globalConfigPath}`);
|
|
2400
|
+
const globalResult = validator.validateFile(globalConfigPath);
|
|
2401
|
+
|
|
2402
|
+
if (globalResult.valid) {
|
|
2403
|
+
console.log(' ✅ Valid');
|
|
2404
|
+
} else {
|
|
2405
|
+
if (globalResult.errors.length > 0) {
|
|
2406
|
+
hasErrors = true;
|
|
2407
|
+
console.log(` ❌ ${globalResult.errors.length} error(s)`);
|
|
2408
|
+
globalResult.errors.forEach(e => {
|
|
2409
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
if (globalResult.warnings.length > 0) {
|
|
2413
|
+
hasWarnings = true;
|
|
2414
|
+
console.log(` ⚠️ ${globalResult.warnings.length} warning(s)`);
|
|
2415
|
+
globalResult.warnings.forEach(e => {
|
|
2416
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
if (globalResult.errors.length === 0 && globalResult.warnings.length === 0) {
|
|
2420
|
+
console.log(' ❌ Validation failed');
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
console.log('');
|
|
2424
|
+
|
|
2425
|
+
// Check project config if in project
|
|
2426
|
+
const projectDir = process.cwd();
|
|
2427
|
+
const projectConfigPath = path.join(projectDir, '.claude', 'settings.json');
|
|
2428
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
2429
|
+
console.log(`Project: ${projectConfigPath}`);
|
|
2430
|
+
const projectResult = validator.validateFile(projectConfigPath, 'settings');
|
|
2431
|
+
if (projectResult.valid) {
|
|
2432
|
+
console.log(' ✅ Valid');
|
|
2433
|
+
} else {
|
|
2434
|
+
if (projectResult.errors.length > 0) {
|
|
2435
|
+
hasErrors = true;
|
|
2436
|
+
console.log(` ❌ ${projectResult.errors.length} error(s)`);
|
|
2437
|
+
projectResult.errors.forEach(e => {
|
|
2438
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
if (projectResult.warnings.length > 0) {
|
|
2442
|
+
hasWarnings = true;
|
|
2443
|
+
console.log(` ⚠️ ${projectResult.warnings.length} warning(s)`);
|
|
2444
|
+
projectResult.warnings.forEach(e => {
|
|
2445
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
if (projectResult.errors.length === 0 && projectResult.warnings.length === 0) {
|
|
2449
|
+
console.log(' ❌ Validation failed');
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
console.log('');
|
|
2454
|
+
|
|
2455
|
+
if (hasErrors) {
|
|
2456
|
+
console.log('❌ Configuration validation failed');
|
|
2457
|
+
process.exit(1);
|
|
2458
|
+
} else if (hasWarnings) {
|
|
2459
|
+
console.log('⚠️ Configuration has warnings (but is valid)');
|
|
2460
|
+
} else {
|
|
2461
|
+
console.log('✅ All configurations are valid');
|
|
2462
|
+
}
|
|
2463
|
+
},
|
|
2464
|
+
|
|
2465
|
+
'config:backup': () => {
|
|
2466
|
+
const { ConfigManager } = require('./config-manager');
|
|
2467
|
+
const manager = new ConfigManager();
|
|
2468
|
+
const backupPath = manager._createBackup();
|
|
2469
|
+
console.log('✅ Config backed up to:', backupPath);
|
|
2470
|
+
},
|
|
2471
|
+
|
|
2472
|
+
'config:rollback': (version) => {
|
|
2473
|
+
const { ConfigManager } = require('./config-manager');
|
|
2474
|
+
const manager = new ConfigManager();
|
|
2475
|
+
|
|
2476
|
+
const backups = manager.listBackups();
|
|
2477
|
+
if (backups.length === 0) {
|
|
2478
|
+
console.log('❌ No backups available');
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (!version) {
|
|
2483
|
+
console.log('Available backups:');
|
|
2484
|
+
backups.forEach((b, i) => {
|
|
2485
|
+
console.log(` ${i + 1}. ${b.version} (${new Date(b.timestamp).toLocaleString()})`);
|
|
2486
|
+
});
|
|
2487
|
+
console.log('');
|
|
2488
|
+
console.log('Usage: smc config:rollback <version>');
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
const result = manager.rollback(version);
|
|
2494
|
+
console.log('✅ Rolled back to:', result.restoredFrom);
|
|
2495
|
+
} catch (e) {
|
|
2496
|
+
console.log('❌', e.message);
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
|
|
2500
|
+
'config:diff': (file1, file2) => {
|
|
2501
|
+
const { ConfigManager } = require('./config-manager');
|
|
2502
|
+
const manager = new ConfigManager();
|
|
2503
|
+
|
|
2504
|
+
if (!file1) {
|
|
2505
|
+
const backups = manager.listBackups();
|
|
2506
|
+
if (backups.length === 0) {
|
|
2507
|
+
console.log('❌ No backups available');
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
file1 = path.join(manager.backupDir, backups[0].file);
|
|
2511
|
+
file2 = null; // Current config
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const changes = manager.diff(file1, file2);
|
|
2515
|
+
if (changes.length === 0) {
|
|
2516
|
+
console.log('✅ No differences found');
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
console.log('📊 Config Diff:');
|
|
2521
|
+
console.log('');
|
|
2522
|
+
for (const change of changes) {
|
|
2523
|
+
const icon = { added: '+', removed: '-', changed: '~' }[change.type];
|
|
2524
|
+
console.log(` ${icon} ${change.path}`);
|
|
2525
|
+
if (change.type !== 'removed') {
|
|
2526
|
+
console.log(` from: ${JSON.stringify(change.from)}`);
|
|
2527
|
+
}
|
|
2528
|
+
if (change.type !== 'added') {
|
|
2529
|
+
console.log(` to: ${JSON.stringify(change.to)}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
706
2532
|
}
|
|
707
2533
|
};
|
|
708
2534
|
|
|
@@ -754,10 +2580,10 @@ sumulige-claude skill:list
|
|
|
754
2580
|
* @param {string} cmd - Command name
|
|
755
2581
|
* @param {Array} args - Command arguments
|
|
756
2582
|
*/
|
|
757
|
-
function runCommand(cmd, args) {
|
|
2583
|
+
async function runCommand(cmd, args) {
|
|
758
2584
|
const command = commands[cmd];
|
|
759
2585
|
if (command) {
|
|
760
|
-
command(...args);
|
|
2586
|
+
await command(...args);
|
|
761
2587
|
}
|
|
762
2588
|
}
|
|
763
2589
|
|