@nimblebrain/mpak 0.0.1 → 0.1.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.
Files changed (101) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/CLAUDE.md +85 -34
  3. package/README.md +245 -57
  4. package/dist/commands/packages/run.d.ts +12 -1
  5. package/dist/commands/packages/run.d.ts.map +1 -1
  6. package/dist/commands/packages/run.js +116 -45
  7. package/dist/commands/packages/run.js.map +1 -1
  8. package/dist/commands/search.d.ts +12 -0
  9. package/dist/commands/search.d.ts.map +1 -0
  10. package/dist/commands/search.js +144 -0
  11. package/dist/commands/search.js.map +1 -0
  12. package/dist/commands/skills/index.d.ts +8 -0
  13. package/dist/commands/skills/index.d.ts.map +1 -0
  14. package/dist/commands/skills/index.js +8 -0
  15. package/dist/commands/skills/index.js.map +1 -0
  16. package/dist/commands/skills/install.d.ts +9 -0
  17. package/dist/commands/skills/install.d.ts.map +1 -0
  18. package/dist/commands/skills/install.js +110 -0
  19. package/dist/commands/skills/install.js.map +1 -0
  20. package/dist/commands/skills/list.d.ts +8 -0
  21. package/dist/commands/skills/list.d.ts.map +1 -0
  22. package/dist/commands/skills/list.js +89 -0
  23. package/dist/commands/skills/list.js.map +1 -0
  24. package/dist/commands/skills/pack.d.ts +22 -0
  25. package/dist/commands/skills/pack.d.ts.map +1 -0
  26. package/dist/commands/skills/pack.js +116 -0
  27. package/dist/commands/skills/pack.js.map +1 -0
  28. package/dist/commands/skills/pull.d.ts +9 -0
  29. package/dist/commands/skills/pull.d.ts.map +1 -0
  30. package/dist/commands/skills/pull.js +68 -0
  31. package/dist/commands/skills/pull.js.map +1 -0
  32. package/dist/commands/skills/search.d.ts +14 -0
  33. package/dist/commands/skills/search.d.ts.map +1 -0
  34. package/dist/commands/skills/search.js +53 -0
  35. package/dist/commands/skills/search.js.map +1 -0
  36. package/dist/commands/skills/show.d.ts +8 -0
  37. package/dist/commands/skills/show.d.ts.map +1 -0
  38. package/dist/commands/skills/show.js +64 -0
  39. package/dist/commands/skills/show.js.map +1 -0
  40. package/dist/commands/skills/validate.d.ts +25 -0
  41. package/dist/commands/skills/validate.d.ts.map +1 -0
  42. package/dist/commands/skills/validate.js +191 -0
  43. package/dist/commands/skills/validate.js.map +1 -0
  44. package/dist/index.js +0 -0
  45. package/dist/lib/api/skills-client.d.ts +30 -0
  46. package/dist/lib/api/skills-client.d.ts.map +1 -0
  47. package/dist/lib/api/skills-client.js +110 -0
  48. package/dist/lib/api/skills-client.js.map +1 -0
  49. package/dist/program.d.ts +5 -1
  50. package/dist/program.d.ts.map +1 -1
  51. package/dist/program.js +112 -35
  52. package/dist/program.js.map +1 -1
  53. package/dist/schemas/generated/api-responses.d.ts +541 -0
  54. package/dist/schemas/generated/api-responses.d.ts.map +1 -0
  55. package/dist/schemas/generated/api-responses.js +313 -0
  56. package/dist/schemas/generated/api-responses.js.map +1 -0
  57. package/dist/schemas/generated/auth.d.ts +18 -0
  58. package/dist/schemas/generated/auth.d.ts.map +1 -0
  59. package/dist/schemas/generated/auth.js +18 -0
  60. package/dist/schemas/generated/auth.js.map +1 -0
  61. package/dist/schemas/generated/index.d.ts +5 -0
  62. package/dist/schemas/generated/index.d.ts.map +1 -0
  63. package/dist/schemas/generated/index.js +6 -0
  64. package/dist/schemas/generated/index.js.map +1 -0
  65. package/dist/schemas/generated/package.d.ts +43 -0
  66. package/dist/schemas/generated/package.d.ts.map +1 -0
  67. package/dist/schemas/generated/package.js +20 -0
  68. package/dist/schemas/generated/package.js.map +1 -0
  69. package/dist/schemas/generated/skill.d.ts +381 -0
  70. package/dist/schemas/generated/skill.d.ts.map +1 -0
  71. package/dist/schemas/generated/skill.js +216 -0
  72. package/dist/schemas/generated/skill.js.map +1 -0
  73. package/dist/utils/config-manager.d.ts +13 -1
  74. package/dist/utils/config-manager.d.ts.map +1 -1
  75. package/dist/utils/config-manager.js +76 -11
  76. package/dist/utils/config-manager.js.map +1 -1
  77. package/package.json +6 -2
  78. package/src/commands/packages/run.test.ts +40 -1
  79. package/src/commands/packages/run.ts +131 -46
  80. package/src/commands/search.ts +191 -0
  81. package/src/commands/skills/index.ts +7 -0
  82. package/src/commands/skills/install.ts +129 -0
  83. package/src/commands/skills/list.ts +116 -0
  84. package/src/commands/skills/pack.test.ts +260 -0
  85. package/src/commands/skills/pack.ts +145 -0
  86. package/src/commands/skills/pull.ts +88 -0
  87. package/src/commands/skills/search.ts +73 -0
  88. package/src/commands/skills/show.ts +72 -0
  89. package/src/commands/skills/validate.test.ts +466 -0
  90. package/src/commands/skills/validate.ts +227 -0
  91. package/src/lib/api/skills-client.ts +148 -0
  92. package/src/program.test.ts +1 -3
  93. package/src/program.ts +140 -36
  94. package/src/schemas/config.v1.schema.json +37 -0
  95. package/src/schemas/generated/api-responses.ts +386 -0
  96. package/src/schemas/generated/auth.ts +21 -0
  97. package/src/schemas/generated/index.ts +5 -0
  98. package/src/schemas/generated/package.ts +29 -0
  99. package/src/schemas/generated/skill.ts +271 -0
  100. package/src/utils/config-manager.test.ts +182 -2
  101. 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.1.0",
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",
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
- import { parsePackageSpec, getCacheDir, resolveArgs, substituteUserConfig, substituteEnvVars } from './run.js';
4
+ import { parsePackageSpec, getCacheDir, resolveArgs, substituteUserConfig, substituteEnvVars, getLocalCacheDir, localBundleNeedsExtract } from './run.js';
5
5
 
