prpm 0.0.9 → 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/list.js +28 -18
- 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
|
@@ -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",
|
|
@@ -113,6 +113,15 @@
|
|
|
113
113
|
"BSD-3-Clause"
|
|
114
114
|
]
|
|
115
115
|
},
|
|
116
|
+
"license_text": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "Full text of the license file for proper attribution"
|
|
119
|
+
},
|
|
120
|
+
"license_url": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"format": "uri",
|
|
123
|
+
"description": "URL to the license file in the repository"
|
|
124
|
+
},
|
|
116
125
|
"repository": {
|
|
117
126
|
"type": "string",
|
|
118
127
|
"format": "uri",
|
|
@@ -131,6 +140,14 @@
|
|
|
131
140
|
"format": "uri",
|
|
132
141
|
"description": "Documentation URL"
|
|
133
142
|
},
|
|
143
|
+
"organization": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Organization name or ID to publish this package under. If not specified, publishes to personal account.",
|
|
146
|
+
"examples": [
|
|
147
|
+
"my-team",
|
|
148
|
+
"my-company"
|
|
149
|
+
]
|
|
150
|
+
},
|
|
134
151
|
"tags": {
|
|
135
152
|
"type": "array",
|
|
136
153
|
"description": "Package tags for categorization",
|
|
@@ -353,6 +370,19 @@
|
|
|
353
370
|
"README.md"
|
|
354
371
|
]
|
|
355
372
|
},
|
|
373
|
+
{
|
|
374
|
+
"name": "@company/team-package",
|
|
375
|
+
"version": "1.0.0",
|
|
376
|
+
"description": "A package published under organization account",
|
|
377
|
+
"format": "cursor",
|
|
378
|
+
"author": "Team Name",
|
|
379
|
+
"organization": "my-company",
|
|
380
|
+
"license": "MIT",
|
|
381
|
+
"files": [
|
|
382
|
+
".cursor/rules/guidelines.mdc",
|
|
383
|
+
"README.md"
|
|
384
|
+
]
|
|
385
|
+
},
|
|
356
386
|
{
|
|
357
387
|
"name": "@username/cursor-rules",
|
|
358
388
|
"version": "1.0.0",
|