claude-cli-advanced-starter-pack 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +527 -345
- package/package.json +1 -1
- package/src/commands/init.js +258 -37
- package/src/commands/test-setup.js +7 -6
- package/src/data/releases.json +72 -5
- package/src/testing/config.js +213 -84
- package/src/utils/smart-merge.js +457 -0
- package/src/utils/version-check.js +213 -0
- package/templates/commands/create-task-list.template.md +332 -17
- package/templates/commands/update-smart.template.md +111 -0
- package/templates/hooks/ccasp-update-check.template.js +74 -0
- package/templates/hooks/usage-tracking.template.js +222 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Merge Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides asset comparison, diff generation, and merge exploration
|
|
5
|
+
* for the CCASP update system. When users have customized assets
|
|
6
|
+
* (commands, skills, agents, hooks), this module helps them understand
|
|
7
|
+
* what changes an update would bring and offers intelligent merge options.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
11
|
+
import { join, basename, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import {
|
|
15
|
+
loadUsageTracking,
|
|
16
|
+
getCustomizedUsedAssets,
|
|
17
|
+
isAssetCustomized,
|
|
18
|
+
} from './version-check.js';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Asset type to file path mapping
|
|
25
|
+
*/
|
|
26
|
+
const ASSET_PATHS = {
|
|
27
|
+
commands: {
|
|
28
|
+
local: (projectDir, name) => join(projectDir, '.claude', 'commands', `${name}.md`),
|
|
29
|
+
template: (name) => join(__dirname, '..', '..', 'templates', 'commands', `${name}.template.md`),
|
|
30
|
+
extension: '.md',
|
|
31
|
+
},
|
|
32
|
+
skills: {
|
|
33
|
+
local: (projectDir, name) => join(projectDir, '.claude', 'skills', name, 'SKILL.md'),
|
|
34
|
+
template: (name) => join(__dirname, '..', '..', 'templates', 'skills', name, 'SKILL.template.md'),
|
|
35
|
+
extension: '.md',
|
|
36
|
+
},
|
|
37
|
+
agents: {
|
|
38
|
+
local: (projectDir, name) => join(projectDir, '.claude', 'agents', `${name}.md`),
|
|
39
|
+
template: (name) => join(__dirname, '..', '..', 'templates', 'agents', `${name}.template.md`),
|
|
40
|
+
extension: '.md',
|
|
41
|
+
},
|
|
42
|
+
hooks: {
|
|
43
|
+
local: (projectDir, name) => join(projectDir, '.claude', 'hooks', `${name}.js`),
|
|
44
|
+
template: (name) => join(__dirname, '..', '..', 'templates', 'hooks', `${name}.template.js`),
|
|
45
|
+
extension: '.js',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the local (user's) version of an asset
|
|
51
|
+
*/
|
|
52
|
+
export function getLocalAsset(assetType, assetName, projectDir = process.cwd()) {
|
|
53
|
+
const pathConfig = ASSET_PATHS[assetType];
|
|
54
|
+
if (!pathConfig) return null;
|
|
55
|
+
|
|
56
|
+
const localPath = pathConfig.local(projectDir, assetName);
|
|
57
|
+
|
|
58
|
+
if (!existsSync(localPath)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return {
|
|
64
|
+
path: localPath,
|
|
65
|
+
content: readFileSync(localPath, 'utf8'),
|
|
66
|
+
stats: statSync(localPath),
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the template (package) version of an asset
|
|
75
|
+
*/
|
|
76
|
+
export function getTemplateAsset(assetType, assetName) {
|
|
77
|
+
const pathConfig = ASSET_PATHS[assetType];
|
|
78
|
+
if (!pathConfig) return null;
|
|
79
|
+
|
|
80
|
+
const templatePath = pathConfig.template(assetName);
|
|
81
|
+
|
|
82
|
+
if (!existsSync(templatePath)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return {
|
|
88
|
+
path: templatePath,
|
|
89
|
+
content: readFileSync(templatePath, 'utf8'),
|
|
90
|
+
stats: statSync(templatePath),
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Compare two versions of an asset and generate a diff summary
|
|
99
|
+
* Returns an object with change analysis
|
|
100
|
+
*/
|
|
101
|
+
export function compareAssetVersions(localContent, templateContent) {
|
|
102
|
+
if (!localContent || !templateContent) {
|
|
103
|
+
return {
|
|
104
|
+
identical: false,
|
|
105
|
+
hasChanges: true,
|
|
106
|
+
error: 'Missing content for comparison',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Normalize line endings
|
|
111
|
+
const normalizedLocal = localContent.replace(/\r\n/g, '\n').trim();
|
|
112
|
+
const normalizedTemplate = templateContent.replace(/\r\n/g, '\n').trim();
|
|
113
|
+
|
|
114
|
+
if (normalizedLocal === normalizedTemplate) {
|
|
115
|
+
return {
|
|
116
|
+
identical: true,
|
|
117
|
+
hasChanges: false,
|
|
118
|
+
summary: 'No differences found',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Split into lines for analysis
|
|
123
|
+
const localLines = normalizedLocal.split('\n');
|
|
124
|
+
const templateLines = normalizedTemplate.split('\n');
|
|
125
|
+
|
|
126
|
+
// Simple line-by-line diff analysis
|
|
127
|
+
const changes = {
|
|
128
|
+
added: [],
|
|
129
|
+
removed: [],
|
|
130
|
+
modified: [],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Create line maps for comparison
|
|
134
|
+
const localLineSet = new Set(localLines);
|
|
135
|
+
const templateLineSet = new Set(templateLines);
|
|
136
|
+
|
|
137
|
+
// Lines in template but not in local (would be added by update)
|
|
138
|
+
for (const line of templateLines) {
|
|
139
|
+
if (!localLineSet.has(line) && line.trim()) {
|
|
140
|
+
changes.added.push(line);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Lines in local but not in template (user customizations)
|
|
145
|
+
for (const line of localLines) {
|
|
146
|
+
if (!templateLineSet.has(line) && line.trim()) {
|
|
147
|
+
changes.removed.push(line);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Analyze change significance
|
|
152
|
+
const significance = analyzeChangeSignificance(changes, localContent, templateContent);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
identical: false,
|
|
156
|
+
hasChanges: true,
|
|
157
|
+
changes,
|
|
158
|
+
significance,
|
|
159
|
+
stats: {
|
|
160
|
+
localLines: localLines.length,
|
|
161
|
+
templateLines: templateLines.length,
|
|
162
|
+
addedLines: changes.added.length,
|
|
163
|
+
removedLines: changes.removed.length,
|
|
164
|
+
},
|
|
165
|
+
summary: generateChangeSummary(changes, significance),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Analyze the significance of changes
|
|
171
|
+
*/
|
|
172
|
+
function analyzeChangeSignificance(changes, localContent, templateContent) {
|
|
173
|
+
const significance = {
|
|
174
|
+
level: 'low', // low, medium, high
|
|
175
|
+
reasons: [],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Check for structural changes (high significance)
|
|
179
|
+
const structuralPatterns = [
|
|
180
|
+
/^#{1,3}\s/, // Markdown headers
|
|
181
|
+
/^---$/, // YAML frontmatter
|
|
182
|
+
/^export\s+(default\s+)?function/, // Function exports
|
|
183
|
+
/^module\.exports/, // CommonJS exports
|
|
184
|
+
/^import\s+/, // Import statements
|
|
185
|
+
/^const\s+\w+\s*=\s*require/, // Require statements
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const line of changes.added.concat(changes.removed)) {
|
|
189
|
+
for (const pattern of structuralPatterns) {
|
|
190
|
+
if (pattern.test(line)) {
|
|
191
|
+
significance.level = 'high';
|
|
192
|
+
significance.reasons.push('Structural changes detected');
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check for configuration changes (medium significance)
|
|
199
|
+
const configPatterns = [
|
|
200
|
+
/^\s*"?\w+"?\s*:\s*/, // JSON/YAML config
|
|
201
|
+
/^[A-Z_]+\s*=/, // Environment variables
|
|
202
|
+
/timeout|limit|threshold|max|min/i, // Configuration values
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
if (significance.level !== 'high') {
|
|
206
|
+
for (const line of changes.added.concat(changes.removed)) {
|
|
207
|
+
for (const pattern of configPatterns) {
|
|
208
|
+
if (pattern.test(line)) {
|
|
209
|
+
significance.level = 'medium';
|
|
210
|
+
significance.reasons.push('Configuration changes detected');
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for comment-only changes (low significance)
|
|
218
|
+
const commentPatterns = [
|
|
219
|
+
/^\/\//, // JS comments
|
|
220
|
+
/^\/\*/, // Block comment start
|
|
221
|
+
/^\*/, // Block comment middle
|
|
222
|
+
/^#(?!#)/, // Shell/Python/Ruby comments (not markdown headers)
|
|
223
|
+
/^<!--/, // HTML comments
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const allChangesAreComments = [...changes.added, ...changes.removed].every((line) =>
|
|
227
|
+
commentPatterns.some((p) => p.test(line.trim()))
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (allChangesAreComments && changes.added.length + changes.removed.length > 0) {
|
|
231
|
+
significance.level = 'low';
|
|
232
|
+
significance.reasons = ['Only comment changes'];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Volume-based significance boost
|
|
236
|
+
const totalChanges = changes.added.length + changes.removed.length;
|
|
237
|
+
const totalLines = Math.max(localContent.split('\n').length, templateContent.split('\n').length);
|
|
238
|
+
const changeRatio = totalChanges / totalLines;
|
|
239
|
+
|
|
240
|
+
if (changeRatio > 0.5 && significance.level !== 'high') {
|
|
241
|
+
significance.level = 'high';
|
|
242
|
+
significance.reasons.push(`Large change volume (${Math.round(changeRatio * 100)}% of file)`);
|
|
243
|
+
} else if (changeRatio > 0.2 && significance.level === 'low') {
|
|
244
|
+
significance.level = 'medium';
|
|
245
|
+
significance.reasons.push(`Moderate change volume (${Math.round(changeRatio * 100)}% of file)`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return significance;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Generate a human-readable summary of changes
|
|
253
|
+
*/
|
|
254
|
+
function generateChangeSummary(changes, significance) {
|
|
255
|
+
const parts = [];
|
|
256
|
+
|
|
257
|
+
if (changes.added.length > 0) {
|
|
258
|
+
parts.push(`${changes.added.length} line(s) would be added`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (changes.removed.length > 0) {
|
|
262
|
+
parts.push(`${changes.removed.length} line(s) would be removed/replaced`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (significance.reasons.length > 0) {
|
|
266
|
+
parts.push(significance.reasons[0]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return parts.join('; ') || 'Minor changes';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Generate a detailed diff for display
|
|
274
|
+
* Uses unified diff format if git is available, otherwise simple comparison
|
|
275
|
+
*/
|
|
276
|
+
export function generateDetailedDiff(localPath, templatePath) {
|
|
277
|
+
try {
|
|
278
|
+
// Try using git diff for nice formatting
|
|
279
|
+
const diff = execSync(
|
|
280
|
+
`git diff --no-index --color=never "${templatePath}" "${localPath}"`,
|
|
281
|
+
{
|
|
282
|
+
encoding: 'utf8',
|
|
283
|
+
timeout: 5000,
|
|
284
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
return diff;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// git diff returns exit code 1 when files differ (which is expected)
|
|
290
|
+
if (error.stdout) {
|
|
291
|
+
return error.stdout;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Fall back to simple comparison
|
|
295
|
+
const local = readFileSync(localPath, 'utf8');
|
|
296
|
+
const template = readFileSync(templatePath, 'utf8');
|
|
297
|
+
|
|
298
|
+
return `--- Template (update)\n+++ Local (current)\n\n` +
|
|
299
|
+
`Template version:\n${template}\n\n` +
|
|
300
|
+
`Local version:\n${local}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all assets that need merge consideration during update
|
|
306
|
+
* Returns assets that are:
|
|
307
|
+
* 1. Used by the user (tracked in usage-tracking.json)
|
|
308
|
+
* 2. Customized (differ from template)
|
|
309
|
+
* 3. Have updates available (template has changed)
|
|
310
|
+
*/
|
|
311
|
+
export function getAssetsNeedingMerge(projectDir = process.cwd()) {
|
|
312
|
+
const customizedAssets = getCustomizedUsedAssets(projectDir);
|
|
313
|
+
const assetsNeedingMerge = {
|
|
314
|
+
commands: [],
|
|
315
|
+
skills: [],
|
|
316
|
+
agents: [],
|
|
317
|
+
hooks: [],
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const [assetType, assets] of Object.entries(customizedAssets)) {
|
|
321
|
+
for (const [assetName, usageData] of Object.entries(assets)) {
|
|
322
|
+
const local = getLocalAsset(assetType, assetName, projectDir);
|
|
323
|
+
const template = getTemplateAsset(assetType, assetName);
|
|
324
|
+
|
|
325
|
+
// Skip if no template exists (user-created asset)
|
|
326
|
+
if (!template) continue;
|
|
327
|
+
|
|
328
|
+
// Skip if no local exists (shouldn't happen, but safety check)
|
|
329
|
+
if (!local) continue;
|
|
330
|
+
|
|
331
|
+
const comparison = compareAssetVersions(local.content, template.content);
|
|
332
|
+
|
|
333
|
+
// Only include if there are actual differences
|
|
334
|
+
if (comparison.hasChanges && !comparison.identical) {
|
|
335
|
+
assetsNeedingMerge[assetType].push({
|
|
336
|
+
name: assetName,
|
|
337
|
+
usageData,
|
|
338
|
+
comparison,
|
|
339
|
+
localPath: local.path,
|
|
340
|
+
templatePath: template.path,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return assetsNeedingMerge;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Generate a merge exploration prompt for Claude
|
|
351
|
+
* This creates a structured prompt that helps Claude explain the merge options
|
|
352
|
+
*/
|
|
353
|
+
export function generateMergeExplanation(assetType, assetName, comparison, localContent, templateContent) {
|
|
354
|
+
const assetLabel = assetType.slice(0, -1); // Remove 's' (commands -> command)
|
|
355
|
+
|
|
356
|
+
let explanation = `## Merge Analysis: ${assetName} (${assetLabel})\n\n`;
|
|
357
|
+
|
|
358
|
+
// Significance indicator
|
|
359
|
+
const significanceEmoji = {
|
|
360
|
+
low: '🟢',
|
|
361
|
+
medium: '🟡',
|
|
362
|
+
high: '🔴',
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
explanation += `**Change Significance:** ${significanceEmoji[comparison.significance.level]} ${comparison.significance.level.toUpperCase()}\n\n`;
|
|
366
|
+
|
|
367
|
+
// Summary
|
|
368
|
+
explanation += `### Summary\n`;
|
|
369
|
+
explanation += `${comparison.summary}\n\n`;
|
|
370
|
+
|
|
371
|
+
// Stats
|
|
372
|
+
explanation += `### Change Statistics\n`;
|
|
373
|
+
explanation += `- Your version: ${comparison.stats.localLines} lines\n`;
|
|
374
|
+
explanation += `- Update version: ${comparison.stats.templateLines} lines\n`;
|
|
375
|
+
explanation += `- Lines that would be added: ${comparison.stats.addedLines}\n`;
|
|
376
|
+
explanation += `- Lines that would be removed/changed: ${comparison.stats.removedLines}\n\n`;
|
|
377
|
+
|
|
378
|
+
// Key changes preview
|
|
379
|
+
if (comparison.changes.added.length > 0) {
|
|
380
|
+
explanation += `### New in Update (would be added)\n`;
|
|
381
|
+
explanation += '```\n';
|
|
382
|
+
explanation += comparison.changes.added.slice(0, 10).join('\n');
|
|
383
|
+
if (comparison.changes.added.length > 10) {
|
|
384
|
+
explanation += `\n... and ${comparison.changes.added.length - 10} more lines`;
|
|
385
|
+
}
|
|
386
|
+
explanation += '\n```\n\n';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (comparison.changes.removed.length > 0) {
|
|
390
|
+
explanation += `### Your Customizations (would be replaced)\n`;
|
|
391
|
+
explanation += '```\n';
|
|
392
|
+
explanation += comparison.changes.removed.slice(0, 10).join('\n');
|
|
393
|
+
if (comparison.changes.removed.length > 10) {
|
|
394
|
+
explanation += `\n... and ${comparison.changes.removed.length - 10} more lines`;
|
|
395
|
+
}
|
|
396
|
+
explanation += '\n```\n\n';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Recommendations
|
|
400
|
+
explanation += `### Recommendation\n`;
|
|
401
|
+
|
|
402
|
+
if (comparison.significance.level === 'low') {
|
|
403
|
+
explanation += `This update has minor changes. You can likely **replace** safely, `;
|
|
404
|
+
explanation += `but review the diff if your customizations are important.\n`;
|
|
405
|
+
} else if (comparison.significance.level === 'medium') {
|
|
406
|
+
explanation += `This update has moderate changes. Consider **exploring the merge** to `;
|
|
407
|
+
explanation += `understand what would change and preserve your customizations.\n`;
|
|
408
|
+
} else {
|
|
409
|
+
explanation += `This update has significant structural changes. **Strongly recommend** `;
|
|
410
|
+
explanation += `exploring the merge carefully before deciding. Your customizations may `;
|
|
411
|
+
explanation += `be incompatible with the new version.\n`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return explanation;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Generate options display for merge decision
|
|
419
|
+
*/
|
|
420
|
+
export function formatMergeOptions(assetName, useCount) {
|
|
421
|
+
return `
|
|
422
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
423
|
+
│ ${assetName.padEnd(55)} │
|
|
424
|
+
│ Used ${useCount} time(s) • Customized │
|
|
425
|
+
├─────────────────────────────────────────────────────────────┤
|
|
426
|
+
│ [E] Explore merge - Claude analyzes both versions │
|
|
427
|
+
│ [R] Replace - Use new version (lose customizations) │
|
|
428
|
+
│ [S] Skip - Keep your version (miss update) │
|
|
429
|
+
│ [D] Show diff - View raw differences │
|
|
430
|
+
└─────────────────────────────────────────────────────────────┘
|
|
431
|
+
`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get count of assets needing merge attention
|
|
436
|
+
*/
|
|
437
|
+
export function getMergeAttentionCount(projectDir = process.cwd()) {
|
|
438
|
+
const assets = getAssetsNeedingMerge(projectDir);
|
|
439
|
+
let count = 0;
|
|
440
|
+
|
|
441
|
+
for (const assetList of Object.values(assets)) {
|
|
442
|
+
count += assetList.length;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return count;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export default {
|
|
449
|
+
getLocalAsset,
|
|
450
|
+
getTemplateAsset,
|
|
451
|
+
compareAssetVersions,
|
|
452
|
+
generateDetailedDiff,
|
|
453
|
+
getAssetsNeedingMerge,
|
|
454
|
+
generateMergeExplanation,
|
|
455
|
+
formatMergeOptions,
|
|
456
|
+
getMergeAttentionCount,
|
|
457
|
+
};
|
|
@@ -87,6 +87,210 @@ export function saveUpdateState(state, projectDir = process.cwd()) {
|
|
|
87
87
|
writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// USAGE TRACKING SYSTEM
|
|
92
|
+
// Tracks which commands, skills, agents, and hooks the user has used
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the usage tracking file path
|
|
97
|
+
*/
|
|
98
|
+
function getUsageTrackingPath(projectDir = process.cwd()) {
|
|
99
|
+
return join(projectDir, '.claude', 'config', 'usage-tracking.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Default usage tracking structure
|
|
104
|
+
*/
|
|
105
|
+
function getDefaultUsageTracking() {
|
|
106
|
+
return {
|
|
107
|
+
version: '1.0.0',
|
|
108
|
+
assets: {
|
|
109
|
+
commands: {},
|
|
110
|
+
skills: {},
|
|
111
|
+
agents: {},
|
|
112
|
+
hooks: {},
|
|
113
|
+
},
|
|
114
|
+
_lastModified: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load the usage tracking data
|
|
120
|
+
*/
|
|
121
|
+
export function loadUsageTracking(projectDir = process.cwd()) {
|
|
122
|
+
const trackingPath = getUsageTrackingPath(projectDir);
|
|
123
|
+
|
|
124
|
+
if (!existsSync(trackingPath)) {
|
|
125
|
+
return getDefaultUsageTracking();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse(readFileSync(trackingPath, 'utf8'));
|
|
130
|
+
// Ensure all required keys exist
|
|
131
|
+
return {
|
|
132
|
+
...getDefaultUsageTracking(),
|
|
133
|
+
...data,
|
|
134
|
+
assets: {
|
|
135
|
+
commands: {},
|
|
136
|
+
skills: {},
|
|
137
|
+
agents: {},
|
|
138
|
+
hooks: {},
|
|
139
|
+
...data.assets,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return getDefaultUsageTracking();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save the usage tracking data
|
|
149
|
+
*/
|
|
150
|
+
export function saveUsageTracking(tracking, projectDir = process.cwd()) {
|
|
151
|
+
const trackingPath = getUsageTrackingPath(projectDir);
|
|
152
|
+
const configDir = dirname(trackingPath);
|
|
153
|
+
|
|
154
|
+
if (!existsSync(configDir)) {
|
|
155
|
+
mkdirSync(configDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
tracking._lastModified = new Date().toISOString();
|
|
159
|
+
writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Track usage of an asset (command, skill, agent, or hook)
|
|
164
|
+
* @param {string} assetType - 'commands' | 'skills' | 'agents' | 'hooks'
|
|
165
|
+
* @param {string} assetName - Name of the asset (e.g., 'create-task-list')
|
|
166
|
+
* @param {object} options - Additional options
|
|
167
|
+
* @param {boolean} options.customized - Whether the asset has been customized by user
|
|
168
|
+
* @param {string} projectDir - Project directory
|
|
169
|
+
*/
|
|
170
|
+
export function trackAssetUsage(assetType, assetName, options = {}, projectDir = process.cwd()) {
|
|
171
|
+
const tracking = loadUsageTracking(projectDir);
|
|
172
|
+
|
|
173
|
+
if (!tracking.assets[assetType]) {
|
|
174
|
+
tracking.assets[assetType] = {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const existing = tracking.assets[assetType][assetName] || {
|
|
178
|
+
firstUsed: new Date().toISOString(),
|
|
179
|
+
useCount: 0,
|
|
180
|
+
customized: false,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
tracking.assets[assetType][assetName] = {
|
|
184
|
+
...existing,
|
|
185
|
+
lastUsed: new Date().toISOString(),
|
|
186
|
+
useCount: existing.useCount + 1,
|
|
187
|
+
customized: options.customized !== undefined ? options.customized : existing.customized,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
saveUsageTracking(tracking, projectDir);
|
|
191
|
+
|
|
192
|
+
return tracking.assets[assetType][assetName];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Mark an asset as customized (user has modified it from the template)
|
|
197
|
+
*/
|
|
198
|
+
export function markAssetCustomized(assetType, assetName, projectDir = process.cwd()) {
|
|
199
|
+
const tracking = loadUsageTracking(projectDir);
|
|
200
|
+
|
|
201
|
+
if (!tracking.assets[assetType]) {
|
|
202
|
+
tracking.assets[assetType] = {};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!tracking.assets[assetType][assetName]) {
|
|
206
|
+
tracking.assets[assetType][assetName] = {
|
|
207
|
+
firstUsed: new Date().toISOString(),
|
|
208
|
+
lastUsed: new Date().toISOString(),
|
|
209
|
+
useCount: 0,
|
|
210
|
+
customized: true,
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
tracking.assets[assetType][assetName].customized = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
saveUsageTracking(tracking, projectDir);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all assets that have been used
|
|
221
|
+
* @param {object} options - Filter options
|
|
222
|
+
* @param {boolean} options.customizedOnly - Only return customized assets
|
|
223
|
+
* @param {string[]} options.assetTypes - Filter by asset types
|
|
224
|
+
*/
|
|
225
|
+
export function getUsedAssets(options = {}, projectDir = process.cwd()) {
|
|
226
|
+
const tracking = loadUsageTracking(projectDir);
|
|
227
|
+
const { customizedOnly = false, assetTypes = ['commands', 'skills', 'agents', 'hooks'] } = options;
|
|
228
|
+
|
|
229
|
+
const result = {};
|
|
230
|
+
|
|
231
|
+
for (const type of assetTypes) {
|
|
232
|
+
if (!tracking.assets[type]) continue;
|
|
233
|
+
|
|
234
|
+
result[type] = {};
|
|
235
|
+
|
|
236
|
+
for (const [name, data] of Object.entries(tracking.assets[type])) {
|
|
237
|
+
if (customizedOnly && !data.customized) continue;
|
|
238
|
+
result[type][name] = data;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get assets that are both used AND customized (for smart merge detection)
|
|
247
|
+
*/
|
|
248
|
+
export function getCustomizedUsedAssets(projectDir = process.cwd()) {
|
|
249
|
+
return getUsedAssets({ customizedOnly: true }, projectDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if an asset has been customized
|
|
254
|
+
*/
|
|
255
|
+
export function isAssetCustomized(assetType, assetName, projectDir = process.cwd()) {
|
|
256
|
+
const tracking = loadUsageTracking(projectDir);
|
|
257
|
+
return tracking.assets[assetType]?.[assetName]?.customized === true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get usage statistics summary
|
|
262
|
+
*/
|
|
263
|
+
export function getUsageStats(projectDir = process.cwd()) {
|
|
264
|
+
const tracking = loadUsageTracking(projectDir);
|
|
265
|
+
|
|
266
|
+
const stats = {
|
|
267
|
+
totalAssets: 0,
|
|
268
|
+
totalUsage: 0,
|
|
269
|
+
customizedCount: 0,
|
|
270
|
+
byType: {},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
for (const [type, assets] of Object.entries(tracking.assets)) {
|
|
274
|
+
const typeStats = {
|
|
275
|
+
count: Object.keys(assets).length,
|
|
276
|
+
totalUsage: 0,
|
|
277
|
+
customized: 0,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
for (const data of Object.values(assets)) {
|
|
281
|
+
typeStats.totalUsage += data.useCount || 0;
|
|
282
|
+
if (data.customized) typeStats.customized++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
stats.byType[type] = typeStats;
|
|
286
|
+
stats.totalAssets += typeStats.count;
|
|
287
|
+
stats.totalUsage += typeStats.totalUsage;
|
|
288
|
+
stats.customizedCount += typeStats.customized;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return stats;
|
|
292
|
+
}
|
|
293
|
+
|
|
90
294
|
/**
|
|
91
295
|
* Compare two semantic versions
|
|
92
296
|
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
@@ -509,4 +713,13 @@ export default {
|
|
|
509
713
|
getAvailableFeatures,
|
|
510
714
|
formatUpdateBanner,
|
|
511
715
|
formatUpdateMarkdown,
|
|
716
|
+
// Usage tracking exports
|
|
717
|
+
loadUsageTracking,
|
|
718
|
+
saveUsageTracking,
|
|
719
|
+
trackAssetUsage,
|
|
720
|
+
markAssetCustomized,
|
|
721
|
+
getUsedAssets,
|
|
722
|
+
getCustomizedUsedAssets,
|
|
723
|
+
isAssetCustomized,
|
|
724
|
+
getUsageStats,
|
|
512
725
|
};
|