skillsets 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,9 @@ Command-line tool for discovering, installing, and contributing verified Claude
8
8
  # Browse available skillsets
9
9
  npx skillsets list
10
10
 
11
+ # Sort by popularity
12
+ npx skillsets list --sort downloads
13
+
11
14
  # Search by keyword
12
15
  npx skillsets search "sdlc"
13
16
 
@@ -19,7 +22,7 @@ npx skillsets install @supercollectible/The_Skillset
19
22
 
20
23
  | Command | Purpose |
21
24
  |---------|---------|
22
- | `list` | Browse all available skillsets |
25
+ | `list` | Browse all skillsets with live star/download counts |
23
26
  | `search <query>` | Fuzzy search by name, description, or tags |
24
27
  | `install <id>` | Install skillset to current directory |
25
28
  | `verify` | Verify installed skillset checksums |
@@ -27,6 +30,25 @@ npx skillsets install @supercollectible/The_Skillset
27
30
  | `audit` | Validate skillset before submission |
28
31
  | `submit` | Open PR to registry (requires `gh` CLI) |
29
32
 
33
+ ## Options
34
+
35
+ ### list
36
+ - `-l, --limit <n>` - Limit number of results
37
+ - `-s, --sort <field>` - Sort by: `name`, `stars`, `downloads`
38
+ - `--json` - Output as JSON
39
+
40
+ ### search
41
+ - `-t, --tags <tags...>` - Filter by tags
42
+ - `-l, --limit <n>` - Limit results (default: 10)
43
+
44
+ ### install
45
+ - `-f, --force` - Overwrite existing files
46
+ - `-b, --backup` - Backup existing files before install
47
+
48
+ ## Live Stats
49
+
50
+ The CLI fetches live star and download counts from the API, so you always see current numbers (not stale build-time data).
51
+
30
52
  ## Development
31
53
 
