prpm 0.1.17 → 1.0.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 (46) hide show
  1. package/dist/index.js +14257 -107
  2. package/package.json +11 -9
  3. package/dist/__tests__/e2e/test-helpers.js +0 -151
  4. package/dist/commands/buy-credits.js +0 -224
  5. package/dist/commands/catalog.js +0 -365
  6. package/dist/commands/collections.js +0 -655
  7. package/dist/commands/config.js +0 -161
  8. package/dist/commands/credits.js +0 -186
  9. package/dist/commands/index.js +0 -184
  10. package/dist/commands/info.js +0 -78
  11. package/dist/commands/init.js +0 -684
  12. package/dist/commands/install.js +0 -789
  13. package/dist/commands/list.js +0 -189
  14. package/dist/commands/login.js +0 -316
  15. package/dist/commands/outdated.js +0 -130
  16. package/dist/commands/playground.js +0 -570
  17. package/dist/commands/popular.js +0 -33
  18. package/dist/commands/publish.js +0 -803
  19. package/dist/commands/schema.js +0 -41
  20. package/dist/commands/search.js +0 -446
  21. package/dist/commands/subscribe.js +0 -211
  22. package/dist/commands/telemetry.js +0 -104
  23. package/dist/commands/trending.js +0 -86
  24. package/dist/commands/uninstall.js +0 -120
  25. package/dist/commands/update.js +0 -121
  26. package/dist/commands/upgrade.js +0 -121
  27. package/dist/commands/whoami.js +0 -83
  28. package/dist/core/claude-config.js +0 -91
  29. package/dist/core/cursor-config.js +0 -130
  30. package/dist/core/downloader.js +0 -64
  31. package/dist/core/errors.js +0 -29
  32. package/dist/core/filesystem.js +0 -242
  33. package/dist/core/lockfile.js +0 -292
  34. package/dist/core/marketplace-converter.js +0 -224
  35. package/dist/core/registry-client.js +0 -305
  36. package/dist/core/schema-validator.js +0 -74
  37. package/dist/core/telemetry.js +0 -253
  38. package/dist/core/user-config.js +0 -147
  39. package/dist/types/registry.js +0 -12
  40. package/dist/types.js +0 -36
  41. package/dist/utils/license-extractor.js +0 -122
  42. package/dist/utils/multi-package.js +0 -117
  43. package/dist/utils/parallel-publisher.js +0 -144
  44. package/dist/utils/script-executor.js +0 -72
  45. package/dist/utils/snippet-extractor.js +0 -77
  46. package/dist/utils/webapp-url.js +0 -44
