@nimblebrain/mpak 0.1.0 → 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.
Files changed (151) hide show
  1. package/LICENSE +10 -198
  2. package/README.md +50 -383
  3. package/dist/index.d.ts +0 -2
  4. package/dist/index.js +2113 -4
  5. package/dist/index.js.map +1 -1
  6. package/package.json +32 -29
  7. package/.claude/settings.local.json +0 -19
  8. package/.env.example +0 -13
  9. package/.github/workflows/ci.yml +0 -27
  10. package/CLAUDE.md +0 -283
  11. package/dist/commands/config.d.ts +0 -31
  12. package/dist/commands/config.d.ts.map +0 -1
  13. package/dist/commands/config.js +0 -129
  14. package/dist/commands/config.js.map +0 -1
  15. package/dist/commands/packages/pull.d.ts +0 -11
  16. package/dist/commands/packages/pull.d.ts.map +0 -1
  17. package/dist/commands/packages/pull.js +0 -72
  18. package/dist/commands/packages/pull.js.map +0 -1
  19. package/dist/commands/packages/run.d.ts +0 -47
  20. package/dist/commands/packages/run.d.ts.map +0 -1
  21. package/dist/commands/packages/run.js +0 -419
  22. package/dist/commands/packages/run.js.map +0 -1
  23. package/dist/commands/packages/search.d.ts +0 -12
  24. package/dist/commands/packages/search.d.ts.map +0 -1
  25. package/dist/commands/packages/search.js +0 -63
  26. package/dist/commands/packages/search.js.map +0 -1
  27. package/dist/commands/packages/show.d.ts +0 -8
  28. package/dist/commands/packages/show.d.ts.map +0 -1
  29. package/dist/commands/packages/show.js +0 -109
  30. package/dist/commands/packages/show.js.map +0 -1
  31. package/dist/commands/search.d.ts +0 -12
  32. package/dist/commands/search.d.ts.map +0 -1
  33. package/dist/commands/search.js +0 -144
  34. package/dist/commands/search.js.map +0 -1
  35. package/dist/commands/skills/index.d.ts +0 -8
  36. package/dist/commands/skills/index.d.ts.map +0 -1
  37. package/dist/commands/skills/index.js +0 -8
  38. package/dist/commands/skills/index.js.map +0 -1
  39. package/dist/commands/skills/install.d.ts +0 -9
  40. package/dist/commands/skills/install.d.ts.map +0 -1
  41. package/dist/commands/skills/install.js +0 -110
  42. package/dist/commands/skills/install.js.map +0 -1
  43. package/dist/commands/skills/list.d.ts +0 -8
  44. package/dist/commands/skills/list.d.ts.map +0 -1
  45. package/dist/commands/skills/list.js +0 -89
  46. package/dist/commands/skills/list.js.map +0 -1
  47. package/dist/commands/skills/pack.d.ts +0 -22
  48. package/dist/commands/skills/pack.d.ts.map +0 -1
  49. package/dist/commands/skills/pack.js +0 -116
  50. package/dist/commands/skills/pack.js.map +0 -1
  51. package/dist/commands/skills/pull.d.ts +0 -9
  52. package/dist/commands/skills/pull.d.ts.map +0 -1
  53. package/dist/commands/skills/pull.js +0 -68
  54. package/dist/commands/skills/pull.js.map +0 -1
  55. package/dist/commands/skills/search.d.ts +0 -14
  56. package/dist/commands/skills/search.d.ts.map +0 -1
  57. package/dist/commands/skills/search.js +0 -53
  58. package/dist/commands/skills/search.js.map +0 -1
  59. package/dist/commands/skills/show.d.ts +0 -8
  60. package/dist/commands/skills/show.d.ts.map +0 -1
  61. package/dist/commands/skills/show.js +0 -64
  62. package/dist/commands/skills/show.js.map +0 -1
  63. package/dist/commands/skills/validate.d.ts +0 -25
  64. package/dist/commands/skills/validate.d.ts.map +0 -1
  65. package/dist/commands/skills/validate.js +0 -191
  66. package/dist/commands/skills/validate.js.map +0 -1
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/lib/api/registry-client.d.ts +0 -63
  69. package/dist/lib/api/registry-client.d.ts.map +0 -1
  70. package/dist/lib/api/registry-client.js +0 -167
  71. package/dist/lib/api/registry-client.js.map +0 -1
  72. package/dist/lib/api/skills-client.d.ts +0 -30
  73. package/dist/lib/api/skills-client.d.ts.map +0 -1
  74. package/dist/lib/api/skills-client.js +0 -110
  75. package/dist/lib/api/skills-client.js.map +0 -1
  76. package/dist/program.d.ts +0 -12
  77. package/dist/program.d.ts.map +0 -1
  78. package/dist/program.js +0 -186
  79. package/dist/program.js.map +0 -1
  80. package/dist/schemas/generated/api-responses.d.ts +0 -541
  81. package/dist/schemas/generated/api-responses.d.ts.map +0 -1
  82. package/dist/schemas/generated/api-responses.js +0 -313
  83. package/dist/schemas/generated/api-responses.js.map +0 -1
  84. package/dist/schemas/generated/auth.d.ts +0 -18
  85. package/dist/schemas/generated/auth.d.ts.map +0 -1
  86. package/dist/schemas/generated/auth.js +0 -18
  87. package/dist/schemas/generated/auth.js.map +0 -1
  88. package/dist/schemas/generated/index.d.ts +0 -5
  89. package/dist/schemas/generated/index.d.ts.map +0 -1
  90. package/dist/schemas/generated/index.js +0 -6
  91. package/dist/schemas/generated/index.js.map +0 -1
  92. package/dist/schemas/generated/package.d.ts +0 -43
  93. package/dist/schemas/generated/package.d.ts.map +0 -1
  94. package/dist/schemas/generated/package.js +0 -20
  95. package/dist/schemas/generated/package.js.map +0 -1
  96. package/dist/schemas/generated/skill.d.ts +0 -381
  97. package/dist/schemas/generated/skill.d.ts.map +0 -1
  98. package/dist/schemas/generated/skill.js +0 -216
  99. package/dist/schemas/generated/skill.js.map +0 -1
  100. package/dist/utils/config-manager.d.ts +0 -66
  101. package/dist/utils/config-manager.d.ts.map +0 -1
  102. package/dist/utils/config-manager.js +0 -193
  103. package/dist/utils/config-manager.js.map +0 -1
  104. package/dist/utils/errors.d.ts +0 -12
  105. package/dist/utils/errors.d.ts.map +0 -1
  106. package/dist/utils/errors.js +0 -27
  107. package/dist/utils/errors.js.map +0 -1
  108. package/dist/utils/version.d.ts +0 -5
  109. package/dist/utils/version.d.ts.map +0 -1
  110. package/dist/utils/version.js +0 -19
  111. package/dist/utils/version.js.map +0 -1
  112. package/eslint.config.js +0 -63
  113. package/src/commands/config.ts +0 -162
  114. package/src/commands/packages/pull.ts +0 -96
  115. package/src/commands/packages/run.test.ts +0 -261
  116. package/src/commands/packages/run.ts +0 -536
  117. package/src/commands/packages/search.ts +0 -83
  118. package/src/commands/packages/show.ts +0 -128
  119. package/src/commands/search.ts +0 -191
  120. package/src/commands/skills/index.ts +0 -7
  121. package/src/commands/skills/install.ts +0 -129
  122. package/src/commands/skills/list.ts +0 -116
  123. package/src/commands/skills/pack.test.ts +0 -260
  124. package/src/commands/skills/pack.ts +0 -145
  125. package/src/commands/skills/pull.ts +0 -88
  126. package/src/commands/skills/search.ts +0 -73
  127. package/src/commands/skills/show.ts +0 -72
  128. package/src/commands/skills/validate.test.ts +0 -466
  129. package/src/commands/skills/validate.ts +0 -227
  130. package/src/index.ts +0 -11
  131. package/src/lib/api/registry-client.ts +0 -223
  132. package/src/lib/api/schema.d.ts +0 -520
  133. package/src/lib/api/skills-client.ts +0 -148
  134. package/src/program.test.ts +0 -22
  135. package/src/program.ts +0 -226
  136. package/src/schemas/config.v1.schema.json +0 -37
  137. package/src/schemas/generated/api-responses.ts +0 -386
  138. package/src/schemas/generated/auth.ts +0 -21
  139. package/src/schemas/generated/index.ts +0 -5
  140. package/src/schemas/generated/package.ts +0 -29
  141. package/src/schemas/generated/skill.ts +0 -271
  142. package/src/utils/config-manager.test.ts +0 -330
  143. package/src/utils/config-manager.ts +0 -272
  144. package/src/utils/errors.test.ts +0 -25
  145. package/src/utils/errors.ts +0 -33
  146. package/src/utils/version.test.ts +0 -16
  147. package/src/utils/version.ts +0 -18
  148. package/test/integration/registry-client.test.ts +0 -180
  149. package/tsconfig.check.json +0 -9
  150. package/tsconfig.json +0 -25
  151. package/vitest.config.ts +0 -14