32
54
  ```bash
@@ -3,7 +3,7 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { detectConflicts, backupFiles } from '../lib/filesystem.js';
5
5
  import { verifyChecksums } from '../lib/checksum.js';
6
- import { REGISTRY_REPO } from '../lib/constants.js';
6
+ import { REGISTRY_REPO, DOWNLOADS_URL } from '../lib/constants.js';
7
7
  export async function install(skillsetId, options) {
8
8
  const spinner = ora(`Installing ${skillsetId}...`).start();
9
9
  // Check for conflicts
@@ -45,6 +45,12 @@ export async function install(skillsetId, options) {
45
45
  process.exit(1);
46
46
  }
47
47
  spinner.succeed(`Successfully installed ${skillsetId}`);
48
+ // Track download (non-blocking, silent fail)
49
+ fetch(DOWNLOADS_URL, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ skillset: skillsetId }),
53
+ }).catch(() => { });
48
54
  // Print next steps
49
55
  console.log(chalk.green('\n✓ Installation complete!'));
50
56
  console.log(chalk.gray('\nNext steps:'));
@@ -1,6 +1,6 @@
1
1
  interface ListOptions {
2
2
  limit?: string;
3
- sort?: 'name' | 'stars' | 'recent';
3
+ sort?: 'name' | 'stars' | 'downloads' | 'recent';
4
4
  json?: boolean;
5
5
  }
6
6
  export declare function list(options: ListOptions): Promise<void>;
@@ -1,17 +1,22 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { fetchSearchIndex } from '../lib/api.js';
3
+ import { fetchSearchIndex, fetchStats, mergeStats } from '../lib/api.js';
4
4
  export async function list(options) {
5
5
  const spinner = ora('Fetching skillsets...').start();
6
6
  try {
7
- const index = await fetchSearchIndex();
7
+ // Fetch index and live stats in parallel
8
+ const [index, stats] = await Promise.all([fetchSearchIndex(), fetchStats()]);
8
9
  spinner.stop();
9
- let skillsets = [...index.skillsets];
10
+ // Merge live stats into skillsets
11
+ let skillsets = mergeStats(index.skillsets, stats);
10
12
  // Sort
11
13
  const sortBy = options.sort || 'name';
12
14
  if (sortBy === 'stars') {
13
15
  skillsets.sort((a, b) => b.stars - a.stars);
14
16
  }
17
+ else if (sortBy === 'downloads') {
18
+ skillsets.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0));
19
+ }
15
20
  else if (sortBy === 'name') {
16
21
  skillsets.sort((a, b) => a.name.localeCompare(b.name));
17
22
  }
@@ -38,17 +43,20 @@ export async function list(options) {
38
43
  console.log(chalk.gray(padEnd('NAME', 30) +
39
44
  padEnd('AUTHOR', 20) +
40
45
  padEnd('STARS', 8) +
46
+ padEnd('INSTALLS', 10) +
41
47
  'DESCRIPTION'));
42
- console.log(chalk.gray('─'.repeat(100)));
48
+ console.log(chalk.gray('─'.repeat(110)));
43
49
  // Rows
44
50
  for (const s of skillsets) {
45
51
  const name = padEnd(s.name, 30);
46
52
  const author = padEnd(s.author.handle, 20);
47
53
  const stars = padEnd(`★ ${s.stars}`, 8);
48
- const desc = truncate(s.description, 40);
54
+ const downloads = padEnd(`↓ ${s.downloads ?? 0}`, 10);
55
+ const desc = truncate(s.description, 32);
49
56
  console.log(chalk.bold(name) +
50
57
  chalk.gray(author) +
51
58
  chalk.yellow(stars) +
59
+ chalk.gray(downloads) +
52
60
  desc);
53
61
  }
54
62
  console.log('');
@@ -1,13 +1,15 @@
1
1
  import Fuse from 'fuse.js';
2
2
  import chalk from 'chalk';
3
- import { fetchSearchIndex } from '../lib/api.js';
3
+ import { fetchSearchIndex, fetchStats, mergeStats } from '../lib/api.js';
4
4
  import { DEFAULT_SEARCH_LIMIT } from '../lib/constants.js';
5
5
  export async function search(query, options) {
6
6
  console.log(chalk.blue(`Searching for: ${query}`));
7
- // Fetch index from CDN
8
- const index = await fetchSearchIndex();
7
+ // Fetch index and live stats in parallel
8
+ const [index, stats] = await Promise.all([fetchSearchIndex(), fetchStats()]);
9
+ // Merge live stats into skillsets
10
+ const skillsetsWithStats = mergeStats(index.skillsets, stats);
9
11
  // Filter by tags if provided
10
- let filtered = index.skillsets;
12
+ let filtered = skillsetsWithStats;
11
13
  if (options.tags && options.tags.length > 0) {
12
14
  filtered = filtered.filter((skillset) => options.tags.some((tag) => skillset.tags.includes(tag)));
13
15
  }
@@ -28,7 +30,7 @@ export async function search(query, options) {
28
30
  console.log(chalk.bold(item.name));
29
31
  console.log(` ${item.description}`);
30
32
  console.log(` ${chalk.gray(`by ${item.author.handle}`)}`);
31
- console.log(` ${chalk.yellow(`★ ${item.stars}`)} ${chalk.gray(`• v${item.version}`)}`);
33
+ console.log(` ${chalk.yellow(`★ ${item.stars}`)} ${chalk.gray(`↓ ${item.downloads ?? 0}`)} ${chalk.gray(`• v${item.version}`)}`);
32
34
  console.log(` ${chalk.cyan(`npx skillsets install ${item.id}`)}`);
33
35
  console.log();
34
36
  });
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ program
17
17
  .command('list')
18
18
  .description('List all available skillsets')
19
19
  .option('-l, --limit <number>', 'Limit results')
20
- .option('-s, --sort <field>', 'Sort by: name, stars (default: name)')
20
+ .option('-s, --sort <field>', 'Sort by: name, stars, downloads (default: name)')
21
21
  .option('--json', 'Output as JSON')
22
22
  .action(async (options) => {
23
23
  try {
package/dist/lib/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SearchIndex, SearchIndexEntry } from '../types/index.js';
1
+ import type { SearchIndex, SearchIndexEntry, StatsResponse } from '../types/index.js';
2
2
  /**
3
3
  * Fetches the search index from the CDN.
4
4
  * Implements 1-hour local cache to reduce network requests.
@@ -8,3 +8,12 @@ export declare function fetchSearchIndex(): Promise<SearchIndex>;
8
8
  * Fetches metadata for a specific skillset by ID.
9
9
  */
10
10
  export declare function fetchSkillsetMetadata(skillsetId: string): Promise<SearchIndexEntry | undefined>;
11
+ /**
12
+ * Fetches live star and download counts from the API.
13
+ * Implements 1-minute local cache.
14
+ */
15
+ export declare function fetchStats(): Promise<StatsResponse>;
16
+ /**
17
+ * Merges live stats into skillset entries.
18
+ */
19
+ export declare function mergeStats(skillsets: SearchIndexEntry[], stats: StatsResponse): SearchIndexEntry[];
package/dist/lib/api.js CHANGED
@@ -1,6 +1,8 @@
1
- import { SEARCH_INDEX_URL, CACHE_TTL_MS } from './constants.js';
1
+ import { SEARCH_INDEX_URL, STATS_URL, CACHE_TTL_MS } from './constants.js';
2
2
  let cachedIndex = null;
3
3
  let cacheTime = 0;
4
+ let cachedStats = null;
5
+ let statsCacheTime = 0;
4
6
  /**
5
7
  * Fetches the search index from the CDN.
6
8
  * Implements 1-hour local cache to reduce network requests.
@@ -27,3 +29,40 @@ export async function fetchSkillsetMetadata(skillsetId) {
27
29
  const index = await fetchSearchIndex();
28
30
  return index.skillsets.find((s) => s.id === skillsetId);
29
31
  }
32
+ /**
33
+ * Fetches live star and download counts from the API.
34
+ * Implements 1-minute local cache.
35
+ */
36
+ export async function fetchStats() {
37
+ const now = Date.now();
38
+ const STATS_CACHE_TTL_MS = 60 * 1000; // 1 minute for stats
39
+ // Return cached if still valid
40
+ if (cachedStats && now - statsCacheTime < STATS_CACHE_TTL_MS) {
41
+ return cachedStats;
42
+ }
43
+ try {
44
+ const response = await fetch(STATS_URL);
45
+ if (!response.ok) {
46
+ // Return empty stats on error, don't break the command
47
+ return { stars: {}, downloads: {} };
48
+ }
49
+ const data = (await response.json());
50
+ cachedStats = data;
51
+ statsCacheTime = now;
52
+ return data;
53
+ }
54
+ catch {
55
+ // Return empty stats on network error
56
+ return { stars: {}, downloads: {} };
57
+ }
58
+ }
59
+ /**
60
+ * Merges live stats into skillset entries.
61
+ */
62
+ export function mergeStats(skillsets, stats) {
63
+ return skillsets.map((s) => ({
64
+ ...s,
65
+ stars: stats.stars[s.id] ?? s.stars,
66
+ downloads: stats.downloads[s.id] ?? 0,
67
+ }));
68
+ }
@@ -1,5 +1,7 @@
1
1
  export declare const CDN_BASE_URL = "https://skillsets.cc";
2
2
  export declare const SEARCH_INDEX_URL = "https://skillsets.cc/search-index.json";
3
+ export declare const STATS_URL = "https://skillsets.cc/api/stats/counts";
4
+ export declare const DOWNLOADS_URL = "https://skillsets.cc/api/downloads";
3
5
  export declare const REGISTRY_REPO = "skillsets-cc/main";
4
6
  export declare const CACHE_TTL_MS: number;
5
7
  export declare const DEFAULT_SEARCH_LIMIT = 10;
@@ -1,5 +1,7 @@
1
1
  export const CDN_BASE_URL = 'https://skillsets.cc';
2
2
  export const SEARCH_INDEX_URL = `${CDN_BASE_URL}/search-index.json`;
3
+ export const STATS_URL = `${CDN_BASE_URL}/api/stats/counts`;
4
+ export const DOWNLOADS_URL = `${CDN_BASE_URL}/api/downloads`;
3
5
  export const REGISTRY_REPO = 'skillsets-cc/main';
4
6
  export const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
5
7
  export const DEFAULT_SEARCH_LIMIT = 10;
@@ -13,6 +13,7 @@ export interface SearchIndexEntry {
13
13
  url?: string;
14
14
  };
15
15
  stars: number;
16
+ downloads?: number;
16
17
  version: string;
17
18
  status: 'active' | 'deprecated' | 'archived';
18
19
  verification: {
@@ -28,6 +29,10 @@ export interface SearchIndexEntry {
28
29
  checksum: string;
29
30
  files: Record<string, string>;
30
31
  }
32
+ export interface StatsResponse {
33
+ stars: Record<string, number>;
34
+ downloads: Record<string, number>;
35
+ }
31
36
  export interface Skillset {
32
37
  schema_version: string;
33
38
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsets",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tool for discovering and installing verified skillsets",
5
5
  "type": "module",
6
6
  "bin": {