prpm 0.0.10 → 0.0.11
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/dist/commands/init.js +17 -40
- package/dist/commands/install.js +30 -11
- package/dist/commands/publish.js +211 -52
- package/dist/commands/search.js +3 -7
- package/dist/commands/uninstall.js +59 -21
- package/dist/core/filesystem.js +2 -2
- package/dist/core/marketplace-converter.js +28 -7
- package/dist/core/registry-client.js +31 -4
- package/dist/types/registry.js +7 -0
- package/dist/types.js +30 -0
- package/dist/utils/license-extractor.js +122 -0
- package/dist/utils/multi-package.js +117 -0
- package/dist/utils/parallel-publisher.js +144 -0
- package/dist/utils/snippet-extractor.js +70 -0
- package/package.json +3 -3
- package/schemas/prpm-manifest.schema.json +30 -0
package/dist/core/filesystem.js
CHANGED
|
@@ -18,7 +18,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
18
|
/**
|
|
19
19
|
* Get the destination directory for a package based on format and subtype
|
|
20
20
|
*/
|
|
21
|
-
function getDestinationDir(format, subtype) {
|
|
21
|
+
function getDestinationDir(format, subtype, name) {
|
|
22
22
|
switch (format) {
|
|
23
23
|
case 'cursor':
|
|
24
24
|
if (subtype === 'agent')
|
|
@@ -28,7 +28,7 @@ function getDestinationDir(format, subtype) {
|
|
|
28
28
|
return '.cursor/rules';
|
|
29
29
|
case 'claude':
|
|
30
30
|
if (subtype === 'skill')
|
|
31
|
-
return
|
|
31
|
+
return `.claude/skills/${name}`;
|
|
32
32
|
if (subtype === 'slash-command')
|
|
33
33
|
return '.claude/commands';
|
|
34
34
|
if (subtype === 'agent')
|
|
@@ -42,7 +42,8 @@ function marketplaceToManifest(marketplace, pluginIndex = 0) {
|
|
|
42
42
|
}
|
|
43
43
|
// Generate package name from plugin name
|
|
44
44
|
// Format: @owner/plugin-name
|
|
45
|
-
const
|
|
45
|
+
const ownerName = typeof marketplace.owner === 'string' ? marketplace.owner : marketplace.owner.name;
|
|
46
|
+
const packageName = generatePackageName(ownerName, plugin.name);
|
|
46
47
|
// Collect all files that should be included
|
|
47
48
|
const files = collectFiles(plugin);
|
|
48
49
|
// Determine the main file
|
|
@@ -54,13 +55,25 @@ function marketplaceToManifest(marketplace, pluginIndex = 0) {
|
|
|
54
55
|
].slice(0, 20); // Max 20 keywords
|
|
55
56
|
// Extract tags from keywords (first 10)
|
|
56
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;
|
|
57
70
|
const manifest = {
|
|
58
71
|
name: packageName,
|
|
59
|
-
version
|
|
60
|
-
description
|
|
72
|
+
version,
|
|
73
|
+
description,
|
|
61
74
|
format,
|
|
62
75
|
subtype,
|
|
63
|
-
author
|
|
76
|
+
author,
|
|
64
77
|
files,
|
|
65
78
|
tags,
|
|
66
79
|
keywords,
|
|
@@ -126,7 +139,7 @@ function collectFiles(plugin) {
|
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
// Add standard files if they're not already included
|
|
129
|
-
const standardFiles = ['README.md', 'LICENSE', '.claude/marketplace.json'];
|
|
142
|
+
const standardFiles = ['README.md', 'LICENSE', '.claude/marketplace.json', '.claude-plugin/marketplace.json'];
|
|
130
143
|
for (const file of standardFiles) {
|
|
131
144
|
files.add(file);
|
|
132
145
|
}
|
|
@@ -185,10 +198,18 @@ function validateMarketplaceJson(data) {
|
|
|
185
198
|
if (!marketplace.name || typeof marketplace.name !== 'string') {
|
|
186
199
|
return false;
|
|
187
200
|
}
|
|
188
|
-
|
|
201
|
+
// owner can be either string or object with name property
|
|
202
|
+
if (!marketplace.owner) {
|
|
189
203
|
return false;
|
|
190
204
|
}
|
|
191
|
-
if (
|
|
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) {
|
|
192
213
|
return false;
|
|
193
214
|
}
|
|
194
215
|
if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length === 0) {
|
|
@@ -115,13 +115,17 @@ class RegistryClient {
|
|
|
115
115
|
/**
|
|
116
116
|
* Publish a package (requires authentication)
|
|
117
117
|
*/
|
|
118
|
-
async publish(manifest, tarball) {
|
|
118
|
+
async publish(manifest, tarball, options) {
|
|
119
119
|
if (!this.token) {
|
|
120
120
|
throw new Error('Authentication required. Run `prpm login` first.');
|
|
121
121
|
}
|
|
122
122
|
const formData = new FormData();
|
|
123
123
|
formData.append('manifest', JSON.stringify(manifest));
|
|
124
124
|
formData.append('tarball', new Blob([tarball]), 'package.tar.gz');
|
|
125
|
+
// Add org_id if provided
|
|
126
|
+
if (options?.orgId) {
|
|
127
|
+
formData.append('org_id', options.orgId);
|
|
128
|
+
}
|
|
125
129
|
const response = await this.fetch('/api/v1/packages', {
|
|
126
130
|
method: 'POST',
|
|
127
131
|
body: formData,
|
|
@@ -205,6 +209,12 @@ class RegistryClient {
|
|
|
205
209
|
*/
|
|
206
210
|
async fetch(path, options = {}, retries = 3) {
|
|
207
211
|
const url = `${this.baseUrl}${path}`;
|
|
212
|
+
// Debug logging
|
|
213
|
+
if (process.env.DEBUG || process.env.PRPM_DEBUG) {
|
|
214
|
+
console.error(`[DEBUG] Fetching: ${url}`);
|
|
215
|
+
console.error(`[DEBUG] Method: ${options.method || 'GET'}`);
|
|
216
|
+
console.error(`[DEBUG] Has token: ${!!this.token}`);
|
|
217
|
+
}
|
|
208
218
|
const headers = {
|
|
209
219
|
'Content-Type': 'application/json',
|
|
210
220
|
...options.headers,
|
|
@@ -215,6 +225,9 @@ class RegistryClient {
|
|
|
215
225
|
let lastError = null;
|
|
216
226
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
217
227
|
try {
|
|
228
|
+
if (process.env.DEBUG || process.env.PRPM_DEBUG) {
|
|
229
|
+
console.error(`[DEBUG] Attempt ${attempt + 1}/${retries}`);
|
|
230
|
+
}
|
|
218
231
|
const response = await fetch(url, {
|
|
219
232
|
...options,
|
|
220
233
|
headers,
|
|
@@ -242,21 +255,35 @@ class RegistryClient {
|
|
|
242
255
|
}
|
|
243
256
|
catch (error) {
|
|
244
257
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
258
|
+
if (process.env.DEBUG || process.env.PRPM_DEBUG) {
|
|
259
|
+
console.error(`[DEBUG] Error on attempt ${attempt + 1}:`, lastError.message);
|
|
260
|
+
console.error(`[DEBUG] Error type:`, lastError.constructor.name);
|
|
261
|
+
console.error(`[DEBUG] Full error:`, lastError);
|
|
262
|
+
}
|
|
245
263
|
// Network errors - retry with exponential backoff
|
|
246
264
|
if (attempt < retries - 1 && (lastError.message.includes('fetch failed') ||
|
|
247
265
|
lastError.message.includes('ECONNREFUSED') ||
|
|
248
266
|
lastError.message.includes('ETIMEDOUT'))) {
|
|
249
267
|
const waitTime = Math.pow(2, attempt) * 1000;
|
|
268
|
+
if (process.env.DEBUG || process.env.PRPM_DEBUG) {
|
|
269
|
+
console.error(`[DEBUG] Retrying after ${waitTime}ms...`);
|
|
270
|
+
}
|
|
250
271
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
251
272
|
continue;
|
|
252
273
|
}
|
|
253
|
-
// If it's not a retryable error or we're out of retries, throw
|
|
274
|
+
// If it's not a retryable error or we're out of retries, throw with more context
|
|
254
275
|
if (attempt === retries - 1) {
|
|
255
|
-
|
|
276
|
+
const enhancedError = new Error(`Failed to connect to registry at ${url}\n` +
|
|
277
|
+
`Original error: ${lastError.message}\n\n` +
|
|
278
|
+
`💡 Possible causes:\n` +
|
|
279
|
+
` - Registry server is not running\n` +
|
|
280
|
+
` - Network connection issue\n` +
|
|
281
|
+
` - Incorrect PRPM_REGISTRY_URL (currently: ${this.baseUrl})`);
|
|
282
|
+
throw enhancedError;
|
|
256
283
|
}
|
|
257
284
|
}
|
|
258
285
|
}
|
|
259
|
-
throw lastError || new Error(
|
|
286
|
+
throw lastError || new Error(`Request failed after ${retries} retries to ${url}`);
|
|
260
287
|
}
|
|
261
288
|
}
|
|
262
289
|
exports.RegistryClient = RegistryClient;
|
package/dist/types/registry.js
CHANGED
|
@@ -3,3 +3,10 @@
|
|
|
3
3
|
* Registry API types for CLI
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isMultiPackageManifest = isMultiPackageManifest;
|
|
7
|
+
/**
|
|
8
|
+
* Type guard to check if manifest is multi-package
|
|
9
|
+
*/
|
|
10
|
+
function isMultiPackageManifest(manifest) {
|
|
11
|
+
return 'packages' in manifest && Array.isArray(manifest.packages);
|
|
12
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -3,3 +3,33 @@
|
|
|
3
3
|
* Core types for the Prompt Package Manager
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SUBTYPES = exports.FORMATS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Available formats as a constant array
|
|
9
|
+
* Useful for CLI prompts, validation, etc.
|
|
10
|
+
*/
|
|
11
|
+
exports.FORMATS = [
|
|
12
|
+
'cursor',
|
|
13
|
+
'claude',
|
|
14
|
+
'continue',
|
|
15
|
+
'windsurf',
|
|
16
|
+
'copilot',
|
|
17
|
+
'kiro',
|
|
18
|
+
'agents.md',
|
|
19
|
+
'generic',
|
|
20
|
+
'mcp',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Available subtypes as a constant array
|
|
24
|
+
* Useful for CLI prompts, validation, etc.
|
|
25
|
+
*/
|
|
26
|
+
exports.SUBTYPES = [
|
|
27
|
+
'rule',
|
|
28
|
+
'agent',
|
|
29
|
+
'skill',
|
|
30
|
+
'slash-command',
|
|
31
|
+
'prompt',
|
|
32
|
+
'collection',
|
|
33
|
+
'chatmode',
|
|
34
|
+
'tool',
|
|
35
|
+
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* License extraction utilities
|
|
4
|
+
* Extracts license information from LICENSE files for proper attribution
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.extractLicenseInfo = extractLicenseInfo;
|
|
8
|
+
exports.validateLicenseInfo = validateLicenseInfo;
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
/**
|
|
13
|
+
* Common license file names to search for
|
|
14
|
+
*/
|
|
15
|
+
const LICENSE_FILE_PATTERNS = [
|
|
16
|
+
'LICENSE',
|
|
17
|
+
'LICENSE.md',
|
|
18
|
+
'LICENSE.txt',
|
|
19
|
+
'LICENCE',
|
|
20
|
+
'LICENCE.md',
|
|
21
|
+
'LICENCE.txt',
|
|
22
|
+
'LICENSE-MIT',
|
|
23
|
+
'LICENSE-APACHE',
|
|
24
|
+
'COPYING',
|
|
25
|
+
'COPYING.txt',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* License type detection patterns
|
|
29
|
+
* Ordered by specificity - more specific patterns first
|
|
30
|
+
*/
|
|
31
|
+
const LICENSE_PATTERNS = [
|
|
32
|
+
{ pattern: /MIT License/i, type: 'MIT' },
|
|
33
|
+
{ pattern: /Apache License[\s\S]*Version 2\.0/i, type: 'Apache-2.0' },
|
|
34
|
+
{ pattern: /GNU GENERAL PUBLIC LICENSE[\s\S]*Version 3/i, type: 'GPL-3.0' },
|
|
35
|
+
{ pattern: /GNU GENERAL PUBLIC LICENSE[\s\S]*Version 2/i, type: 'GPL-2.0' },
|
|
36
|
+
{ pattern: /GNU LESSER GENERAL PUBLIC LICENSE[\s\S]*Version 3/i, type: 'LGPL-3.0' },
|
|
37
|
+
{ pattern: /GNU LESSER GENERAL PUBLIC LICENSE[\s\S]*Version 2/i, type: 'LGPL-2.1' },
|
|
38
|
+
{ pattern: /BSD 3-Clause License/i, type: 'BSD-3-Clause' },
|
|
39
|
+
{ pattern: /BSD 2-Clause License/i, type: 'BSD-2-Clause' },
|
|
40
|
+
{ pattern: /Mozilla Public License[\s\S]*Version 2\.0/i, type: 'MPL-2.0' },
|
|
41
|
+
{ pattern: /ISC License/i, type: 'ISC' },
|
|
42
|
+
{ pattern: /The Unlicense/i, type: 'Unlicense' },
|
|
43
|
+
{ pattern: /Creative Commons Zero[\s\S]*1\.0/i, type: 'CC0-1.0' },
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Detect license type from license text
|
|
47
|
+
*/
|
|
48
|
+
function detectLicenseType(text) {
|
|
49
|
+
for (const { pattern, type } of LICENSE_PATTERNS) {
|
|
50
|
+
if (pattern.test(text)) {
|
|
51
|
+
return type;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate GitHub raw URL for license file
|
|
58
|
+
*/
|
|
59
|
+
function generateLicenseUrl(repositoryUrl, fileName) {
|
|
60
|
+
if (!repositoryUrl) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// Extract owner/repo from GitHub URL
|
|
64
|
+
const githubMatch = repositoryUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
|
|
65
|
+
if (!githubMatch) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const [, owner, repo] = githubMatch;
|
|
69
|
+
// Use raw.githubusercontent.com for direct file access
|
|
70
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/main/${fileName}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find and extract license information from the current directory
|
|
74
|
+
*/
|
|
75
|
+
async function extractLicenseInfo(repositoryUrl) {
|
|
76
|
+
const cwd = process.cwd();
|
|
77
|
+
// Try each license file pattern
|
|
78
|
+
for (const fileName of LICENSE_FILE_PATTERNS) {
|
|
79
|
+
const filePath = (0, path_1.join)(cwd, fileName);
|
|
80
|
+
try {
|
|
81
|
+
// Check if file exists
|
|
82
|
+
await (0, promises_1.access)(filePath, fs_1.constants.R_OK);
|
|
83
|
+
// Read license file
|
|
84
|
+
const text = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
85
|
+
// Detect license type
|
|
86
|
+
const type = detectLicenseType(text);
|
|
87
|
+
// Generate license URL if repository is provided
|
|
88
|
+
const url = generateLicenseUrl(repositoryUrl, fileName);
|
|
89
|
+
return {
|
|
90
|
+
type,
|
|
91
|
+
text,
|
|
92
|
+
url,
|
|
93
|
+
fileName,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// File doesn't exist or can't be read, try next pattern
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// No license file found
|
|
102
|
+
return {
|
|
103
|
+
type: null,
|
|
104
|
+
text: null,
|
|
105
|
+
url: null,
|
|
106
|
+
fileName: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Display license information if found
|
|
111
|
+
*/
|
|
112
|
+
function validateLicenseInfo(licenseInfo, packageName) {
|
|
113
|
+
if (licenseInfo.text && licenseInfo.type) {
|
|
114
|
+
console.log(` License: ${licenseInfo.type} (${licenseInfo.fileName})`);
|
|
115
|
+
}
|
|
116
|
+
else if (licenseInfo.text && !licenseInfo.type) {
|
|
117
|
+
console.log(` License: Found (${licenseInfo.fileName})`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(` License: Not found (package will be published without license)`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-package manifest utilities
|
|
4
|
+
* Handles merging root-level fields with package-level fields
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.mergePackageFields = mergePackageFields;
|
|
8
|
+
exports.getPackagesWithInheritance = getPackagesWithInheritance;
|
|
9
|
+
exports.validateMultiPackageManifest = validateMultiPackageManifest;
|
|
10
|
+
exports.filterPackages = filterPackages;
|
|
11
|
+
/**
|
|
12
|
+
* Fields that can be inherited from root to packages
|
|
13
|
+
*/
|
|
14
|
+
const INHERITABLE_FIELDS = [
|
|
15
|
+
'author',
|
|
16
|
+
'license',
|
|
17
|
+
'repository',
|
|
18
|
+
'homepage',
|
|
19
|
+
'documentation',
|
|
20
|
+
'organization',
|
|
21
|
+
'keywords',
|
|
22
|
+
'tags',
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Merge root-level fields into a package manifest
|
|
26
|
+
* Package-level fields take precedence over root-level fields
|
|
27
|
+
*/
|
|
28
|
+
function mergePackageFields(root, pkg) {
|
|
29
|
+
const merged = { ...pkg };
|
|
30
|
+
// Inherit each inheritable field if not defined in package
|
|
31
|
+
for (const field of INHERITABLE_FIELDS) {
|
|
32
|
+
if (merged[field] === undefined && root[field] !== undefined) {
|
|
33
|
+
// @ts-ignore - dynamic field access
|
|
34
|
+
merged[field] = root[field];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get all packages from a multi-package manifest with inherited fields
|
|
41
|
+
*/
|
|
42
|
+
function getPackagesWithInheritance(manifest) {
|
|
43
|
+
return manifest.packages.map(pkg => mergePackageFields(manifest, pkg));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate multi-package manifest
|
|
47
|
+
*/
|
|
48
|
+
function validateMultiPackageManifest(manifest) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
// Check packages array exists and is not empty
|
|
51
|
+
if (!manifest.packages || !Array.isArray(manifest.packages)) {
|
|
52
|
+
errors.push('packages field must be an array');
|
|
53
|
+
return { valid: false, errors };
|
|
54
|
+
}
|
|
55
|
+
if (manifest.packages.length === 0) {
|
|
56
|
+
errors.push('packages array must contain at least one package');
|
|
57
|
+
return { valid: false, errors };
|
|
58
|
+
}
|
|
59
|
+
// Check for duplicate package names
|
|
60
|
+
const names = new Set();
|
|
61
|
+
for (let i = 0; i < manifest.packages.length; i++) {
|
|
62
|
+
const pkg = manifest.packages[i];
|
|
63
|
+
if (names.has(pkg.name)) {
|
|
64
|
+
errors.push(`Duplicate package name: ${pkg.name}`);
|
|
65
|
+
}
|
|
66
|
+
names.add(pkg.name);
|
|
67
|
+
}
|
|
68
|
+
// Validate each package has required fields
|
|
69
|
+
for (let i = 0; i < manifest.packages.length; i++) {
|
|
70
|
+
const pkg = manifest.packages[i];
|
|
71
|
+
const pkgPrefix = `Package ${i} (${pkg.name || 'unnamed'})`;
|
|
72
|
+
if (!pkg.name) {
|
|
73
|
+
errors.push(`${pkgPrefix}: missing required field 'name'`);
|
|
74
|
+
}
|
|
75
|
+
if (!pkg.version) {
|
|
76
|
+
errors.push(`${pkgPrefix}: missing required field 'version'`);
|
|
77
|
+
}
|
|
78
|
+
if (!pkg.description) {
|
|
79
|
+
errors.push(`${pkgPrefix}: missing required field 'description'`);
|
|
80
|
+
}
|
|
81
|
+
if (!pkg.format) {
|
|
82
|
+
errors.push(`${pkgPrefix}: missing required field 'format'`);
|
|
83
|
+
}
|
|
84
|
+
if (!pkg.files || pkg.files.length === 0) {
|
|
85
|
+
errors.push(`${pkgPrefix}: must have at least one file in 'files' array`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
valid: errors.length === 0,
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Filter packages by name or pattern
|
|
95
|
+
*/
|
|
96
|
+
function filterPackages(packages, filter) {
|
|
97
|
+
// If filter is a number, treat as index
|
|
98
|
+
if (typeof filter === 'number') {
|
|
99
|
+
if (filter < 0 || filter >= packages.length) {
|
|
100
|
+
throw new Error(`Package index ${filter} out of range (0-${packages.length - 1})`);
|
|
101
|
+
}
|
|
102
|
+
return [packages[filter]];
|
|
103
|
+
}
|
|
104
|
+
// If exact match, return that package
|
|
105
|
+
const exactMatch = packages.find(pkg => pkg.name === filter);
|
|
106
|
+
if (exactMatch) {
|
|
107
|
+
return [exactMatch];
|
|
108
|
+
}
|
|
109
|
+
// Try as glob pattern
|
|
110
|
+
const pattern = filter.replace(/\*/g, '.*');
|
|
111
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
112
|
+
const matches = packages.filter(pkg => regex.test(pkg.name));
|
|
113
|
+
if (matches.length === 0) {
|
|
114
|
+
throw new Error(`No packages match filter: ${filter}`);
|
|
115
|
+
}
|
|
116
|
+
return matches;
|
|
117
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Parallel publishing utilities with concurrency control
|
|
4
|
+
* Optimizes multi-package publishing performance
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.publishInParallel = publishInParallel;
|
|
8
|
+
exports.withRetry = withRetry;
|
|
9
|
+
exports.formatDuration = formatDuration;
|
|
10
|
+
exports.calculateStats = calculateStats;
|
|
11
|
+
/**
|
|
12
|
+
* Execute tasks in parallel with concurrency limit
|
|
13
|
+
*/
|
|
14
|
+
async function publishInParallel(tasks, options = {}) {
|
|
15
|
+
const { concurrency = 5, continueOnError = false, onProgress, onSuccess, onError, } = options;
|
|
16
|
+
const results = new Array(tasks.length);
|
|
17
|
+
let completed = 0;
|
|
18
|
+
let hasError = false;
|
|
19
|
+
let taskIndex = 0;
|
|
20
|
+
async function executeTask(task, index) {
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
const result = await task.execute();
|
|
24
|
+
const duration = Date.now() - startTime;
|
|
25
|
+
results[index] = {
|
|
26
|
+
success: true,
|
|
27
|
+
name: task.name,
|
|
28
|
+
result,
|
|
29
|
+
duration,
|
|
30
|
+
};
|
|
31
|
+
completed++;
|
|
32
|
+
onProgress?.(completed, tasks.length, task.name);
|
|
33
|
+
onSuccess?.(task.name, result);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const duration = Date.now() - startTime;
|
|
37
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
38
|
+
results[index] = {
|
|
39
|
+
success: false,
|
|
40
|
+
name: task.name,
|
|
41
|
+
error: err,
|
|
42
|
+
duration,
|
|
43
|
+
};
|
|
44
|
+
completed++;
|
|
45
|
+
hasError = true;
|
|
46
|
+
onProgress?.(completed, tasks.length, task.name);
|
|
47
|
+
onError?.(task.name, err);
|
|
48
|
+
// If not continuing on error, mark hasError to skip remaining tasks
|
|
49
|
+
if (!continueOnError) {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Execute tasks with concurrency control
|
|
55
|
+
const executing = new Set();
|
|
56
|
+
while (taskIndex < tasks.length || executing.size > 0) {
|
|
57
|
+
// Fill up to concurrency limit
|
|
58
|
+
while (taskIndex < tasks.length && executing.size < concurrency) {
|
|
59
|
+
// If in strict mode and we've encountered an error, skip remaining tasks
|
|
60
|
+
if (!continueOnError && hasError) {
|
|
61
|
+
results[taskIndex] = {
|
|
62
|
+
success: false,
|
|
63
|
+
name: tasks[taskIndex].name,
|
|
64
|
+
error: new Error('Skipped due to previous failure'),
|
|
65
|
+
duration: 0,
|
|
66
|
+
};
|
|
67
|
+
taskIndex++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const currentIndex = taskIndex;
|
|
71
|
+
const currentTask = tasks[taskIndex];
|
|
72
|
+
taskIndex++;
|
|
73
|
+
const promise = executeTask(currentTask, currentIndex)
|
|
74
|
+
.catch(() => {
|
|
75
|
+
// Errors already handled in executeTask
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
executing.delete(promise);
|
|
79
|
+
});
|
|
80
|
+
executing.add(promise);
|
|
81
|
+
}
|
|
82
|
+
// Wait for at least one task to complete
|
|
83
|
+
if (executing.size > 0) {
|
|
84
|
+
await Promise.race(executing);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Retry a task with exponential backoff
|
|
91
|
+
*/
|
|
92
|
+
async function withRetry(fn, options = {}) {
|
|
93
|
+
const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000, backoffFactor = 2, } = options;
|
|
94
|
+
let lastError;
|
|
95
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
96
|
+
try {
|
|
97
|
+
return await fn();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
101
|
+
// Don't retry on last attempt
|
|
102
|
+
if (attempt === maxRetries - 1) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
// Calculate delay with exponential backoff
|
|
106
|
+
const delay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
|
107
|
+
await sleep(delay);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw lastError || new Error('Max retries exceeded');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Sleep utility
|
|
114
|
+
*/
|
|
115
|
+
function sleep(ms) {
|
|
116
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Format duration in human-readable format
|
|
120
|
+
*/
|
|
121
|
+
function formatDuration(ms) {
|
|
122
|
+
if (ms < 1000) {
|
|
123
|
+
return `${ms}ms`;
|
|
124
|
+
}
|
|
125
|
+
const seconds = (ms / 1000).toFixed(1);
|
|
126
|
+
return `${seconds}s`;
|
|
127
|
+
}
|
|
128
|
+
function calculateStats(results) {
|
|
129
|
+
const succeeded = results.filter(r => r.success && r.result !== undefined).length;
|
|
130
|
+
const failed = results.filter(r => !r.success && r.error && r.error.message !== 'Skipped due to previous failure').length;
|
|
131
|
+
const skipped = results.filter(r => r.error?.message === 'Skipped due to previous failure').length;
|
|
132
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
133
|
+
const completedCount = succeeded + failed;
|
|
134
|
+
const avgDuration = completedCount > 0 ? totalDuration / completedCount : 0;
|
|
135
|
+
return {
|
|
136
|
+
total: results.length,
|
|
137
|
+
succeeded,
|
|
138
|
+
failed,
|
|
139
|
+
skipped,
|
|
140
|
+
totalDuration,
|
|
141
|
+
avgDuration,
|
|
142
|
+
successRate: results.length > 0 ? succeeded / results.length : 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Snippet extraction utilities
|
|
4
|
+
* Extracts preview content from package files for display in modals
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.extractSnippet = extractSnippet;
|
|
8
|
+
exports.validateSnippet = validateSnippet;
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const MAX_SNIPPET_LENGTH = 2000;
|
|
12
|
+
/**
|
|
13
|
+
* Extract a preview snippet from package files
|
|
14
|
+
* Takes the first file in the package and extracts ~2000 characters
|
|
15
|
+
*/
|
|
16
|
+
async function extractSnippet(manifest) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
try {
|
|
19
|
+
// Get the first file from the manifest
|
|
20
|
+
const firstFile = manifest.files[0];
|
|
21
|
+
if (!firstFile) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// Get file path (handle both string and object formats)
|
|
25
|
+
const filePath = typeof firstFile === 'string'
|
|
26
|
+
? firstFile
|
|
27
|
+
: firstFile.path;
|
|
28
|
+
// If there's a main file specified, prefer that
|
|
29
|
+
const targetFile = manifest.main || filePath;
|
|
30
|
+
const fullPath = (0, path_1.join)(cwd, targetFile);
|
|
31
|
+
// Check if path is a directory
|
|
32
|
+
const stats = await (0, promises_1.stat)(fullPath);
|
|
33
|
+
if (stats.isDirectory()) {
|
|
34
|
+
console.warn(`⚠️ Skipping snippet extraction: "${targetFile}" is a directory`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// Read the file content
|
|
38
|
+
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
|
39
|
+
// Extract first N characters, trying to break at a reasonable point
|
|
40
|
+
if (content.length <= MAX_SNIPPET_LENGTH) {
|
|
41
|
+
return content.trim();
|
|
42
|
+
}
|
|
43
|
+
// Try to break at a newline near the limit
|
|
44
|
+
let snippet = content.substring(0, MAX_SNIPPET_LENGTH);
|
|
45
|
+
const lastNewline = snippet.lastIndexOf('\n');
|
|
46
|
+
if (lastNewline > MAX_SNIPPET_LENGTH * 0.8) {
|
|
47
|
+
// If we found a newline in the last 20%, break there
|
|
48
|
+
snippet = snippet.substring(0, lastNewline);
|
|
49
|
+
}
|
|
50
|
+
return snippet.trim() + '\n\n[... content truncated ...]';
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
// If we can't read the file, return null (snippet is optional)
|
|
54
|
+
console.warn('⚠️ Could not extract snippet:', error instanceof Error ? error.message : 'Unknown error');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate snippet and warn if issues found
|
|
60
|
+
*/
|
|
61
|
+
function validateSnippet(snippet, packageName) {
|
|
62
|
+
if (!snippet) {
|
|
63
|
+
console.warn(`⚠️ Warning: No content snippet extracted for package "${packageName}"`);
|
|
64
|
+
console.warn(' A preview snippet helps users see what the prompt contains before installing.');
|
|
65
|
+
console.warn('');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(` Snippet: ${snippet.length} characters extracted`);
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prpm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Prompt Package Manager CLI - Install and manage prompt-based files",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@octokit/rest": "^22.0.0",
|
|
48
|
-
"@pr-pm/registry-client": "^1.2.
|
|
49
|
-
"@pr-pm/types": "^0.1.
|
|
48
|
+
"@pr-pm/registry-client": "^1.2.5",
|
|
49
|
+
"@pr-pm/types": "^0.1.5",
|
|
50
50
|
"ajv": "^8.17.1",
|
|
51
51
|
"ajv-formats": "^3.0.1",
|
|
52
52
|
"commander": "^11.1.0",
|