prpm 0.0.11 → 0.0.13
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/__tests__/e2e/test-helpers.js +1 -1
- package/dist/commands/catalog.js +348 -0
- package/dist/commands/collections.js +19 -0
- package/dist/commands/install.js +1 -0
- package/dist/commands/list.js +14 -5
- package/dist/commands/login.js +15 -4
- package/dist/commands/publish.js +124 -6
- package/dist/core/filesystem.js +54 -1
- package/dist/core/lockfile.js +52 -0
- package/dist/core/user-config.js +88 -22
- package/dist/index.js +2 -0
- package/dist/utils/snippet-extractor.js +27 -12
- package/package.json +3 -3
- package/schemas/prpm-manifest.schema.json +58 -0
|
@@ -77,7 +77,7 @@ async function createMockCollection(testDir, id, packages) {
|
|
|
77
77
|
async function createMockConfig(configPath, options) {
|
|
78
78
|
const config = {
|
|
79
79
|
token: options.token || 'test-token-123',
|
|
80
|
-
registryUrl: options.registryUrl || 'http://localhost:
|
|
80
|
+
registryUrl: options.registryUrl || 'http://localhost:3111',
|
|
81
81
|
};
|
|
82
82
|
await (0, promises_1.mkdir)((0, path_1.join)(configPath, '..'), { recursive: true });
|
|
83
83
|
await (0, promises_1.writeFile)(configPath, JSON.stringify(config, null, 2));
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Catalog command - Discover and catalog existing packages
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleCatalog = handleCatalog;
|
|
7
|
+
exports.createCatalogCommand = createCatalogCommand;
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const promises_1 = require("fs/promises");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const telemetry_1 = require("../core/telemetry");
|
|
12
|
+
/**
|
|
13
|
+
* Detect format and subtype from file path and content
|
|
14
|
+
*/
|
|
15
|
+
function detectPackageInfo(filePath, content) {
|
|
16
|
+
const fileName = (0, path_1.basename)(filePath);
|
|
17
|
+
const lowerFileName = fileName.toLowerCase();
|
|
18
|
+
// Claude skills - SKILL.md files
|
|
19
|
+
if (fileName === 'SKILL.md') {
|
|
20
|
+
const dirName = (0, path_1.basename)((0, path_1.join)(filePath, '..'));
|
|
21
|
+
return {
|
|
22
|
+
format: 'claude',
|
|
23
|
+
subtype: 'skill',
|
|
24
|
+
name: dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Claude agents
|
|
28
|
+
if (filePath.includes('.claude/agents') || filePath.includes('.claude-plugin/agents')) {
|
|
29
|
+
return {
|
|
30
|
+
format: 'claude',
|
|
31
|
+
subtype: 'agent',
|
|
32
|
+
name: fileName.replace(/\.(md|txt)$/, '').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Cursor rules
|
|
36
|
+
if (filePath.includes('.cursor/rules') || lowerFileName.endsWith('.mdc')) {
|
|
37
|
+
return {
|
|
38
|
+
format: 'cursor',
|
|
39
|
+
subtype: 'rule',
|
|
40
|
+
name: fileName.replace(/\.(md|mdc|txt)$/, '').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Windsurf rules
|
|
44
|
+
if (filePath.includes('.windsurf/rules')) {
|
|
45
|
+
return {
|
|
46
|
+
format: 'windsurf',
|
|
47
|
+
subtype: 'rule',
|
|
48
|
+
name: fileName.replace(/\.(md|txt)$/, '').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Continue config
|
|
52
|
+
if (filePath.includes('.continue/prompts')) {
|
|
53
|
+
return {
|
|
54
|
+
format: 'continue',
|
|
55
|
+
subtype: 'prompt',
|
|
56
|
+
name: fileName.replace(/\.(md|txt)$/, '').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Generic markdown files in root that look like prompts
|
|
60
|
+
if (lowerFileName.endsWith('.md') && content.length > 50) {
|
|
61
|
+
return {
|
|
62
|
+
format: 'generic',
|
|
63
|
+
subtype: 'prompt',
|
|
64
|
+
name: fileName.replace(/\.md$/, '').toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Recursively scan directories for packages
|
|
71
|
+
*/
|
|
72
|
+
async function scanDirectory(dirPath, baseDir, scanDir, maxDepth = 5, currentDepth = 0) {
|
|
73
|
+
if (currentDepth > maxDepth) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const discovered = [];
|
|
77
|
+
try {
|
|
78
|
+
const entries = await (0, promises_1.readdir)(dirPath, { withFileTypes: true });
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
81
|
+
const relativePath = (0, path_1.relative)(baseDir, fullPath);
|
|
82
|
+
// Skip node_modules, .git, and other common dirs
|
|
83
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist' || entry.name === 'build') {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
// Recursively scan subdirectories
|
|
88
|
+
const subDirPackages = await scanDirectory(fullPath, baseDir, scanDir, maxDepth, currentDepth + 1);
|
|
89
|
+
discovered.push(...subDirPackages);
|
|
90
|
+
}
|
|
91
|
+
else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdc') || entry.name.endsWith('.txt'))) {
|
|
92
|
+
// Check if this is a package file
|
|
93
|
+
try {
|
|
94
|
+
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
|
95
|
+
const packageInfo = detectPackageInfo(fullPath, content);
|
|
96
|
+
if (packageInfo) {
|
|
97
|
+
discovered.push({
|
|
98
|
+
path: relativePath,
|
|
99
|
+
format: packageInfo.format,
|
|
100
|
+
subtype: packageInfo.subtype,
|
|
101
|
+
name: packageInfo.name,
|
|
102
|
+
files: [relativePath],
|
|
103
|
+
scanDir,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Skip files we can't read
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// Skip directories we can't read
|
|
115
|
+
}
|
|
116
|
+
return discovered;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract description from file content
|
|
120
|
+
* Tries multiple strategies:
|
|
121
|
+
* 1. YAML frontmatter (---\ndescription: ...\n---)
|
|
122
|
+
* 2. Markdown description field (description: ...)
|
|
123
|
+
* 3. First substantial paragraph after title
|
|
124
|
+
*/
|
|
125
|
+
function extractDescription(content) {
|
|
126
|
+
const lines = content.split('\n');
|
|
127
|
+
// Strategy 1: YAML frontmatter
|
|
128
|
+
if (lines[0]?.trim() === '---') {
|
|
129
|
+
let foundClosing = false;
|
|
130
|
+
let frontmatterLines = [];
|
|
131
|
+
for (let i = 1; i < lines.length && i < 50; i++) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
if (line.trim() === '---') {
|
|
134
|
+
foundClosing = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
frontmatterLines.push(line);
|
|
138
|
+
}
|
|
139
|
+
if (foundClosing && frontmatterLines.length > 0) {
|
|
140
|
+
// Parse YAML-like frontmatter
|
|
141
|
+
for (const line of frontmatterLines) {
|
|
142
|
+
const match = line.match(/^description:\s*(.+)$/i);
|
|
143
|
+
if (match) {
|
|
144
|
+
return match[1].trim().replace(/^["']|["']$/g, '').substring(0, 200);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Strategy 2: Look for "description:" field anywhere in first 20 lines
|
|
150
|
+
for (let i = 0; i < Math.min(lines.length, 20); i++) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
const match = line.match(/^description:\s*(.+)$/i);
|
|
153
|
+
if (match) {
|
|
154
|
+
return match[1].trim().replace(/^["']|["']$/g, '').substring(0, 200);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Strategy 3: First substantial non-header paragraph
|
|
158
|
+
let foundTitle = false;
|
|
159
|
+
for (let i = 0; i < Math.min(lines.length, 30); i++) {
|
|
160
|
+
const line = lines[i].trim();
|
|
161
|
+
// Skip empty lines and YAML frontmatter
|
|
162
|
+
if (line === '' || line === '---') {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Skip markdown headers
|
|
166
|
+
if (line.startsWith('#')) {
|
|
167
|
+
foundTitle = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// Found a substantial line after the title
|
|
171
|
+
if (foundTitle && line.length >= 20 && !line.startsWith('```')) {
|
|
172
|
+
return line.substring(0, 200);
|
|
173
|
+
}
|
|
174
|
+
// If no title found yet but line is substantial, use it
|
|
175
|
+
if (!foundTitle && line.length >= 30) {
|
|
176
|
+
return line.substring(0, 200);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Discover packages in specified directories
|
|
183
|
+
*/
|
|
184
|
+
async function handleCatalog(directories, options) {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
let success = false;
|
|
187
|
+
let error;
|
|
188
|
+
try {
|
|
189
|
+
console.log('🔍 Scanning for packages...\n');
|
|
190
|
+
const allDiscovered = [];
|
|
191
|
+
// Scan each directory
|
|
192
|
+
for (const dir of directories) {
|
|
193
|
+
console.log(` Scanning ${dir}...`);
|
|
194
|
+
try {
|
|
195
|
+
const dirStat = await (0, promises_1.stat)(dir);
|
|
196
|
+
if (!dirStat.isDirectory()) {
|
|
197
|
+
console.log(` ⚠️ Skipping ${dir} (not a directory)`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const discovered = await scanDirectory(dir, dir, dir);
|
|
201
|
+
allDiscovered.push(...discovered);
|
|
202
|
+
console.log(` Found ${discovered.length} package(s) in ${dir}`);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.log(` ⚠️ Could not access ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(`\n✨ Discovered ${allDiscovered.length} package(s) total:\n`);
|
|
209
|
+
if (allDiscovered.length === 0) {
|
|
210
|
+
console.log('No packages found. Try scanning different directories.');
|
|
211
|
+
success = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Display discovered packages
|
|
215
|
+
const byFormat = new Map();
|
|
216
|
+
for (const pkg of allDiscovered) {
|
|
217
|
+
if (!byFormat.has(pkg.format)) {
|
|
218
|
+
byFormat.set(pkg.format, []);
|
|
219
|
+
}
|
|
220
|
+
byFormat.get(pkg.format).push(pkg);
|
|
221
|
+
}
|
|
222
|
+
for (const [format, packages] of byFormat.entries()) {
|
|
223
|
+
console.log(`📦 ${format} (${packages.length}):`);
|
|
224
|
+
for (const pkg of packages) {
|
|
225
|
+
console.log(` - ${pkg.name} (${pkg.subtype}): ${pkg.path}`);
|
|
226
|
+
}
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
if (options.dryRun) {
|
|
230
|
+
console.log('🔍 Dry run - would update prpm.json\n');
|
|
231
|
+
success = true;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Load or create prpm.json
|
|
235
|
+
const prpmJsonPath = options.output || (0, path_1.join)(process.cwd(), 'prpm.json');
|
|
236
|
+
let manifest;
|
|
237
|
+
if (options.append) {
|
|
238
|
+
try {
|
|
239
|
+
const existingContent = await (0, promises_1.readFile)(prpmJsonPath, 'utf-8');
|
|
240
|
+
const existing = JSON.parse(existingContent);
|
|
241
|
+
// Check if it's a multi-package manifest
|
|
242
|
+
if ('packages' in existing && Array.isArray(existing.packages)) {
|
|
243
|
+
manifest = existing;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Convert single package to multi-package
|
|
247
|
+
manifest = {
|
|
248
|
+
name: 'multi-package',
|
|
249
|
+
version: '1.0.0',
|
|
250
|
+
packages: [existing],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
// File doesn't exist, create new
|
|
256
|
+
manifest = {
|
|
257
|
+
name: 'multi-package',
|
|
258
|
+
version: '1.0.0',
|
|
259
|
+
packages: [],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
manifest = {
|
|
265
|
+
name: 'multi-package',
|
|
266
|
+
version: '1.0.0',
|
|
267
|
+
packages: [],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// Convert discovered packages to manifests
|
|
271
|
+
const existingNames = new Set(manifest.packages.map(p => p.name));
|
|
272
|
+
let addedCount = 0;
|
|
273
|
+
for (const discovered of allDiscovered) {
|
|
274
|
+
// Skip if already exists
|
|
275
|
+
if (existingNames.has(discovered.name)) {
|
|
276
|
+
console.log(` ⚠️ Skipping ${discovered.name} (already in prpm.json)`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Extract description from first file
|
|
280
|
+
let description = `${discovered.format} ${discovered.subtype}`;
|
|
281
|
+
try {
|
|
282
|
+
const firstFilePath = (0, path_1.join)(process.cwd(), discovered.scanDir, discovered.files[0]);
|
|
283
|
+
const content = await (0, promises_1.readFile)(firstFilePath, 'utf-8');
|
|
284
|
+
const extractedDesc = extractDescription(content);
|
|
285
|
+
if (extractedDesc) {
|
|
286
|
+
description = extractedDesc;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
// Use default description
|
|
291
|
+
}
|
|
292
|
+
const packageManifest = {
|
|
293
|
+
name: discovered.name,
|
|
294
|
+
version: '1.0.0',
|
|
295
|
+
description,
|
|
296
|
+
author: '', // User should fill this in
|
|
297
|
+
format: discovered.format,
|
|
298
|
+
subtype: discovered.subtype,
|
|
299
|
+
files: discovered.files,
|
|
300
|
+
};
|
|
301
|
+
manifest.packages.push(packageManifest);
|
|
302
|
+
addedCount++;
|
|
303
|
+
}
|
|
304
|
+
// Write updated prpm.json
|
|
305
|
+
await (0, promises_1.writeFile)(prpmJsonPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
306
|
+
console.log(`\n✅ Updated ${prpmJsonPath}`);
|
|
307
|
+
console.log(` Added ${addedCount} new package(s)`);
|
|
308
|
+
console.log(` Total: ${manifest.packages.length} package(s)\n`);
|
|
309
|
+
console.log('💡 Next steps:');
|
|
310
|
+
console.log(' 1. Review and edit package metadata in prpm.json');
|
|
311
|
+
console.log(' 2. Add author, license, and other fields');
|
|
312
|
+
console.log(' 3. Run: prpm publish --dry-run to validate');
|
|
313
|
+
console.log(' 4. Run: prpm publish to publish packages\n');
|
|
314
|
+
success = true;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
error = err instanceof Error ? err.message : String(err);
|
|
318
|
+
console.error(`\n❌ Failed to catalog packages: ${error}\n`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await telemetry_1.telemetry.track({
|
|
323
|
+
command: 'catalog',
|
|
324
|
+
success,
|
|
325
|
+
error,
|
|
326
|
+
duration: Date.now() - startTime,
|
|
327
|
+
data: {
|
|
328
|
+
directories: directories.length,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
await telemetry_1.telemetry.shutdown();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Create the catalog command
|
|
336
|
+
*/
|
|
337
|
+
function createCatalogCommand() {
|
|
338
|
+
return new commander_1.Command('catalog')
|
|
339
|
+
.description('Discover and catalog existing packages from directories')
|
|
340
|
+
.argument('[directories...]', 'Directories to scan for packages (defaults to current directory)', ['.'])
|
|
341
|
+
.option('-o, --output <path>', 'Output path for prpm.json (default: ./prpm.json)')
|
|
342
|
+
.option('-a, --append', 'Append to existing prpm.json instead of overwriting')
|
|
343
|
+
.option('--dry-run', 'Show what would be cataloged without making changes')
|
|
344
|
+
.action(async (directories, options) => {
|
|
345
|
+
await handleCatalog(directories, options);
|
|
346
|
+
process.exit(0);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
@@ -47,6 +47,7 @@ const registry_client_1 = require("@pr-pm/registry-client");
|
|
|
47
47
|
const user_config_1 = require("../core/user-config");
|
|
48
48
|
const install_1 = require("./install");
|
|
49
49
|
const telemetry_1 = require("../core/telemetry");
|
|
50
|
+
const lockfile_1 = require("../core/lockfile");
|
|
50
51
|
/**
|
|
51
52
|
* Search collections by query
|
|
52
53
|
*/
|
|
@@ -494,6 +495,7 @@ async function handleCollectionInstall(collectionSpec, options) {
|
|
|
494
495
|
return;
|
|
495
496
|
}
|
|
496
497
|
// Install packages sequentially
|
|
498
|
+
const installedPackageIds = [];
|
|
497
499
|
for (let i = 0; i < packages.length; i++) {
|
|
498
500
|
const pkg = packages[i];
|
|
499
501
|
const progress = `${i + 1}/${packages.length}`;
|
|
@@ -501,8 +503,14 @@ async function handleCollectionInstall(collectionSpec, options) {
|
|
|
501
503
|
console.log(`\n ${progress} Installing ${pkg.packageId}@${pkg.version}...`);
|
|
502
504
|
await (0, install_1.handleInstall)(`${pkg.packageId}@${pkg.version}`, {
|
|
503
505
|
as: pkg.format,
|
|
506
|
+
fromCollection: {
|
|
507
|
+
scope,
|
|
508
|
+
name_slug,
|
|
509
|
+
version: collection.version || version || '1.0.0',
|
|
510
|
+
},
|
|
504
511
|
});
|
|
505
512
|
console.log(` ${progress} ✓ ${pkg.packageId}`);
|
|
513
|
+
installedPackageIds.push(pkg.packageId);
|
|
506
514
|
packagesInstalled++;
|
|
507
515
|
}
|
|
508
516
|
catch (error) {
|
|
@@ -514,11 +522,22 @@ async function handleCollectionInstall(collectionSpec, options) {
|
|
|
514
522
|
}
|
|
515
523
|
}
|
|
516
524
|
}
|
|
525
|
+
// Update lockfile with collection info
|
|
526
|
+
const lockfile = (await (0, lockfile_1.readLockfile)()) || (0, lockfile_1.createLockfile)();
|
|
527
|
+
const collectionKey = `@${scope}/${name_slug}`;
|
|
528
|
+
(0, lockfile_1.addCollectionToLockfile)(lockfile, collectionKey, {
|
|
529
|
+
scope,
|
|
530
|
+
name_slug,
|
|
531
|
+
version: collection.version || version || '1.0.0',
|
|
532
|
+
packages: installedPackageIds,
|
|
533
|
+
});
|
|
534
|
+
await (0, lockfile_1.writeLockfile)(lockfile);
|
|
517
535
|
console.log(`\n✅ Collection installed successfully!`);
|
|
518
536
|
console.log(` ${packagesInstalled}/${packages.length} packages installed`);
|
|
519
537
|
if (packagesFailed > 0) {
|
|
520
538
|
console.log(` ${packagesFailed} optional packages failed`);
|
|
521
539
|
}
|
|
540
|
+
console.log(` 🔒 Collection tracked in lock file`);
|
|
522
541
|
console.log('');
|
|
523
542
|
await telemetry_1.telemetry.track({
|
|
524
543
|
command: 'collections:install',
|
package/dist/commands/install.js
CHANGED
|
@@ -364,6 +364,7 @@ async function handleInstall(packageSpec, options) {
|
|
|
364
364
|
format: pkg.format, // Preserve original package format
|
|
365
365
|
subtype: pkg.subtype, // Preserve original package subtype
|
|
366
366
|
installedPath: destPath,
|
|
367
|
+
fromCollection: options.fromCollection,
|
|
367
368
|
});
|
|
368
369
|
(0, lockfile_1.setPackageIntegrity)(updatedLockfile, packageId, tarball);
|
|
369
370
|
await (0, lockfile_1.writeLockfile)(updatedLockfile);
|
package/dist/commands/list.js
CHANGED
|
@@ -40,6 +40,13 @@ function getDestinationDir(type) {
|
|
|
40
40
|
return '.prompts';
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Strip author namespace from package ID
|
|
45
|
+
*/
|
|
46
|
+
function stripAuthorNamespace(packageId) {
|
|
47
|
+
const parts = packageId.split('/');
|
|
48
|
+
return parts[parts.length - 1];
|
|
49
|
+
}
|
|
43
50
|
/**
|
|
44
51
|
* Find the actual file location for a package
|
|
45
52
|
*/
|
|
@@ -47,11 +54,13 @@ async function findPackageLocation(id, format, subtype) {
|
|
|
47
54
|
if (!format)
|
|
48
55
|
return null;
|
|
49
56
|
const baseDir = getDestinationDir(format);
|
|
57
|
+
// Strip author namespace to get actual package name used in file system
|
|
58
|
+
const packageName = stripAuthorNamespace(id);
|
|
50
59
|
// Try different file extensions based on format
|
|
51
60
|
const extensions = format === 'cursor' ? ['.mdc', '.md'] : ['.md'];
|
|
52
|
-
// Try direct file: <dir>/<
|
|
61
|
+
// Try direct file: <dir>/<packageName>.ext
|
|
53
62
|
for (const ext of extensions) {
|
|
54
|
-
const directPath = path_1.default.join(baseDir, `${
|
|
63
|
+
const directPath = path_1.default.join(baseDir, `${packageName}${ext}`);
|
|
55
64
|
try {
|
|
56
65
|
await fs_1.promises.access(directPath);
|
|
57
66
|
return directPath;
|
|
@@ -60,9 +69,9 @@ async function findPackageLocation(id, format, subtype) {
|
|
|
60
69
|
// File doesn't exist, continue
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
|
-
// Try subdirectory: <dir>/<
|
|
72
|
+
// Try subdirectory: <dir>/<packageName>/SKILL.md or <dir>/<packageName>/AGENT.md
|
|
64
73
|
if (subtype === 'skill') {
|
|
65
|
-
const skillPath = path_1.default.join(baseDir,
|
|
74
|
+
const skillPath = path_1.default.join(baseDir, packageName, 'SKILL.md');
|
|
66
75
|
try {
|
|
67
76
|
await fs_1.promises.access(skillPath);
|
|
68
77
|
return skillPath;
|
|
@@ -72,7 +81,7 @@ async function findPackageLocation(id, format, subtype) {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
if (subtype === 'agent' || format === 'claude') {
|
|
75
|
-
const agentPath = path_1.default.join(baseDir,
|
|
84
|
+
const agentPath = path_1.default.join(baseDir, packageName, 'AGENT.md');
|
|
76
85
|
try {
|
|
77
86
|
await fs_1.promises.access(agentPath);
|
|
78
87
|
return agentPath;
|
package/dist/commands/login.js
CHANGED
|
@@ -140,10 +140,21 @@ async function loginWithOAuth(registryUrl) {
|
|
|
140
140
|
}
|
|
141
141
|
// Create the CLI auth URL with session token, callback, and userId
|
|
142
142
|
const callbackUrl = 'http://localhost:8765/callback';
|
|
143
|
-
// Determine webapp URL
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
:
|
|
143
|
+
// Determine webapp URL based on registry URL
|
|
144
|
+
let webappUrl;
|
|
145
|
+
if (registryUrl.includes('localhost') || registryUrl.includes('127.0.0.1')) {
|
|
146
|
+
// Local development: registry on port 3111, webapp on port 5173
|
|
147
|
+
webappUrl = registryUrl.replace(':3111', ':5173');
|
|
148
|
+
}
|
|
149
|
+
else if (registryUrl.includes('registry.prpm.dev')) {
|
|
150
|
+
// Production: always use prpm.dev webapp
|
|
151
|
+
webappUrl = 'https://prpm.dev';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Custom registry: assume webapp is on same host without port
|
|
155
|
+
const url = new URL(registryUrl);
|
|
156
|
+
webappUrl = `${url.protocol}//${url.hostname}`;
|
|
157
|
+
}
|
|
147
158
|
const authUrl = `${webappUrl}/cli-auth?sessionToken=${encodeURIComponent(connectSessionToken)}&cliCallback=${encodeURIComponent(callbackUrl)}&userId=${encodeURIComponent(userId)}`;
|
|
148
159
|
console.log(` Please open this link in your browser to authenticate:`);
|
|
149
160
|
console.log(` ${authUrl}\n`);
|
package/dist/commands/publish.js
CHANGED
|
@@ -54,27 +54,88 @@ const snippet_extractor_1 = require("../utils/snippet-extractor");
|
|
|
54
54
|
/**
|
|
55
55
|
* Try to find and load manifest files
|
|
56
56
|
* Checks for:
|
|
57
|
-
* 1. prpm.json (native format) - returns single manifest
|
|
57
|
+
* 1. prpm.json (native format) - returns single manifest or array of packages
|
|
58
58
|
* 2. .claude/marketplace.json (Claude format) - returns all plugins as separate manifests
|
|
59
59
|
* 3. .claude-plugin/marketplace.json (Claude format - alternative location) - returns all plugins
|
|
60
60
|
*/
|
|
61
61
|
async function findAndLoadManifests() {
|
|
62
62
|
// Try prpm.json first (native format)
|
|
63
63
|
const prpmJsonPath = (0, path_1.join)(process.cwd(), 'prpm.json');
|
|
64
|
+
let prpmJsonExists = false;
|
|
65
|
+
let prpmJsonError = null;
|
|
64
66
|
try {
|
|
65
67
|
const content = await (0, promises_1.readFile)(prpmJsonPath, 'utf-8');
|
|
66
|
-
|
|
68
|
+
prpmJsonExists = true;
|
|
69
|
+
// Try to parse JSON
|
|
70
|
+
let manifest;
|
|
71
|
+
try {
|
|
72
|
+
manifest = JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch (parseError) {
|
|
75
|
+
// JSON parse error - provide specific error message
|
|
76
|
+
const error = parseError;
|
|
77
|
+
throw new Error(`Invalid JSON in prpm.json:\n\n` +
|
|
78
|
+
`${error.message}\n\n` +
|
|
79
|
+
`Please check your prpm.json file for syntax errors:\n` +
|
|
80
|
+
` - Missing or extra commas\n` +
|
|
81
|
+
` - Unclosed quotes or brackets\n` +
|
|
82
|
+
` - Invalid JSON syntax\n\n` +
|
|
83
|
+
`You can validate your JSON at https://jsonlint.com/`);
|
|
84
|
+
}
|
|
85
|
+
// Check if this is a multi-package manifest
|
|
86
|
+
if ('packages' in manifest && Array.isArray(manifest.packages)) {
|
|
87
|
+
const multiManifest = manifest;
|
|
88
|
+
// Validate each package in the array
|
|
89
|
+
const validatedManifests = multiManifest.packages.map((pkg, idx) => {
|
|
90
|
+
// Inherit top-level fields if not specified in package - using explicit undefined checks
|
|
91
|
+
const packageWithDefaults = {
|
|
92
|
+
name: pkg.name,
|
|
93
|
+
version: pkg.version,
|
|
94
|
+
description: pkg.description,
|
|
95
|
+
format: pkg.format,
|
|
96
|
+
files: pkg.files,
|
|
97
|
+
author: pkg.author !== undefined ? pkg.author : multiManifest.author,
|
|
98
|
+
license: pkg.license !== undefined ? pkg.license : multiManifest.license,
|
|
99
|
+
repository: pkg.repository !== undefined ? pkg.repository : multiManifest.repository,
|
|
100
|
+
homepage: pkg.homepage !== undefined ? pkg.homepage : multiManifest.homepage,
|
|
101
|
+
documentation: pkg.documentation !== undefined ? pkg.documentation : multiManifest.documentation,
|
|
102
|
+
organization: pkg.organization !== undefined ? pkg.organization : multiManifest.organization,
|
|
103
|
+
private: pkg.private !== undefined ? pkg.private : multiManifest.private,
|
|
104
|
+
tags: pkg.tags !== undefined ? pkg.tags : multiManifest.tags,
|
|
105
|
+
keywords: pkg.keywords !== undefined ? pkg.keywords : multiManifest.keywords,
|
|
106
|
+
subtype: pkg.subtype,
|
|
107
|
+
dependencies: pkg.dependencies,
|
|
108
|
+
peerDependencies: pkg.peerDependencies,
|
|
109
|
+
engines: pkg.engines,
|
|
110
|
+
main: pkg.main,
|
|
111
|
+
};
|
|
112
|
+
// Debug: Log inheritance only if DEBUG env var is set
|
|
113
|
+
if (process.env.DEBUG) {
|
|
114
|
+
console.log(`\n🔍 Package ${pkg.name} inheritance:`);
|
|
115
|
+
console.log(` - Package-level private: ${pkg.private}`);
|
|
116
|
+
console.log(` - Top-level private: ${multiManifest.private}`);
|
|
117
|
+
console.log(` - Inherited private: ${packageWithDefaults.private}`);
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
120
|
+
return validateManifest(packageWithDefaults);
|
|
121
|
+
});
|
|
122
|
+
return { manifests: validatedManifests, source: 'prpm.json (multi-package)' };
|
|
123
|
+
}
|
|
124
|
+
// Single package manifest
|
|
67
125
|
const validated = validateManifest(manifest);
|
|
68
126
|
return { manifests: [validated], source: 'prpm.json' };
|
|
69
127
|
}
|
|
70
128
|
catch (error) {
|
|
71
|
-
//
|
|
72
|
-
|
|
129
|
+
// Store error for later
|
|
130
|
+
prpmJsonError = error;
|
|
131
|
+
// If it's a validation or parsing error, throw it immediately (don't try marketplace.json)
|
|
132
|
+
if (prpmJsonExists && error instanceof Error && (error.message.includes('Invalid JSON') ||
|
|
133
|
+
error.message.includes('Manifest validation failed') ||
|
|
73
134
|
error.message.includes('Claude skill') ||
|
|
74
135
|
error.message.includes('SKILL.md'))) {
|
|
75
136
|
throw error;
|
|
76
137
|
}
|
|
77
|
-
// Otherwise, prpm.json not found or
|
|
138
|
+
// Otherwise, prpm.json not found or other error, try marketplace.json
|
|
78
139
|
}
|
|
79
140
|
// Try .claude/marketplace.json (Claude format)
|
|
80
141
|
const marketplaceJsonPath = (0, path_1.join)(process.cwd(), '.claude', 'marketplace.json');
|
|
@@ -302,6 +363,29 @@ async function handlePublish(options) {
|
|
|
302
363
|
console.log(' Could not fetch user organizations, publishing as personal packages');
|
|
303
364
|
}
|
|
304
365
|
console.log('');
|
|
366
|
+
// Check for duplicate package names
|
|
367
|
+
if (manifests.length > 1) {
|
|
368
|
+
const nameMap = new Map();
|
|
369
|
+
const duplicates = [];
|
|
370
|
+
manifests.forEach((manifest, index) => {
|
|
371
|
+
const existingIndex = nameMap.get(manifest.name);
|
|
372
|
+
if (existingIndex !== undefined) {
|
|
373
|
+
duplicates.push(` - "${manifest.name}" appears in positions ${existingIndex + 1} and ${index + 1}`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
nameMap.set(manifest.name, index);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
if (duplicates.length > 0) {
|
|
380
|
+
console.error('❌ Duplicate package names detected:\n');
|
|
381
|
+
duplicates.forEach(dup => console.error(dup));
|
|
382
|
+
console.error('\n⚠️ Each package must have a unique name.');
|
|
383
|
+
console.error(' Package names are globally unique per author/organization.');
|
|
384
|
+
console.error(' If you want to publish the same package for different formats,');
|
|
385
|
+
console.error(' use different names (e.g., "react-rules-cursor" vs "react-rules-claude").\n');
|
|
386
|
+
throw new Error('Cannot publish packages with duplicate names');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
305
389
|
// Track published packages
|
|
306
390
|
const publishedPackages = [];
|
|
307
391
|
const failedPackages = [];
|
|
@@ -316,6 +400,39 @@ async function handlePublish(options) {
|
|
|
316
400
|
console.log(`${'='.repeat(60)}\n`);
|
|
317
401
|
}
|
|
318
402
|
try {
|
|
403
|
+
// Debug: Log access override logic only if DEBUG env var is set
|
|
404
|
+
if (process.env.DEBUG) {
|
|
405
|
+
console.log(`\n🔍 Before access override:`);
|
|
406
|
+
console.log(` - manifest.private: ${manifest.private}`);
|
|
407
|
+
console.log(` - options.access: ${options.access}`);
|
|
408
|
+
}
|
|
409
|
+
// Determine access level:
|
|
410
|
+
// 1. If --access flag is provided, it overrides manifest setting
|
|
411
|
+
// 2. Otherwise, use manifest setting (defaults to false/public if not specified)
|
|
412
|
+
let isPrivate;
|
|
413
|
+
if (options.access !== undefined) {
|
|
414
|
+
// CLI flag explicitly provided - use it
|
|
415
|
+
isPrivate = options.access === 'private';
|
|
416
|
+
if (process.env.DEBUG) {
|
|
417
|
+
console.log(` - Using CLI flag override: ${options.access}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// No CLI flag - use manifest setting
|
|
422
|
+
isPrivate = manifest.private || false;
|
|
423
|
+
if (process.env.DEBUG) {
|
|
424
|
+
console.log(` - Using manifest setting: ${isPrivate}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (process.env.DEBUG) {
|
|
428
|
+
console.log(` - calculated isPrivate: ${isPrivate}`);
|
|
429
|
+
}
|
|
430
|
+
// Update manifest with final private setting
|
|
431
|
+
manifest.private = isPrivate;
|
|
432
|
+
if (process.env.DEBUG) {
|
|
433
|
+
console.log(` - final manifest.private: ${manifest.private}`);
|
|
434
|
+
console.log('');
|
|
435
|
+
}
|
|
319
436
|
let selectedOrgId;
|
|
320
437
|
// Check if organization is specified in manifest
|
|
321
438
|
if (manifest.organization && userInfo) {
|
|
@@ -334,6 +451,7 @@ async function handlePublish(options) {
|
|
|
334
451
|
console.log(` Package: ${manifest.name}@${manifest.version}`);
|
|
335
452
|
console.log(` Format: ${manifest.format} | Subtype: ${manifest.subtype}`);
|
|
336
453
|
console.log(` Description: ${manifest.description}`);
|
|
454
|
+
console.log(` Access: ${manifest.private ? 'private' : 'public'}`);
|
|
337
455
|
if (selectedOrgId && userInfo) {
|
|
338
456
|
const selectedOrg = userInfo.organizations.find((org) => org.id === selectedOrgId);
|
|
339
457
|
console.log(` Publishing to: ${selectedOrg?.name || 'organization'}`);
|
|
@@ -512,7 +630,7 @@ async function handlePublish(options) {
|
|
|
512
630
|
function createPublishCommand() {
|
|
513
631
|
return new commander_1.Command('publish')
|
|
514
632
|
.description('Publish a package to the registry')
|
|
515
|
-
.option('--access <type>', 'Package access (public or private)
|
|
633
|
+
.option('--access <type>', 'Package access (public or private) - overrides manifest setting')
|
|
516
634
|
.option('--tag <tag>', 'NPM-style tag (e.g., latest, beta)', 'latest')
|
|
517
635
|
.option('--dry-run', 'Validate package without publishing')
|
|
518
636
|
.action(async (options) => {
|
package/dist/core/filesystem.js
CHANGED
|
@@ -13,12 +13,19 @@ exports.deleteFile = deleteFile;
|
|
|
13
13
|
exports.fileExists = fileExists;
|
|
14
14
|
exports.generateId = generateId;
|
|
15
15
|
exports.stripAuthorNamespace = stripAuthorNamespace;
|
|
16
|
+
exports.getInstalledFilePath = getInstalledFilePath;
|
|
17
|
+
exports.getInstalledFilePaths = getInstalledFilePaths;
|
|
16
18
|
const fs_1 = require("fs");
|
|
17
19
|
const path_1 = __importDefault(require("path"));
|
|
18
20
|
/**
|
|
19
21
|
* Get the destination directory for a package based on format and subtype
|
|
22
|
+
* @param format - Package format (cursor, claude, etc.)
|
|
23
|
+
* @param subtype - Package subtype (skill, agent, rule, etc.)
|
|
24
|
+
* @param name - Package name (optional, only needed for Claude skills which create subdirectories)
|
|
20
25
|
*/
|
|
21
26
|
function getDestinationDir(format, subtype, name) {
|
|
27
|
+
// Strip author namespace from package name to avoid nested directories
|
|
28
|
+
const packageName = stripAuthorNamespace(name);
|
|
22
29
|
switch (format) {
|
|
23
30
|
case 'cursor':
|
|
24
31
|
if (subtype === 'agent')
|
|
@@ -27,8 +34,11 @@ function getDestinationDir(format, subtype, name) {
|
|
|
27
34
|
return '.cursor/commands';
|
|
28
35
|
return '.cursor/rules';
|
|
29
36
|
case 'claude':
|
|
37
|
+
// Only create subdirectory for skills if name is provided
|
|
38
|
+
if (subtype === 'skill' && packageName)
|
|
39
|
+
return `.claude/skills/${packageName}`;
|
|
30
40
|
if (subtype === 'skill')
|
|
31
|
-
return
|
|
41
|
+
return '.claude/skills';
|
|
32
42
|
if (subtype === 'slash-command')
|
|
33
43
|
return '.claude/commands';
|
|
34
44
|
if (subtype === 'agent')
|
|
@@ -124,7 +134,50 @@ function generateId(filename) {
|
|
|
124
134
|
* stripAuthorNamespace('git-workflow-manager') // 'git-workflow-manager'
|
|
125
135
|
*/
|
|
126
136
|
function stripAuthorNamespace(packageId) {
|
|
137
|
+
// Handle undefined or empty string
|
|
138
|
+
if (!packageId) {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
127
141
|
// Split by '/' and get the last segment (the actual package name)
|
|
128
142
|
const parts = packageId.split('/');
|
|
129
143
|
return parts[parts.length - 1];
|
|
130
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Get the expected installed file path for a package
|
|
147
|
+
* This matches the logic used by the install command to determine where files are placed
|
|
148
|
+
*
|
|
149
|
+
* @param packageName - Full package name (e.g., '@prpm/typescript-rules')
|
|
150
|
+
* @param format - Package format
|
|
151
|
+
* @param subtype - Package subtype
|
|
152
|
+
* @param fileName - Optional specific file name (defaults to main file)
|
|
153
|
+
* @returns Path where the file will be installed relative to working directory
|
|
154
|
+
*/
|
|
155
|
+
function getInstalledFilePath(packageName, format, subtype, fileName) {
|
|
156
|
+
const destDir = getDestinationDir(format, subtype, packageName);
|
|
157
|
+
const packageBaseName = stripAuthorNamespace(packageName);
|
|
158
|
+
// If a specific file name is provided, use it
|
|
159
|
+
if (fileName) {
|
|
160
|
+
return path_1.default.join(destDir, fileName);
|
|
161
|
+
}
|
|
162
|
+
// Claude skills always use SKILL.md
|
|
163
|
+
if (format === 'claude' && subtype === 'skill') {
|
|
164
|
+
return path_1.default.join(destDir, 'SKILL.md');
|
|
165
|
+
}
|
|
166
|
+
// Determine file extension
|
|
167
|
+
const fileExtension = format === 'cursor' ? 'mdc' : 'md';
|
|
168
|
+
// For other formats, use package name as filename
|
|
169
|
+
return path_1.default.join(destDir, `${packageBaseName}.${fileExtension}`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get all expected installed file paths for a multi-file package
|
|
173
|
+
*
|
|
174
|
+
* @param packageName - Full package name
|
|
175
|
+
* @param format - Package format
|
|
176
|
+
* @param subtype - Package subtype
|
|
177
|
+
* @param fileNames - Array of file names in the package
|
|
178
|
+
* @returns Array of paths where files will be installed
|
|
179
|
+
*/
|
|
180
|
+
function getInstalledFilePaths(packageName, format, subtype, fileNames) {
|
|
181
|
+
const destDir = getDestinationDir(format, subtype, packageName);
|
|
182
|
+
return fileNames.map(fileName => path_1.default.join(destDir, fileName));
|
|
183
|
+
}
|
package/dist/core/lockfile.js
CHANGED
|
@@ -18,6 +18,10 @@ exports.addPackage = addPackage;
|
|
|
18
18
|
exports.removePackage = removePackage;
|
|
19
19
|
exports.listPackages = listPackages;
|
|
20
20
|
exports.getPackage = getPackage;
|
|
21
|
+
exports.addCollectionToLockfile = addCollectionToLockfile;
|
|
22
|
+
exports.getCollectionFromLockfile = getCollectionFromLockfile;
|
|
23
|
+
exports.removeCollectionFromLockfile = removeCollectionFromLockfile;
|
|
24
|
+
exports.listCollectionsFromLockfile = listCollectionsFromLockfile;
|
|
21
25
|
const fs_1 = require("fs");
|
|
22
26
|
const path_1 = require("path");
|
|
23
27
|
const crypto_1 = require("crypto");
|
|
@@ -75,6 +79,7 @@ function addToLockfile(lockfile, packageId, packageInfo) {
|
|
|
75
79
|
format: packageInfo.format,
|
|
76
80
|
subtype: packageInfo.subtype,
|
|
77
81
|
installedPath: packageInfo.installedPath,
|
|
82
|
+
fromCollection: packageInfo.fromCollection,
|
|
78
83
|
};
|
|
79
84
|
lockfile.generated = new Date().toISOString();
|
|
80
85
|
}
|
|
@@ -237,3 +242,50 @@ async function getPackage(packageId) {
|
|
|
237
242
|
}
|
|
238
243
|
return lockfile.packages[packageId];
|
|
239
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Add or update collection in lock file
|
|
247
|
+
*/
|
|
248
|
+
function addCollectionToLockfile(lockfile, collectionKey, collectionInfo) {
|
|
249
|
+
if (!lockfile.collections) {
|
|
250
|
+
lockfile.collections = {};
|
|
251
|
+
}
|
|
252
|
+
lockfile.collections[collectionKey] = {
|
|
253
|
+
scope: collectionInfo.scope,
|
|
254
|
+
name_slug: collectionInfo.name_slug,
|
|
255
|
+
version: collectionInfo.version,
|
|
256
|
+
installedAt: new Date().toISOString(),
|
|
257
|
+
packages: collectionInfo.packages,
|
|
258
|
+
};
|
|
259
|
+
lockfile.generated = new Date().toISOString();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get collection from lock file
|
|
263
|
+
*/
|
|
264
|
+
function getCollectionFromLockfile(lockfile, collectionKey) {
|
|
265
|
+
if (!lockfile || !lockfile.collections) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return lockfile.collections[collectionKey] || null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Remove collection from lock file
|
|
272
|
+
*/
|
|
273
|
+
function removeCollectionFromLockfile(lockfile, collectionKey) {
|
|
274
|
+
if (!lockfile.collections) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
delete lockfile.collections[collectionKey];
|
|
278
|
+
lockfile.generated = new Date().toISOString();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* List all collections in lock file
|
|
282
|
+
*/
|
|
283
|
+
function listCollectionsFromLockfile(lockfile) {
|
|
284
|
+
if (!lockfile || !lockfile.collections) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return Object.entries(lockfile.collections).map(([key, collection]) => ({
|
|
288
|
+
key,
|
|
289
|
+
...collection,
|
|
290
|
+
}));
|
|
291
|
+
}
|
package/dist/core/user-config.js
CHANGED
|
@@ -1,58 +1,124 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* User configuration management for ~/.prpmrc
|
|
3
|
+
* User configuration management for ~/.prpmrc and .prpmrc
|
|
4
4
|
* Stores global settings like registry URL and authentication token
|
|
5
|
+
* Supports both user-level (~/.prpmrc) and repository-level (.prpmrc) config
|
|
5
6
|
*/
|
|
6
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
8
|
exports.getConfig = getConfig;
|
|
8
9
|
exports.saveConfig = saveConfig;
|
|
10
|
+
exports.saveRepoConfig = saveRepoConfig;
|
|
11
|
+
exports.getRepoConfig = getRepoConfig;
|
|
12
|
+
exports.getUserConfig = getUserConfig;
|
|
9
13
|
exports.updateConfig = updateConfig;
|
|
10
14
|
exports.clearAuth = clearAuth;
|
|
11
15
|
exports.getRegistryUrl = getRegistryUrl;
|
|
12
16
|
const fs_1 = require("fs");
|
|
13
17
|
const path_1 = require("path");
|
|
14
18
|
const os_1 = require("os");
|
|
15
|
-
const
|
|
19
|
+
const USER_CONFIG_FILE = (0, path_1.join)((0, os_1.homedir)(), '.prpmrc');
|
|
20
|
+
const REPO_CONFIG_FILE = '.prpmrc';
|
|
16
21
|
const DEFAULT_REGISTRY_URL = 'https://registry.prpm.dev';
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
23
|
+
* Load configuration from a file
|
|
19
24
|
*/
|
|
20
|
-
async function
|
|
25
|
+
async function loadConfigFile(filePath) {
|
|
21
26
|
try {
|
|
22
|
-
const data = await fs_1.promises.readFile(
|
|
23
|
-
|
|
24
|
-
// Allow environment variable to override registry URL
|
|
25
|
-
if (process.env.PRPM_REGISTRY_URL) {
|
|
26
|
-
config.registryUrl = process.env.PRPM_REGISTRY_URL;
|
|
27
|
-
}
|
|
28
|
-
else if (!config.registryUrl) {
|
|
29
|
-
config.registryUrl = DEFAULT_REGISTRY_URL;
|
|
30
|
-
}
|
|
31
|
-
return config;
|
|
27
|
+
const data = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
28
|
+
return JSON.parse(data);
|
|
32
29
|
}
|
|
33
30
|
catch (error) {
|
|
34
|
-
// If file doesn't exist, return default config
|
|
35
31
|
if (error.code === 'ENOENT') {
|
|
36
|
-
return
|
|
37
|
-
registryUrl: process.env.PRPM_REGISTRY_URL || DEFAULT_REGISTRY_URL,
|
|
38
|
-
telemetryEnabled: true,
|
|
39
|
-
};
|
|
32
|
+
return null;
|
|
40
33
|
}
|
|
41
|
-
throw new Error(`Failed to read
|
|
34
|
+
throw new Error(`Failed to read config from ${filePath}: ${error}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get merged configuration from user and repository levels
|
|
39
|
+
* Priority: CLI flags > environment > repo config > user config > defaults
|
|
40
|
+
*/
|
|
41
|
+
async function getConfig() {
|
|
42
|
+
// Load user-level config (~/.prpmrc)
|
|
43
|
+
const userConfig = await loadConfigFile(USER_CONFIG_FILE);
|
|
44
|
+
// Load repository-level config (./prpmrc)
|
|
45
|
+
const repoConfigPath = (0, path_1.join)(process.cwd(), REPO_CONFIG_FILE);
|
|
46
|
+
const repoConfig = await loadConfigFile(repoConfigPath);
|
|
47
|
+
// Merge configs (repo overrides user)
|
|
48
|
+
const config = {
|
|
49
|
+
...userConfig,
|
|
50
|
+
...repoConfig,
|
|
51
|
+
};
|
|
52
|
+
// Deep merge nested objects
|
|
53
|
+
if (userConfig?.cursor || repoConfig?.cursor) {
|
|
54
|
+
config.cursor = {
|
|
55
|
+
...userConfig?.cursor,
|
|
56
|
+
...repoConfig?.cursor,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (userConfig?.claude || repoConfig?.claude) {
|
|
60
|
+
config.claude = {
|
|
61
|
+
...userConfig?.claude,
|
|
62
|
+
...repoConfig?.claude,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (userConfig?.collections || repoConfig?.collections) {
|
|
66
|
+
config.collections = {
|
|
67
|
+
...userConfig?.collections,
|
|
68
|
+
...repoConfig?.collections,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Allow environment variable to override registry URL
|
|
72
|
+
if (process.env.PRPM_REGISTRY_URL) {
|
|
73
|
+
config.registryUrl = process.env.PRPM_REGISTRY_URL;
|
|
42
74
|
}
|
|
75
|
+
else if (!config.registryUrl) {
|
|
76
|
+
config.registryUrl = DEFAULT_REGISTRY_URL;
|
|
77
|
+
}
|
|
78
|
+
// Set defaults
|
|
79
|
+
if (config.telemetryEnabled === undefined) {
|
|
80
|
+
config.telemetryEnabled = true;
|
|
81
|
+
}
|
|
82
|
+
return config;
|
|
43
83
|
}
|
|
44
84
|
/**
|
|
45
|
-
* Save user configuration
|
|
85
|
+
* Save user configuration to ~/.prpmrc
|
|
46
86
|
*/
|
|
47
87
|
async function saveConfig(config) {
|
|
48
88
|
try {
|
|
49
89
|
const data = JSON.stringify(config, null, 2);
|
|
50
|
-
await fs_1.promises.writeFile(
|
|
90
|
+
await fs_1.promises.writeFile(USER_CONFIG_FILE, data, 'utf-8');
|
|
51
91
|
}
|
|
52
92
|
catch (error) {
|
|
53
93
|
throw new Error(`Failed to save user config: ${error}`);
|
|
54
94
|
}
|
|
55
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Save repository configuration to ./.prpmrc
|
|
98
|
+
*/
|
|
99
|
+
async function saveRepoConfig(config) {
|
|
100
|
+
try {
|
|
101
|
+
const repoConfigPath = (0, path_1.join)(process.cwd(), REPO_CONFIG_FILE);
|
|
102
|
+
const data = JSON.stringify(config, null, 2);
|
|
103
|
+
await fs_1.promises.writeFile(repoConfigPath, data, 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new Error(`Failed to save repository config: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get repository-level configuration only
|
|
111
|
+
*/
|
|
112
|
+
async function getRepoConfig() {
|
|
113
|
+
const repoConfigPath = (0, path_1.join)(process.cwd(), REPO_CONFIG_FILE);
|
|
114
|
+
return await loadConfigFile(repoConfigPath);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get user-level configuration only
|
|
118
|
+
*/
|
|
119
|
+
async function getUserConfig() {
|
|
120
|
+
return await loadConfigFile(USER_CONFIG_FILE);
|
|
121
|
+
}
|
|
56
122
|
/**
|
|
57
123
|
* Update specific config values
|
|
58
124
|
*/
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const upgrade_1 = require("./commands/upgrade");
|
|
|
25
25
|
const schema_1 = require("./commands/schema");
|
|
26
26
|
const init_1 = require("./commands/init");
|
|
27
27
|
const config_1 = require("./commands/config");
|
|
28
|
+
const catalog_1 = require("./commands/catalog");
|
|
28
29
|
const telemetry_2 = require("./core/telemetry");
|
|
29
30
|
// Read version from package.json
|
|
30
31
|
function getVersion() {
|
|
@@ -44,6 +45,7 @@ program
|
|
|
44
45
|
.version(getVersion());
|
|
45
46
|
// Package creation commands
|
|
46
47
|
program.addCommand((0, init_1.createInitCommand)());
|
|
48
|
+
program.addCommand((0, catalog_1.createCatalogCommand)());
|
|
47
49
|
// Registry commands (new)
|
|
48
50
|
program.addCommand((0, search_1.createSearchCommand)());
|
|
49
51
|
program.addCommand((0, install_1.createInstallCommand)());
|
|
@@ -8,30 +8,45 @@ exports.extractSnippet = extractSnippet;
|
|
|
8
8
|
exports.validateSnippet = validateSnippet;
|
|
9
9
|
const promises_1 = require("fs/promises");
|
|
10
10
|
const path_1 = require("path");
|
|
11
|
+
const filesystem_1 = require("../core/filesystem");
|
|
11
12
|
const MAX_SNIPPET_LENGTH = 2000;
|
|
12
13
|
/**
|
|
13
14
|
* Extract a preview snippet from package files
|
|
14
|
-
*
|
|
15
|
+
* Uses the same path logic as the install command to determine where files will be placed
|
|
15
16
|
*/
|
|
16
17
|
async function extractSnippet(manifest) {
|
|
17
18
|
const cwd = process.cwd();
|
|
18
19
|
try {
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Validate manifest has required fields
|
|
21
|
+
if (!manifest.format || !manifest.name || !manifest.subtype) {
|
|
22
|
+
console.warn('⚠️ Cannot extract snippet: manifest missing format, name, or subtype');
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
// Determine which file to extract snippet from
|
|
26
|
+
let targetFilePath;
|
|
27
|
+
if (manifest.main) {
|
|
28
|
+
// If main file is specified, use it directly (for multi-file packages)
|
|
29
|
+
targetFilePath = (0, filesystem_1.getInstalledFilePath)(manifest.name, manifest.format, manifest.subtype, manifest.main);
|
|
30
|
+
}
|
|
31
|
+
else if (manifest.files && manifest.files.length > 0) {
|
|
32
|
+
// Get the first file from the manifest
|
|
33
|
+
const firstFile = manifest.files[0];
|
|
34
|
+
const fileName = typeof firstFile === 'string'
|
|
35
|
+
? firstFile
|
|
36
|
+
: firstFile.path;
|
|
37
|
+
// For single-file packages or when no main is specified,
|
|
38
|
+
// use the format-aware path construction
|
|
39
|
+
targetFilePath = (0, filesystem_1.getInstalledFilePath)(manifest.name, manifest.format, manifest.subtype, fileName);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// No files specified, try to construct the default path
|
|
43
|
+
targetFilePath = (0, filesystem_1.getInstalledFilePath)(manifest.name, manifest.format, manifest.subtype);
|
|
44
|
+
}
|
|
45
|
+
const fullPath = (0, path_1.join)(cwd, targetFilePath);
|
|
31
46
|
// Check if path is a directory
|
|
32
47
|
const stats = await (0, promises_1.stat)(fullPath);
|
|
33
48
|
if (stats.isDirectory()) {
|
|
34
|
-
console.warn(`⚠️ Skipping snippet extraction: "${
|
|
49
|
+
console.warn(`⚠️ Skipping snippet extraction: "${targetFilePath}" is a directory`);
|
|
35
50
|
return null;
|
|
36
51
|
}
|
|
37
52
|
// Read the file content
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prpm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
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.7",
|
|
49
|
+
"@pr-pm/types": "^0.1.7",
|
|
50
50
|
"ajv": "^8.17.1",
|
|
51
51
|
"ajv-formats": "^3.0.1",
|
|
52
52
|
"commander": "^11.1.0",
|
|
@@ -148,6 +148,11 @@
|
|
|
148
148
|
"my-company"
|
|
149
149
|
]
|
|
150
150
|
},
|
|
151
|
+
"private": {
|
|
152
|
+
"type": "boolean",
|
|
153
|
+
"description": "Whether the package is private. Private packages are only accessible to the owner/organization members. Defaults to false (public).",
|
|
154
|
+
"default": false
|
|
155
|
+
},
|
|
151
156
|
"tags": {
|
|
152
157
|
"type": "array",
|
|
153
158
|
"description": "Package tags for categorization",
|
|
@@ -354,6 +359,14 @@
|
|
|
354
359
|
"node": ">=18.0.0"
|
|
355
360
|
}
|
|
356
361
|
]
|
|
362
|
+
},
|
|
363
|
+
"packages": {
|
|
364
|
+
"type": "array",
|
|
365
|
+
"description": "Array of packages to publish from a single manifest (multi-package publishing). Packages inherit top-level fields unless overridden.",
|
|
366
|
+
"items": {
|
|
367
|
+
"$ref": "#"
|
|
368
|
+
},
|
|
369
|
+
"minItems": 1
|
|
357
370
|
}
|
|
358
371
|
},
|
|
359
372
|
"additionalProperties": false,
|
|
@@ -491,6 +504,51 @@
|
|
|
491
504
|
"files": [
|
|
492
505
|
".windsurfrules"
|
|
493
506
|
]
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"name": "@company/private-package",
|
|
510
|
+
"version": "1.0.0",
|
|
511
|
+
"description": "A private package only accessible to organization members",
|
|
512
|
+
"format": "claude",
|
|
513
|
+
"subtype": "skill",
|
|
514
|
+
"author": "Company Team",
|
|
515
|
+
"organization": "my-company",
|
|
516
|
+
"private": true,
|
|
517
|
+
"license": "Proprietary",
|
|
518
|
+
"files": [
|
|
519
|
+
"internal-skill.md",
|
|
520
|
+
"README.md"
|
|
521
|
+
]
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"name": "@username/multi-package-example",
|
|
525
|
+
"version": "1.0.0",
|
|
526
|
+
"description": "Multi-package manifest example",
|
|
527
|
+
"author": "Your Name",
|
|
528
|
+
"license": "MIT",
|
|
529
|
+
"repository": "https://github.com/username/multi-package",
|
|
530
|
+
"packages": [
|
|
531
|
+
{
|
|
532
|
+
"name": "@username/package-one",
|
|
533
|
+
"version": "1.0.0",
|
|
534
|
+
"description": "First package in the multi-package manifest",
|
|
535
|
+
"format": "claude",
|
|
536
|
+
"subtype": "skill",
|
|
537
|
+
"files": [
|
|
538
|
+
"package-one/SKILL.md"
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"name": "@username/package-two",
|
|
543
|
+
"version": "1.0.0",
|
|
544
|
+
"description": "Second package with different settings",
|
|
545
|
+
"format": "cursor",
|
|
546
|
+
"private": true,
|
|
547
|
+
"files": [
|
|
548
|
+
"package-two/.cursor/rules/main.mdc"
|
|
549
|
+
]
|
|
550
|
+
}
|
|
551
|
+
]
|
|
494
552
|
}
|
|
495
553
|
]
|
|
496
554
|
}
|