prpm 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -109,21 +109,21 @@ prpm add https://example.com/my-rules.md --as claude
109
109
 
110
110
  ---
111
111
 
112
- #### `prpm remove <id>`
112
+ #### `prpm uninstall <id>`
113
113
 
114
114
  Remove an installed package.
115
115
 
116
116
  ```bash
117
- prpm remove react-rules
117
+ prpm uninstall react-rules
118
118
  ```
119
119
 
120
120
  **Examples:**
121
121
  ```bash
122
122
  # Remove by package ID
123
- prpm remove typescript-rules
123
+ prpm uninstall typescript-rules
124
124
 
125
125
  # Remove cursor rules
126
- prpm remove cursor-rules
126
+ prpm uninstall cursor-rules
127
127
  ```
128
128
 
129
129
  ---
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * E2E Test Helpers
4
+ * Shared utilities for end-to-end CLI testing
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createTestDir = createTestDir;
8
+ exports.cleanupTestDir = cleanupTestDir;
9
+ exports.createMockPackage = createMockPackage;
10
+ exports.createMockCollection = createMockCollection;
11
+ exports.createMockConfig = createMockConfig;
12
+ exports.createMockFetch = createMockFetch;
13
+ exports.delay = delay;
14
+ exports.setupGlobalMocks = setupGlobalMocks;
15
+ exports.mockProcessExit = mockProcessExit;
16
+ const promises_1 = require("fs/promises");
17
+ const path_1 = require("path");
18
+ const os_1 = require("os");
19
+ /**
20
+ * Create a temporary test directory
21
+ */
22
+ async function createTestDir() {
23
+ const testDir = (0, path_1.join)((0, os_1.tmpdir)(), `prpm-e2e-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
24
+ await (0, promises_1.mkdir)(testDir, { recursive: true });
25
+ return testDir;
26
+ }
27
+ /**
28
+ * Clean up test directory
29
+ */
30
+ async function cleanupTestDir(testDir) {
31
+ try {
32
+ await (0, promises_1.rm)(testDir, { recursive: true, force: true });
33
+ }
34
+ catch {
35
+ // Ignore cleanup errors
36
+ }
37
+ }
38
+ /**
39
+ * Create a mock package manifest
40
+ */
41
+ async function createMockPackage(testDir, name, type = 'cursor', version = '1.0.0') {
42
+ const manifest = {
43
+ name,
44
+ version,
45
+ description: `Test package ${name}`,
46
+ type,
47
+ author: 'test-author',
48
+ tags: ['test', type],
49
+ };
50
+ const manifestPath = (0, path_1.join)(testDir, 'prpm.json');
51
+ await (0, promises_1.writeFile)(manifestPath, JSON.stringify(manifest, null, 2));
52
+ // Create a sample .cursorrules file
53
+ const rulesPath = (0, path_1.join)(testDir, '.cursorrules');
54
+ await (0, promises_1.writeFile)(rulesPath, '# Test cursor rules\n\nAlways write tests.\n');
55
+ return manifestPath;
56
+ }
57
+ /**
58
+ * Create a mock collection manifest
59
+ */
60
+ async function createMockCollection(testDir, id, packages) {
61
+ const manifest = {
62
+ id,
63
+ name: `Test Collection ${id}`,
64
+ description: 'A test collection for E2E testing',
65
+ category: 'development',
66
+ tags: ['test', 'automation'],
67
+ packages,
68
+ icon: 'šŸ“¦',
69
+ };
70
+ const manifestPath = (0, path_1.join)(testDir, 'collection.json');
71
+ await (0, promises_1.writeFile)(manifestPath, JSON.stringify(manifest, null, 2));
72
+ return manifestPath;
73
+ }
74
+ /**
75
+ * Create a mock user config
76
+ */
77
+ async function createMockConfig(configPath, options) {
78
+ const config = {
79
+ token: options.token || 'test-token-123',
80
+ registryUrl: options.registryUrl || 'http://localhost:3000',
81
+ };
82
+ await (0, promises_1.mkdir)((0, path_1.join)(configPath, '..'), { recursive: true });
83
+ await (0, promises_1.writeFile)(configPath, JSON.stringify(config, null, 2));
84
+ }
85
+ /**
86
+ * Mock fetch response for registry API
87
+ */
88
+ function createMockFetch() {
89
+ const responses = new Map();
90
+ const mockFetch = jest.fn(async (url, options) => {
91
+ const key = `${options?.method || 'GET'} ${url}`;
92
+ const response = responses.get(key) || responses.get(url);
93
+ if (!response) {
94
+ return {
95
+ ok: false,
96
+ status: 404,
97
+ statusText: 'Not Found',
98
+ json: async () => ({ error: 'Not found' }),
99
+ };
100
+ }
101
+ if (typeof response === 'function') {
102
+ return response(url, options);
103
+ }
104
+ return {
105
+ ok: true,
106
+ status: 200,
107
+ json: async () => response,
108
+ arrayBuffer: async () => Buffer.from('mock-data').buffer,
109
+ };
110
+ });
111
+ return {
112
+ fetch: mockFetch,
113
+ addResponse: (key, response) => {
114
+ responses.set(key, response);
115
+ },
116
+ clear: () => {
117
+ responses.clear();
118
+ mockFetch.mockClear();
119
+ },
120
+ };
121
+ }
122
+ /**
123
+ * Wait for async operations
124
+ */
125
+ function delay(ms) {
126
+ return new Promise(resolve => setTimeout(resolve, ms));
127
+ }
128
+ /**
129
+ * Setup global mocks for E2E tests
130
+ */
131
+ function setupGlobalMocks() {
132
+ // Mock console to reduce noise
133
+ beforeAll(() => {
134
+ jest.spyOn(console, 'log').mockImplementation();
135
+ jest.spyOn(console, 'error').mockImplementation();
136
+ jest.spyOn(console, 'warn').mockImplementation();
137
+ });
138
+ afterAll(() => {
139
+ jest.restoreAllMocks();
140
+ });
141
+ }
142
+ /**
143
+ * Mock process.exit to throw instead of exiting
144
+ */
145
+ function mockProcessExit() {
146
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
147
+ throw new Error(`Process exited with code ${code}`);
148
+ });
149
+ return mockExit;
150
+ }
@@ -2,10 +2,44 @@
2
2
  /**
3
3
  * Collections command - Manage package collections
4
4
  */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.handleCollectionsSearch = handleCollectionsSearch;
7
40
  exports.handleCollectionsList = handleCollectionsList;
8
41
  exports.handleCollectionInfo = handleCollectionInfo;
42
+ exports.handleCollectionPublish = handleCollectionPublish;
9
43
  exports.handleCollectionInstall = handleCollectionInstall;
10
44
  exports.createCollectionsCommand = createCollectionsCommand;
11
45
  const commander_1 = require("commander");
@@ -22,20 +56,15 @@ async function handleCollectionsSearch(query, options) {
22
56
  const config = await (0, user_config_1.getConfig)();
23
57
  const client = (0, registry_client_1.getRegistryClient)(config);
24
58
  console.log(`šŸ” Searching collections for "${query}"...\n`);
25
- // Get all collections and filter by query
59
+ // Use server-side search with full-text index
26
60
  const result = await client.getCollections({
61
+ query,
27
62
  category: options.category,
28
63
  tag: options.tag,
29
64
  official: options.official,
30
65
  limit: options.limit || 50,
31
66
  });
32
- // Filter collections by search query (name, description, tags)
33
- const queryLower = query.toLowerCase();
34
- const filtered = result.collections.filter(c => c.name.toLowerCase().includes(queryLower) ||
35
- c.description.toLowerCase().includes(queryLower) ||
36
- c.tags.some(tag => tag.toLowerCase().includes(queryLower)) ||
37
- c.id.toLowerCase().includes(queryLower));
38
- if (filtered.length === 0) {
67
+ if (result.collections.length === 0) {
39
68
  console.log('No collections found matching your search.');
40
69
  console.log('\nšŸ’” Try:');
41
70
  console.log(' - Broadening your search terms');
@@ -43,45 +72,50 @@ async function handleCollectionsSearch(query, options) {
43
72
  console.log(' - Browsing all: prpm collections list');
44
73
  return;
45
74
  }
46
- console.log(`✨ Found ${filtered.length} collection(s):\n`);
75
+ console.log(`✨ Found ${result.collections.length} collection(s):\n`);
47
76
  // Group by official vs community
48
- const official = filtered.filter(c => c.official);
49
- const community = filtered.filter(c => !c.official);
77
+ const official = result.collections.filter(c => c.official);
78
+ const community = result.collections.filter(c => !c.official);
50
79
  if (official.length > 0) {
51
- console.log('šŸ“¦ Official Collections:\n');
80
+ console.log(`šŸ“¦ Official Collections (${official.length}):\n`);
52
81
  official.forEach(c => {
53
- const fullName = `@${c.scope}/${c.id}`.padEnd(35);
82
+ const fullName = c.name_slug.padEnd(35);
54
83
  const pkgCount = `(${c.package_count} packages)`.padEnd(15);
55
84
  console.log(` ${c.icon || 'šŸ“¦'} ${fullName} ${pkgCount} ${c.name}`);
56
85
  if (c.description) {
57
86
  console.log(` ${c.description.substring(0, 70)}${c.description.length > 70 ? '...' : ''}`);
58
87
  }
88
+ console.log(` šŸ‘¤ by @${c.author}${c.verified ? ' āœ“' : ''}`);
59
89
  console.log(` ā¬‡ļø ${c.downloads.toLocaleString()} installs Ā· ⭐ ${c.stars.toLocaleString()} stars`);
60
90
  console.log('');
61
91
  });
62
92
  }
63
93
  if (community.length > 0) {
64
- console.log('\n🌟 Community Collections:\n');
94
+ console.log(`\n🌟 Community Collections (${community.length}):\n`);
65
95
  community.forEach(c => {
66
- const fullName = `@${c.scope}/${c.id}`.padEnd(35);
96
+ const fullName = c.name_slug.padEnd(35);
67
97
  const pkgCount = `(${c.package_count} packages)`.padEnd(15);
68
98
  console.log(` ${c.icon || 'šŸ“¦'} ${fullName} ${pkgCount} ${c.name}`);
69
99
  if (c.description) {
70
100
  console.log(` ${c.description.substring(0, 70)}${c.description.length > 70 ? '...' : ''}`);
71
101
  }
102
+ console.log(` šŸ‘¤ by @${c.author}${c.verified ? ' āœ“' : ''}`);
72
103
  console.log(` ā¬‡ļø ${c.downloads.toLocaleString()} installs Ā· ⭐ ${c.stars.toLocaleString()} stars`);
73
104
  console.log('');
74
105
  });
75
106
  }
76
- console.log(`\nšŸ’” View details: prpm collection info <collection>`);
77
- console.log(`šŸ’” Install: prpm install @collection/<name>`);
107
+ // Show results count
108
+ console.log(`\nšŸ“Š Found: ${result.collections.length} matching collection${result.collections.length === 1 ? '' : 's'} (searched ${result.total} total)\n`);
109
+ console.log(`šŸ’” View details: prpm collection info <collection>`);
110
+ console.log(`šŸ’” Install: prpm install <collection>`);
78
111
  await telemetry_1.telemetry.track({
79
112
  command: 'collections:search',
80
113
  success: true,
81
114
  duration: Date.now() - startTime,
82
115
  data: {
83
116
  query: query.substring(0, 100),
84
- count: filtered.length,
117
+ count: result.collections.length,
118
+ total: result.total,
85
119
  filters: options,
86
120
  },
87
121
  });
@@ -97,6 +131,9 @@ async function handleCollectionsSearch(query, options) {
97
131
  });
98
132
  process.exit(1);
99
133
  }
134
+ finally {
135
+ await telemetry_1.telemetry.shutdown();
136
+ }
100
137
  }
101
138
  /**
102
139
  * List available collections
@@ -112,7 +149,7 @@ async function handleCollectionsList(options) {
112
149
  tag: options.tag,
113
150
  official: options.official,
114
151
  scope: options.scope,
115
- limit: 50,
152
+ limit: 500, // Increased limit to show more collections
116
153
  });
117
154
  if (result.collections.length === 0) {
118
155
  console.log('No collections found matching your criteria.');
@@ -122,39 +159,51 @@ async function handleCollectionsList(options) {
122
159
  const official = result.collections.filter(c => c.official);
123
160
  const community = result.collections.filter(c => !c.official);
124
161
  if (official.length > 0) {
125
- console.log('šŸ“¦ Official Collections:\n');
162
+ console.log(`šŸ“¦ Official Collections (${official.length}):\n`);
126
163
  official.forEach(c => {
127
- const fullName = `@${c.scope}/${c.id}`.padEnd(35);
164
+ const fullName = c.name_slug.padEnd(35);
128
165
  const pkgCount = `(${c.package_count} packages)`.padEnd(15);
129
166
  console.log(` ${c.icon || 'šŸ“¦'} ${fullName} ${pkgCount} ${c.name}`);
130
167
  if (c.description) {
131
168
  console.log(` ${c.description.substring(0, 70)}${c.description.length > 70 ? '...' : ''}`);
132
169
  }
170
+ console.log(` šŸ‘¤ by @${c.author}${c.verified ? ' āœ“' : ''}`);
133
171
  console.log(` ā¬‡ļø ${c.downloads.toLocaleString()} installs Ā· ⭐ ${c.stars.toLocaleString()} stars`);
134
172
  console.log('');
135
173
  });
136
174
  }
137
175
  if (community.length > 0) {
138
- console.log('\n🌟 Community Collections:\n');
176
+ console.log(`\n🌟 Community Collections (${community.length}):\n`);
139
177
  community.forEach(c => {
140
- const fullName = `@${c.scope}/${c.id}`.padEnd(35);
178
+ const fullName = c.name_slug.padEnd(35);
141
179
  const pkgCount = `(${c.package_count} packages)`.padEnd(15);
142
180
  console.log(` ${c.icon || 'šŸ“¦'} ${fullName} ${pkgCount} ${c.name}`);
143
181
  if (c.description) {
144
182
  console.log(` ${c.description.substring(0, 70)}${c.description.length > 70 ? '...' : ''}`);
145
183
  }
184
+ console.log(` šŸ‘¤ by @${c.author}${c.verified ? ' āœ“' : ''}`);
146
185
  console.log(` ā¬‡ļø ${c.downloads.toLocaleString()} installs Ā· ⭐ ${c.stars.toLocaleString()} stars`);
147
186
  console.log('');
148
187
  });
149
188
  }
150
- console.log(`\nšŸ’” View details: prpm collection info <collection>`);
151
- console.log(`šŸ’” Install: prpm install @collection/<name>`);
189
+ // Show total from API (which includes all collections, not just the ones returned)
190
+ const showing = result.collections.length;
191
+ const total = result.total;
192
+ if (showing < total) {
193
+ console.log(`\nšŸ“Š Showing ${showing} of ${total} collection${total === 1 ? '' : 's'}\n`);
194
+ }
195
+ else {
196
+ console.log(`\nšŸ“Š Total: ${total} collection${total === 1 ? '' : 's'}\n`);
197
+ }
198
+ console.log(`šŸ’” View details: prpm collection info <collection>`);
199
+ console.log(`šŸ’” Install: prpm install <collection>`);
152
200
  await telemetry_1.telemetry.track({
153
201
  command: 'collections:list',
154
202
  success: true,
155
203
  duration: Date.now() - startTime,
156
204
  data: {
157
205
  count: result.collections.length,
206
+ total: result.total,
158
207
  filters: options,
159
208
  },
160
209
  });
@@ -170,6 +219,9 @@ async function handleCollectionsList(options) {
170
219
  });
171
220
  process.exit(1);
172
221
  }
222
+ finally {
223
+ await telemetry_1.telemetry.shutdown();
224
+ }
173
225
  }
174
226
  /**
175
227
  * Show collection details
@@ -177,16 +229,28 @@ async function handleCollectionsList(options) {
177
229
  async function handleCollectionInfo(collectionSpec) {
178
230
  const startTime = Date.now();
179
231
  try {
180
- // Parse collection spec: @scope/id or scope/id
181
- const match = collectionSpec.match(/^@?([^/]+)\/([^/@]+)(?:@(.+))?$/);
182
- if (!match) {
183
- throw new Error('Invalid collection format. Use: @scope/id or scope/id[@version]');
232
+ // Parse collection spec: @scope/name_slug, scope/name_slug, or just name_slug (defaults to 'collection' scope)
233
+ let scope;
234
+ let name_slug;
235
+ let version;
236
+ const matchWithScope = collectionSpec.match(/^@?([^/]+)\/([^/@]+)(?:@(.+))?$/);
237
+ if (matchWithScope) {
238
+ // Has explicit scope: @scope/name or scope/name
239
+ [, scope, name_slug, version] = matchWithScope;
240
+ }
241
+ else {
242
+ // No scope, assume 'collection' scope: just name or name@version
243
+ const matchNoScope = collectionSpec.match(/^([^/@]+)(?:@(.+))?$/);
244
+ if (!matchNoScope) {
245
+ throw new Error('Invalid collection format. Use: name, @scope/name, or scope/name (optionally with @version)');
246
+ }
247
+ [, name_slug, version] = matchNoScope;
248
+ scope = 'collection'; // Default scope
184
249
  }
185
- const [, scope, id, version] = match;
186
250
  const config = await (0, user_config_1.getConfig)();
187
251
  const client = (0, registry_client_1.getRegistryClient)(config);
188
- console.log(`šŸ“¦ Loading collection: @${scope}/${id}...\n`);
189
- const collection = await client.getCollection(scope, id, version);
252
+ console.log(`šŸ“¦ Loading collection: ${scope === 'collection' ? name_slug : `@${scope}/${name_slug}`}...\n`);
253
+ const collection = await client.getCollection(scope, name_slug, version);
190
254
  // Header
191
255
  console.log(`${collection.icon || 'šŸ“¦'} ${collection.name}`);
192
256
  console.log(`${'='.repeat(collection.name.length + 2)}`);
@@ -215,9 +279,9 @@ async function handleCollectionInfo(collectionSpec) {
215
279
  if (requiredPkgs.length > 0) {
216
280
  console.log(' Required:');
217
281
  requiredPkgs.forEach((pkg, i) => {
218
- console.log(` ${i + 1}. āœ“ ${pkg.packageId}@${pkg.version || 'latest'}`);
219
- if (pkg.package) {
220
- console.log(` ${pkg.package.description || pkg.package.display_name}`);
282
+ console.log(` ${i + 1}. āœ“ ${pkg?.package?.name}@${pkg.version || 'latest'}`);
283
+ if (pkg.package && pkg.package.description) {
284
+ console.log(` ${pkg.package.description}`);
221
285
  }
222
286
  if (pkg.reason) {
223
287
  console.log(` šŸ’” ${pkg.reason}`);
@@ -228,9 +292,9 @@ async function handleCollectionInfo(collectionSpec) {
228
292
  if (optionalPkgs.length > 0) {
229
293
  console.log(' Optional:');
230
294
  optionalPkgs.forEach((pkg, i) => {
231
- console.log(` ${i + 1}. ā—‹ ${pkg.packageId}@${pkg.version || 'latest'}`);
232
- if (pkg.package) {
233
- console.log(` ${pkg.package.description || pkg.package.display_name}`);
295
+ console.log(` ${i + 1}. ā—‹ ${pkg?.package?.name}@${pkg.version || 'latest'}`);
296
+ if (pkg.package && pkg.package.description) {
297
+ console.log(` ${pkg.package.description}`);
234
298
  }
235
299
  if (pkg.reason) {
236
300
  console.log(` šŸ’” ${pkg.reason}`);
@@ -240,9 +304,17 @@ async function handleCollectionInfo(collectionSpec) {
240
304
  }
241
305
  // Installation
242
306
  console.log('šŸ’” Install:');
243
- console.log(` prpm install @${scope}/${id}`);
244
- if (optionalPkgs.length > 0) {
245
- console.log(` prpm install @${scope}/${id} --skip-optional # Skip optional packages`);
307
+ if (scope === 'collection') {
308
+ console.log(` prpm install ${name_slug}`);
309
+ if (optionalPkgs.length > 0) {
310
+ console.log(` prpm install ${name_slug} --skip-optional # Skip optional packages`);
311
+ }
312
+ }
313
+ else {
314
+ console.log(` prpm install @${scope}/${name_slug}`);
315
+ if (optionalPkgs.length > 0) {
316
+ console.log(` prpm install @${scope}/${name_slug} --skip-optional # Skip optional packages`);
317
+ }
246
318
  }
247
319
  console.log('');
248
320
  await telemetry_1.telemetry.track({
@@ -251,7 +323,7 @@ async function handleCollectionInfo(collectionSpec) {
251
323
  duration: Date.now() - startTime,
252
324
  data: {
253
325
  scope,
254
- id,
326
+ name_slug,
255
327
  packageCount: collection.packages.length,
256
328
  },
257
329
  });
@@ -267,6 +339,109 @@ async function handleCollectionInfo(collectionSpec) {
267
339
  });
268
340
  process.exit(1);
269
341
  }
342
+ finally {
343
+ await telemetry_1.telemetry.shutdown();
344
+ }
345
+ }
346
+ /**
347
+ * Publish/create a collection
348
+ */
349
+ async function handleCollectionPublish(manifestPath = './collection.json') {
350
+ const startTime = Date.now();
351
+ try {
352
+ const config = await (0, user_config_1.getConfig)();
353
+ const client = (0, registry_client_1.getRegistryClient)(config);
354
+ // Check authentication
355
+ if (!config.token) {
356
+ console.error('\nāŒ Authentication required. Run `prpm login` first.\n');
357
+ process.exit(1);
358
+ }
359
+ console.log('šŸ“¦ Publishing collection...\n');
360
+ // Read collection manifest
361
+ const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
362
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
363
+ const manifest = JSON.parse(manifestContent);
364
+ // Validate manifest
365
+ const required = ['id', 'name', 'description', 'packages'];
366
+ const missing = required.filter(field => !manifest[field]);
367
+ if (missing.length > 0) {
368
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
369
+ }
370
+ // Validate id format (must be lowercase alphanumeric with hyphens)
371
+ if (!/^[a-z0-9-]+$/.test(manifest.id)) {
372
+ throw new Error('Collection id must be lowercase alphanumeric with hyphens only');
373
+ }
374
+ // Validate name length
375
+ if (manifest.name.length < 3) {
376
+ throw new Error('Collection name must be at least 3 characters');
377
+ }
378
+ // Validate description length
379
+ if (manifest.description.length < 10) {
380
+ throw new Error('Collection description must be at least 10 characters');
381
+ }
382
+ // Validate packages array
383
+ if (!Array.isArray(manifest.packages) || manifest.packages.length === 0) {
384
+ throw new Error('Collection must include at least one package');
385
+ }
386
+ // Validate each package
387
+ manifest.packages.forEach((pkg, idx) => {
388
+ if (!pkg.packageId) {
389
+ throw new Error(`Package at index ${idx} is missing packageId`);
390
+ }
391
+ });
392
+ console.log(`šŸ” Validating collection manifest...`);
393
+ console.log(` Collection: ${manifest.name}`);
394
+ console.log(` ID: ${manifest.id}`);
395
+ console.log(` Packages: ${manifest.packages.length}`);
396
+ console.log('');
397
+ // Publish to registry
398
+ console.log('šŸš€ Publishing to registry...\n');
399
+ const result = await client.createCollection({
400
+ id: manifest.id,
401
+ name: manifest.name,
402
+ description: manifest.description,
403
+ category: manifest.category,
404
+ tags: manifest.tags,
405
+ packages: manifest.packages.map((pkg) => ({
406
+ packageId: pkg.packageId,
407
+ version: pkg.version,
408
+ required: pkg.required !== false,
409
+ reason: pkg.reason,
410
+ })),
411
+ icon: manifest.icon,
412
+ });
413
+ console.log(`āœ… Collection published successfully!`);
414
+ console.log(` Scope: ${result.scope}`);
415
+ console.log(` Name: ${result.name_slug}`);
416
+ console.log(` Version: ${result.version || '1.0.0'}`);
417
+ console.log('');
418
+ console.log(`šŸ’” View: prpm collection info @${result.scope}/${result.name_slug}`);
419
+ console.log(`šŸ’” Install: prpm install @${result.scope}/${result.name_slug}`);
420
+ console.log('');
421
+ await telemetry_1.telemetry.track({
422
+ command: 'collections:publish',
423
+ success: true,
424
+ duration: Date.now() - startTime,
425
+ data: {
426
+ id: manifest.id,
427
+ packageCount: manifest.packages.length,
428
+ },
429
+ });
430
+ }
431
+ catch (error) {
432
+ const errorMessage = error instanceof Error ? error.message : String(error);
433
+ console.error(`\nāŒ Failed to publish collection: ${errorMessage}\n`);
434
+ await telemetry_1.telemetry.track({
435
+ command: 'collections:publish',
436
+ success: false,
437
+ error: errorMessage,
438
+ duration: Date.now() - startTime,
439
+ });
440
+ process.exit(1);
441
+ }
442
+ finally {
443
+ await telemetry_1.telemetry.shutdown();
444
+ }
270
445
  }
