@nimblebrain/mpak-sdk 0.1.3 → 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/README.md +241 -115
- package/dist/index.cjs +867 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +484 -159
- package/dist/index.d.ts +484 -159
- package/dist/index.js +857 -164
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,29 +15,42 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
31
21
|
var index_exports = {};
|
|
32
22
|
__export(index_exports, {
|
|
23
|
+
Mpak: () => Mpak,
|
|
24
|
+
MpakBundleCache: () => MpakBundleCache,
|
|
25
|
+
MpakCacheCorruptedError: () => MpakCacheCorruptedError,
|
|
33
26
|
MpakClient: () => MpakClient,
|
|
27
|
+
MpakConfigCorruptedError: () => MpakConfigCorruptedError,
|
|
28
|
+
MpakConfigError: () => MpakConfigError,
|
|
29
|
+
MpakConfigManager: () => MpakConfigManager,
|
|
34
30
|
MpakError: () => MpakError,
|
|
35
31
|
MpakIntegrityError: () => MpakIntegrityError,
|
|
32
|
+
MpakInvalidBundleError: () => MpakInvalidBundleError,
|
|
36
33
|
MpakNetworkError: () => MpakNetworkError,
|
|
37
|
-
MpakNotFoundError: () => MpakNotFoundError
|
|
34
|
+
MpakNotFoundError: () => MpakNotFoundError,
|
|
35
|
+
parsePackageSpec: () => parsePackageSpec
|
|
38
36
|
});
|
|
39
37
|
module.exports = __toCommonJS(index_exports);
|
|
40
38
|
|
|
39
|
+
// src/mpakSDK.ts
|
|
40
|
+
var import_node_child_process2 = require("child_process");
|
|
41
|
+
var import_node_fs4 = require("fs");
|
|
42
|
+
var import_node_path4 = require("path");
|
|
43
|
+
var import_mpak_schemas2 = require("@nimblebrain/mpak-schemas");
|
|
44
|
+
|
|
45
|
+
// src/cache.ts
|
|
46
|
+
var import_node_crypto3 = require("crypto");
|
|
47
|
+
var import_node_fs2 = require("fs");
|
|
48
|
+
var import_node_os = require("os");
|
|
49
|
+
var import_node_path2 = require("path");
|
|
50
|
+
var import_mpak_schemas = require("@nimblebrain/mpak-schemas");
|
|
51
|
+
|
|
41
52
|
// src/client.ts
|
|
42
|
-
var
|
|
53
|
+
var import_node_crypto = require("crypto");
|
|
43
54
|
|
|
44
55
|
// src/errors.ts
|
|
45
56
|
var MpakError = class extends Error {
|
|
@@ -74,11 +85,44 @@ var MpakNetworkError = class extends MpakError {
|
|
|
74
85
|
this.name = "MpakNetworkError";
|
|
75
86
|
}
|
|
76
87
|
};
|
|
88
|
+
var MpakConfigCorruptedError = class extends MpakError {
|
|
89
|
+
constructor(message, configPath, cause) {
|
|
90
|
+
super(message, "CONFIG_CORRUPTED");
|
|
91
|
+
this.configPath = configPath;
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
this.name = "MpakConfigCorruptedError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var MpakCacheCorruptedError = class extends MpakError {
|
|
97
|
+
constructor(message, filePath, cause) {
|
|
98
|
+
super(message, "CACHE_CORRUPTED");
|
|
99
|
+
this.filePath = filePath;
|
|
100
|
+
this.cause = cause;
|
|
101
|
+
this.name = "MpakCacheCorruptedError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var MpakInvalidBundleError = class extends MpakError {
|
|
105
|
+
constructor(message, bundlePath, cause) {
|
|
106
|
+
super(message, "INVALID_BUNDLE");
|
|
107
|
+
this.bundlePath = bundlePath;
|
|
108
|
+
this.cause = cause;
|
|
109
|
+
this.name = "MpakInvalidBundleError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var MpakConfigError = class extends MpakError {
|
|
113
|
+
constructor(packageName, missingFields) {
|
|
114
|
+
const fieldNames = missingFields.map((f) => f.title).join(", ");
|
|
115
|
+
super(`Missing required config for ${packageName}: ${fieldNames}`, "CONFIG_MISSING");
|
|
116
|
+
this.packageName = packageName;
|
|
117
|
+
this.missingFields = missingFields;
|
|
118
|
+
this.name = "MpakConfigError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
77
121
|
|
|
78
122
|
// src/client.ts
|
|
79
123
|
var DEFAULT_REGISTRY_URL = "https://registry.mpak.dev";
|
|
80
124
|
var DEFAULT_TIMEOUT = 3e4;
|
|
81
|
-
var MpakClient = class {
|
|
125
|
+
var MpakClient = class _MpakClient {
|
|
82
126
|
registryUrl;
|
|
83
127
|
timeout;
|
|
84
128
|
userAgent;
|
|
@@ -190,7 +234,6 @@ var MpakClient = class {
|
|
|
190
234
|
if (params.q) searchParams.set("q", params.q);
|
|
191
235
|
if (params.tags) searchParams.set("tags", params.tags);
|
|
192
236
|
if (params.category) searchParams.set("category", params.category);
|
|
193
|
-
if (params.surface) searchParams.set("surface", params.surface);
|
|
194
237
|
if (params.sort) searchParams.set("sort", params.sort);
|
|
195
238
|
if (params.limit) searchParams.set("limit", String(params.limit));
|
|
196
239
|
if (params.offset) searchParams.set("offset", String(params.offset));
|
|
@@ -254,179 +297,55 @@ var MpakClient = class {
|
|
|
254
297
|
}
|
|
255
298
|
return response.json();
|
|
256
299
|
}
|
|
300
|
+
// ===========================================================================
|
|
301
|
+
// Download Methods
|
|
302
|
+
// ===========================================================================
|
|
257
303
|
/**
|
|
258
|
-
* Download
|
|
259
|
-
*
|
|
260
|
-
* @throws {MpakIntegrityError} If expectedSha256 is provided and doesn't match (fail-closed)
|
|
261
|
-
*/
|
|
262
|
-
async downloadSkillContent(downloadUrl, expectedSha256) {
|
|
263
|
-
const response = await this.fetchWithTimeout(downloadUrl);
|
|
264
|
-
if (!response.ok) {
|
|
265
|
-
throw new MpakNetworkError(`Failed to download skill: HTTP ${response.status}`);
|
|
266
|
-
}
|
|
267
|
-
const content = await response.text();
|
|
268
|
-
if (expectedSha256) {
|
|
269
|
-
const actualHash = this.computeSha256(content);
|
|
270
|
-
if (actualHash !== expectedSha256) {
|
|
271
|
-
throw new MpakIntegrityError(expectedSha256, actualHash);
|
|
272
|
-
}
|
|
273
|
-
return { content, verified: true };
|
|
274
|
-
}
|
|
275
|
-
return { content, verified: false };
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Resolve a skill reference to actual content
|
|
279
|
-
*
|
|
280
|
-
* Supports mpak, github, and url sources. This is the main method for
|
|
281
|
-
* fetching skill content from any supported source.
|
|
304
|
+
* Download content from a URL and verify its SHA-256 integrity.
|
|
282
305
|
*
|
|
283
|
-
* @throws {
|
|
284
|
-
* @throws {MpakIntegrityError} If integrity check fails (fail-closed)
|
|
306
|
+
* @throws {MpakIntegrityError} If SHA-256 doesn't match
|
|
285
307
|
* @throws {MpakNetworkError} For network failures
|
|
286
|
-
*
|
|
287
|
-
* @example
|
|
288
|
-
* ```typescript
|
|
289
|
-
* // Resolve from mpak registry
|
|
290
|
-
* const skill = await client.resolveSkillRef({
|
|
291
|
-
* source: 'mpak',
|
|
292
|
-
* name: '@nimblebraininc/folk-crm',
|
|
293
|
-
* version: '1.3.0',
|
|
294
|
-
* });
|
|
295
|
-
*
|
|
296
|
-
* // Resolve from GitHub
|
|
297
|
-
* const skill = await client.resolveSkillRef({
|
|
298
|
-
* source: 'github',
|
|
299
|
-
* name: '@example/my-skill',
|
|
300
|
-
* version: 'v1.0.0',
|
|
301
|
-
* repo: 'owner/repo',
|
|
302
|
-
* path: 'skills/my-skill/SKILL.md',
|
|
303
|
-
* });
|
|
304
|
-
*
|
|
305
|
-
* // Resolve from URL
|
|
306
|
-
* const skill = await client.resolveSkillRef({
|
|
307
|
-
* source: 'url',
|
|
308
|
-
* name: '@example/custom',
|
|
309
|
-
* version: '1.0.0',
|
|
310
|
-
* url: 'https://example.com/skill.md',
|
|
311
|
-
* });
|
|
312
|
-
* ```
|
|
313
|
-
*/
|
|
314
|
-
async resolveSkillRef(ref) {
|
|
315
|
-
switch (ref.source) {
|
|
316
|
-
case "mpak":
|
|
317
|
-
return this.resolveMpakSkill(ref);
|
|
318
|
-
case "github":
|
|
319
|
-
return this.resolveGithubSkill(ref);
|
|
320
|
-
case "url":
|
|
321
|
-
return this.resolveUrlSkill(ref);
|
|
322
|
-
default: {
|
|
323
|
-
const _exhaustive = ref;
|
|
324
|
-
throw new Error(`Unknown skill source: ${_exhaustive.source}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Resolve a skill from mpak registry
|
|
330
|
-
*
|
|
331
|
-
* The API returns a ZIP bundle containing SKILL.md and metadata.
|
|
332
|
-
*/
|
|
333
|
-
async resolveMpakSkill(ref) {
|
|
334
|
-
const url = `${this.registryUrl}/v1/skills/${ref.name}/versions/${ref.version}/download`;
|
|
335
|
-
const response = await this.fetchWithTimeout(url);
|
|
336
|
-
if (response.status === 404) {
|
|
337
|
-
throw new MpakNotFoundError(`${ref.name}@${ref.version}`);
|
|
338
|
-
}
|
|
339
|
-
if (!response.ok) {
|
|
340
|
-
throw new MpakNetworkError(`Failed to fetch skill: HTTP ${response.status}`);
|
|
341
|
-
}
|
|
342
|
-
const zipBuffer = await response.arrayBuffer();
|
|
343
|
-
const content = await this.extractSkillFromZip(zipBuffer, ref.name);
|
|
344
|
-
if (ref.integrity) {
|
|
345
|
-
this.verifyIntegrityOrThrow(content, ref.integrity);
|
|
346
|
-
return { content, version: ref.version, source: "mpak", verified: true };
|
|
347
|
-
}
|
|
348
|
-
return { content, version: ref.version, source: "mpak", verified: false };
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Resolve a skill from GitHub releases
|
|
352
308
|
*/
|
|
353
|
-
async
|
|
354
|
-
const url = `https://github.com/${ref.repo}/releases/download/${ref.version}/${ref.path}`;
|
|
309
|
+
async downloadContent(url, sha256) {
|
|
355
310
|
const response = await this.fetchWithTimeout(url);
|
|
356
311
|
if (!response.ok) {
|
|
357
|
-
throw new
|
|
358
|
-
}
|
|
359
|
-
const content = await response.text();
|
|
360
|
-
if (ref.integrity) {
|
|
361
|
-
this.verifyIntegrityOrThrow(content, ref.integrity);
|
|
362
|
-
return {
|
|
363
|
-
content,
|
|
364
|
-
version: ref.version,
|
|
365
|
-
source: "github",
|
|
366
|
-
verified: true
|
|
367
|
-
};
|
|
312
|
+
throw new MpakNetworkError(`Failed to download: HTTP ${response.status}`);
|
|
368
313
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
verified: false
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Resolve a skill from a direct URL
|
|
378
|
-
*/
|
|
379
|
-
async resolveUrlSkill(ref) {
|
|
380
|
-
const response = await this.fetchWithTimeout(ref.url);
|
|
381
|
-
if (!response.ok) {
|
|
382
|
-
throw new MpakNotFoundError(`url:${ref.url}`);
|
|
383
|
-
}
|
|
384
|
-
const content = await response.text();
|
|
385
|
-
if (ref.integrity) {
|
|
386
|
-
this.verifyIntegrityOrThrow(content, ref.integrity);
|
|
387
|
-
return { content, version: ref.version, source: "url", verified: true };
|
|
314
|
+
const downloadedRawData = new Uint8Array(await response.arrayBuffer());
|
|
315
|
+
const computedHash = this.computeSha256(downloadedRawData);
|
|
316
|
+
if (computedHash !== sha256) {
|
|
317
|
+
throw new MpakIntegrityError(sha256, computedHash);
|
|
388
318
|
}
|
|
389
|
-
return
|
|
319
|
+
return downloadedRawData;
|
|
390
320
|
}
|
|
391
321
|
/**
|
|
392
|
-
*
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const skillPath = `${folderName}/SKILL.md`;
|
|
399
|
-
const skillFile = zip.file(skillPath);
|
|
400
|
-
if (!skillFile) {
|
|
401
|
-
const altFile = zip.file("SKILL.md");
|
|
402
|
-
if (!altFile) {
|
|
403
|
-
throw new MpakNotFoundError(`SKILL.md not found in bundle for ${skillName}`);
|
|
404
|
-
}
|
|
405
|
-
return altFile.async("string");
|
|
406
|
-
}
|
|
407
|
-
return skillFile.async("string");
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Verify content integrity and throw if mismatch (fail-closed)
|
|
322
|
+
* Download a bundle by name, with optional version and platform.
|
|
323
|
+
* Defaults to latest version and auto-detected platform.
|
|
324
|
+
*
|
|
325
|
+
* @throws {MpakNotFoundError} If bundle not found
|
|
326
|
+
* @throws {MpakIntegrityError} If SHA-256 doesn't match
|
|
327
|
+
* @throws {MpakNetworkError} For network failures
|
|
411
328
|
*/
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
329
|
+
async downloadBundle(name, version, platform) {
|
|
330
|
+
const resolvedPlatform = platform ?? _MpakClient.detectPlatform();
|
|
331
|
+
const resolvedVersion = version ?? "latest";
|
|
332
|
+
const downloadInfo = await this.getBundleDownload(name, resolvedVersion, resolvedPlatform);
|
|
333
|
+
const data = await this.downloadContent(downloadInfo.url, downloadInfo.bundle.sha256);
|
|
334
|
+
return { data, metadata: downloadInfo.bundle };
|
|
418
335
|
}
|
|
419
336
|
/**
|
|
420
|
-
*
|
|
337
|
+
* Download a skill bundle by name, with optional version.
|
|
338
|
+
* Defaults to latest version.
|
|
339
|
+
*
|
|
340
|
+
* @throws {MpakNotFoundError} If skill not found
|
|
341
|
+
* @throws {MpakIntegrityError} If SHA-256 doesn't match
|
|
342
|
+
* @throws {MpakNetworkError} For network failures
|
|
421
343
|
*/
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return integrity.slice(7);
|
|
428
|
-
}
|
|
429
|
-
return integrity;
|
|
344
|
+
async downloadSkillBundle(name, version) {
|
|
345
|
+
const resolvedVersion = version ?? "latest";
|
|
346
|
+
const downloadInfo = await this.getSkillVersionDownload(name, resolvedVersion);
|
|
347
|
+
const data = await this.downloadContent(downloadInfo.url, downloadInfo.skill.sha256);
|
|
348
|
+
return { data, metadata: downloadInfo.skill };
|
|
430
349
|
}
|
|
431
350
|
// ===========================================================================
|
|
432
351
|
// Utility Methods
|
|
@@ -468,14 +387,17 @@ var MpakClient = class {
|
|
|
468
387
|
* Compute SHA256 hash of content
|
|
469
388
|
*/
|
|
470
389
|
computeSha256(content) {
|
|
471
|
-
return (0,
|
|
390
|
+
return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
472
391
|
}
|
|
473
392
|
/**
|
|
474
393
|
* Validate that a name is scoped (@scope/name)
|
|
475
394
|
*/
|
|
476
395
|
validateScopedName(name) {
|
|
477
396
|
if (!name.startsWith("@")) {
|
|
478
|
-
throw new
|
|
397
|
+
throw new MpakError(
|
|
398
|
+
"Package name must be scoped (e.g., @scope/package-name)",
|
|
399
|
+
"INVALID_SPEC"
|
|
400
|
+
);
|
|
479
401
|
}
|
|
480
402
|
}
|
|
481
403
|
/**
|
|
@@ -508,12 +430,781 @@ var MpakClient = class {
|
|
|
508
430
|
}
|
|
509
431
|
}
|
|
510
432
|
};
|
|
433
|
+
|
|
434
|
+
// src/helpers.ts
|
|
435
|
+
var import_node_child_process = require("child_process");
|
|
436
|
+
var import_node_crypto2 = require("crypto");
|
|
437
|
+
var import_node_fs = require("fs");
|
|
438
|
+
var import_node_path = require("path");
|
|
439
|
+
var MAX_UNCOMPRESSED_SIZE = 500 * 1024 * 1024;
|
|
440
|
+
var UPDATE_CHECK_TTL_MS = 60 * 60 * 1e3;
|
|
441
|
+
function isSemverEqual(a, b) {
|
|
442
|
+
return a.replace(/^v/, "") === b.replace(/^v/, "");
|
|
443
|
+
}
|
|
444
|
+
function extractZip(zipPath, destDir) {
|
|
445
|
+
try {
|
|
446
|
+
const listOutput = (0, import_node_child_process.execFileSync)("unzip", ["-l", zipPath], {
|
|
447
|
+
stdio: "pipe",
|
|
448
|
+
encoding: "utf8"
|
|
449
|
+
});
|
|
450
|
+
const totalMatch = listOutput.match(/^\s*(\d+)\s+\d+\s+files?$/m);
|
|
451
|
+
if (totalMatch) {
|
|
452
|
+
const totalSize = parseInt(totalMatch[1] ?? "0", 10);
|
|
453
|
+
if (totalSize > MAX_UNCOMPRESSED_SIZE) {
|
|
454
|
+
throw new MpakCacheCorruptedError(
|
|
455
|
+
`Bundle uncompressed size (${Math.round(totalSize / 1024 / 1024)}MB) exceeds maximum allowed (${MAX_UNCOMPRESSED_SIZE / (1024 * 1024)}MB)`,
|
|
456
|
+
zipPath
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error instanceof MpakCacheCorruptedError) {
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
465
|
+
throw new MpakCacheCorruptedError(
|
|
466
|
+
`Cannot verify bundle size before extraction: ${message}`,
|
|
467
|
+
zipPath,
|
|
468
|
+
error instanceof Error ? error : void 0
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
(0, import_node_fs.mkdirSync)(destDir, { recursive: true });
|
|
472
|
+
(0, import_node_child_process.execFileSync)("unzip", ["-o", "-q", zipPath, "-d", destDir], {
|
|
473
|
+
stdio: "pipe"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function hashBundlePath(bundlePath) {
|
|
477
|
+
return (0, import_node_crypto2.createHash)("md5").update((0, import_node_path.resolve)(bundlePath)).digest("hex").slice(0, 12);
|
|
478
|
+
}
|
|
479
|
+
function localBundleNeedsExtract(bundlePath, cacheDir) {
|
|
480
|
+
const metaPath = (0, import_node_path.join)(cacheDir, ".mpak-local-meta.json");
|
|
481
|
+
if (!(0, import_node_fs.existsSync)(metaPath)) return true;
|
|
482
|
+
try {
|
|
483
|
+
const meta = JSON.parse((0, import_node_fs.readFileSync)(metaPath, "utf8"));
|
|
484
|
+
if (!meta.extractedAt) return true;
|
|
485
|
+
const bundleStat = (0, import_node_fs.statSync)(bundlePath);
|
|
486
|
+
return bundleStat.mtimeMs > new Date(meta.extractedAt).getTime();
|
|
487
|
+
} catch {
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function readJsonFromFile(filePath, schema) {
|
|
492
|
+
if (!(0, import_node_fs.existsSync)(filePath)) {
|
|
493
|
+
throw new MpakError(`File does not exist: ${filePath}`, "FILE_NOT_FOUND");
|
|
494
|
+
}
|
|
495
|
+
let raw;
|
|
496
|
+
try {
|
|
497
|
+
raw = JSON.parse((0, import_node_fs.readFileSync)(filePath, "utf8"));
|
|
498
|
+
} catch {
|
|
499
|
+
throw new MpakError(`File is not valid JSON: ${filePath}`, "INVALID_JSON");
|
|
500
|
+
}
|
|
501
|
+
const result = schema.safeParse(raw);
|
|
502
|
+
if (!result.success) {
|
|
503
|
+
throw new MpakError(
|
|
504
|
+
`File failed validation: ${filePath} \u2014 ${result.error.issues[0]?.message ?? "unknown error"}`,
|
|
505
|
+
"VALIDATION_FAILED"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return result.data;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/cache.ts
|
|
512
|
+
var MpakBundleCache = class {
|
|
513
|
+
cacheHome;
|
|
514
|
+
mpakClient;
|
|
515
|
+
constructor(client, options) {
|
|
516
|
+
this.mpakClient = client;
|
|
517
|
+
this.cacheHome = (0, import_node_path2.join)(options?.mpakHome ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".mpak"), "cache");
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Compute the cache path for a package. Does not create the directory.
|
|
521
|
+
* @example getPackageCachePath('@scope/name') => '<cacheBase>/scope-name'
|
|
522
|
+
*/
|
|
523
|
+
getBundleCacheDirName(packageName) {
|
|
524
|
+
const safeName = packageName.replace("@", "").replace("/", "-");
|
|
525
|
+
return (0, import_node_path2.join)(this.cacheHome, safeName);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Read and validate cache metadata for a package.
|
|
529
|
+
* Returns `null` if the package does not exist in the cache.
|
|
530
|
+
* throws Error if metadata is corrupt
|
|
531
|
+
*/
|
|
532
|
+
getBundleMetadata(packageName) {
|
|
533
|
+
const packageCacheDir = this.getBundleCacheDirName(packageName);
|
|
534
|
+
if (!(0, import_node_fs2.existsSync)(packageCacheDir)) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const metaPath = (0, import_node_path2.join)(packageCacheDir, ".mpak-meta.json");
|
|
538
|
+
try {
|
|
539
|
+
return readJsonFromFile(metaPath, import_mpak_schemas.CacheMetadataSchema);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
throw new MpakCacheCorruptedError(
|
|
542
|
+
err instanceof Error ? err.message : String(err),
|
|
543
|
+
metaPath,
|
|
544
|
+
err instanceof Error ? err : void 0
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Read and validate the MCPB manifest from a cached package.
|
|
550
|
+
* Returns `null` if the package is not cached (directory doesn't exist).
|
|
551
|
+
*
|
|
552
|
+
* @throws {MpakCacheCorruptedError} If the cache directory exists but
|
|
553
|
+
* `manifest.json` is missing, contains invalid JSON, or fails schema validation.
|
|
554
|
+
*/
|
|
555
|
+
getBundleManifest(packageName) {
|
|
556
|
+
const dir = this.getBundleCacheDirName(packageName);
|
|
557
|
+
if (!(0, import_node_fs2.existsSync)(dir)) {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
const manifestPath = (0, import_node_path2.join)(dir, "manifest.json");
|
|
561
|
+
try {
|
|
562
|
+
return readJsonFromFile(manifestPath, import_mpak_schemas.McpbManifestSchema);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
throw new MpakCacheCorruptedError(
|
|
565
|
+
err instanceof Error ? err.message : String(err),
|
|
566
|
+
manifestPath,
|
|
567
|
+
err instanceof Error ? err : void 0
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Scan the cache directory and return metadata for every cached registry bundle.
|
|
573
|
+
* Skips the `_local/` directory (local dev bundles) and entries with
|
|
574
|
+
* missing/corrupt metadata or manifests.
|
|
575
|
+
*/
|
|
576
|
+
listCachedBundles() {
|
|
577
|
+
if (!(0, import_node_fs2.existsSync)(this.cacheHome)) return [];
|
|
578
|
+
const entries = (0, import_node_fs2.readdirSync)(this.cacheHome, { withFileTypes: true });
|
|
579
|
+
const bundles = [];
|
|
580
|
+
for (const entry of entries) {
|
|
581
|
+
if (!entry.isDirectory() || entry.name === "_local") continue;
|
|
582
|
+
const cacheDir = (0, import_node_path2.join)(this.cacheHome, entry.name);
|
|
583
|
+
try {
|
|
584
|
+
const manifest = readJsonFromFile((0, import_node_path2.join)(cacheDir, "manifest.json"), import_mpak_schemas.McpbManifestSchema);
|
|
585
|
+
const meta = this.getBundleMetadata(manifest.name);
|
|
586
|
+
if (!meta) continue;
|
|
587
|
+
bundles.push({
|
|
588
|
+
name: manifest.name,
|
|
589
|
+
version: meta.version,
|
|
590
|
+
pulledAt: meta.pulledAt,
|
|
591
|
+
cacheDir
|
|
592
|
+
});
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return bundles;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Remove a cached bundle from disk.
|
|
600
|
+
* @returns `true` if the bundle was cached and removed, `false` if it wasn't cached.
|
|
601
|
+
*/
|
|
602
|
+
removeCachedBundle(packageName) {
|
|
603
|
+
const dir = this.getBundleCacheDirName(packageName);
|
|
604
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return false;
|
|
605
|
+
(0, import_node_fs2.rmSync)(dir, { recursive: true, force: true });
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Load a bundle into the local cache, downloading from the registry only
|
|
610
|
+
* if the cache is missing or stale. Returns the cache directory and version.
|
|
611
|
+
*
|
|
612
|
+
* Requires an `MpakClient` to be provided at construction time.
|
|
613
|
+
*
|
|
614
|
+
* @param name - Scoped package name (e.g. `@scope/bundle`)
|
|
615
|
+
* @param options.version - Specific version to load. Omit for "latest".
|
|
616
|
+
* @param options.force - Skip cache checks and always re-download.
|
|
617
|
+
*
|
|
618
|
+
* @returns `cacheDir` — path to the extracted bundle on disk,
|
|
619
|
+
* `version` — the resolved version string,
|
|
620
|
+
* `pulled` — whether a download actually occurred.
|
|
621
|
+
*
|
|
622
|
+
* @throws If no `MpakClient` was provided at construction time.
|
|
623
|
+
*/
|
|
624
|
+
async loadBundle(name, options) {
|
|
625
|
+
const { version: requestedVersion, force = false } = options ?? {};
|
|
626
|
+
const cacheDir = this.getBundleCacheDirName(name);
|
|
627
|
+
let cachedMeta = null;
|
|
628
|
+
try {
|
|
629
|
+
cachedMeta = this.getBundleMetadata(name);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
if (!options?.force && !!cachedMeta && (!requestedVersion || isSemverEqual(cachedMeta.version, requestedVersion))) {
|
|
633
|
+
return { cacheDir, version: cachedMeta.version, pulled: false };
|
|
634
|
+
}
|
|
635
|
+
const platform = MpakClient.detectPlatform();
|
|
636
|
+
const downloadInfo = await this.mpakClient.getBundleDownload(
|
|
637
|
+
name,
|
|
638
|
+
requestedVersion ?? "latest",
|
|
639
|
+
platform
|
|
640
|
+
);
|
|
641
|
+
if (!force && cachedMeta && isSemverEqual(cachedMeta.version, downloadInfo.bundle.version)) {
|
|
642
|
+
this.writeCacheMetadata(name, {
|
|
643
|
+
...cachedMeta,
|
|
644
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
645
|
+
});
|
|
646
|
+
return { cacheDir, version: cachedMeta.version, pulled: false };
|
|
647
|
+
}
|
|
648
|
+
await this.downloadAndExtract(name, downloadInfo);
|
|
649
|
+
return { cacheDir, version: downloadInfo.bundle.version, pulled: true };
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Fire-and-forget background check for bundle updates.
|
|
653
|
+
* Return the latest version string if an update is available, null otherwise (not cached, skipped, up-to-date, or error).
|
|
654
|
+
* The caller can just check `if (result) { console.log("update available: " + result) }`
|
|
655
|
+
* @param packageName - Scoped package name (e.g. `@scope/bundle`)
|
|
656
|
+
*/
|
|
657
|
+
async checkForUpdate(packageName, options) {
|
|
658
|
+
const cachedMeta = this.getBundleMetadata(packageName);
|
|
659
|
+
if (!cachedMeta) return null;
|
|
660
|
+
if (!options?.force && cachedMeta.lastCheckedAt) {
|
|
661
|
+
const elapsed = Date.now() - new Date(cachedMeta.lastCheckedAt).getTime();
|
|
662
|
+
if (elapsed < UPDATE_CHECK_TTL_MS) return null;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
const detail = await this.mpakClient.getBundle(packageName);
|
|
666
|
+
this.writeCacheMetadata(packageName, {
|
|
667
|
+
...cachedMeta,
|
|
668
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
669
|
+
});
|
|
670
|
+
if (!isSemverEqual(detail.latest_version, cachedMeta.version)) {
|
|
671
|
+
return detail.latest_version;
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// ===========================================================================
|
|
679
|
+
// Private methods
|
|
680
|
+
// ===========================================================================
|
|
681
|
+
/**
|
|
682
|
+
* Write cache metadata for a package.
|
|
683
|
+
* @throws If the metadata fails schema validation.
|
|
684
|
+
*/
|
|
685
|
+
writeCacheMetadata(packageName, metadata) {
|
|
686
|
+
const metaPath = (0, import_node_path2.join)(this.getBundleCacheDirName(packageName), ".mpak-meta.json");
|
|
687
|
+
(0, import_node_fs2.writeFileSync)(metaPath, JSON.stringify(metadata, null, 2));
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Download a bundle using pre-resolved download info, extract it into
|
|
691
|
+
* the cache, and write metadata.
|
|
692
|
+
*/
|
|
693
|
+
async downloadAndExtract(name, downloadInfo) {
|
|
694
|
+
const bundle = downloadInfo.bundle;
|
|
695
|
+
const cacheDir = this.getBundleCacheDirName(name);
|
|
696
|
+
const tempPath = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `mpak-${Date.now()}-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}.mcpb`);
|
|
697
|
+
try {
|
|
698
|
+
const data = await this.mpakClient.downloadContent(downloadInfo.url, bundle.sha256);
|
|
699
|
+
(0, import_node_fs2.writeFileSync)(tempPath, data);
|
|
700
|
+
if ((0, import_node_fs2.existsSync)(cacheDir)) {
|
|
701
|
+
(0, import_node_fs2.rmSync)(cacheDir, { recursive: true, force: true });
|
|
702
|
+
}
|
|
703
|
+
extractZip(tempPath, cacheDir);
|
|
704
|
+
this.writeCacheMetadata(name, {
|
|
705
|
+
version: bundle.version,
|
|
706
|
+
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
707
|
+
platform: bundle.platform
|
|
708
|
+
});
|
|
709
|
+
} finally {
|
|
710
|
+
(0, import_node_fs2.rmSync)(tempPath, { force: true });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/config-manager.ts
|
|
716
|
+
var import_node_fs3 = require("fs");
|
|
717
|
+
var import_node_os2 = require("os");
|
|
718
|
+
var import_node_path3 = require("path");
|
|
719
|
+
var import_zod = require("zod");
|
|
720
|
+
var CONFIG_VERSION = "1.0.0";
|
|
721
|
+
var PackageConfigSchema = import_zod.z.record(import_zod.z.string(), import_zod.z.string());
|
|
722
|
+
var MpakConfigSchema = import_zod.z.object({
|
|
723
|
+
version: import_zod.z.string(),
|
|
724
|
+
lastUpdated: import_zod.z.string(),
|
|
725
|
+
registryUrl: import_zod.z.string().optional(),
|
|
726
|
+
packages: import_zod.z.record(import_zod.z.string(), PackageConfigSchema).optional()
|
|
727
|
+
}).strict();
|
|
728
|
+
var MpakConfigManager = class {
|
|
729
|
+
mpakHome;
|
|
730
|
+
configFile;
|
|
731
|
+
config = null;
|
|
732
|
+
constructor(options) {
|
|
733
|
+
this.mpakHome = (0, import_node_path3.resolve)(options?.mpakHome ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".mpak"));
|
|
734
|
+
this.configFile = (0, import_node_path3.join)(this.mpakHome, "config.json");
|
|
735
|
+
if (options?.registryUrl !== void 0) {
|
|
736
|
+
this.setRegistryUrl(options.registryUrl);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// ===========================================================================
|
|
740
|
+
// Public methods
|
|
741
|
+
// ===========================================================================
|
|
742
|
+
/**
|
|
743
|
+
* Resolve the registry URL with a 2-tier fallback:
|
|
744
|
+
* 1. Saved value in config file
|
|
745
|
+
* 2. Default: `https://registry.mpak.dev`
|
|
746
|
+
*
|
|
747
|
+
* @returns The resolved registry URL
|
|
748
|
+
*/
|
|
749
|
+
getRegistryUrl() {
|
|
750
|
+
const config = this.loadConfig();
|
|
751
|
+
return config.registryUrl || "https://registry.mpak.dev";
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get all stored user config values for a package.
|
|
755
|
+
*
|
|
756
|
+
* @param packageName - Scoped package name (e.g. `@scope/bundle`)
|
|
757
|
+
* @returns The key-value map, or `undefined` if the package has no stored config
|
|
758
|
+
*/
|
|
759
|
+
getPackageConfig(packageName) {
|
|
760
|
+
const config = this.loadConfig();
|
|
761
|
+
return config.packages?.[packageName];
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Store a user config value for a package. Creates the package entry if needed.
|
|
765
|
+
*
|
|
766
|
+
* @param packageName - Scoped package name (e.g. `@scope/bundle`)
|
|
767
|
+
* @param key - The config key (e.g. `api_key`)
|
|
768
|
+
* @param value - The value to store
|
|
769
|
+
*/
|
|
770
|
+
setPackageConfigValue(packageName, key, value) {
|
|
771
|
+
const config = this.loadConfig();
|
|
772
|
+
if (!config.packages) {
|
|
773
|
+
config.packages = {};
|
|
774
|
+
}
|
|
775
|
+
if (!config.packages[packageName]) {
|
|
776
|
+
config.packages[packageName] = {};
|
|
777
|
+
}
|
|
778
|
+
config.packages[packageName][key] = value;
|
|
779
|
+
this.saveConfig();
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Remove all stored config for a package.
|
|
783
|
+
*
|
|
784
|
+
* @param packageName - Scoped package name (e.g. `@scope/bundle`)
|
|
785
|
+
* @returns `true` if the package had config that was removed, `false` if it didn't exist
|
|
786
|
+
*/
|
|
787
|
+
clearPackageConfig(packageName) {
|
|
788
|
+
const config = this.loadConfig();
|
|
789
|
+
if (config.packages?.[packageName]) {
|
|
790
|
+
delete config.packages[packageName];
|
|
791
|
+
this.saveConfig();
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Remove a single config value for a package. If this was the last key,
|
|
798
|
+
* the package entry is cleaned up entirely.
|
|
799
|
+
*
|
|
800
|
+
* @param packageName - Scoped package name (e.g. `@scope/bundle`)
|
|
801
|
+
* @param key - The config key to remove
|
|
802
|
+
* @returns `true` if the key existed and was removed, `false` otherwise
|
|
803
|
+
*/
|
|
804
|
+
clearPackageConfigValue(packageName, key) {
|
|
805
|
+
const config = this.loadConfig();
|
|
806
|
+
if (config.packages?.[packageName]?.[key] !== void 0) {
|
|
807
|
+
delete config.packages[packageName][key];
|
|
808
|
+
if (Object.keys(config.packages[packageName]).length === 0) {
|
|
809
|
+
delete config.packages[packageName];
|
|
810
|
+
}
|
|
811
|
+
this.saveConfig();
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* List all package names that have stored user config.
|
|
818
|
+
*
|
|
819
|
+
* @returns Array of scoped package names (e.g. `['@scope/pkg1', '@scope/pkg2']`)
|
|
820
|
+
*/
|
|
821
|
+
getPackageNames() {
|
|
822
|
+
const config = this.loadConfig();
|
|
823
|
+
return Object.keys(config.packages || {});
|
|
824
|
+
}
|
|
825
|
+
// ===========================================================================
|
|
826
|
+
// Private methods
|
|
827
|
+
// ===========================================================================
|
|
828
|
+
/**
|
|
829
|
+
* Load the config from disk, or create a fresh one if the file doesn't exist yet.
|
|
830
|
+
* The result is cached — subsequent calls return the in-memory copy without
|
|
831
|
+
* re-reading the file.
|
|
832
|
+
*
|
|
833
|
+
* @returns The validated config object
|
|
834
|
+
* @throws {MpakConfigCorruptedError} If the file exists but contains invalid JSON or fails schema validation
|
|
835
|
+
*/
|
|
836
|
+
loadConfig() {
|
|
837
|
+
if (this.config) {
|
|
838
|
+
return this.config;
|
|
839
|
+
}
|
|
840
|
+
if (!(0, import_node_fs3.existsSync)(this.configFile)) {
|
|
841
|
+
this.config = {
|
|
842
|
+
version: CONFIG_VERSION,
|
|
843
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
+
};
|
|
845
|
+
return this.config;
|
|
846
|
+
}
|
|
847
|
+
this.config = this.readAndValidateConfig();
|
|
848
|
+
return this.config;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Read the config file from disk, parse JSON, and validate against the schema.
|
|
852
|
+
*
|
|
853
|
+
* @returns The validated config object
|
|
854
|
+
* @throws {MpakConfigCorruptedError} If the file can't be read, contains invalid JSON,
|
|
855
|
+
* or doesn't match the expected schema
|
|
856
|
+
*/
|
|
857
|
+
readAndValidateConfig() {
|
|
858
|
+
let configJson;
|
|
859
|
+
try {
|
|
860
|
+
configJson = (0, import_node_fs3.readFileSync)(this.configFile, "utf8");
|
|
861
|
+
} catch (err) {
|
|
862
|
+
throw new MpakConfigCorruptedError(
|
|
863
|
+
`Failed to read config file: ${err instanceof Error ? err.message : String(err)}`,
|
|
864
|
+
this.configFile,
|
|
865
|
+
err instanceof Error ? err : void 0
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
let parsed;
|
|
869
|
+
try {
|
|
870
|
+
parsed = JSON.parse(configJson);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
throw new MpakConfigCorruptedError(
|
|
873
|
+
`Config file contains invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
874
|
+
this.configFile,
|
|
875
|
+
err instanceof Error ? err : void 0
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const result = MpakConfigSchema.safeParse(parsed);
|
|
879
|
+
if (!result.success) {
|
|
880
|
+
const message = result.error.issues[0]?.message ?? "Invalid config";
|
|
881
|
+
throw new MpakConfigCorruptedError(message, this.configFile);
|
|
882
|
+
}
|
|
883
|
+
return result.data;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Flush the in-memory config to disk. Creates the config directory if needed,
|
|
887
|
+
* validates against the schema, updates `lastUpdated`, and writes
|
|
888
|
+
* with mode `0o600` (owner read/write only — config may contain secrets).
|
|
889
|
+
*
|
|
890
|
+
* @throws {MpakConfigCorruptedError} If the in-memory config fails schema validation
|
|
891
|
+
*/
|
|
892
|
+
saveConfig() {
|
|
893
|
+
if (!this.config) {
|
|
894
|
+
throw new MpakConfigCorruptedError(
|
|
895
|
+
`saveConfig called before config was loaded`,
|
|
896
|
+
this.configFile
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
if (!(0, import_node_fs3.existsSync)(this.mpakHome)) {
|
|
900
|
+
(0, import_node_fs3.mkdirSync)(this.mpakHome, { recursive: true, mode: 448 });
|
|
901
|
+
}
|
|
902
|
+
this.config.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
903
|
+
const result = MpakConfigSchema.safeParse(this.config);
|
|
904
|
+
if (!result.success) {
|
|
905
|
+
const message = result.error.issues[0]?.message ?? "Invalid config";
|
|
906
|
+
throw new MpakConfigCorruptedError(message, this.configFile);
|
|
907
|
+
}
|
|
908
|
+
const configJson = JSON.stringify(result.data, null, 2);
|
|
909
|
+
(0, import_node_fs3.writeFileSync)(this.configFile, configJson, { mode: 384 });
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Persist a custom registry URL to the config file.
|
|
913
|
+
*
|
|
914
|
+
* @param url - The registry URL to save (e.g. `https://registry.example.com`)
|
|
915
|
+
*/
|
|
916
|
+
setRegistryUrl(url) {
|
|
917
|
+
const config = this.loadConfig();
|
|
918
|
+
config.registryUrl = url;
|
|
919
|
+
this.saveConfig();
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/mpakSDK.ts
|
|
924
|
+
var Mpak = class _Mpak {
|
|
925
|
+
/** User configuration manager (`config.json`). */
|
|
926
|
+
configManager;
|
|
927
|
+
/** Registry API client. */
|
|
928
|
+
client;
|
|
929
|
+
/** Local bundle cache. */
|
|
930
|
+
bundleCache;
|
|
931
|
+
constructor(options) {
|
|
932
|
+
const configOptions = {};
|
|
933
|
+
if (options?.mpakHome !== void 0) configOptions.mpakHome = options.mpakHome;
|
|
934
|
+
if (options?.registryUrl !== void 0) configOptions.registryUrl = options.registryUrl;
|
|
935
|
+
this.configManager = new MpakConfigManager(configOptions);
|
|
936
|
+
const clientConfig = {
|
|
937
|
+
registryUrl: this.configManager.getRegistryUrl()
|
|
938
|
+
};
|
|
939
|
+
if (options?.timeout !== void 0) clientConfig.timeout = options.timeout;
|
|
940
|
+
if (options?.userAgent !== void 0) clientConfig.userAgent = options.userAgent;
|
|
941
|
+
this.client = new MpakClient(clientConfig);
|
|
942
|
+
this.bundleCache = new MpakBundleCache(this.client, {
|
|
943
|
+
mpakHome: this.configManager.mpakHome
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Prepare a bundle for execution.
|
|
948
|
+
*
|
|
949
|
+
* Accepts either a registry spec (`{ name, version? }`) or a local bundle
|
|
950
|
+
* spec (`{ local }`). Downloads/extracts as needed, reads the manifest,
|
|
951
|
+
* validates user config, and resolves the command, args, and env needed
|
|
952
|
+
* to spawn the MCP server process.
|
|
953
|
+
*
|
|
954
|
+
* @param spec - Which bundle to prepare. See {@link PrepareServerSpec}.
|
|
955
|
+
* @param options - Force re-download/re-extract, extra env, and workspace dir.
|
|
956
|
+
*
|
|
957
|
+
* @throws {MpakConfigError} If required user config values are missing.
|
|
958
|
+
* @throws {MpakCacheCorruptedError} If the manifest is missing or corrupt after download.
|
|
959
|
+
*/
|
|
960
|
+
async prepareServer(spec, options) {
|
|
961
|
+
let cacheDir;
|
|
962
|
+
let name;
|
|
963
|
+
let version;
|
|
964
|
+
let manifest;
|
|
965
|
+
if ("local" in spec) {
|
|
966
|
+
({ cacheDir, name, version, manifest } = await this.prepareLocalBundle(spec.local, options));
|
|
967
|
+
} else {
|
|
968
|
+
({ cacheDir, name, version, manifest } = await this.prepareRegistryBundle(
|
|
969
|
+
spec.name,
|
|
970
|
+
spec.version,
|
|
971
|
+
options
|
|
972
|
+
));
|
|
973
|
+
}
|
|
974
|
+
const userConfigValues = this.gatherUserConfig(name, manifest);
|
|
975
|
+
const { command, args, env } = this.resolveCommand(manifest, cacheDir, userConfigValues);
|
|
976
|
+
env["MPAK_WORKSPACE"] = options?.workspaceDir ?? (0, import_node_path4.join)(process.cwd(), ".mpak");
|
|
977
|
+
if (options?.env) {
|
|
978
|
+
Object.assign(env, options.env);
|
|
979
|
+
}
|
|
980
|
+
return { command, args, env, cwd: cacheDir, name, version };
|
|
981
|
+
}
|
|
982
|
+
// ===========================================================================
|
|
983
|
+
// Private helpers
|
|
984
|
+
// ===========================================================================
|
|
985
|
+
/**
|
|
986
|
+
* Load a registry bundle into cache and read its manifest.
|
|
987
|
+
*/
|
|
988
|
+
async prepareRegistryBundle(packageName, version, options) {
|
|
989
|
+
const loadOptions = {};
|
|
990
|
+
if (version !== void 0) loadOptions.version = version;
|
|
991
|
+
if (options?.force !== void 0) loadOptions.force = options.force;
|
|
992
|
+
const loadResult = await this.bundleCache.loadBundle(packageName, loadOptions);
|
|
993
|
+
const manifest = this.bundleCache.getBundleManifest(packageName);
|
|
994
|
+
if (!manifest) {
|
|
995
|
+
throw new MpakCacheCorruptedError(
|
|
996
|
+
`Manifest file missing for ${packageName}`,
|
|
997
|
+
(0, import_node_path4.join)(this.bundleCache.cacheHome, packageName)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
cacheDir: loadResult.cacheDir,
|
|
1002
|
+
name: packageName,
|
|
1003
|
+
version: loadResult.version,
|
|
1004
|
+
manifest
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Extract a local `.mcpb` bundle (if stale) and read its manifest.
|
|
1009
|
+
* Local bundles are cached under `<cacheHome>/_local/<hash>`.
|
|
1010
|
+
*
|
|
1011
|
+
* The caller is responsible for validating that `bundlePath` exists
|
|
1012
|
+
* and has a `.mcpb` extension before calling this method.
|
|
1013
|
+
*/
|
|
1014
|
+
async prepareLocalBundle(bundlePath, options) {
|
|
1015
|
+
const absolutePath = (0, import_node_path4.resolve)(bundlePath);
|
|
1016
|
+
const hash = hashBundlePath(absolutePath);
|
|
1017
|
+
const cacheDir = (0, import_node_path4.join)(this.bundleCache.cacheHome, "_local", hash);
|
|
1018
|
+
const needsExtract = options?.force || localBundleNeedsExtract(absolutePath, cacheDir);
|
|
1019
|
+
if (needsExtract) {
|
|
1020
|
+
if ((0, import_node_fs4.existsSync)(cacheDir)) {
|
|
1021
|
+
(0, import_node_fs4.rmSync)(cacheDir, { recursive: true, force: true });
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
extractZip(absolutePath, cacheDir);
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
throw new MpakInvalidBundleError(
|
|
1027
|
+
err instanceof Error ? err.message : String(err),
|
|
1028
|
+
absolutePath,
|
|
1029
|
+
err instanceof Error ? err : void 0
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
(0, import_node_fs4.writeFileSync)(
|
|
1033
|
+
(0, import_node_path4.join)(cacheDir, ".mpak-local-meta.json"),
|
|
1034
|
+
JSON.stringify({
|
|
1035
|
+
localPath: absolutePath,
|
|
1036
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
let manifest;
|
|
1041
|
+
try {
|
|
1042
|
+
manifest = readJsonFromFile((0, import_node_path4.join)(cacheDir, "manifest.json"), import_mpak_schemas2.McpbManifestSchema);
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
throw new MpakInvalidBundleError(
|
|
1045
|
+
err instanceof Error ? err.message : String(err),
|
|
1046
|
+
absolutePath,
|
|
1047
|
+
err instanceof Error ? err : void 0
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
return { cacheDir, name: manifest.name, version: manifest.version, manifest };
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Gather stored user config values and validate that all required fields are present.
|
|
1054
|
+
* @throws If required config values are missing.
|
|
1055
|
+
*/
|
|
1056
|
+
gatherUserConfig(packageName, manifest) {
|
|
1057
|
+
if (!manifest.user_config || Object.keys(manifest.user_config).length === 0) {
|
|
1058
|
+
return {};
|
|
1059
|
+
}
|
|
1060
|
+
const storedConfig = this.configManager.getPackageConfig(packageName) ?? {};
|
|
1061
|
+
const result = {};
|
|
1062
|
+
const missingFields = [];
|
|
1063
|
+
for (const [fieldName, fieldData] of Object.entries(manifest.user_config)) {
|
|
1064
|
+
const storedValue = storedConfig[fieldName];
|
|
1065
|
+
if (storedValue !== void 0) {
|
|
1066
|
+
result[fieldName] = storedValue;
|
|
1067
|
+
} else if (fieldData.default !== void 0 && fieldData.default !== null) {
|
|
1068
|
+
result[fieldName] = String(fieldData.default);
|
|
1069
|
+
} else if (fieldData.required) {
|
|
1070
|
+
const field = {
|
|
1071
|
+
key: fieldName,
|
|
1072
|
+
title: fieldData.title ?? fieldName,
|
|
1073
|
+
sensitive: fieldData.sensitive ?? false
|
|
1074
|
+
};
|
|
1075
|
+
if (fieldData.description !== void 0) {
|
|
1076
|
+
field.description = fieldData.description;
|
|
1077
|
+
}
|
|
1078
|
+
missingFields.push(field);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (missingFields.length > 0) {
|
|
1082
|
+
throw new MpakConfigError(packageName, missingFields);
|
|
1083
|
+
}
|
|
1084
|
+
return result;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Resolve the manifest's `server` block into a spawnable command, args, and env.
|
|
1088
|
+
*
|
|
1089
|
+
* Handles three server types:
|
|
1090
|
+
* - **binary** — runs the compiled executable at `entry_point`, chmod'd +x.
|
|
1091
|
+
* - **node** — runs `mcp_config.command` (default `"node"`) with `mcp_config.args`,
|
|
1092
|
+
* or falls back to `node <entry_point>` when args are empty.
|
|
1093
|
+
* - **python** — like node, but resolves `python3`/`python` at runtime and
|
|
1094
|
+
* prepends `<cacheDir>/deps` to `PYTHONPATH` for bundled dependencies.
|
|
1095
|
+
*
|
|
1096
|
+
* All `${__dirname}` placeholders in args are replaced with `cacheDir`.
|
|
1097
|
+
* All `${user_config.*}` placeholders in env are replaced with gathered user values.
|
|
1098
|
+
*
|
|
1099
|
+
* @throws For unsupported server types.
|
|
1100
|
+
*/
|
|
1101
|
+
resolveCommand(manifest, cacheDir, userConfigValues) {
|
|
1102
|
+
const { type, entry_point, mcp_config } = manifest.server;
|
|
1103
|
+
const env = _Mpak.substituteEnvVars(mcp_config.env, userConfigValues);
|
|
1104
|
+
let command;
|
|
1105
|
+
let args;
|
|
1106
|
+
switch (type) {
|
|
1107
|
+
case "binary": {
|
|
1108
|
+
command = (0, import_node_path4.join)(cacheDir, entry_point);
|
|
1109
|
+
args = _Mpak.resolveArgs(mcp_config.args ?? [], cacheDir);
|
|
1110
|
+
try {
|
|
1111
|
+
(0, import_node_fs4.chmodSync)(command, 493);
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case "node": {
|
|
1117
|
+
command = mcp_config.command || "node";
|
|
1118
|
+
args = mcp_config.args.length > 0 ? _Mpak.resolveArgs(mcp_config.args, cacheDir) : [(0, import_node_path4.join)(cacheDir, entry_point)];
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "python": {
|
|
1122
|
+
command = mcp_config.command === "python" ? _Mpak.findPythonCommand() : mcp_config.command || _Mpak.findPythonCommand();
|
|
1123
|
+
args = mcp_config.args.length > 0 ? _Mpak.resolveArgs(mcp_config.args, cacheDir) : [(0, import_node_path4.join)(cacheDir, entry_point)];
|
|
1124
|
+
const depsDir = (0, import_node_path4.join)(cacheDir, "deps");
|
|
1125
|
+
env["PYTHONPATH"] = env["PYTHONPATH"] ? `${depsDir}:${env["PYTHONPATH"]}` : depsDir;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
case "uv": {
|
|
1129
|
+
command = mcp_config.command || "uv";
|
|
1130
|
+
args = mcp_config.args.length > 0 ? _Mpak.resolveArgs(mcp_config.args, cacheDir) : ["run", (0, import_node_path4.join)(cacheDir, entry_point)];
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
default: {
|
|
1134
|
+
const _exhaustive = type;
|
|
1135
|
+
throw new MpakCacheCorruptedError(
|
|
1136
|
+
`Unsupported server type "${_exhaustive}" in manifest for ${manifest.name}`,
|
|
1137
|
+
cacheDir
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return { command, args, env };
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Substitute `${__dirname}` placeholders in args.
|
|
1145
|
+
*/
|
|
1146
|
+
static resolveArgs(args, cacheDir) {
|
|
1147
|
+
return args.map((arg) => arg.replace(/\$\{__dirname\}/g, cacheDir));
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Substitute `${user_config.*}` placeholders in env vars.
|
|
1151
|
+
*/
|
|
1152
|
+
static substituteEnvVars(env, userConfigValues) {
|
|
1153
|
+
if (!env) return {};
|
|
1154
|
+
const result = {};
|
|
1155
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1156
|
+
result[key] = value.replace(
|
|
1157
|
+
/\$\{user_config\.([^}]+)\}/g,
|
|
1158
|
+
(match, configKey) => userConfigValues[configKey] ?? match
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Find a working Python executable. Tries `python3` first, falls back to `python`.
|
|
1165
|
+
*/
|
|
1166
|
+
static findPythonCommand() {
|
|
1167
|
+
const result = (0, import_node_child_process2.spawnSync)("python3", ["--version"], { stdio: "pipe" });
|
|
1168
|
+
if (result.status === 0) {
|
|
1169
|
+
return "python3";
|
|
1170
|
+
}
|
|
1171
|
+
return "python";
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// src/utils.ts
|
|
1176
|
+
function parsePackageSpec(spec) {
|
|
1177
|
+
const lastAtIndex = spec.lastIndexOf("@");
|
|
1178
|
+
let name;
|
|
1179
|
+
let version;
|
|
1180
|
+
if (lastAtIndex > 0) {
|
|
1181
|
+
name = spec.substring(0, lastAtIndex);
|
|
1182
|
+
version = spec.substring(lastAtIndex + 1);
|
|
1183
|
+
} else {
|
|
1184
|
+
name = spec;
|
|
1185
|
+
}
|
|
1186
|
+
if (!name.startsWith("@") || !name.includes("/")) {
|
|
1187
|
+
throw new MpakError(
|
|
1188
|
+
`Invalid package spec: "${spec}". Expected scoped format: @scope/name`,
|
|
1189
|
+
"INVALID_SPEC"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
return version ? { name, version } : { name };
|
|
1193
|
+
}
|
|
511
1194
|
// Annotate the CommonJS export names for ESM import in node:
|
|
512
1195
|
0 && (module.exports = {
|
|
1196
|
+
Mpak,
|
|
1197
|
+
MpakBundleCache,
|
|
1198
|
+
MpakCacheCorruptedError,
|
|
513
1199
|
MpakClient,
|
|
1200
|
+
MpakConfigCorruptedError,
|
|
1201
|
+
MpakConfigError,
|
|
1202
|
+
MpakConfigManager,
|
|
514
1203
|
MpakError,
|
|
515
1204
|
MpakIntegrityError,
|
|
1205
|
+
MpakInvalidBundleError,
|
|
516
1206
|
MpakNetworkError,
|
|
517
|
-
MpakNotFoundError
|
|
1207
|
+
MpakNotFoundError,
|
|
1208
|
+
parsePackageSpec
|
|
518
1209
|
});
|
|
519
1210
|
//# sourceMappingURL=index.cjs.map
|