@nimblebrain/mpak 0.0.1 → 0.0.2

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.
Files changed (94) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +73 -34
  3. package/README.md +222 -57
  4. package/dist/commands/search.d.ts +12 -0
  5. package/dist/commands/search.d.ts.map +1 -0
  6. package/dist/commands/search.js +144 -0
  7. package/dist/commands/search.js.map +1 -0
  8. package/dist/commands/skills/index.d.ts +8 -0
  9. package/dist/commands/skills/index.d.ts.map +1 -0
  10. package/dist/commands/skills/index.js +8 -0
  11. package/dist/commands/skills/index.js.map +1 -0
  12. package/dist/commands/skills/install.d.ts +9 -0
  13. package/dist/commands/skills/install.d.ts.map +1 -0
  14. package/dist/commands/skills/install.js +110 -0
  15. package/dist/commands/skills/install.js.map +1 -0
  16. package/dist/commands/skills/list.d.ts +8 -0
  17. package/dist/commands/skills/list.d.ts.map +1 -0
  18. package/dist/commands/skills/list.js +89 -0
  19. package/dist/commands/skills/list.js.map +1 -0
  20. package/dist/commands/skills/pack.d.ts +22 -0
  21. package/dist/commands/skills/pack.d.ts.map +1 -0
  22. package/dist/commands/skills/pack.js +116 -0
  23. package/dist/commands/skills/pack.js.map +1 -0
  24. package/dist/commands/skills/pull.d.ts +9 -0
  25. package/dist/commands/skills/pull.d.ts.map +1 -0
  26. package/dist/commands/skills/pull.js +68 -0
  27. package/dist/commands/skills/pull.js.map +1 -0
  28. package/dist/commands/skills/search.d.ts +14 -0
  29. package/dist/commands/skills/search.d.ts.map +1 -0
  30. package/dist/commands/skills/search.js +53 -0
  31. package/dist/commands/skills/search.js.map +1 -0
  32. package/dist/commands/skills/show.d.ts +8 -0
  33. package/dist/commands/skills/show.d.ts.map +1 -0
  34. package/dist/commands/skills/show.js +64 -0
  35. package/dist/commands/skills/show.js.map +1 -0
  36. package/dist/commands/skills/validate.d.ts +25 -0
  37. package/dist/commands/skills/validate.d.ts.map +1 -0
  38. package/dist/commands/skills/validate.js +191 -0
  39. package/dist/commands/skills/validate.js.map +1 -0
  40. package/dist/lib/api/skills-client.d.ts +30 -0
  41. package/dist/lib/api/skills-client.d.ts.map +1 -0
  42. package/dist/lib/api/skills-client.js +110 -0
  43. package/dist/lib/api/skills-client.js.map +1 -0
  44. package/dist/program.d.ts +5 -1
  45. package/dist/program.d.ts.map +1 -1
  46. package/dist/program.js +98 -33
  47. package/dist/program.js.map +1 -1
  48. package/dist/schemas/generated/api-responses.d.ts +541 -0
  49. package/dist/schemas/generated/api-responses.d.ts.map +1 -0
  50. package/dist/schemas/generated/api-responses.js +313 -0
  51. package/dist/schemas/generated/api-responses.js.map +1 -0
  52. package/dist/schemas/generated/auth.d.ts +18 -0
  53. package/dist/schemas/generated/auth.d.ts.map +1 -0
  54. package/dist/schemas/generated/auth.js +18 -0
  55. package/dist/schemas/generated/auth.js.map +1 -0
  56. package/dist/schemas/generated/index.d.ts +5 -0
  57. package/dist/schemas/generated/index.d.ts.map +1 -0
  58. package/dist/schemas/generated/index.js +6 -0
  59. package/dist/schemas/generated/index.js.map +1 -0
  60. package/dist/schemas/generated/package.d.ts +43 -0
  61. package/dist/schemas/generated/package.d.ts.map +1 -0
  62. package/dist/schemas/generated/package.js +20 -0
  63. package/dist/schemas/generated/package.js.map +1 -0
  64. package/dist/schemas/generated/skill.d.ts +381 -0
  65. package/dist/schemas/generated/skill.d.ts.map +1 -0
  66. package/dist/schemas/generated/skill.js +216 -0
  67. package/dist/schemas/generated/skill.js.map +1 -0
  68. package/dist/utils/config-manager.d.ts +13 -1
  69. package/dist/utils/config-manager.d.ts.map +1 -1
  70. package/dist/utils/config-manager.js +76 -11
  71. package/dist/utils/config-manager.js.map +1 -1
  72. package/package.json +6 -2
  73. package/src/commands/search.ts +191 -0
  74. package/src/commands/skills/index.ts +7 -0
  75. package/src/commands/skills/install.ts +129 -0
  76. package/src/commands/skills/list.ts +116 -0
  77. package/src/commands/skills/pack.test.ts +260 -0
  78. package/src/commands/skills/pack.ts +145 -0
  79. package/src/commands/skills/pull.ts +88 -0
  80. package/src/commands/skills/search.ts +73 -0
  81. package/src/commands/skills/show.ts +72 -0
  82. package/src/commands/skills/validate.test.ts +466 -0
  83. package/src/commands/skills/validate.ts +227 -0
  84. package/src/lib/api/skills-client.ts +148 -0
  85. package/src/program.test.ts +1 -3
  86. package/src/program.ts +125 -35
  87. package/src/schemas/config.v1.schema.json +37 -0
  88. package/src/schemas/generated/api-responses.ts +386 -0
  89. package/src/schemas/generated/auth.ts +21 -0
  90. package/src/schemas/generated/index.ts +5 -0
  91. package/src/schemas/generated/package.ts +29 -0
  92. package/src/schemas/generated/skill.ts +271 -0
  93. package/src/utils/config-manager.test.ts +182 -2
  94. package/src/utils/config-manager.ts +126 -12