@@ -1,224 +0,0 @@
1
- "use strict";
2
- /**
3
- * Converter for Claude marketplace.json format to PRPM manifest
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.marketplaceToManifest = marketplaceToManifest;
7
- exports.validateMarketplaceJson = validateMarketplaceJson;
8
- /**
9
- * Convert marketplace.json to PRPM manifest format
10
- *
11
- * Strategy:
12
- * - If multiple plugins exist, create a manifest for the first plugin (user can publish others separately)
13
- * - If the plugin has agents/skills/commands, prefer those over the root plugin info
14
- * - Map marketplace fields to PRPM manifest fields
15
- *
16
- * @param marketplace - The marketplace.json content
17
- * @param pluginIndex - Which plugin to convert (default: 0, for the first plugin)
18
- * @returns PRPM manifest
19
- */
20
- function marketplaceToManifest(marketplace, pluginIndex = 0) {
21
- if (!marketplace.plugins || marketplace.plugins.length === 0) {
22
- throw new Error('marketplace.json must contain at least one plugin');
23
- }
24
- if (pluginIndex >= marketplace.plugins.length) {
25
- throw new Error(`Plugin index ${pluginIndex} out of range. Found ${marketplace.plugins.length} plugins.`);
26
- }
27
- const plugin = marketplace.plugins[pluginIndex];
28
- // Determine package format and subtype based on what the plugin contains
29
- let format = 'claude';
30
- let subtype = 'rule';
31
- if (plugin.agents && plugin.agents.length > 0) {
32
- format = 'claude';
33
- subtype = 'agent';
34
- }
35
- else if (plugin.skills && plugin.skills.length > 0) {
36
- format = 'claude';
37
- subtype = 'skill';
38
- }
39
- else if (plugin.commands && plugin.commands.length > 0) {
40
- format = 'claude';
41
- subtype = 'slash-command';
42
- }
43
- // Generate package name from plugin name
44
- // Format: @owner/plugin-name
45
- const ownerName = typeof marketplace.owner === 'string' ? marketplace.owner : marketplace.owner.name;
46
- const packageName = generatePackageName(ownerName, plugin.name);
47
- // Collect all files that should be included
48
- const files = collectFiles(plugin);
49
- // Determine the main file
50
- const main = determineMainFile(plugin);
51
- // Collect keywords from both marketplace and plugin
52
- const keywords = [
53
- ...(marketplace.keywords || []),
54
- ...(plugin.keywords || []),
55
- ].slice(0, 20); // Max 20 keywords
56
- // Extract tags from keywords (first 10)
57
- const tags = keywords.slice(0, 10);
58
- // Get description from plugin, metadata, or root
59
- const description = plugin.description ||
60
- marketplace.metadata?.description ||
61
- marketplace.description ||
62
- '';
63
- // Get version from plugin, metadata, or root
64
- const version = plugin.version ||
65
- marketplace.metadata?.version ||
66
- marketplace.version ||
67
- '1.0.0';
68
- // Get author - prefer plugin.author, fallback to owner name
69
- const author = plugin.author || ownerName;
70
- const manifest = {
71
- name: packageName,
72
- version,
73
- description,
74
- format,
75
- subtype,
76
- author,
77
- files,
78
- tags,
79
- keywords,
80
- };
81
- // Add optional fields if available
82
- if (marketplace.githubUrl) {
83
- manifest.repository = marketplace.githubUrl;
84
- }
85
- if (marketplace.websiteUrl) {
86
- manifest.homepage = marketplace.websiteUrl;
87
- }
88
- if (plugin.category) {
89
- manifest.category = plugin.category;
90
- }
91
- if (main) {
92
- manifest.main = main;
93
- }
94
- return manifest;
95
- }
96
- /**
97
- * Generate PRPM-compatible package name from owner and plugin name
98
- */
99
- function generatePackageName(owner, pluginName) {
100
- // Sanitize owner and plugin name
101
- const sanitizedOwner = owner.toLowerCase().replace(/[^a-z0-9-]/g, '-');
102
- const sanitizedName = pluginName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
103
- // Remove leading/trailing hyphens
104
- const cleanOwner = sanitizedOwner.replace(/^-+|-+$/g, '');
105
- const cleanName = sanitizedName.replace(/^-+|-+$/g, '');
106
- return `@${cleanOwner}/${cleanName}`;
107
- }
108
- /**
109
- * Collect all files referenced in the plugin
110
- */
111
- function collectFiles(plugin) {
112
- const files = new Set();
113
- // Add plugin source if it's a file path
114
- if (plugin.source && !plugin.source.startsWith('http')) {
115
- files.add(plugin.source);
116
- }
117
- // Add agent files
118
- if (plugin.agents) {
119
- for (const agent of plugin.agents) {
120
- if (agent.source && !agent.source.startsWith('http')) {
121
- files.add(agent.source);
122
- }
123
- }
124
- }
125
- // Add skill files
126
- if (plugin.skills) {
127
- for (const skill of plugin.skills) {
128
- if (skill.source && !skill.source.startsWith('http')) {
129
- files.add(skill.source);
130
- }
131
- }
132
- }
133
- // Add command files
134
- if (plugin.commands) {
135
- for (const command of plugin.commands) {
136
- if (command.source && !command.source.startsWith('http')) {
137
- files.add(command.source);
138
- }
139
- }
140
- }
141
- // Add standard files if they're not already included
142
- const standardFiles = ['README.md', 'LICENSE', '.claude/marketplace.json', '.claude-plugin/marketplace.json'];
143
- for (const file of standardFiles) {
144
- files.add(file);
145
- }
146
- return Array.from(files);
147
- }
148
- /**
149
- * Determine the main entry file for the package
150
- * Only set main if there's a single clear entry point
151
- */
152
- function determineMainFile(plugin) {
153
- const agentCount = plugin.agents?.length || 0;
154
- const skillCount = plugin.skills?.length || 0;
155
- const commandCount = plugin.commands?.length || 0;
156
- // Only set main if there's exactly one item total
157
- const totalCount = agentCount + skillCount + commandCount;
158
- if (totalCount !== 1) {
159
- // Multiple items or no items - no clear main file
160
- return undefined;
161
- }
162
- // Single agent
163
- if (agentCount === 1) {
164
- const source = plugin.agents[0].source;
165
- if (source && !source.startsWith('http')) {
166
- return source;
167
- }
168
- }
169
- // Single skill
170
- if (skillCount === 1) {
171
- const source = plugin.skills[0].source;
172
- if (source && !source.startsWith('http')) {
173
- return source;
174
- }
175
- }
176
- // Single command
177
- if (commandCount === 1) {
178
- const source = plugin.commands[0].source;
179
- if (source && !source.startsWith('http')) {
180
- return source;
181
- }
182
- }
183
- // Otherwise, use plugin source if available
184
- if (plugin.source && !plugin.source.startsWith('http')) {
185
- return plugin.source;
186
- }
187
- return undefined;
188
- }
189
- /**
190
- * Validate marketplace.json structure
191
- */
192
- function validateMarketplaceJson(data) {
193
- if (!data || typeof data !== 'object') {
194
- return false;
195
- }
196
- const marketplace = data;
197
- // Check required fields
198
- if (!marketplace.name || typeof marketplace.name !== 'string') {
199
- return false;
200
- }
201
- // owner can be either string or object with name property
202
- if (!marketplace.owner) {
203
- return false;
204
- }
205
- if (typeof marketplace.owner !== 'string' &&
206
- (typeof marketplace.owner !== 'object' || !marketplace.owner.name)) {
207
- return false;
208
- }
209
- // description can be at root or in metadata
210
- const hasDescription = (marketplace.description && typeof marketplace.description === 'string') ||
211
- (marketplace.metadata?.description && typeof marketplace.metadata.description === 'string');
212
- if (!hasDescription) {
213
- return false;
214
- }
215
- if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length === 0) {
216
- return false;
217
- }
218
- // Validate first plugin has required fields
219
- const plugin = marketplace.plugins[0];
220
- if (!plugin.name || !plugin.description || !plugin.version) {
221
- return false;
222
- }
223
- return true;
224
- }
@@ -1,305 +0,0 @@
1
- "use strict";
2
- /**
3
- * Registry API Client
4
- * Handles all communication with the PRMP Registry
5
- */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.RegistryClient = void 0;
11
- exports.getRegistryClient = getRegistryClient;
12
- const package_json_1 = __importDefault(require("../../package.json"));
13
- // CLI version for User-Agent header (exempts from rate limiting)
14
- const CLI_VERSION = package_json_1.default.version;
15
- class RegistryClient {
16
- constructor(config) {
17
- this.baseUrl = config.url.replace(/\/$/, ''); // Remove trailing slash
18
- this.token = config.token;
19
- }
20
- /**
21
- * Search for packages in the registry
22
- */
23
- async search(query, options) {
24
- const params = new URLSearchParams({ q: query });
25
- if (options?.format)
26
- params.append('format', options.format);
27
- if (options?.subtype)
28
- params.append('subtype', options.subtype);
29
- if (options?.tags)
30
- options.tags.forEach(tag => params.append('tags', tag));
31
- if (options?.limit)
32
- params.append('limit', options.limit.toString());
33
- if (options?.offset)
34
- params.append('offset', options.offset.toString());
35
- const response = await this.fetch(`/api/v1/search?${params}`);
36
- return response.json();
37
- }
38
- /**
39
- * Get package information
40
- */
41
- async getPackage(packageId) {
42
- const response = await this.fetch(`/api/v1/packages/${packageId}`);
43
- return response.json();
44
- }
45
- /**
46
- * Get specific package version
47
- */
48
- async getPackageVersion(packageId, version) {
49
- const response = await this.fetch(`/api/v1/packages/${packageId}/${version}`);
50
- return response.json();
51
- }
52
- /**
53
- * Get package dependencies
54
- */
55
- async getPackageDependencies(packageId, version) {
56
- const versionPath = version ? `/${version}` : '';
57
- const response = await this.fetch(`/api/v1/packages/${packageId}${versionPath}/dependencies`);
58
- return response.json();
59
- }
60
- /**
61
- * Get all versions for a package
62
- */
63
- async getPackageVersions(packageId) {
64
- const response = await this.fetch(`/api/v1/packages/${packageId}/versions`);
65
- return response.json();
66
- }
67
- /**
68
- * Resolve dependency tree
69
- */
70
- async resolveDependencies(packageId, version) {
71
- const params = new URLSearchParams();
72
- if (version)
73
- params.append('version', version);
74
- const response = await this.fetch(`/api/v1/packages/${packageId}/resolve?${params}`);
75
- return response.json();
76
- }
77
- /**
78
- * Download package tarball
79
- */
80
- async downloadPackage(tarballUrl, options = {}) {
81
- // If format is specified and tarballUrl is from registry, append format param
82
- let url = tarballUrl;
83
- if (options.format && tarballUrl.includes(this.baseUrl)) {
84
- const urlObj = new URL(tarballUrl);
85
- urlObj.searchParams.set('format', options.format);
86
- url = urlObj.toString();
87
- }
88
- const response = await fetch(url);
89
- if (!response.ok) {
90
- throw new Error(`Failed to download package: ${response.statusText}`);
91
- }
92
- const arrayBuffer = await response.arrayBuffer();
93
- return Buffer.from(arrayBuffer);
94
- }
95
- /**
96
- * Get trending packages
97
- */
98
- async getTrending(format, subtype, limit = 20) {
99
- const params = new URLSearchParams({ limit: limit.toString() });
100
- if (format)
101
- params.append('format', format);
102
- if (subtype)
103
- params.append('subtype', subtype);
104
- const response = await this.fetch(`/api/v1/search/trending?${params}`);
105
- const data = await response.json();
106
- return data.packages;
107
- }
108
- /**
109
- * Get featured packages
110
- */
111
- async getFeatured(format, subtype, limit = 20) {
112
- const params = new URLSearchParams({ limit: limit.toString() });
113
- if (format)
114
- params.append('format', format);
115
- if (subtype)
116
- params.append('subtype', subtype);
117
- const response = await this.fetch(`/api/v1/search/featured?${params}`);
118
- const data = await response.json();
119
- return data.packages;
120
- }
121
- /**
122
- * Publish a package (requires authentication)
123
- */
124
- async publish(manifest, tarball, options) {
125
- if (!this.token) {
126
- throw new Error('Authentication required. Run `prpm login` first.');
127
- }
128
- const formData = new FormData();
129
- formData.append('manifest', JSON.stringify(manifest));
130
- formData.append('tarball', new Blob([tarball]), 'package.tar.gz');
131
- // Add org_id if provided
132
- if (options?.orgId) {
133
- formData.append('org_id', options.orgId);
134
- }
135
- const response = await this.fetch('/api/v1/packages', {
136
- method: 'POST',
137
- body: formData,
138
- });
139
- return response.json();
140
- }
141
- /**
142
- * Login and get authentication token
143
- */
144
- async login() {
145
- // This will open browser for GitHub OAuth
146
- // For now, return placeholder - will implement OAuth flow
147
- throw new Error('Login not yet implemented. Coming soon!');
148
- }
149
- /**
150
- * Get current user info
151
- */
152
- async whoami() {
153
- if (!this.token) {
154
- throw new Error('Not authenticated. Run `prpm login` first.');
155
- }
156
- const response = await this.fetch('/api/v1/auth/me');
157
- return response.json();
158
- }
159
- /**
160
- * Get collections
161
- */
162
- async getCollections(options) {
163
- const params = new URLSearchParams();
164
- if (options?.category)
165
- params.append('category', options.category);
166
- if (options?.tag)
167
- params.append('tag', options.tag);
168
- if (options?.official)
169
- params.append('official', 'true');
170
- if (options?.scope)
171
- params.append('scope', options.scope);
172
- if (options?.limit)
173
- params.append('limit', options.limit.toString());
174
- if (options?.offset)
175
- params.append('offset', options.offset.toString());
176
- const response = await this.fetch(`/api/v1/collections?${params}`);
177
- return response.json();
178
- }
179
- /**
180
- * Get collection details
181
- */
182
- async getCollection(scope, id, version) {
183
- const versionPath = version ? `/${version}` : '/1.0.0';
184
- const response = await this.fetch(`/api/v1/collections/${scope}/${id}${versionPath}`);
185
- return response.json();
186
- }
187
- /**
188
- * Install collection (get installation plan)
189
- */
190
- async installCollection(options) {
191
- const params = new URLSearchParams();
192
- if (options.format)
193
- params.append('format', options.format);
194
- if (options.skipOptional)
195
- params.append('skipOptional', 'true');
196
- const versionPath = options.version ? `@${options.version}` : '';
197
- const response = await this.fetch(`/api/v1/collections/${options.scope}/${options.id}${versionPath}/install?${params}`, { method: 'POST' });
198
- return response.json();
199
- }
200
- /**
201
- * Create a collection (requires authentication)
202
- */
203
- async createCollection(data) {
204
- if (!this.token) {
205
- throw new Error('Authentication required. Run `prpm login` first.');
206
- }
207
- const response = await this.fetch('/api/v1/collections', {
208
- method: 'POST',
209
- body: JSON.stringify(data),
210
- });
211
- return response.json();
212
- }
213
- /**
214
- * Helper method for making authenticated requests with retry logic
215
- */
216
- async fetch(path, options = {}, retries = 3) {
217
- const url = `${this.baseUrl}${path}`;
218
- // Debug logging
219
- if (process.env.DEBUG || process.env.PRPM_DEBUG) {
220
- console.error(`[DEBUG] Fetching: ${url}`);
221
- console.error(`[DEBUG] Method: ${options.method || 'GET'}`);
222
- console.error(`[DEBUG] Has token: ${!!this.token}`);
223
- }
224
- const headers = {
225
- 'Content-Type': 'application/json',
226
- 'User-Agent': `prpm-cli/${CLI_VERSION}`,
227
- ...options.headers,
228
- };
229
- if (this.token) {
230
- headers['Authorization'] = `Bearer ${this.token}`;
231
- }
232
- let lastError = null;
233
- for (let attempt = 0; attempt < retries; attempt++) {
234
- try {
235
- if (process.env.DEBUG || process.env.PRPM_DEBUG) {
236
- console.error(`[DEBUG] Attempt ${attempt + 1}/${retries}`);
237
- }
238
- const response = await fetch(url, {
239
- ...options,
240
- headers,
241
- });
242
- // Handle rate limiting with retry
243
- if (response.status === 429) {
244
- const retryAfter = response.headers.get('Retry-After');
245
- const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
246
- if (attempt < retries - 1) {
247
- await new Promise(resolve => setTimeout(resolve, waitTime));
248
- continue;
249
- }
250
- }
251
- // Handle server errors with retry
252
- if (response.status >= 500 && response.status < 600 && attempt < retries - 1) {
253
- const waitTime = Math.pow(2, attempt) * 1000;
254
- await new Promise(resolve => setTimeout(resolve, waitTime));
255
- continue;
256
- }
257
- if (!response.ok) {
258
- const error = await response.json().catch(() => ({ error: response.statusText }));
259
- throw new Error(error.error || error.message || `HTTP ${response.status}: ${response.statusText}`);
260
- }
261
- return response;
262
- }
263
- catch (error) {
264
- lastError = error instanceof Error ? error : new Error(String(error));
265
- if (process.env.DEBUG || process.env.PRPM_DEBUG) {
266
- console.error(`[DEBUG] Error on attempt ${attempt + 1}:`, lastError.message);
267
- console.error(`[DEBUG] Error type:`, lastError.constructor.name);
268
- console.error(`[DEBUG] Full error:`, lastError);
269
- }
270
- // Network errors - retry with exponential backoff
271
- if (attempt < retries - 1 && (lastError.message.includes('fetch failed') ||
272
- lastError.message.includes('ECONNREFUSED') ||
273
- lastError.message.includes('ETIMEDOUT'))) {
274
- const waitTime = Math.pow(2, attempt) * 1000;
275
- if (process.env.DEBUG || process.env.PRPM_DEBUG) {
276
- console.error(`[DEBUG] Retrying after ${waitTime}ms...`);
277
- }
278
- await new Promise(resolve => setTimeout(resolve, waitTime));
279
- continue;
280
- }
281
- // If it's not a retryable error or we're out of retries, throw with more context
282
- if (attempt === retries - 1) {
283
- const enhancedError = new Error(`Failed to connect to registry at ${url}\n` +
284
- `Original error: ${lastError.message}\n\n` +
285
- `💡 Possible causes:\n` +
286
- ` - Registry server is not running\n` +
287
- ` - Network connection issue\n` +
288
- ` - Incorrect PRPM_REGISTRY_URL (currently: ${this.baseUrl})`);
289
- throw enhancedError;
290
- }
291
- }
292
- }
293
- throw lastError || new Error(`Request failed after ${retries} retries to ${url}`);
294
- }
295
- }
296
- exports.RegistryClient = RegistryClient;
297
- /**
298
- * Get registry client with configuration
299
- */
300
- function getRegistryClient(config) {
301
- return new RegistryClient({
302
- url: config.registryUrl || 'https://registry.prpm.dev',
303
- token: config.token,
304
- });
305
- }
@@ -1,74 +0,0 @@
1
- "use strict";
2
- /**
3
- * JSON Schema validation for PRPM manifests
4
- */
5
- var __importDefault = (this && this.__importDefault) || function (mod) {
6
- return (mod && mod.__esModule) ? mod : { "default": mod };
7
- };
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.validateManifestSchema = validateManifestSchema;
10
- exports.getManifestSchema = getManifestSchema;
11
- const ajv_1 = __importDefault(require("ajv"));
12
- const ajv_formats_1 = __importDefault(require("ajv-formats"));
13
- const fs_1 = require("fs");
14
- const path_1 = require("path");
15
- // Load the JSON schema
16
- const schemaPath = (0, path_1.join)(__dirname, '../../schemas/prpm-manifest.schema.json');
17
- let schema;
18
- try {
19
- schema = JSON.parse((0, fs_1.readFileSync)(schemaPath, 'utf-8'));
20
- }
21
- catch (error) {
22
- // Schema file not found, validation will be skipped
23
- console.warn('⚠️ Could not load manifest schema, skipping schema validation');
24
- }
25
- /**
26
- * Validate manifest against JSON schema
27
- */
28
- function validateManifestSchema(manifest) {
29
- if (!schema) {
30
- // Schema not loaded, skip validation
31
- return { valid: true };
32
- }
33
- const ajv = new ajv_1.default({
34
- allErrors: true,
35
- verbose: true,
36
- });
37
- (0, ajv_formats_1.default)(ajv);
38
- const validate = ajv.compile(schema);
39
- const valid = validate(manifest);
40
- if (!valid && validate.errors) {
41
- const errors = validate.errors.map(err => {
42
- const path = err.instancePath || 'manifest';
43
- const message = err.message || 'validation failed';
44
- // Format error messages to be more user-friendly
45
- if (err.keyword === 'required') {
46
- const missingProp = err.params.missingProperty;
47
- return `Missing required field: ${missingProp}`;
48
- }
49
- if (err.keyword === 'pattern') {
50
- return `${path}: ${message}. Value does not match required pattern.`;
51
- }
52
- if (err.keyword === 'enum') {
53
- const allowedValues = err.params.allowedValues;
54
- return `${path}: ${message}. Allowed values: ${allowedValues.join(', ')}`;
55
- }
56
- if (err.keyword === 'minLength' || err.keyword === 'maxLength') {
57
- const limit = err.params.limit;
58
- return `${path}: ${message} (${err.keyword}: ${limit})`;
59
- }
60
- if (err.keyword === 'oneOf') {
61
- return `${path}: must match exactly one schema (check if files array uses either all strings or all objects, not mixed)`;
62
- }
63
- return `${path}: ${message}`;
64
- });
65
- return { valid: false, errors };
66
- }
67
- return { valid: true };
68
- }
69
- /**
70
- * Get the JSON schema (for documentation/export purposes)
71
- */
72
- function getManifestSchema() {
73
- return schema;
74
- }