6
6
  describe('parsePackageSpec', () => {
7
7
  describe('scoped packages', () => {
@@ -220,3 +220,42 @@ describe('substituteEnvVars', () => {
220
220
  });
221
221
  });
222
222
  });
223
+
224
+ describe('getLocalCacheDir', () => {
225
+ const expectedBase = join(homedir(), '.mpak', 'cache', '_local');
226
+
227
+ it('returns consistent hash for same path', () => {
228
+ const dir1 = getLocalCacheDir('/path/to/bundle.mcpb');
229
+ const dir2 = getLocalCacheDir('/path/to/bundle.mcpb');
230
+ expect(dir1).toBe(dir2);
231
+ });
232
+
233
+ it('returns different hash for different paths', () => {
234
+ const dir1 = getLocalCacheDir('/path/to/bundle1.mcpb');
235
+ const dir2 = getLocalCacheDir('/path/to/bundle2.mcpb');
236
+ expect(dir1).not.toBe(dir2);
237
+ });
238
+
239
+ it('includes _local in path', () => {
240
+ const dir = getLocalCacheDir('/path/to/bundle.mcpb');
241
+ expect(dir).toContain('_local');
242
+ expect(dir.startsWith(expectedBase)).toBe(true);
243
+ });
244
+
245
+ it('produces a 12-character hash suffix', () => {
246
+ const dir = getLocalCacheDir('/path/to/bundle.mcpb');
247
+ const hashPart = dir.split('/').pop();
248
+ expect(hashPart).toHaveLength(12);
249
+ });
250
+ });
251
+
252
+ describe('localBundleNeedsExtract', () => {
253
+ it('returns true when cache directory does not exist', () => {
254
+ expect(localBundleNeedsExtract('/any/path.mcpb', '/nonexistent/cache')).toBe(true);
255
+ });
256
+
257
+ it('returns true when meta file does not exist in cache dir', () => {
258
+ // Using a directory that exists but has no .mpak-meta.json
259
+ expect(localBundleNeedsExtract('/any/path.mcpb', '/tmp')).toBe(true);
260
+ });
261
+ });
@@ -1,13 +1,15 @@
1
1
  import { spawn, spawnSync } from 'child_process';
2
2
  import { createInterface } from 'readline';
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, rmSync, statSync } from 'fs';
4
+ import { createHash } from 'crypto';
4
5
  import { homedir } from 'os';
5
- import { join, dirname } from 'path';
6
+ import { join, dirname, resolve, basename } from 'path';
6
7
  import { RegistryClient } from '../../lib/api/registry-client.js';