271
446
  /**
272
447
  * Install a collection
@@ -276,19 +451,31 @@ async function handleCollectionInstall(collectionSpec, options) {
276
451
  let packagesInstalled = 0;
277
452
  let packagesFailed = 0;
278
453
  try {
279
- // Parse collection spec
280
- const match = collectionSpec.match(/^@?([^/]+)\/([^/@]+)(?:@(.+))?$/);
281
- if (!match) {
282
- throw new Error('Invalid collection format. Use: @scope/id or scope/id[@version]');
454
+ // Parse collection spec: @scope/name_slug, scope/name_slug, or just name_slug (defaults to 'collection' scope)
455
+ let scope;
456
+ let name_slug;
457
+ let version;
458
+ const matchWithScope = collectionSpec.match(/^@?([^/]+)\/([^/@]+)(?:@(.+))?$/);
459
+ if (matchWithScope) {
460
+ // Has explicit scope: @scope/name or scope/name
461
+ [, scope, name_slug, version] = matchWithScope;
462
+ }
463
+ else {
464
+ // No scope, assume 'collection' scope: just name or name@version
465
+ const matchNoScope = collectionSpec.match(/^([^/@]+)(?:@(.+))?$/);
466
+ if (!matchNoScope) {
467
+ throw new Error('Invalid collection format. Use: name, @scope/name, or scope/name (optionally with @version)');
468
+ }
469
+ [, name_slug, version] = matchNoScope;
470
+ scope = 'collection'; // Default scope
283
471
  }
284
- const [, scope, id, version] = match;
285
472
  const config = await (0, user_config_1.getConfig)();
286
473
  const client = (0, registry_client_1.getRegistryClient)(config);
287
474
  // Get collection installation plan
288
- console.log(`šŸ“¦ Installing collection: @${scope}/${id}...\n`);
475
+ console.log(`šŸ“¦ Installing collection: ${scope === 'collection' ? name_slug : `@${scope}/${name_slug}`}...\n`);
289
476
  const installResult = await client.installCollection({
290
477
  scope,
291
- id,
478
+ id: name_slug,
292
479
  version,
293
480
  format: options.format,
294
481
  skipOptional: options.skipOptional,
@@ -339,7 +526,7 @@ async function handleCollectionInstall(collectionSpec, options) {
339
526
  duration: Date.now() - startTime,
340
527
  data: {
341
528
  scope,
342
- id,
529
+ name_slug,
343
530
  packageCount: packages.length,
344
531
  installed: packagesInstalled,
345
532
  failed: packagesFailed,
@@ -362,6 +549,9 @@ async function handleCollectionInstall(collectionSpec, options) {
362
549
  });
363
550
  process.exit(1);
364
551
  }
552
+ finally {
553
+ await telemetry_1.telemetry.shutdown();
554
+ }
365
555
  }
366
556
  /**
367
557
  * Create collections command group
@@ -387,7 +577,7 @@ function createCollectionsCommand() {
387
577
  category: options.category,
388
578
  tag: options.tag,
389
579
  official: options.official,
390
- limit: parseInt(options.limit, 10),
580
+ limit: options.limit ? parseInt(options.limit, 10) : 50,
391
581
  });
392
582
  });
393
583
  // List subcommand
@@ -404,6 +594,13 @@ function createCollectionsCommand() {
404
594
  .command('info <collection>')
405
595
  .description('Show collection details')
406
596
  .action(handleCollectionInfo);
597
+ // Publish subcommand
598
+ command
599
+ .command('publish [manifest]')
600
+ .description('Publish a collection from collection.json')
601
+ .action(async (manifest) => {
602
+ await handleCollectionPublish(manifest);
603
+ });
407
604
  // Install handled by main install command with @scope/id syntax
408
605
  return command;
409
606
  }