prpm 0.0.12 → 0.0.14

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.
@@ -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',
@@ -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);
@@ -363,6 +363,29 @@ async function handlePublish(options) {
363
363
  console.log(' Could not fetch user organizations, publishing as personal packages');
364
364
  }
365
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
+ }
366
389
  // Track published packages
367
390
  const publishedPackages = [];
368
391
  const failedPackages = [];
@@ -13,6 +13,8 @@ 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
  /**
@@ -140,3 +142,42 @@ function stripAuthorNamespace(packageId) {
140
142
  const parts = packageId.split('/');
141
143
  return parts[parts.length - 1];
142
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
+ }
@@ -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/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)());
@@ -11,27 +11,34 @@ const path_1 = require("path");
11
11
  const MAX_SNIPPET_LENGTH = 2000;
12
12
  /**
13
13
  * Extract a preview snippet from package files
14
- * Takes the first file in the package and extracts ~2000 characters
14
+ * Uses the same path logic as the install command to determine where files will be placed
15
15
  */
16
16
  async function extractSnippet(manifest) {
17
17
  const cwd = process.cwd();
18
18
  try {
19
- // Get the first file from the manifest
20
- const firstFile = manifest.files[0];
21
- if (!firstFile) {
19
+ // Validate manifest has required fields
20
+ if (!manifest.files || manifest.files.length === 0) {
21
+ console.warn('⚠️ Cannot extract snippet: no files specified in manifest');
22
22
  return null;
23
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);
24
+ // Prefer main file over first file if specified
25
+ let fileName;
26
+ if (manifest.main) {
27
+ fileName = manifest.main;
28
+ }
29
+ else {
30
+ const firstFile = manifest.files[0];
31
+ fileName = typeof firstFile === 'string'
32
+ ? firstFile
33
+ : firstFile.path;
34
+ }
35
+ // Use the file path directly - it should be relative to project root
36
+ // (e.g., ".claude/skills/my-skill/SKILL.md" or ".cursor/rules/my-rule.mdc")
37
+ const fullPath = (0, path_1.join)(cwd, fileName);
31
38
  // Check if path is a directory
32
39
  const stats = await (0, promises_1.stat)(fullPath);
33
40
  if (stats.isDirectory()) {
34
- console.warn(`⚠️ Skipping snippet extraction: "${targetFile}" is a directory`);
41
+ console.warn(`⚠️ Skipping snippet extraction: "${fullPath}" is a directory`);
35
42
  return null;
36
43
  }
37
44
  // Read the file content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
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.6",
49
- "@pr-pm/types": "^0.1.6",
48
+ "@pr-pm/registry-client": "^1.2.8",
49
+ "@pr-pm/types": "^0.1.8",
50
50
  "ajv": "^8.17.1",
51
51
  "ajv-formats": "^3.0.1",
52
52
  "commander": "^11.1.0",