claude-cli-advanced-starter-pack 1.0.4 → 1.0.7

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.
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Version Check Utility
3
+ *
4
+ * Handles npm version checking, comparison, caching, and update state tracking.
5
+ * Designed for background execution with non-blocking checks.
6
+ */
7
+
8
+ import { execSync } from 'child_process';
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ // Package name for npm registry lookup
17
+ const PACKAGE_NAME = 'claude-cli-advanced-starter-pack';
18
+
19
+ // Cache duration: 1 hour (in milliseconds)
20
+ const CACHE_DURATION = 60 * 60 * 1000;
21
+
22
+ // Update notification duration: 1 day (in milliseconds)
23
+ const UPDATE_NOTIFICATION_DURATION = 24 * 60 * 60 * 1000;
24
+
25
+ /**
26
+ * Get the current installed version from package.json
27
+ */
28
+ export function getCurrentVersion() {
29
+ try {
30
+ const packagePath = join(__dirname, '..', '..', 'package.json');
31
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
32
+ return packageJson.version;
33
+ } catch {
34
+ return '0.0.0';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Get the state file path for the current project
40
+ */
41
+ function getStateFilePath(projectDir = process.cwd()) {
42
+ return join(projectDir, '.claude', 'config', 'ccasp-state.json');
43
+ }
44
+
45
+ /**
46
+ * Load the update state from the project's .claude folder
47
+ */
48
+ export function loadUpdateState(projectDir = process.cwd()) {
49
+ const statePath = getStateFilePath(projectDir);
50
+
51
+ if (!existsSync(statePath)) {
52
+ return {
53
+ lastCheckTimestamp: 0,
54
+ lastCheckResult: null,
55
+ lastSeenVersion: null,
56
+ dismissedVersions: [],
57
+ installedFeatures: [],
58
+ skippedFeatures: [],
59
+ };
60
+ }
61
+
62
+ try {
63
+ return JSON.parse(readFileSync(statePath, 'utf8'));
64
+ } catch {
65
+ return {
66
+ lastCheckTimestamp: 0,
67
+ lastCheckResult: null,
68
+ lastSeenVersion: null,
69
+ dismissedVersions: [],
70
+ installedFeatures: [],
71
+ skippedFeatures: [],
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save the update state to the project's .claude folder
78
+ */
79
+ export function saveUpdateState(state, projectDir = process.cwd()) {
80
+ const statePath = getStateFilePath(projectDir);
81
+ const configDir = dirname(statePath);
82
+
83
+ if (!existsSync(configDir)) {
84
+ mkdirSync(configDir, { recursive: true });
85
+ }
86
+
87
+ writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
88
+ }
89
+
90
+ /**
91
+ * Compare two semantic versions
92
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
93
+ */
94
+ export function compareVersions(v1, v2) {
95
+ const parts1 = v1.split('.').map(Number);
96
+ const parts2 = v2.split('.').map(Number);
97
+
98
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
99
+ const p1 = parts1[i] || 0;
100
+ const p2 = parts2[i] || 0;
101
+
102
+ if (p1 > p2) return 1;
103
+ if (p1 < p2) return -1;
104
+ }
105
+
106
+ return 0;
107
+ }
108
+
109
+ /**
110
+ * Check npm registry for the latest version
111
+ * Returns null if check fails (network error, etc.)
112
+ */
113
+ export async function checkLatestVersion() {
114
+ try {
115
+ // Use npm view command to get latest version
116
+ const result = execSync(`npm view ${PACKAGE_NAME} version`, {
117
+ encoding: 'utf8',
118
+ timeout: 10000, // 10 second timeout
119
+ stdio: ['pipe', 'pipe', 'pipe'],
120
+ });
121
+
122
+ return result.trim();
123
+ } catch {
124
+ // Silently fail - network might be unavailable
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Get detailed package info from npm registry
131
+ */
132
+ export async function getPackageInfo() {
133
+ try {
134
+ const result = execSync(`npm view ${PACKAGE_NAME} --json`, {
135
+ encoding: 'utf8',
136
+ timeout: 15000,
137
+ stdio: ['pipe', 'pipe', 'pipe'],
138
+ });
139
+
140
+ return JSON.parse(result);
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Load release notes from the releases.json file
148
+ */
149
+ export function loadReleaseNotes() {
150
+ try {
151
+ const releasesPath = join(__dirname, '..', 'data', 'releases.json');
152
+ if (existsSync(releasesPath)) {
153
+ return JSON.parse(readFileSync(releasesPath, 'utf8'));
154
+ }
155
+ } catch {
156
+ // Fall through to default
157
+ }
158
+
159
+ return { releases: [] };
160
+ }
161
+
162
+ /**
163
+ * Get release notes for a specific version
164
+ */
165
+ export function getReleaseNotes(version) {
166
+ const { releases } = loadReleaseNotes();
167
+ return releases.find((r) => r.version === version) || null;
168
+ }
169
+
170
+ /**
171
+ * Get all release notes since a specific version
172
+ */
173
+ export function getReleasesSince(sinceVersion) {
174
+ const { releases } = loadReleaseNotes();
175
+
176
+ return releases.filter((r) => compareVersions(r.version, sinceVersion) > 0);
177
+ }
178
+
179
+ /**
180
+ * Get new features available since a specific version
181
+ */
182
+ export function getNewFeaturesSince(sinceVersion) {
183
+ const releases = getReleasesSince(sinceVersion);
184
+
185
+ const features = {
186
+ commands: [],
187
+ agents: [],
188
+ skills: [],
189
+ hooks: [],
190
+ other: [],
191
+ };
192
+
193
+ for (const release of releases) {
194
+ if (release.newFeatures) {
195
+ if (release.newFeatures.commands) {
196
+ features.commands.push(...release.newFeatures.commands);
197
+ }
198
+ if (release.newFeatures.agents) {
199
+ features.agents.push(...release.newFeatures.agents);
200
+ }
201
+ if (release.newFeatures.skills) {
202
+ features.skills.push(...release.newFeatures.skills);
203
+ }
204
+ if (release.newFeatures.hooks) {
205
+ features.hooks.push(...release.newFeatures.hooks);
206
+ }
207
+ if (release.newFeatures.other) {
208
+ features.other.push(...release.newFeatures.other);
209
+ }
210
+ }
211
+ }
212
+
213
+ return features;
214
+ }
215
+
216
+ /**
217
+ * Check if update notification should be shown
218
+ * Returns false if:
219
+ * - Update was dismissed and is now more than 1 day old
220
+ * - User has already seen this version
221
+ */
222
+ export function shouldShowUpdateNotification(state, latestVersion) {
223
+ const currentVersion = getCurrentVersion();
224
+
225
+ // No update available
226
+ if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) {
227
+ return false;
228
+ }
229
+
230
+ // Check if this version was dismissed
231
+ if (state.dismissedVersions?.includes(latestVersion)) {
232
+ return false;
233
+ }
234
+
235
+ // Check if we have a cached check result with timestamp
236
+ if (state.lastCheckResult && state.lastCheckTimestamp) {
237
+ const timeSinceCheck = Date.now() - state.lastCheckTimestamp;
238
+
239
+ // If check is more than 1 day old, don't show notification
240
+ // (user needs to run check again to see new updates)
241
+ if (timeSinceCheck > UPDATE_NOTIFICATION_DURATION) {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * Perform a version check (with caching)
251
+ * This is the main entry point for version checking
252
+ */
253
+ export async function performVersionCheck(projectDir = process.cwd(), forceCheck = false) {
254
+ const state = loadUpdateState(projectDir);
255
+ const currentVersion = getCurrentVersion();
256
+ const now = Date.now();
257
+
258
+ // Check if we have a recent cached result
259
+ if (!forceCheck && state.lastCheckTimestamp && state.lastCheckResult) {
260
+ const timeSinceCheck = now - state.lastCheckTimestamp;
261
+
262
+ if (timeSinceCheck < CACHE_DURATION) {
263
+ // Return cached result
264
+ return {
265
+ currentVersion,
266
+ latestVersion: state.lastCheckResult.latestVersion,
267
+ updateAvailable: compareVersions(state.lastCheckResult.latestVersion, currentVersion) > 0,
268
+ cached: true,
269
+ shouldNotify: shouldShowUpdateNotification(state, state.lastCheckResult.latestVersion),
270
+ newFeatures: getNewFeaturesSince(currentVersion),
271
+ releaseNotes: getReleasesSince(currentVersion),
272
+ };
273
+ }
274
+ }
275
+
276
+ // Perform fresh check
277
+ const latestVersion = await checkLatestVersion();
278
+
279
+ if (latestVersion) {
280
+ // Update state with new check result
281
+ state.lastCheckTimestamp = now;
282
+ state.lastCheckResult = { latestVersion };
283
+ saveUpdateState(state, projectDir);
284
+ }
285
+
286
+ const updateAvailable = latestVersion && compareVersions(latestVersion, currentVersion) > 0;
287
+
288
+ return {
289
+ currentVersion,
290
+ latestVersion: latestVersion || state.lastCheckResult?.latestVersion || currentVersion,
291
+ updateAvailable,
292
+ cached: false,
293
+ shouldNotify: shouldShowUpdateNotification(state, latestVersion),
294
+ newFeatures: updateAvailable ? getNewFeaturesSince(currentVersion) : null,
295
+ releaseNotes: updateAvailable ? getReleasesSince(currentVersion) : [],
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Dismiss update notification for a specific version
301
+ */
302
+ export function dismissUpdateNotification(version, projectDir = process.cwd()) {
303
+ const state = loadUpdateState(projectDir);
304
+
305
+ if (!state.dismissedVersions) {
306
+ state.dismissedVersions = [];
307
+ }
308
+
309
+ if (!state.dismissedVersions.includes(version)) {
310
+ state.dismissedVersions.push(version);
311
+ }
312
+
313
+ state.lastSeenVersion = version;
314
+ saveUpdateState(state, projectDir);
315
+ }
316
+
317
+ /**
318
+ * Mark a feature as installed
319
+ */
320
+ export function markFeatureInstalled(featureName, projectDir = process.cwd()) {
321
+ const state = loadUpdateState(projectDir);
322
+
323
+ if (!state.installedFeatures) {
324
+ state.installedFeatures = [];
325
+ }
326
+
327
+ if (!state.installedFeatures.includes(featureName)) {
328
+ state.installedFeatures.push(featureName);
329
+ }
330
+
331
+ // Remove from skipped if it was there
332
+ if (state.skippedFeatures) {
333
+ state.skippedFeatures = state.skippedFeatures.filter((f) => f !== featureName);
334
+ }
335
+
336
+ saveUpdateState(state, projectDir);
337
+ }
338
+
339
+ /**
340
+ * Mark a feature as skipped (user chose not to install)
341
+ */
342
+ export function markFeatureSkipped(featureName, projectDir = process.cwd()) {
343
+ const state = loadUpdateState(projectDir);
344
+
345
+ if (!state.skippedFeatures) {
346
+ state.skippedFeatures = [];
347
+ }
348
+
349
+ if (!state.skippedFeatures.includes(featureName)) {
350
+ state.skippedFeatures.push(featureName);
351
+ }
352
+
353
+ saveUpdateState(state, projectDir);
354
+ }
355
+
356
+ /**
357
+ * Get features that are available but not yet installed or skipped
358
+ */
359
+ export function getAvailableFeatures(projectDir = process.cwd()) {
360
+ const state = loadUpdateState(projectDir);
361
+ const currentVersion = getCurrentVersion();
362
+ const allNewFeatures = getNewFeaturesSince('0.0.0'); // Get all features ever added
363
+
364
+ const installed = state.installedFeatures || [];
365
+ const skipped = state.skippedFeatures || [];
366
+
367
+ const available = {
368
+ commands: allNewFeatures.commands.filter((f) => !installed.includes(f.name) && !skipped.includes(f.name)),
369
+ agents: allNewFeatures.agents.filter((f) => !installed.includes(f.name) && !skipped.includes(f.name)),
370
+ skills: allNewFeatures.skills.filter((f) => !installed.includes(f.name) && !skipped.includes(f.name)),
371
+ hooks: allNewFeatures.hooks.filter((f) => !installed.includes(f.name) && !skipped.includes(f.name)),
372
+ other: allNewFeatures.other.filter((f) => !installed.includes(f.name) && !skipped.includes(f.name)),
373
+ };
374
+
375
+ return available;
376
+ }
377
+
378
+ /**
379
+ * Format update notification for terminal display
380
+ */
381
+ export function formatUpdateBanner(checkResult) {
382
+ if (!checkResult.updateAvailable || !checkResult.shouldNotify) {
383
+ return null;
384
+ }
385
+
386
+ const lines = [
387
+ '',
388
+ ` ┌${'─'.repeat(60)}┐`,
389
+ ` │ ${'🆕 UPDATE AVAILABLE'.padEnd(58)} │`,
390
+ ` │ ${`v${checkResult.currentVersion} → v${checkResult.latestVersion}`.padEnd(58)} │`,
391
+ ` ├${'─'.repeat(60)}┤`,
392
+ ];
393
+
394
+ // Add summary of new features
395
+ if (checkResult.newFeatures) {
396
+ const { commands, agents, skills, hooks } = checkResult.newFeatures;
397
+ const featureCounts = [];
398
+
399
+ if (commands.length > 0) featureCounts.push(`${commands.length} command(s)`);
400
+ if (agents.length > 0) featureCounts.push(`${agents.length} agent(s)`);
401
+ if (skills.length > 0) featureCounts.push(`${skills.length} skill(s)`);
402
+ if (hooks.length > 0) featureCounts.push(`${hooks.length} hook(s)`);
403
+
404
+ if (featureCounts.length > 0) {
405
+ lines.push(` │ ${`New: ${featureCounts.join(', ')}`.padEnd(58)} │`);
406
+ }
407
+ }
408
+
409
+ lines.push(` │ ${' '.repeat(58)} │`);
410
+ lines.push(` │ ${'Run: npm update -g claude-cli-advanced-starter-pack'.padEnd(58)} │`);
411
+ lines.push(` │ ${'Then: ccasp wizard → Prior Releases to add features'.padEnd(58)} │`);
412
+ lines.push(` └${'─'.repeat(60)}┘`);
413
+ lines.push('');
414
+
415
+ return lines.join('\n');
416
+ }
417
+
418
+ /**
419
+ * Format update notification for Claude Code CLI (markdown)
420
+ */
421
+ export function formatUpdateMarkdown(checkResult) {
422
+ if (!checkResult.updateAvailable) {
423
+ return null;
424
+ }
425
+
426
+ let md = `## 🆕 Update Available\n\n`;
427
+ md += `**Current:** v${checkResult.currentVersion} → **Latest:** v${checkResult.latestVersion}\n\n`;
428
+
429
+ if (checkResult.releaseNotes && checkResult.releaseNotes.length > 0) {
430
+ md += `### What's New\n\n`;
431
+
432
+ for (const release of checkResult.releaseNotes) {
433
+ md += `#### v${release.version} (${release.date})\n`;
434
+ md += `${release.summary}\n\n`;
435
+
436
+ if (release.highlights && release.highlights.length > 0) {
437
+ for (const highlight of release.highlights) {
438
+ md += `- ${highlight}\n`;
439
+ }
440
+ md += '\n';
441
+ }
442
+ }
443
+ }
444
+
445
+ if (checkResult.newFeatures) {
446
+ const { commands, agents, skills, hooks } = checkResult.newFeatures;
447
+ const hasFeatures = commands.length + agents.length + skills.length + hooks.length > 0;
448
+
449
+ if (hasFeatures) {
450
+ md += `### New Features Available\n\n`;
451
+
452
+ if (commands.length > 0) {
453
+ md += `**Commands:**\n`;
454
+ for (const cmd of commands) {
455
+ md += `- \`/${cmd.name}\` - ${cmd.description}\n`;
456
+ }
457
+ md += '\n';
458
+ }
459
+
460
+ if (agents.length > 0) {
461
+ md += `**Agents:**\n`;
462
+ for (const agent of agents) {
463
+ md += `- \`${agent.name}\` - ${agent.description}\n`;
464
+ }
465
+ md += '\n';
466
+ }
467
+
468
+ if (skills.length > 0) {
469
+ md += `**Skills:**\n`;
470
+ for (const skill of skills) {
471
+ md += `- \`${skill.name}\` - ${skill.description}\n`;
472
+ }
473
+ md += '\n';
474
+ }
475
+
476
+ if (hooks.length > 0) {
477
+ md += `**Hooks:**\n`;
478
+ for (const hook of hooks) {
479
+ md += `- \`${hook.name}\` - ${hook.description}\n`;
480
+ }
481
+ md += '\n';
482
+ }
483
+ }
484
+ }
485
+
486
+ md += `### Update Instructions\n\n`;
487
+ md += `\`\`\`bash\nnpm update -g claude-cli-advanced-starter-pack\n\`\`\`\n\n`;
488
+ md += `After updating, run \`/update-check\` to add new features to your project.\n`;
489
+
490
+ return md;
491
+ }
492
+
493
+ export default {
494
+ getCurrentVersion,
495
+ loadUpdateState,
496
+ saveUpdateState,
497
+ compareVersions,
498
+ checkLatestVersion,
499
+ getPackageInfo,
500
+ loadReleaseNotes,
501
+ getReleaseNotes,
502
+ getReleasesSince,
503
+ getNewFeaturesSince,
504
+ shouldShowUpdateNotification,
505
+ performVersionCheck,
506
+ dismissUpdateNotification,
507
+ markFeatureInstalled,
508
+ markFeatureSkipped,
509
+ getAvailableFeatures,
510
+ formatUpdateBanner,
511
+ formatUpdateMarkdown,
512
+ };