@nimblebrain/mpak 0.0.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.
Files changed (151) hide show
  1. package/LICENSE +10 -198
  2. package/README.md +50 -360
  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 -271
  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 -36
  20. package/dist/commands/packages/run.d.ts.map +0 -1
  21. package/dist/commands/packages/run.js +0 -348
  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 -174
  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 -222
  116. package/src/commands/packages/run.ts +0 -451
  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 -212
  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,222 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { homedir } from 'os';
3
- import { join } from 'path';
4
- import { parsePackageSpec, getCacheDir, resolveArgs, substituteUserConfig, substituteEnvVars } from './run.js';
5
-
6
- describe('parsePackageSpec', () => {
7
- describe('scoped packages', () => {
8
- it('parses @scope/name without version', () => {
9
- expect(parsePackageSpec('@scope/name')).toEqual({
10
- name: '@scope/name',
11
- });
12
- });
13
-
14
- it('parses @scope/name@1.0.0', () => {
15
- expect(parsePackageSpec('@scope/name@1.0.0')).toEqual({
16
- name: '@scope/name',
17
- version: '1.0.0',
18
- });
19
- });
20
-
21
- it('parses prerelease versions @scope/name@1.0.0-beta.1', () => {
22
- expect(parsePackageSpec('@scope/name@1.0.0-beta.1')).toEqual({
23
- name: '@scope/name',
24
- version: '1.0.0-beta.1',
25
- });
26
- });
27
-
28
- it('parses version with build metadata @scope/name@1.0.0+build.123', () => {
29
- expect(parsePackageSpec('@scope/name@1.0.0+build.123')).toEqual({
30
- name: '@scope/name',
31
- version: '1.0.0+build.123',
32
- });
33
- });
34
- });
35
-
36
- describe('edge cases', () => {
37
- it('handles package name with multiple slashes @org/sub/name', () => {
38
- // This is technically invalid per npm spec, but we should handle gracefully
39
- const result = parsePackageSpec('@org/sub/name');
40
- expect(result.name).toBe('@org/sub/name');
41
- });
42
-
43
- it('handles unscoped package name', () => {
44
- expect(parsePackageSpec('simple-name')).toEqual({
45
- name: 'simple-name',
46
- });
47
- });
48
-
49
- it('treats unscoped@version as invalid (mpak requires scoped packages)', () => {
50
- // mpak only supports scoped packages (@scope/name)
51
- // An unscoped name with @ is treated as the full name, not name@version
52
- expect(parsePackageSpec('unscoped@1.0.0')).toEqual({
53
- name: 'unscoped@1.0.0',
54
- });
55
- });
56
-
57
- it('handles empty string', () => {
58
- expect(parsePackageSpec('')).toEqual({ name: '' });
59
- });
60
-
61
- it('handles @ only', () => {
62
- expect(parsePackageSpec('@')).toEqual({ name: '@' });
63
- });
64
- });
65
- });
66
-
67
- describe('getCacheDir', () => {
68
- const expectedBase = join(homedir(), '.mpak', 'cache');
69
-
70
- it('converts @scope/name to scope-name', () => {
71
- expect(getCacheDir('@nimblebraininc/echo')).toBe(
72
- join(expectedBase, 'nimblebraininc-echo')
73
- );
74
- });
75
-
76
- it('handles simple scoped names', () => {
77
- expect(getCacheDir('@foo/bar')).toBe(join(expectedBase, 'foo-bar'));
78
- });
79
-
80
- it('handles unscoped names', () => {
81
- expect(getCacheDir('simple')).toBe(join(expectedBase, 'simple'));
82
- });
83
- });
84
-
85
- describe('resolveArgs', () => {
86
- const cacheDir = '/Users/test/.mpak/cache/scope-name';
87
-
88
- it('resolves ${__dirname} placeholder', () => {
89
- expect(resolveArgs(['${__dirname}/dist/index.js'], cacheDir)).toEqual([
90
- `${cacheDir}/dist/index.js`,
91
- ]);
92
- });
93
-
94
- it('resolves multiple ${__dirname} in single arg', () => {
95
- expect(
96
- resolveArgs(['--config=${__dirname}/config.json'], cacheDir)
97
- ).toEqual([`--config=${cacheDir}/config.json`]);
98
- });
99
-
100
- it('resolves ${__dirname} in multiple args', () => {
101
- expect(
102
- resolveArgs(
103
- ['${__dirname}/index.js', '--config', '${__dirname}/config.json'],
104
- cacheDir
105
- )
106
- ).toEqual([
107
- `${cacheDir}/index.js`,
108
- '--config',
109
- `${cacheDir}/config.json`,
110
- ]);
111
- });
112
-
113
- it('leaves args without placeholders unchanged', () => {
114
- expect(resolveArgs(['-m', 'mcp_echo.server'], cacheDir)).toEqual([
115
- '-m',
116
- 'mcp_echo.server',
117
- ]);
118
- });
119
-
120
- it('handles empty args array', () => {
121
- expect(resolveArgs([], cacheDir)).toEqual([]);
122
- });
123
-
124
- it('handles Windows-style paths in cacheDir', () => {
125
- const winPath = 'C:\\Users\\test\\.mpak\\cache\\scope-name';
126
- expect(resolveArgs(['${__dirname}\\dist\\index.js'], winPath)).toEqual([
127
- `${winPath}\\dist\\index.js`,
128
- ]);
129
- });
130
- });
131
-
132
- describe('substituteUserConfig', () => {
133
- it('substitutes single user_config variable', () => {
134
- expect(
135
- substituteUserConfig('${user_config.api_key}', { api_key: 'secret123' })
136
- ).toBe('secret123');
137
- });
138
-
139
- it('substitutes multiple user_config variables', () => {
140
- expect(
141
- substituteUserConfig('key=${user_config.key}&secret=${user_config.secret}', {
142
- key: 'mykey',
143
- secret: 'mysecret',
144
- })
145
- ).toBe('key=mykey&secret=mysecret');
146
- });
147
-
148
- it('leaves unmatched variables unchanged', () => {
149
- expect(
150
- substituteUserConfig('${user_config.missing}', { other: 'value' })
151
- ).toBe('${user_config.missing}');
152
- });
153
-
154
- it('handles mixed matched and unmatched variables', () => {
155
- expect(
156
- substituteUserConfig('${user_config.found}-${user_config.missing}', {
157
- found: 'yes',
158
- })
159
- ).toBe('yes-${user_config.missing}');
160
- });
161
-
162
- it('handles empty config values', () => {
163
- expect(
164
- substituteUserConfig('${user_config.empty}', { empty: '' })
165
- ).toBe('');
166
- });
167
-
168
- it('handles values with special characters', () => {
169
- expect(
170
- substituteUserConfig('${user_config.key}', { key: 'abc$def{ghi}' })
171
- ).toBe('abc$def{ghi}');
172
- });
173
-
174
- it('leaves non-user_config placeholders unchanged', () => {
175
- expect(
176
- substituteUserConfig('${__dirname}/path', { dirname: '/cache' })
177
- ).toBe('${__dirname}/path');
178
- });
179
- });
180
-
181
- describe('substituteEnvVars', () => {
182
- it('substitutes user_config in all env vars', () => {
183
- const env = {
184
- API_KEY: '${user_config.api_key}',
185
- DEBUG: 'true',
186
- TOKEN: '${user_config.token}',
187
- };
188
- const values = { api_key: 'key123', token: 'tok456' };
189
-
190
- expect(substituteEnvVars(env, values)).toEqual({
191
- API_KEY: 'key123',
192
- DEBUG: 'true',
193
- TOKEN: 'tok456',
194
- });
195
- });
196
-
197
- it('handles undefined env', () => {
198
- expect(substituteEnvVars(undefined, { key: 'value' })).toEqual({});
199
- });
200
-
201
- it('handles empty env', () => {
202
- expect(substituteEnvVars({}, { key: 'value' })).toEqual({});
203
- });
204
-
205
- it('preserves env vars without placeholders', () => {
206
- const env = { PATH: '/usr/bin', HOME: '/home/user' };
207
- expect(substituteEnvVars(env, {})).toEqual(env);
208
- });
209
-
210
- it('leaves unsubstituted placeholders as-is', () => {
211
- const env = {
212
- API_KEY: '${user_config.api_key}',
213
- DEBUG: 'true',
214
- };
215
- // api_key not provided, so placeholder remains
216
- // (process.env will override this at merge time)
217
- expect(substituteEnvVars(env, {})).toEqual({
218
- API_KEY: '${user_config.api_key}',
219
- DEBUG: 'true',
220
- });
221
- });
222
- });
@@ -1,451 +0,0 @@
1
- import { spawn, spawnSync } from 'child_process';
2
- import { createInterface } from 'readline';
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
4
- import { homedir } from 'os';
5
- import { join, dirname } from 'path';
6
- import { RegistryClient } from '../../lib/api/registry-client.js';
7
- import { ConfigManager } from '../../utils/config-manager.js';
8
-
9
- export interface RunOptions {
10
- update?: boolean;
11
- }
12
-
13
- interface McpConfig {
14
- command: string;
15
- args: string[];
16
- env?: Record<string, string>;
17
- }
18
-
19
- /**
20
- * User configuration field definition (MCPB v0.3 spec)
21
- */
22
- interface UserConfigField {
23
- type: 'string' | 'number' | 'boolean';
24
- title?: string;
25
- description?: string;
26
- sensitive?: boolean;
27
- required?: boolean;
28
- default?: string | number | boolean;
29
- }
30
-
31
- interface McpbManifest {
32
- manifest_version: string;
33
- name: string;
34
- version: string;
35
- description: string;
36
- user_config?: Record<string, UserConfigField>;
37
- server: {
38
- type: 'node' | 'python' | 'binary';
39
- entry_point: string;
40
- mcp_config: McpConfig;
41
- };
42
- }
43
-
44
- interface CacheMetadata {
45
- version: string;
46
- pulledAt: string;
47
- platform: { os: string; arch: string };
48
- }
49
-
50
- /**
51
- * Parse package specification into name and version
52
- * @example parsePackageSpec('@scope/name') => { name: '@scope/name' }
53
- * @example parsePackageSpec('@scope/name@1.0.0') => { name: '@scope/name', version: '1.0.0' }
54
- */
55
- export function parsePackageSpec(spec: string): { name: string; version?: string } {
56
- const lastAtIndex = spec.lastIndexOf('@');
57
-
58
- if (lastAtIndex <= 0) {
59
- return { name: spec };
60
- }
61
-
62
- const name = spec.substring(0, lastAtIndex);
63
- const version = spec.substring(lastAtIndex + 1);
64
-
65
- if (!name.startsWith('@')) {
66
- return { name: spec };
67
- }
68
-
69
- return { name, version };
70
- }
71
-
72
- /**
73
- * Get cache directory for a package
74
- * @example getCacheDir('@scope/name') => '~/.mpak/cache/scope-name'
75
- */
76
- export function getCacheDir(packageName: string): string {
77
- const cacheBase = join(homedir(), '.mpak', 'cache');
78
- // @scope/name -> scope/name
79
- const safeName = packageName.replace('@', '').replace('/', '-');
80
- return join(cacheBase, safeName);
81
- }
82
-
83
- /**
84
- * Read cache metadata
85
- */
86
- function getCacheMetadata(cacheDir: string): CacheMetadata | null {
87
- const metaPath = join(cacheDir, '.mpak-meta.json');
88
- if (!existsSync(metaPath)) {
89
- return null;
90
- }
91
- try {
92
- return JSON.parse(readFileSync(metaPath, 'utf8'));
93
- } catch {
94
- return null;
95
- }
96
- }
97
-
98
- /**
99
- * Write cache metadata
100
- */
101
- function writeCacheMetadata(cacheDir: string, metadata: CacheMetadata): void {
102
- const metaPath = join(cacheDir, '.mpak-meta.json');
103
- writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
104
- }
105
-
106
- /**
107
- * Extract ZIP file to directory (simple implementation without external deps)
108
- */
109
- async function extractZip(zipPath: string, destDir: string): Promise<void> {
110
- // Use native unzip command (available on macOS, Linux, and Windows with WSL)
111
- const { execSync } = await import('child_process');
112
-
113
- // Ensure destination exists
114
- mkdirSync(destDir, { recursive: true });
115
-
116
- try {
117
- execSync(`unzip -o -q "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
118
- } catch (error: any) {
119
- throw new Error(`Failed to extract bundle: ${error.message}`);
120
- }
121
- }
122
-
123
- /**
124
- * Read manifest from extracted bundle
125
- */
126
- function readManifest(cacheDir: string): McpbManifest {
127
- const manifestPath = join(cacheDir, 'manifest.json');
128
- if (!existsSync(manifestPath)) {
129
- throw new Error(`Manifest not found in bundle: ${manifestPath}`);
130
- }
131
- return JSON.parse(readFileSync(manifestPath, 'utf8'));
132
- }
133
-
134
- /**
135
- * Resolve placeholders in args (e.g., ${__dirname})
136
- * @example resolveArgs(['${__dirname}/index.js'], '/cache') => ['/cache/index.js']
137
- */
138
- export function resolveArgs(args: string[], cacheDir: string): string[] {
139
- return args.map(arg =>
140
- arg.replace(/\$\{__dirname\}/g, cacheDir)
141
- );
142
- }
143
-
144
- /**
145
- * Substitute ${user_config.*} placeholders in a string
146
- * @example substituteUserConfig('${user_config.api_key}', { api_key: 'secret' }) => 'secret'
147
- */
148
- export function substituteUserConfig(
149
- value: string,
150
- userConfigValues: Record<string, string>
151
- ): string {
152
- return value.replace(/\$\{user_config\.([^}]+)\}/g, (match, key) => {
153
- return userConfigValues[key] ?? match;
154
- });
155
- }
156
-
157
- /**
158
- * Substitute ${user_config.*} placeholders in env vars
159
- */
160
- export function substituteEnvVars(
161
- env: Record<string, string> | undefined,
162
- userConfigValues: Record<string, string>
163
- ): Record<string, string> {
164
- if (!env) return {};
165
- const result: Record<string, string> = {};
166
- for (const [key, value] of Object.entries(env)) {
167
- result[key] = substituteUserConfig(value, userConfigValues);
168
- }
169
- return result;
170
- }
171
-
172
- /**
173
- * Prompt user for a config value (interactive terminal input)
174
- */
175
- async function promptForValue(
176
- field: UserConfigField,
177
- key: string
178
- ): Promise<string> {
179
- return new Promise((resolve) => {
180
- const rl = createInterface({
181
- input: process.stdin,
182
- output: process.stderr,
183
- terminal: true,
184
- });
185
-
186
- const label = field.title || key;
187
- const hint = field.description ? ` (${field.description})` : '';
188
- const defaultHint = field.default !== undefined ? ` [${field.default}]` : '';
189
- const prompt = `=> ${label}${hint}${defaultHint}: `;
190
-
191
- // For sensitive fields, we'd ideally hide input, but Node's readline
192
- // doesn't support this natively. We'll just note it's sensitive.
193
- if (field.sensitive) {
194
- process.stderr.write(`=> (sensitive input)\n`);
195
- }
196
-
197
- rl.question(prompt, (answer) => {
198
- rl.close();
199
- // Use default if empty and default exists
200
- if (!answer && field.default !== undefined) {
201
- resolve(String(field.default));
202
- } else {
203
- resolve(answer);
204
- }
205
- });
206
- });
207
- }
208
-
209
- /**
210
- * Check if we're in an interactive terminal
211
- */
212
- function isInteractive(): boolean {
213
- return process.stdin.isTTY === true;
214
- }
215
-
216
- /**
217
- * Gather user config values from stored config
218
- * Prompts for missing required values if interactive
219
- */
220
- async function gatherUserConfigValues(
221
- packageName: string,
222
- userConfig: Record<string, UserConfigField>,
223
- configManager: ConfigManager
224
- ): Promise<Record<string, string>> {
225
- const result: Record<string, string> = {};
226
- const storedConfig = configManager.getPackageConfig(packageName) || {};
227
- const missingRequired: Array<{ key: string; field: UserConfigField }> = [];
228
-
229
- for (const [key, field] of Object.entries(userConfig)) {
230
- // Priority: 1) stored config, 2) default value
231
- const storedValue = storedConfig[key];
232
-
233
- if (storedValue !== undefined) {
234
- result[key] = storedValue;
235
- } else if (field.default !== undefined) {
236
- result[key] = String(field.default);
237
- } else if (field.required) {
238
- missingRequired.push({ key, field });
239
- }
240
- }
241
-
242
- // Prompt for missing required values if interactive
243
- if (missingRequired.length > 0) {
244
- if (!isInteractive()) {
245
- const missingKeys = missingRequired.map(m => m.key).join(', ');
246
- process.stderr.write(`=> Error: Missing required config: ${missingKeys}\n`);
247
- process.stderr.write(`=> Run 'mpak config set ${packageName} <key>=<value>' to set values\n`);
248
- process.exit(1);
249
- }
250
-
251
- process.stderr.write(`=> Package requires configuration:\n`);
252
- for (const { key, field } of missingRequired) {
253
- const value = await promptForValue(field, key);
254
- if (!value && field.required) {
255
- process.stderr.write(`=> Error: ${field.title || key} is required\n`);
256
- process.exit(1);
257
- }
258
- result[key] = value;
259
-
260
- // Offer to save the value
261
- if (value) {
262
- const rl = createInterface({
263
- input: process.stdin,
264
- output: process.stderr,
265
- terminal: true,
266
- });
267
- await new Promise<void>((resolve) => {
268
- rl.question(`=> Save ${field.title || key} for future runs? [Y/n]: `, (answer) => {
269
- rl.close();
270
- if (answer.toLowerCase() !== 'n') {
271
- configManager.setPackageConfigValue(packageName, key, value);
272
- process.stderr.write(`=> Saved to ~/.mpak/config.json\n`);
273
- }
274
- resolve();
275
- });
276
- });
277
- }
278
- }
279
- }
280
-
281
- return result;
282
- }
283
-
284
- /**
285
- * Find Python executable (tries python3 first, then python)
286
- */
287
- function findPythonCommand(): string {
288
- // Try python3 first (preferred on macOS/Linux)
289
- const result = spawnSync('python3', ['--version'], { stdio: 'pipe' });
290
- if (result.status === 0) {
291
- return 'python3';
292
- }
293
- // Fall back to python
294
- return 'python';
295
- }
296
-
297
- /**
298
- * Run a package from the registry
299
- */
300
- export async function handleRun(
301
- packageSpec: string,
302
- options: RunOptions = {}
303
- ): 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
- }
321
- }
322
-
323
- if (needsPull) {
324
- // Fetch download info
325
- const downloadInfo = await client.getDownloadInfo(name, requestedVersion, platform);
326
- const bundle = downloadInfo.bundle;
327
-
328
- // Check if cached version is already the latest
329
- if (cachedMeta && cachedMeta.version === bundle.version && !options.update) {
330
- needsPull = false;
331
- }
332
-
333
- if (needsPull) {
334
- // Download to temp file
335
- const tempPath = join(homedir(), '.mpak', 'tmp', `${Date.now()}.mcpb`);
336
- mkdirSync(dirname(tempPath), { recursive: true });
337
-
338
- process.stderr.write(`=> Pulling ${name}@${bundle.version}...\n`);
339
- await client.downloadBundle(downloadInfo.url, tempPath);
340
-
341
- // Clear old cache and extract
342
- const { rmSync } = await import('fs');
343
- if (existsSync(cacheDir)) {
344
- rmSync(cacheDir, { recursive: true, force: true });
345
- }
346
- mkdirSync(cacheDir, { recursive: true });
347
-
348
- await extractZip(tempPath, cacheDir);
349
-
350
- // Write metadata
351
- writeCacheMetadata(cacheDir, {
352
- version: bundle.version,
353
- pulledAt: new Date().toISOString(),
354
- platform: bundle.platform,
355
- });
356
-
357
- // Cleanup temp file
358
- rmSync(tempPath, { force: true });
359
-
360
- process.stderr.write(`=> Cached ${name}@${bundle.version}\n`);
361
- }
362
- }
363
-
364
- // Read manifest and execute
365
- const manifest = readManifest(cacheDir);
366
- const { type, entry_point, mcp_config } = manifest.server;
367
-
368
- // Handle user_config substitution
369
- let userConfigValues: Record<string, string> = {};
370
- if (manifest.user_config && Object.keys(manifest.user_config).length > 0) {
371
- const configManager = new ConfigManager();
372
- userConfigValues = await gatherUserConfigValues(name, manifest.user_config, configManager);
373
- }
374
-
375
- // Substitute user_config placeholders in env vars
376
- // Priority: process.env (from parent like Claude Desktop) > substituted values (from mpak config)
377
- const substitutedEnv = substituteEnvVars(mcp_config.env, userConfigValues);
378
-
379
- let command: string;
380
- let args: string[];
381
- let env: Record<string, string | undefined> = { ...substitutedEnv, ...process.env };
382
-
383
- switch (type) {
384
- case 'binary': {
385
- // For binary, the entry_point is the executable path relative to bundle
386
- command = join(cacheDir, entry_point);
387
- args = resolveArgs(mcp_config.args || [], cacheDir);
388
-
389
- // Ensure binary is executable
390
- try {
391
- chmodSync(command, 0o755);
392
- } catch {
393
- // Ignore chmod errors on Windows
394
- }
395
- break;
396
- }
397
-
398
- case 'node': {
399
- command = mcp_config.command || 'node';
400
- // Use mcp_config.args directly if provided, otherwise fall back to entry_point
401
- if (mcp_config.args && mcp_config.args.length > 0) {
402
- args = resolveArgs(mcp_config.args, cacheDir);
403
- } else {
404
- args = [join(cacheDir, entry_point)];
405
- }
406
- break;
407
- }
408
-
409
- case 'python': {
410
- // Use manifest command if specified, otherwise auto-detect python
411
- command = mcp_config.command === 'python' ? findPythonCommand() : (mcp_config.command || findPythonCommand());
412
-
413
- // Use mcp_config.args directly if provided, otherwise fall back to entry_point
414
- if (mcp_config.args && mcp_config.args.length > 0) {
415
- args = resolveArgs(mcp_config.args, cacheDir);
416
- } else {
417
- args = [join(cacheDir, entry_point)];
418
- }
419
-
420
- // Set PYTHONPATH to deps/ directory for dependency resolution
421
- const depsDir = join(cacheDir, 'deps');
422
- const existingPythonPath = process.env.PYTHONPATH;
423
- env.PYTHONPATH = existingPythonPath ? `${depsDir}:${existingPythonPath}` : depsDir;
424
- break;
425
- }
426
-
427
- default:
428
- throw new Error(`Unsupported server type: ${type}`);
429
- }
430
-
431
- // Spawn with stdio passthrough for MCP
432
- const child = spawn(command, args, {
433
- stdio: ['inherit', 'inherit', 'inherit'],
434
- env,
435
- cwd: cacheDir,
436
- });
437
-
438
- // Forward signals
439
- process.on('SIGINT', () => child.kill('SIGINT'));
440
- process.on('SIGTERM', () => child.kill('SIGTERM'));
441
-
442
- // Wait for exit
443
- child.on('exit', (code) => {
444
- process.exit(code ?? 0);
445
- });
446
-
447
- child.on('error', (error) => {
448
- process.stderr.write(`=> Failed to start server: ${error.message}\n`);
449
- process.exit(1);
450
- });
451
- }