7
8
  import { ConfigManager } from '../../utils/config-manager.js';
8
9
 
9
10
  export interface RunOptions {
10
11
  update?: boolean;
12
+ local?: string; // Path to local .mcpb file
11
13
  }
12
14
 
13
15
  interface McpConfig {
@@ -169,6 +171,33 @@ export function substituteEnvVars(
169
171
  return result;
170
172
  }
171
173
 
174
+ /**
175
+ * Get cache directory for a local bundle.
176
+ * Uses hash of absolute path to avoid collisions.
177
+ */
178
+ export function getLocalCacheDir(bundlePath: string): string {
179
+ const absolutePath = resolve(bundlePath);
180
+ const hash = createHash('md5').update(absolutePath).digest('hex').slice(0, 12);
181
+ return join(homedir(), '.mpak', 'cache', '_local', hash);
182
+ }
183
+
184
+ /**
185
+ * Check if local bundle needs re-extraction.
186
+ * Returns true if cache doesn't exist or bundle was modified after extraction.
187
+ */
188
+ export function localBundleNeedsExtract(bundlePath: string, cacheDir: string): boolean {
189
+ const metaPath = join(cacheDir, '.mpak-meta.json');
190
+ if (!existsSync(metaPath)) return true;
191
+
192
+ try {
193
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
194
+ const bundleStat = statSync(bundlePath);
195
+ return bundleStat.mtimeMs > new Date(meta.extractedAt).getTime();
196
+ } catch {
197
+ return true;
198
+ }
199
+ }
200
+
172
201
  /**
173
202
  * Prompt user for a config value (interactive terminal input)
174
203
  */
@@ -295,69 +324,125 @@ function findPythonCommand(): string {
295
324
  }
296
325
 
297
326
  /**
298
- * Run a package from the registry
327
+ * Run a package from the registry or a local bundle file
299
328
  */
300
329
  export async function handleRun(
301
330
  packageSpec: string,
302
331
  options: RunOptions = {}
303
332
  ): Promise<void> {
304
- const { name, version: requestedVersion } = parsePackageSpec(packageSpec);
305
- const client = new RegistryClient();
306
- const platform = RegistryClient.detectPlatform();
307
- const cacheDir = getCacheDir(name);
308
-
309
- let needsPull = true;
310
- let cachedMeta = getCacheMetadata(cacheDir);
311
-
312
- // Check if we have a cached version
313
- if (cachedMeta && !options.update) {
314
- if (requestedVersion) {
315
- // Specific version requested - check if cached version matches
316
- needsPull = cachedMeta.version !== requestedVersion;
317
- } else {
318
- // Latest requested - use cache (user can --update to refresh)
319
- needsPull = false;
320
- }
333
+ // Validate that either --local or package spec is provided
334
+ if (!options.local && !packageSpec) {
335
+ process.stderr.write(`=> Error: Either provide a package name or use --local <path>\n`);
336
+ process.exit(1);
321
337
  }
322
338
 
323
- if (needsPull) {
324
- // Fetch download info
325
- const downloadInfo = await client.getDownloadInfo(name, requestedVersion, platform);
326
- const bundle = downloadInfo.bundle;
339
+ let cacheDir: string;
340
+ let packageName: string;
341
+
342
+ if (options.local) {
343
+ // === LOCAL BUNDLE MODE ===
344
+ const bundlePath = resolve(options.local);
327
345
 
328
- // Check if cached version is already the latest
329
- if (cachedMeta && cachedMeta.version === bundle.version && !options.update) {
330
- needsPull = false;
346
+ // Validate bundle exists
347
+ if (!existsSync(bundlePath)) {
348
+ process.stderr.write(`=> Error: Bundle not found: ${bundlePath}\n`);
349
+ process.exit(1);
331
350
  }
332
351
 
333
- if (needsPull) {
334
- // Download to temp file
335
- const tempPath = join(homedir(), '.mpak', 'tmp', `${Date.now()}.mcpb`);
336
- mkdirSync(dirname(tempPath), { recursive: true });
352
+ // Validate .mcpb extension
353
+ if (!bundlePath.endsWith('.mcpb')) {
354
+ process.stderr.write(`=> Error: Not an MCPB bundle: ${bundlePath}\n`);
355
+ process.exit(1);
356
+ }
337
357
 
338
- process.stderr.write(`=> Pulling ${name}@${bundle.version}...\n`);
339
- await client.downloadBundle(downloadInfo.url, tempPath);
358
+ cacheDir = getLocalCacheDir(bundlePath);
359
+ const needsExtract = options.update || localBundleNeedsExtract(bundlePath, cacheDir);
340
360
 
341
- // Clear old cache and extract
342
- const { rmSync } = await import('fs');
361
+ if (needsExtract) {
362
+ // Clear old extraction
343
363
  if (existsSync(cacheDir)) {
344
364
  rmSync(cacheDir, { recursive: true, force: true });
345
365
  }
346
366
  mkdirSync(cacheDir, { recursive: true });
347
367
 
348
- await extractZip(tempPath, cacheDir);
368
+ process.stderr.write(`=> Extracting ${basename(bundlePath)}...\n`);
369
+ await extractZip(bundlePath, cacheDir);
370
+
371
+ // Write local metadata
372
+ writeFileSync(
373
+ join(cacheDir, '.mpak-meta.json'),
374
+ JSON.stringify({
375
+ localPath: bundlePath,
376
+ extractedAt: new Date().toISOString(),
377
+ })
378
+ );
379
+ }
349
380
 
350
- // Write metadata
351
- writeCacheMetadata(cacheDir, {
352
- version: bundle.version,
353
- pulledAt: new Date().toISOString(),
354
- platform: bundle.platform,
355
- });
381
+ // Read manifest to get package name for config lookup
382
+ const manifest = readManifest(cacheDir);
383
+ packageName = manifest.name;
384
+ process.stderr.write(`=> Running ${packageName} (local)\n`);
385
+
386
+ } else {
387
+ // === REGISTRY MODE ===
388
+ const { name, version: requestedVersion } = parsePackageSpec(packageSpec);
389
+ packageName = name;
390
+ const client = new RegistryClient();
391
+ const platform = RegistryClient.detectPlatform();
392
+ cacheDir = getCacheDir(name);
393
+
394
+ let needsPull = true;
395
+ let cachedMeta = getCacheMetadata(cacheDir);
396
+
397
+ // Check if we have a cached version
398
+ if (cachedMeta && !options.update) {
399
+ if (requestedVersion) {
400
+ // Specific version requested - check if cached version matches
401
+ needsPull = cachedMeta.version !== requestedVersion;
402
+ } else {
403
+ // Latest requested - use cache (user can --update to refresh)
404
+ needsPull = false;
405
+ }
406
+ }
407
+
408
+ if (needsPull) {
409
+ // Fetch download info
410
+ const downloadInfo = await client.getDownloadInfo(name, requestedVersion, platform);
411
+ const bundle = downloadInfo.bundle;
356
412
 
357
- // Cleanup temp file
358
- rmSync(tempPath, { force: true });
413
+ // Check if cached version is already the latest
414
+ if (cachedMeta && cachedMeta.version === bundle.version && !options.update) {
415
+ needsPull = false;
416
+ }
359
417
 
360
- process.stderr.write(`=> Cached ${name}@${bundle.version}\n`);
418
+ if (needsPull) {
419
+ // Download to temp file
420
+ const tempPath = join(homedir(), '.mpak', 'tmp', `${Date.now()}.mcpb`);
421
+ mkdirSync(dirname(tempPath), { recursive: true });
422
+
423
+ process.stderr.write(`=> Pulling ${name}@${bundle.version}...\n`);
424
+ await client.downloadBundle(downloadInfo.url, tempPath);
425
+
426
+ // Clear old cache and extract
427
+ if (existsSync(cacheDir)) {
428
+ rmSync(cacheDir, { recursive: true, force: true });
429
+ }
430
+ mkdirSync(cacheDir, { recursive: true });
431
+
432
+ await extractZip(tempPath, cacheDir);
433
+
434
+ // Write metadata
435
+ writeCacheMetadata(cacheDir, {
436
+ version: bundle.version,
437
+ pulledAt: new Date().toISOString(),
438
+ platform: bundle.platform,
439
+ });
440
+
441
+ // Cleanup temp file
442
+ rmSync(tempPath, { force: true });
443
+
444
+ process.stderr.write(`=> Cached ${name}@${bundle.version}\n`);
445
+ }
361
446
  }
362
447
  }
363
448
 
@@ -369,7 +454,7 @@ export async function handleRun(
369
454
  let userConfigValues: Record<string, string> = {};
370
455
  if (manifest.user_config && Object.keys(manifest.user_config).length > 0) {
371
456
  const configManager = new ConfigManager();
372
- userConfigValues = await gatherUserConfigValues(name, manifest.user_config, configManager);
457
+ userConfigValues = await gatherUserConfigValues(packageName, manifest.user_config, configManager);
373
458
  }
374
459
 
375
460
  // Substitute user_config placeholders in env vars
@@ -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';