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.
@@ -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:3000',
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',
@@ -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);
@@ -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>/<id>.ext
61
+ // Try direct file: <dir>/<packageName>.ext
53
62
  for (const ext of extensions) {
54
- const directPath = path_1.default.join(baseDir, `${id}${ext}`);
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>/<id>/SKILL.md or <dir>/<id>/AGENT.md
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, id, 'SKILL.md');
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, id, 'AGENT.md');
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;
@@ -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 - default to production
144
- const webappUrl = registryUrl.includes('localhost')
145
- ? registryUrl.replace(':3000', ':5173') // Local: localhost:3000 → localhost:5173
146
- : 'https://prpm.dev'; // Production: always use prpm.dev
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`);
@@ -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
- const manifest = JSON.parse(content);
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
- // If it's a validation error, throw it immediately (don't try marketplace.json)
72
- if (error instanceof Error && (error.message.includes('Manifest validation failed') ||
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 invalid JSON, try marketplace.json
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)', 'public')
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) => {
@@ -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 `.claude/skills/${name}`;
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
+ }
@@ -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
+ }
@@ -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 CONFIG_FILE = (0, path_1.join)((0, os_1.homedir)(), '.prpmrc');
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
- * Get user configuration
23
+ * Load configuration from a file
19
24
  */
20
- async function getConfig() {
25
+ async function loadConfigFile(filePath) {
21
26
  try {
22
- const data = await fs_1.promises.readFile(CONFIG_FILE, 'utf-8');
23
- const config = JSON.parse(data);
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 user config: ${error}`);
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(CONFIG_FILE, data, 'utf-8');
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
- * Takes the first file in the package and extracts ~2000 characters
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
- // Get the first file from the manifest
20
- const firstFile = manifest.files[0];
21
- if (!firstFile) {
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
- // 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);
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: "${targetFile}" is a directory`);
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.11",
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.5",
49
- "@pr-pm/types": "^0.1.5",
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
  }