@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.
- package/LICENSE +10 -198
- package/README.md +50 -383
- package/dist/index.d.ts +0 -2
- package/dist/index.js +2113 -4
- package/dist/index.js.map +1 -1
- package/package.json +32 -29
- package/.claude/settings.local.json +0 -19
- package/.env.example +0 -13
- package/.github/workflows/ci.yml +0 -27
- package/CLAUDE.md +0 -283
- package/dist/commands/config.d.ts +0 -31
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -129
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/packages/pull.d.ts +0 -11
- package/dist/commands/packages/pull.d.ts.map +0 -1
- package/dist/commands/packages/pull.js +0 -72
- package/dist/commands/packages/pull.js.map +0 -1
- package/dist/commands/packages/run.d.ts +0 -47
- package/dist/commands/packages/run.d.ts.map +0 -1
- package/dist/commands/packages/run.js +0 -419
- package/dist/commands/packages/run.js.map +0 -1
- package/dist/commands/packages/search.d.ts +0 -12
- package/dist/commands/packages/search.d.ts.map +0 -1
- package/dist/commands/packages/search.js +0 -63
- package/dist/commands/packages/search.js.map +0 -1
- package/dist/commands/packages/show.d.ts +0 -8
- package/dist/commands/packages/show.d.ts.map +0 -1
- package/dist/commands/packages/show.js +0 -109
- package/dist/commands/packages/show.js.map +0 -1
- package/dist/commands/search.d.ts +0 -12
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -144
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/skills/index.d.ts +0 -8
- package/dist/commands/skills/index.d.ts.map +0 -1
- package/dist/commands/skills/index.js +0 -8
- package/dist/commands/skills/index.js.map +0 -1
- package/dist/commands/skills/install.d.ts +0 -9
- package/dist/commands/skills/install.d.ts.map +0 -1
- package/dist/commands/skills/install.js +0 -110
- package/dist/commands/skills/install.js.map +0 -1
- package/dist/commands/skills/list.d.ts +0 -8
- package/dist/commands/skills/list.d.ts.map +0 -1
- package/dist/commands/skills/list.js +0 -89
- package/dist/commands/skills/list.js.map +0 -1
- package/dist/commands/skills/pack.d.ts +0 -22
- package/dist/commands/skills/pack.d.ts.map +0 -1
- package/dist/commands/skills/pack.js +0 -116
- package/dist/commands/skills/pack.js.map +0 -1
- package/dist/commands/skills/pull.d.ts +0 -9
- package/dist/commands/skills/pull.d.ts.map +0 -1
- package/dist/commands/skills/pull.js +0 -68
- package/dist/commands/skills/pull.js.map +0 -1
- package/dist/commands/skills/search.d.ts +0 -14
- package/dist/commands/skills/search.d.ts.map +0 -1
- package/dist/commands/skills/search.js +0 -53
- package/dist/commands/skills/search.js.map +0 -1
- package/dist/commands/skills/show.d.ts +0 -8
- package/dist/commands/skills/show.d.ts.map +0 -1
- package/dist/commands/skills/show.js +0 -64
- package/dist/commands/skills/show.js.map +0 -1
- package/dist/commands/skills/validate.d.ts +0 -25
- package/dist/commands/skills/validate.d.ts.map +0 -1
- package/dist/commands/skills/validate.js +0 -191
- package/dist/commands/skills/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/api/registry-client.d.ts +0 -63
- package/dist/lib/api/registry-client.d.ts.map +0 -1
- package/dist/lib/api/registry-client.js +0 -167
- package/dist/lib/api/registry-client.js.map +0 -1
- package/dist/lib/api/skills-client.d.ts +0 -30
- package/dist/lib/api/skills-client.d.ts.map +0 -1
- package/dist/lib/api/skills-client.js +0 -110
- package/dist/lib/api/skills-client.js.map +0 -1
- package/dist/program.d.ts +0 -12
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -186
- package/dist/program.js.map +0 -1
- package/dist/schemas/generated/api-responses.d.ts +0 -541
- package/dist/schemas/generated/api-responses.d.ts.map +0 -1
- package/dist/schemas/generated/api-responses.js +0 -313
- package/dist/schemas/generated/api-responses.js.map +0 -1
- package/dist/schemas/generated/auth.d.ts +0 -18
- package/dist/schemas/generated/auth.d.ts.map +0 -1
- package/dist/schemas/generated/auth.js +0 -18
- package/dist/schemas/generated/auth.js.map +0 -1
- package/dist/schemas/generated/index.d.ts +0 -5
- package/dist/schemas/generated/index.d.ts.map +0 -1
- package/dist/schemas/generated/index.js +0 -6
- package/dist/schemas/generated/index.js.map +0 -1
- package/dist/schemas/generated/package.d.ts +0 -43
- package/dist/schemas/generated/package.d.ts.map +0 -1
- package/dist/schemas/generated/package.js +0 -20
- package/dist/schemas/generated/package.js.map +0 -1
- package/dist/schemas/generated/skill.d.ts +0 -381
- package/dist/schemas/generated/skill.d.ts.map +0 -1
- package/dist/schemas/generated/skill.js +0 -216
- package/dist/schemas/generated/skill.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -66
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -193
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -12
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/version.d.ts +0 -5
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -19
- package/dist/utils/version.js.map +0 -1
- package/eslint.config.js +0 -63
- package/src/commands/config.ts +0 -162
- package/src/commands/packages/pull.ts +0 -96
- package/src/commands/packages/run.test.ts +0 -261
- package/src/commands/packages/run.ts +0 -536
- package/src/commands/packages/search.ts +0 -83
- package/src/commands/packages/show.ts +0 -128
- package/src/commands/search.ts +0 -191
- package/src/commands/skills/index.ts +0 -7
- package/src/commands/skills/install.ts +0 -129
- package/src/commands/skills/list.ts +0 -116
- package/src/commands/skills/pack.test.ts +0 -260
- package/src/commands/skills/pack.ts +0 -145
- package/src/commands/skills/pull.ts +0 -88
- package/src/commands/skills/search.ts +0 -73
- package/src/commands/skills/show.ts +0 -72
- package/src/commands/skills/validate.test.ts +0 -466
- package/src/commands/skills/validate.ts +0 -227
- package/src/index.ts +0 -11
- package/src/lib/api/registry-client.ts +0 -223
- package/src/lib/api/schema.d.ts +0 -520
- package/src/lib/api/skills-client.ts +0 -148
- package/src/program.test.ts +0 -22
- package/src/program.ts +0 -226
- package/src/schemas/config.v1.schema.json +0 -37
- package/src/schemas/generated/api-responses.ts +0 -386
- package/src/schemas/generated/auth.ts +0 -21
- package/src/schemas/generated/index.ts +0 -5
- package/src/schemas/generated/package.ts +0 -29
- package/src/schemas/generated/skill.ts +0 -271
- package/src/utils/config-manager.test.ts +0 -330
- package/src/utils/config-manager.ts +0 -272
- package/src/utils/errors.test.ts +0 -25
- package/src/utils/errors.ts +0 -33
- package/src/utils/version.test.ts +0 -16
- package/src/utils/version.ts +0 -18
- package/test/integration/registry-client.test.ts +0 -180
- package/tsconfig.check.json +0 -9
- package/tsconfig.json +0 -25
- 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
|
-
}
|