@@ -1,6 +1,67 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
+ /**
5
+ * Current config schema version
6
+ */
7
+ export const CONFIG_VERSION = '1.0.0';
8
+ /**
9
+ * Error thrown when config file is corrupted or invalid
10
+ */
11
+ export class ConfigCorruptedError extends Error {
12
+ configPath;
13
+ cause;
14
+ constructor(message, configPath, cause) {
15
+ super(message);
16
+ this.configPath = configPath;
17
+ this.cause = cause;
18
+ this.name = 'ConfigCorruptedError';
19
+ }
20
+ }
21
+ /**
22
+ * Validates that a parsed object conforms to the MpakConfig schema
23
+ */
24
+ function validateConfig(data, configPath) {
25
+ if (typeof data !== 'object' || data === null) {
26
+ throw new ConfigCorruptedError('Config file must be a JSON object', configPath);
27
+ }
28
+ const obj = data;
29
+ // Required fields
30
+ if (typeof obj.version !== 'string') {
31
+ throw new ConfigCorruptedError('Config missing required field: version (string)', configPath);
32
+ }
33
+ if (typeof obj.lastUpdated !== 'string') {
34
+ throw new ConfigCorruptedError('Config missing required field: lastUpdated (string)', configPath);
35
+ }
36
+ // Optional fields with type validation
37
+ if (obj.registryUrl !== undefined && typeof obj.registryUrl !== 'string') {
38
+ throw new ConfigCorruptedError('Config field registryUrl must be a string', configPath);
39
+ }
40
+ if (obj.packages !== undefined) {
41
+ if (typeof obj.packages !== 'object' || obj.packages === null) {
42
+ throw new ConfigCorruptedError('Config field packages must be an object', configPath);
43
+ }
44
+ // Validate each package config
45
+ for (const [pkgName, pkgConfig] of Object.entries(obj.packages)) {
46
+ if (typeof pkgConfig !== 'object' || pkgConfig === null) {
47
+ throw new ConfigCorruptedError(`Config packages.${pkgName} must be an object`, configPath);
48
+ }
49
+ for (const [key, value] of Object.entries(pkgConfig)) {
50
+ if (typeof value !== 'string') {
51
+ throw new ConfigCorruptedError(`Config packages.${pkgName}.${key} must be a string`, configPath);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ // Check for unknown fields (additionalProperties: false in schema)
57
+ const knownFields = new Set(['version', 'lastUpdated', 'registryUrl', 'packages']);
58
+ for (const key of Object.keys(obj)) {
59
+ if (!knownFields.has(key)) {
60
+ throw new ConfigCorruptedError(`Config contains unknown field: ${key}`, configPath);
61
+ }
62
+ }
63
+ return data;
64
+ }
4
65
  /**
5
66
  * Configuration manager for CLI settings in ~/.mpak/config.json
6
67
  */
@@ -24,25 +85,29 @@ export class ConfigManager {
24
85
  }
25
86
  if (!existsSync(this.configFile)) {
26
87
  this.config = {
27
- version: '1.0.0',
88
+ version: CONFIG_VERSION,
28
89
  lastUpdated: new Date().toISOString(),
29
90
  };
30
91
  this.saveConfig();
31
92
  return this.config;
32
93
  }
94
+ let configJson;
33
95
  try {
34
- const configJson = readFileSync(this.configFile, 'utf8');
35
- this.config = JSON.parse(configJson);
36
- return this.config;
96
+ configJson = readFileSync(this.configFile, 'utf8');
37
97
  }
38
- catch {
39
- this.config = {
40
- version: '1.0.0',
41
- lastUpdated: new Date().toISOString(),
42
- };
43
- this.saveConfig();
44
- return this.config;
98
+ catch (err) {
99
+ throw new ConfigCorruptedError(`Failed to read config file: ${err instanceof Error ? err.message : String(err)}`, this.configFile, err instanceof Error ? err : undefined);
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(configJson);
104
+ }
105
+ catch (err) {
106
+ throw new ConfigCorruptedError(`Config file contains invalid JSON: ${err instanceof Error ? err.message : String(err)}`, this.configFile, err instanceof Error ? err : undefined);
45
107
  }
108
+ // Validate structure against schema
109
+ this.config = validateConfig(parsed, this.configFile);
110
+ return this.config;
46
111
  }
47
112
  saveConfig() {
48
113
  if (!this.config) {
@@ -1 +1 @@
1
- {"version":3,"file":"config-manager.js","sourceRoot":"","sources":["../../src/utils/config-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-manager.js","sourceRoot":"","sources":["../../src/utils/config-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimblebrain/mpak",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "CLI for downloading MCPB bundles from the mpak registry",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -37,9 +37,13 @@
37
37
  "node": ">=18"
38
38
  },
39
39
  "dependencies": {
40
- "commander": "^14.0.2"
40
+ "archiver": "^7.0.1",
41
+ "commander": "^14.0.2",
42
+ "gray-matter": "^4.0.3",
43
+ "zod": "^4.3.5"
41
44
  },
42
45
  "devDependencies": {
46
+ "@types/archiver": "^7.0.0",
43
47
  "@types/node": "^25.0.3",
44
48
  "@vitest/ui": "^4.0.16",
45
49
  "openapi-typescript": "^7.10.1",
@@ -0,0 +1,191 @@
1
+ import { RegistryClient } from '../lib/api/registry-client.js';
2
+ import { searchSkills } from '../lib/api/skills-client.js';
3
+
4
+ export interface UnifiedSearchOptions {
5
+ type?: 'bundle' | 'skill';
6
+ sort?: 'downloads' | 'recent' | 'name';
7
+ limit?: number;
8
+ offset?: number;
9
+ json?: boolean;
10
+ }
11
+
12
+ interface UnifiedResult {
13
+ type: 'bundle' | 'skill';
14
+ name: string;
15
+ description: string;
16
+ downloads: number;
17
+ version: string;
18
+ author?: string;
19
+ // Bundle-specific
20
+ serverType?: string;
21
+ verified?: boolean;
22
+ provenance?: boolean;
23
+ // Skill-specific
24
+ category?: string;
25
+ }
26
+
27
+ /**
28
+ * Unified search across bundles and skills
29
+ */
30
+ export async function handleUnifiedSearch(
31
+ query: string,
32
+ options: UnifiedSearchOptions = {}
33
+ ): Promise<void> {
34
+ try {
35
+ const results: UnifiedResult[] = [];
36
+ let bundleTotal = 0;
37
+ let skillTotal = 0;
38
+
39
+ // Search both in parallel (unless filtered by type)
40
+ const searchBundles = !options.type || options.type === 'bundle';
41
+ const searchSkillsFlag = !options.type || options.type === 'skill';
42
+
43
+ const [bundleResult, skillResult] = await Promise.all([
44
+ searchBundles
45
+ ? new RegistryClient().searchBundles(query, {
46
+ sort: options.sort,
47
+ limit: options.limit,
48
+ offset: options.offset,
49
+ })
50
+ : null,
51
+ searchSkillsFlag
52
+ ? searchSkills({
53
+ q: query,
54
+ sort: options.sort as any,
55
+ limit: options.limit,
56
+ offset: options.offset,
57
+ }).catch(() => null) // Skills API may not be deployed yet
58
+ : null,
59
+ ]);
60
+
61
+ // Process bundle results
62
+ if (bundleResult) {
63
+ bundleTotal = bundleResult.total;
64
+ for (const bundle of bundleResult.bundles) {
65
+ results.push({
66
+ type: 'bundle',
67
+ name: bundle.name,
68
+ description: bundle.description || '',
69
+ downloads: bundle.downloads || 0,
70
+ version: bundle.latest_version,
71
+ author: bundle.author?.name || undefined,
72
+ serverType: bundle.server_type || undefined,
73
+ verified: bundle.verified,
74
+ provenance: !!bundle.provenance,
75
+ });
76
+ }
77
+ }
78
+
79
+ // Process skill results
80
+ if (skillResult) {
81
+ skillTotal = skillResult.total;
82
+ for (const skill of skillResult.skills) {
83
+ results.push({
84
+ type: 'skill',
85
+ name: skill.name,
86
+ description: skill.description || '',
87
+ downloads: skill.downloads || 0,
88
+ version: skill.latest_version,
89
+ author: skill.author?.name || undefined,
90
+ category: skill.category || undefined,
91
+ });
92
+ }
93
+ }
94
+
95
+ // Sort combined results
96
+ if (options.sort === 'downloads') {
97
+ results.sort((a, b) => b.downloads - a.downloads);
98
+ } else if (options.sort === 'name') {
99
+ results.sort((a, b) => a.name.localeCompare(b.name));
100
+ }
101
+ // 'recent' sorting would require timestamps, skip for now
102
+
103
+ // JSON output
104
+ if (options.json) {
105
+ console.log(
106
+ JSON.stringify(
107
+ {
108
+ results,
109
+ totals: { bundles: bundleTotal, skills: skillTotal },
110
+ },
111
+ null,
112
+ 2
113
+ )
114
+ );
115
+ return;
116
+ }
117
+
118
+ // No results
119
+ if (results.length === 0) {
120
+ console.log(`\nNo results found for "${query}"`);
121
+ if (!searchBundles) console.log(' (searched skills only)');
122
+ if (!searchSkillsFlag) console.log(' (searched bundles only)');
123
+ return;
124
+ }
125
+
126
+ // Summary
127
+ const totalResults = bundleTotal + skillTotal;
128
+ const typeFilter = options.type ? ` (${options.type}s only)` : '';
129
+ console.log(`\nFound ${totalResults} result(s) for "${query}"${typeFilter}:\n`);
130
+
131
+ // Table header
132
+ const typeWidth = 10;
133
+ const nameWidth = 38;
134
+ const versionWidth = 12;
135
+ const downloadsWidth = 10;
136
+
137
+ console.log(
138
+ 'TYPE'.padEnd(typeWidth) +
139
+ 'NAME'.padEnd(nameWidth) +
140
+ 'VERSION'.padEnd(versionWidth) +
141
+ 'DOWNLOADS'.padStart(downloadsWidth)
142
+ );
143
+
144
+ // Table rows
145
+ for (const result of results) {
146
+ const typeLabel = result.type === 'bundle' ? 'bundle' : 'skill';
147
+ const name =
148
+ result.name.length > nameWidth - 2
149
+ ? result.name.slice(0, nameWidth - 5) + '...'
150
+ : result.name;
151
+ const version = result.version || '-';
152
+ const downloads = result.downloads.toLocaleString();
153
+
154
+ console.log(
155
+ typeLabel.padEnd(typeWidth) +
156
+ name.padEnd(nameWidth) +
157
+ version.padEnd(versionWidth) +
158
+ downloads.padStart(downloadsWidth)
159
+ );
160
+ }
161
+
162
+ console.log('');
163
+
164
+ // Show breakdown
165
+ if (!options.type) {
166
+ const parts = [];
167
+ if (bundleTotal > 0) parts.push(`${bundleTotal} bundle(s)`);
168
+ if (skillTotal > 0) parts.push(`${skillTotal} skill(s)`);
169
+ if (parts.length > 1) {
170
+ console.log(` ${parts.join(', ')}`);
171
+ }
172
+ }
173
+
174
+ // Pagination hint
175
+ const currentLimit = options.limit || 20;
176
+ const currentOffset = options.offset || 0;
177
+ if (bundleTotal + skillTotal > currentOffset + results.length) {
178
+ console.log(` Use --offset ${currentOffset + currentLimit} to see more results.`);
179
+ }
180
+
181
+ // Hint for more details
182
+ console.log('');
183
+ console.log('Use "mpak bundle show <name>" or "mpak skill show <name>" for details.');
184
+ } catch (error) {
185
+ console.error('Search failed');
186
+ if (error instanceof Error) {
187
+ console.error(` ${error.message}`);
188
+ }
189
+ process.exit(1);
190
+ }
191
+ }
@@ -0,0 +1,7 @@
1
+ export { handleSkillValidate, validateSkillDirectory } from './validate.js';
2
+ export { handleSkillPack, packSkill } from './pack.js';
3
+ export { handleSkillSearch } from './search.js';
4
+ export { handleSkillShow } from './show.js';
5
+ export { handleSkillPull } from './pull.js';
6
+ export { handleSkillInstall } from './install.js';
7
+ export { handleSkillList } from './list.js';
@@ -0,0 +1,129 @@
1
+ import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
2
+ import { join, basename } from 'path';
3
+ import { homedir, tmpdir } from 'os';
4
+ import { getSkillDownloadInfo, downloadSkillBundle } from '../../lib/api/skills-client.js';
5
+ import { execSync } from 'child_process';
6
+
7
+ /**
8
+ * Get the Claude Code skills directory
9
+ */
10
+ function getSkillsDir(): string {
11
+ return join(homedir(), '.claude', 'skills');
12
+ }
13
+
14
+ /**
15
+ * Parse skill spec into name and version
16
+ */
17
+ function parseSkillSpec(spec: string): { name: string; version?: string } {
18
+ const atIndex = spec.lastIndexOf('@');
19
+ if (atIndex <= 0) {
20
+ return { name: spec };
21
+ }
22
+ const slashIndex = spec.indexOf('/');
23
+ if (atIndex > slashIndex) {
24
+ return {
25
+ name: spec.slice(0, atIndex),
26
+ version: spec.slice(atIndex + 1),
27
+ };
28
+ }
29
+ return { name: spec };
30
+ }
31
+
32
+ /**
33
+ * Extract skill name from scoped name
34
+ * @scope/skill-name -> skill-name
35
+ */
36
+ function getShortName(scopedName: string): string {
37
+ const parts = scopedName.replace('@', '').split('/');
38
+ return parts[parts.length - 1];
39
+ }
40
+
41
+ export interface InstallOptions {
42
+ force?: boolean;
43
+ json?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Handle the skill install command
48
+ */
49
+ export async function handleSkillInstall(skillSpec: string, options: InstallOptions): Promise<void> {
50
+ try {
51
+ const { name, version } = parseSkillSpec(skillSpec);
52
+
53
+ // Get download info
54
+ const downloadInfo = await getSkillDownloadInfo(name, version);
55
+ const shortName = getShortName(downloadInfo.skill.name);
56
+ const skillsDir = getSkillsDir();
57
+ const installPath = join(skillsDir, shortName);
58
+
59
+ // Check if already installed
60
+ if (existsSync(installPath) && !options.force) {
61
+ console.error(`Skill "${shortName}" is already installed at ${installPath}`);
62
+ console.error('Use --force to overwrite');
63
+ process.exit(1);
64
+ }
65
+
66
+ console.log(`Pulling ${downloadInfo.skill.name}@${downloadInfo.skill.version}...`);
67
+
68
+ // Download the bundle
69
+ const buffer = await downloadSkillBundle(downloadInfo.url, downloadInfo.skill.sha256);
70
+
71
+ console.log(`Downloaded ${basename(downloadInfo.skill.name)}-${downloadInfo.skill.version}.skill (${formatSize(downloadInfo.skill.size)})`);
72
+
73
+ // Ensure skills directory exists
74
+ if (!existsSync(skillsDir)) {
75
+ mkdirSync(skillsDir, { recursive: true });
76
+ }
77
+
78
+ // Write to temp file
79
+ const tempPath = join(tmpdir(), `skill-${Date.now()}.skill`);
80
+ writeFileSync(tempPath, buffer);
81
+
82
+ // Remove existing installation if force
83
+ if (existsSync(installPath)) {
84
+ rmSync(installPath, { recursive: true });
85
+ }
86
+
87
+ // Extract using unzip
88
+ // The .skill bundle contains: skillName/SKILL.md, skillName/...
89
+ // We extract to the skills directory
90
+ try {
91
+ execSync(`unzip -o "${tempPath}" -d "${skillsDir}"`, { stdio: 'pipe' });
92
+ } catch (err) {
93
+ throw new Error(`Failed to extract skill bundle: ${err}`);
94
+ } finally {
95
+ // Clean up temp file
96
+ rmSync(tempPath, { force: true });
97
+ }
98
+
99
+ if (options.json) {
100
+ console.log(
101
+ JSON.stringify(
102
+ {
103
+ installed: true,
104
+ name: downloadInfo.skill.name,
105
+ shortName,
106
+ version: downloadInfo.skill.version,
107
+ path: installPath,
108
+ },
109
+ null,
110
+ 2
111
+ )
112
+ );
113
+ } else {
114
+ console.log(`Extracting to ${installPath}/`);
115
+ console.log(`\u2713 Installed: ${shortName}`);
116
+ console.log('');
117
+ console.log('Skill available in Claude Code. Restart to activate.');
118
+ }
119
+ } catch (err) {
120
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ function formatSize(bytes: number): string {
126
+ if (bytes < 1024) return `${bytes} B`;
127
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
128
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
129
+ }
@@ -0,0 +1,116 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import matter from 'gray-matter';
5
+
6
+ /**
7
+ * Get the Claude Code skills directory
8
+ */
9
+ function getSkillsDir(): string {
10
+ return join(homedir(), '.claude', 'skills');
11
+ }
12
+
13
+ interface InstalledSkill {
14
+ name: string;
15
+ description: string;
16
+ version: string | null;
17
+ path: string;
18
+ }
19
+
20
+ /**
21
+ * List all installed skills
22
+ */
23
+ function listInstalledSkills(): InstalledSkill[] {
24
+ const skillsDir = getSkillsDir();
25
+
26
+ if (!existsSync(skillsDir)) {
27
+ return [];
28
+ }
29
+
30
+ const skills: InstalledSkill[] = [];
31
+ const entries = readdirSync(skillsDir);
32
+
33
+ for (const entry of entries) {
34
+ // Skip hidden files
35
+ if (entry.startsWith('.')) continue;
36
+
37
+ const skillPath = join(skillsDir, entry);
38
+ const stat = statSync(skillPath);
39
+
40
+ if (!stat.isDirectory()) continue;
41
+
42
+ // Check for SKILL.md
43
+ const skillMdPath = join(skillPath, 'SKILL.md');
44
+ if (!existsSync(skillMdPath)) continue;
45
+
46
+ // Parse SKILL.md
47
+ try {
48
+ const content = readFileSync(skillMdPath, 'utf-8');
49
+ const parsed = matter(content);
50
+
51
+ skills.push({
52
+ name: parsed.data.name || entry,
53
+ description: parsed.data.description || '',
54
+ version: parsed.data.metadata?.version || null,
55
+ path: skillPath,
56
+ });
57
+ } catch {
58
+ // If we can't parse, still include with basic info
59
+ skills.push({
60
+ name: entry,
61
+ description: '(unable to parse SKILL.md)',
62
+ version: null,
63
+ path: skillPath,
64
+ });
65
+ }
66
+ }
67
+
68
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
69
+ }
70
+
71
+ export interface ListOptions {
72
+ json?: boolean;
73
+ }
74
+
75
+ /**
76
+ * Handle the skill list command
77
+ */
78
+ export async function handleSkillList(options: ListOptions): Promise<void> {
79
+ const skills = listInstalledSkills();
80
+
81
+ if (options.json) {
82
+ console.log(JSON.stringify(skills, null, 2));
83
+ return;
84
+ }
85
+
86
+ if (skills.length === 0) {
87
+ console.log('No skills installed.');
88
+ console.log('');
89
+ console.log('Install skills with: mpak skill install <name>');
90
+ console.log('Or create your own in ~/.claude/skills/');
91
+ return;
92
+ }
93
+
94
+ console.log('');
95
+ console.log('Installed skills:');
96
+ console.log('');
97
+
98
+ const nameWidth = Math.max(20, ...skills.map((s) => s.name.length)) + 2;
99
+ const versionWidth = 10;
100
+
101
+ console.log('NAME'.padEnd(nameWidth) + 'VERSION'.padEnd(versionWidth) + 'DESCRIPTION');
102
+
103
+ for (const skill of skills) {
104
+ const name = skill.name.padEnd(nameWidth);
105
+ const version = (skill.version || '-').padEnd(versionWidth);
106
+ const desc =
107
+ skill.description.length > 50
108
+ ? skill.description.slice(0, 47) + '...'
109
+ : skill.description;
110
+
111
+ console.log(name + version + desc);
112
+ }
113
+
114
+ console.log('');
115
+ console.log(`${skills.length} skill(s) installed in ${getSkillsDir()}`);
116
+ }