@@ -1,536 +0,0 @@
1
- import { spawn, spawnSync } from 'child_process';
2
- import { createInterface } from 'readline';
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, rmSync, statSync } from 'fs';
4
- import { createHash } from 'crypto';
5
- import { homedir } from 'os';
6
- import { join, dirname, resolve, basename } from 'path';
7
- import { RegistryClient } from '../../lib/api/registry-client.js';
8
- import { ConfigManager } from '../../utils/config-manager.js';
9
-
10
- export interface RunOptions {
11
- update?: boolean;
12
- local?: string; // Path to local .mcpb file
13
- }
14
-
15
- interface McpConfig {
16
- command: string;
17
- args: string[];
18
- env?: Record<string, string>;
19
- }
20
-
21
- /**
22
- * User configuration field definition (MCPB v0.3 spec)
23
- */
24
- interface UserConfigField {
25
- type: 'string' | 'number' | 'boolean';
26
- title?: string;
27
- description?: string;
28
- sensitive?: boolean;
29
- required?: boolean;
30
- default?: string | number | boolean;
31
- }
32
-
33
- interface McpbManifest {
34
- manifest_version: string;
35
- name: string;
36
- version: string;
37
- description: string;
38
- user_config?: Record<string, UserConfigField>;
39
- server: {
40
- type: 'node' | 'python' | 'binary';
41
- entry_point: string;
42
- mcp_config: McpConfig;
43
- };
44
- }
45
-
46
- interface CacheMetadata {
47
- version: string;
48
- pulledAt: string;
49
- platform: { os: string; arch: string };
50
- }
51
-
52
- /**
53
- * Parse package specification into name and version
54
- * @example parsePackageSpec('@scope/name') => { name: '@scope/name' }
55
- * @example parsePackageSpec('@scope/name@1.0.0') => { name: '@scope/name', version: '1.0.0' }
56
- */
57
- export function parsePackageSpec(spec: string): { name: string; version?: string } {
58
- const lastAtIndex = spec.lastIndexOf('@');
59
-
60
- if (lastAtIndex <= 0) {
61
- return { name: spec };
62
- }
63
-
64
- const name = spec.substring(0, lastAtIndex);
65
- const version = spec.substring(lastAtIndex + 1);
66
-
67
- if (!name.startsWith('@')) {
68
- return { name: spec };
69
- }
70
-
71
- return { name, version };
72
- }
73
-
74
- /**
75
- * Get cache directory for a package
76
- * @example getCacheDir('@scope/name') => '~/.mpak/cache/scope-name'
77
- */
78
- export function getCacheDir(packageName: string): string {
79
- const cacheBase = join(homedir(), '.mpak', 'cache');
80
- // @scope/name -> scope/name
81
- const safeName = packageName.replace('@', '').replace('/', '-');
82
- return join(cacheBase, safeName);
83
- }
84
-
85
- /**
86
- * Read cache metadata
87
- */
88
- function getCacheMetadata(cacheDir: string): CacheMetadata | null {
89
- const metaPath = join(cacheDir, '.mpak-meta.json');
90
- if (!existsSync(metaPath)) {
91
- return null;
92
- }
93
- try {
94
- return JSON.parse(readFileSync(metaPath, 'utf8'));
95
- } catch {
96
- return null;
97
- }
98
- }
99
-
100
- /**
101
- * Write cache metadata
102
- */
103
- function writeCacheMetadata(cacheDir: string, metadata: CacheMetadata): void {
104
- const metaPath = join(cacheDir, '.mpak-meta.json');
105
- writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
106
- }
107
-
108
- /**
109
- * Extract ZIP file to directory (simple implementation without external deps)
110
- */
111
- async function extractZip(zipPath: string, destDir: string): Promise<void> {
112
- // Use native unzip command (available on macOS, Linux, and Windows with WSL)
113
- const { execSync } = await import('child_process');
114
-
115
- // Ensure destination exists
116
- mkdirSync(destDir, { recursive: true });
117
-
118
- try {
119
- execSync(`unzip -o -q "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
120
- } catch (error: any) {
121
- throw new Error(`Failed to extract bundle: ${error.message}`);
122
- }
123
- }
124
-
125
- /**
126
- * Read manifest from extracted bundle
127
- */
128
- function readManifest(cacheDir: string): McpbManifest {
129
- const manifestPath = join(cacheDir, 'manifest.json');
130
- if (!existsSync(manifestPath)) {
131
- throw new Error(`Manifest not found in bundle: ${manifestPath}`);
132
- }
133
- return JSON.parse(readFileSync(manifestPath, 'utf8'));
134
- }
135
-
136
- /**
137
- * Resolve placeholders in args (e.g., ${__dirname})
138
- * @example resolveArgs(['${__dirname}/index.js'], '/cache') => ['/cache/index.js']
139
- */
140
- export function resolveArgs(args: string[], cacheDir: string): string[] {
141
- return args.map(arg =>
142
- arg.replace(/\$\{__dirname\}/g, cacheDir)
143
- );
144
- }
145
-
146
- /**
147
- * Substitute ${user_config.*} placeholders in a string
148
- * @example substituteUserConfig('${user_config.api_key}', { api_key: 'secret' }) => 'secret'
149
- */
150
- export function substituteUserConfig(
151
- value: string,
152
- userConfigValues: Record<string, string>
153
- ): string {
154
- return value.replace(/\$\{user_config\.([^}]+)\}/g, (match, key) => {
155
- return userConfigValues[key] ?? match;
156
- });
157
- }
158
-
159
- /**
160
- * Substitute ${user_config.*} placeholders in env vars
161
- */
162
- export function substituteEnvVars(
163
- env: Record<string, string> | undefined,
164
- userConfigValues: Record<string, string>
165
- ): Record<string, string> {
166
- if (!env) return {};
167
- const result: Record<string, string> = {};
168
- for (const [key, value] of Object.entries(env)) {
169
- result[key] = substituteUserConfig(value, userConfigValues);
170
- }
171
- return result;
172
- }
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
-
201
- /**
202
- * Prompt user for a config value (interactive terminal input)
203
- */
204
- async function promptForValue(
205
- field: UserConfigField,
206
- key: string
207
- ): Promise<string> {
208
- return new Promise((resolve) => {
209
- const rl = createInterface({
210
- input: process.stdin,
211
- output: process.stderr,
212
- terminal: true,
213
- });
214
-
215
- const label = field.title || key;
216
- const hint = field.description ? ` (${field.description})` : '';
217
- const defaultHint = field.default !== undefined ? ` [${field.default}]` : '';
218
- const prompt = `=> ${label}${hint}${defaultHint}: `;
219
-
220
- // For sensitive fields, we'd ideally hide input, but Node's readline
221
- // doesn't support this natively. We'll just note it's sensitive.
222
- if (field.sensitive) {
223
- process.stderr.write(`=> (sensitive input)\n`);
224
- }
225
-
226
- rl.question(prompt, (answer) => {
227
- rl.close();
228
- // Use default if empty and default exists
229
- if (!answer && field.default !== undefined) {
230
- resolve(String(field.default));
231
- } else {
232
- resolve(answer);
233
- }
234
- });
235
- });
236
- }
237
-
238
- /**
239
- * Check if we're in an interactive terminal
240
- */
241
- function isInteractive(): boolean {
242
- return process.stdin.isTTY === true;
243
- }
244
-
245
- /**
246
- * Gather user config values from stored config
247
- * Prompts for missing required values if interactive
248
- */
249
- async function gatherUserConfigValues(
250
- packageName: string,
251
- userConfig: Record<string, UserConfigField>,
252
- configManager: ConfigManager
253
- ): Promise<Record<string, string>> {
254
- const result: Record<string, string> = {};
255
- const storedConfig = configManager.getPackageConfig(packageName) || {};
256
- const missingRequired: Array<{ key: string; field: UserConfigField }> = [];
257
-
258
- for (const [key, field] of Object.entries(userConfig)) {
259
- // Priority: 1) stored config, 2) default value
260
- const storedValue = storedConfig[key];
261
-
262
- if (storedValue !== undefined) {
263
- result[key] = storedValue;
264
- } else if (field.default !== undefined) {
265
- result[key] = String(field.default);
266
- } else if (field.required) {
267
- missingRequired.push({ key, field });
268
- }
269
- }
270
-
271
- // Prompt for missing required values if interactive
272
- if (missingRequired.length > 0) {
273
- if (!isInteractive()) {
274
- const missingKeys = missingRequired.map(m => m.key).join(', ');
275
- process.stderr.write(`=> Error: Missing required config: ${missingKeys}\n`);
276
- process.stderr.write(`=> Run 'mpak config set ${packageName} <key>=<value>' to set values\n`);
277
- process.exit(1);
278
- }
279
-
280
- process.stderr.write(`=> Package requires configuration:\n`);
281
- for (const { key, field } of missingRequired) {
282
- const value = await promptForValue(field, key);
283
- if (!value && field.required) {
284
- process.stderr.write(`=> Error: ${field.title || key} is required\n`);
285
- process.exit(1);
286
- }
287
- result[key] = value;
288
-
289
- // Offer to save the value
290
- if (value) {
291
- const rl = createInterface({
292
- input: process.stdin,
293
- output: process.stderr,
294
- terminal: true,
295
- });
296
- await new Promise<void>((resolve) => {
297
- rl.question(`=> Save ${field.title || key} for future runs? [Y/n]: `, (answer) => {
298
- rl.close();
299
- if (answer.toLowerCase() !== 'n') {
300
- configManager.setPackageConfigValue(packageName, key, value);
301
- process.stderr.write(`=> Saved to ~/.mpak/config.json\n`);
302
- }
303
- resolve();
304
- });
305
- });
306
- }
307
- }
308
- }
309
-
310
- return result;
311
- }
312
-
313
- /**
314
- * Find Python executable (tries python3 first, then python)
315
- */
316
- function findPythonCommand(): string {
317
- // Try python3 first (preferred on macOS/Linux)
318
- const result = spawnSync('python3', ['--version'], { stdio: 'pipe' });
319
- if (result.status === 0) {
320
- return 'python3';
321
- }
322
- // Fall back to python
323
- return 'python';
324
- }
325
-
326
- /**
327
- * Run a package from the registry or a local bundle file
328
- */
329
- export async function handleRun(
330
- packageSpec: string,
331
- options: RunOptions = {}
332
- ): Promise<void> {
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);
337
- }
338
-
339
- let cacheDir: string;
340
- let packageName: string;
341
-
342
- if (options.local) {
343
- // === LOCAL BUNDLE MODE ===
344
- const bundlePath = resolve(options.local);
345
-
346
- // Validate bundle exists
347
- if (!existsSync(bundlePath)) {
348
- process.stderr.write(`=> Error: Bundle not found: ${bundlePath}\n`);
349
- process.exit(1);
350
- }
351
-
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
- }
357
-
358
- cacheDir = getLocalCacheDir(bundlePath);
359
- const needsExtract = options.update || localBundleNeedsExtract(bundlePath, cacheDir);
360
-
361
- if (needsExtract) {
362
- // Clear old extraction
363
- if (existsSync(cacheDir)) {
364
- rmSync(cacheDir, { recursive: true, force: true });
365
- }
366
- mkdirSync(cacheDir, { recursive: true });
367
-
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
- }
380
-
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;
412
-
413
- // Check if cached version is already the latest
414
- if (cachedMeta && cachedMeta.version === bundle.version && !options.update) {
415
- needsPull = false;
416
- }
417
-
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
- }
446
- }
447
- }
448
-
449
- // Read manifest and execute
450
- const manifest = readManifest(cacheDir);
451
- const { type, entry_point, mcp_config } = manifest.server;
452
-
453
- // Handle user_config substitution
454
- let userConfigValues: Record<string, string> = {};
455
- if (manifest.user_config && Object.keys(manifest.user_config).length > 0) {
456
- const configManager = new ConfigManager();
457
- userConfigValues = await gatherUserConfigValues(packageName, manifest.user_config, configManager);
458
- }
459
-
460
- // Substitute user_config placeholders in env vars
461
- // Priority: process.env (from parent like Claude Desktop) > substituted values (from mpak config)
462
- const substitutedEnv = substituteEnvVars(mcp_config.env, userConfigValues);
463
-
464
- let command: string;
465
- let args: string[];
466
- let env: Record<string, string | undefined> = { ...substitutedEnv, ...process.env };
467
-
468
- switch (type) {
469
- case 'binary': {
470
- // For binary, the entry_point is the executable path relative to bundle
471
- command = join(cacheDir, entry_point);
472
- args = resolveArgs(mcp_config.args || [], cacheDir);
473
-
474
- // Ensure binary is executable
475
- try {
476
- chmodSync(command, 0o755);
477
- } catch {
478
- // Ignore chmod errors on Windows
479
- }
480
- break;
481
- }
482
-
483
- case 'node': {
484
- command = mcp_config.command || 'node';
485
- // Use mcp_config.args directly if provided, otherwise fall back to entry_point
486
- if (mcp_config.args && mcp_config.args.length > 0) {
487
- args = resolveArgs(mcp_config.args, cacheDir);
488
- } else {
489
- args = [join(cacheDir, entry_point)];
490
- }
491
- break;
492
- }
493
-
494
- case 'python': {
495
- // Use manifest command if specified, otherwise auto-detect python
496
- command = mcp_config.command === 'python' ? findPythonCommand() : (mcp_config.command || findPythonCommand());
497
-
498
- // Use mcp_config.args directly if provided, otherwise fall back to entry_point
499
- if (mcp_config.args && mcp_config.args.length > 0) {
500
- args = resolveArgs(mcp_config.args, cacheDir);
501
- } else {
502
- args = [join(cacheDir, entry_point)];
503
- }
504
-
505
- // Set PYTHONPATH to deps/ directory for dependency resolution
506
- const depsDir = join(cacheDir, 'deps');
507
- const existingPythonPath = process.env.PYTHONPATH;
508
- env.PYTHONPATH = existingPythonPath ? `${depsDir}:${existingPythonPath}` : depsDir;
509
- break;
510
- }
511
-
512
- default:
513
- throw new Error(`Unsupported server type: ${type}`);
514
- }
515
-
516
- // Spawn with stdio passthrough for MCP
517
- const child = spawn(command, args, {
518
- stdio: ['inherit', 'inherit', 'inherit'],
519
- env,
520
- cwd: cacheDir,
521
- });
522
-
523
- // Forward signals
524
- process.on('SIGINT', () => child.kill('SIGINT'));
525
- process.on('SIGTERM', () => child.kill('SIGTERM'));
526
-
527
- // Wait for exit
528
- child.on('exit', (code) => {
529
- process.exit(code ?? 0);
530
- });
531
-
532
- child.on('error', (error) => {
533
- process.stderr.write(`=> Failed to start server: ${error.message}\n`);
534
- process.exit(1);
535
- });
536
- }
@@ -1,83 +0,0 @@
1
- import { RegistryClient } from '../../lib/api/registry-client.js';
2
-
3
- export interface SearchOptions {
4
- type?: string;
5
- sort?: 'downloads' | 'recent' | 'name';
6
- limit?: number;
7
- offset?: number;
8
- json?: boolean;
9
- }
10
-
11
- /**
12
- * Search bundles (v1 API)
13
- */
14
- export async function handleSearch(
15
- query: string,
16
- options: SearchOptions = {}
17
- ): Promise<void> {
18
- try {
19
- const client = new RegistryClient();
20
- const result = await client.searchBundles(query, {
21
- type: options.type,
22
- sort: options.sort,
23
- limit: options.limit,
24
- offset: options.offset,
25
- });
26
-
27
- if (options.json) {
28
- console.log(JSON.stringify(result, null, 2));
29
- return;
30
- }
31
-
32
- if (result.bundles.length === 0) {
33
- console.log(`\nNo bundles found for "${query}"`);
34
- return;
35
- }
36
-
37
- console.log(`\nFound ${result.total} bundle(s) for "${query}":\n`);
38
-
39
- for (const bundle of result.bundles) {
40
- const verified = bundle.verified ? '✓' : ' ';
41
- const provenance = bundle.provenance ? '🔒' : '';
42
-
43
- console.log(`${verified} ${bundle.name} v${bundle.latest_version} ${provenance}`);
44
-
45
- if (bundle.description) {
46
- console.log(` ${bundle.description}`);
47
- }
48
-
49
- const details = [];
50
- if (bundle.downloads) details.push(`${bundle.downloads} downloads`);
51
- if (bundle.server_type) details.push(bundle.server_type);
52
- if (bundle.author?.name) details.push(`by ${bundle.author.name}`);
53
-
54
- if (details.length > 0) {
55
- console.log(` ${details.join(' • ')}`);
56
- }
57
-
58
- if (bundle.tools && bundle.tools.length > 0) {
59
- const toolNames = bundle.tools.slice(0, 3).map((t) => t.name);
60
- const toolsDisplay =
61
- bundle.tools.length > 3
62
- ? `${toolNames.join(', ')} +${bundle.tools.length - 3} more`
63
- : toolNames.join(', ');
64
- console.log(` Tools: ${toolsDisplay}`);
65
- }
66
-
67
- console.log();
68
- }
69
-
70
- if (result.pagination.has_more) {
71
- const nextOffset = (options.offset || 0) + (options.limit || 20);
72
- console.log(`More results available. Use --offset ${nextOffset} to see more.`);
73
- }
74
-
75
- console.log(`Use "mpak show <bundle>" for more details`);
76
- } catch (error) {
77
- console.error('=> Failed to search bundles');
78
- if (error instanceof Error) {
79
- console.error(` ${error.message}`);
80
- }
81
- process.exit(1);
82
- }
83
- }