prpm 0.1.15 → 0.1.16

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.
@@ -507,6 +507,7 @@ async function handleCollectionInstall(collectionSpec, options) {
507
507
  }
508
508
  // Install packages sequentially
509
509
  const installedPackageIds = [];
510
+ let hasClaudeHooks = false;
510
511
  for (let i = 0; i < packages.length; i++) {
511
512
  const pkg = packages[i];
512
513
  const progress = `${i + 1}/${packages.length}`;
@@ -528,6 +529,10 @@ async function handleCollectionInstall(collectionSpec, options) {
528
529
  if (options.format) {
529
530
  installOptions.as = options.format;
530
531
  }
532
+ // Track if this collection contains Claude hooks
533
+ if (pkg.format === 'claude' && pkg.subtype === 'hook') {
534
+ hasClaudeHooks = true;
535
+ }
531
536
  await (0, install_1.handleInstall)(`${pkg.packageId}@${pkg.version}`, installOptions);
532
537
  console.log(` ${progress} āœ“ ${pkg.packageId}`);
533
538
  installedPackageIds.push(pkg.packageId);
@@ -558,6 +563,11 @@ async function handleCollectionInstall(collectionSpec, options) {
558
563
  console.log(` ${packagesFailed} optional packages failed`);
559
564
  }
560
565
  console.log(` šŸ”’ Collection tracked in lock file`);
566
+ // Show Claude hooks warning if any were installed
567
+ if (hasClaudeHooks) {
568
+ console.log(`\nāš ļø This collection includes Claude hooks that execute automatically.`);
569
+ console.log(` šŸ“– Review hook configurations in .claude/settings.json`);
570
+ }
561
571
  console.log('');
562
572
  await telemetry_1.telemetry.track({
563
573
  command: 'collections:install',
@@ -224,6 +224,20 @@ async function handleInstall(packageSpec, options) {
224
224
  console.log(` ${pkg.name} ${pkg.official ? 'šŸ…' : ''}`);
225
225
  console.log(` ${pkg.description || 'No description'}`);
226
226
  console.log(` ${typeIcon} Type: ${typeLabel}`);
227
+ // Check if this is a Claude hook and show informational message
228
+ if (pkg.format === 'claude' && pkg.subtype === 'hook') {
229
+ // Only show detailed warning if not part of a collection (to avoid spam)
230
+ if (!options.fromCollection) {
231
+ console.log(`\nšŸ“Œ Installing Claude Hook`);
232
+ console.log(` āš ļø Note: Hooks execute shell commands automatically.`);
233
+ console.log(` šŸ“– Review the hook configuration in .claude/settings.json after installation.`);
234
+ console.log();
235
+ }
236
+ else {
237
+ // Brief message for collection installs
238
+ console.log(` šŸŖ Hook (merges into .claude/settings.json)`);
239
+ }
240
+ }
227
241
  // Determine format preference with priority order:
228
242
  // 1. CLI --as flag (highest priority)
229
243
  // 2. defaultFormat from .prpmrc config
@@ -252,7 +266,7 @@ async function handleInstall(packageSpec, options) {
252
266
  // Special handling for Claude packages: default to CLAUDE.md if it doesn't exist
253
267
  // BUT only for packages that are generic rules (not skills, agents, or commands)
254
268
  if (!options.as && pkg.format === 'claude' && pkg.subtype === 'rule') {
255
- const { fileExists } = await Promise.resolve().then(() => __importStar(require('../core/filesystem.js')));
269
+ const { fileExists } = await Promise.resolve().then(() => __importStar(require('../core/filesystem')));
256
270
  const claudeMdExists = await fileExists('CLAUDE.md');
257
271
  if (!claudeMdExists) {
258
272
  // CLAUDE.md doesn't exist, install as CLAUDE.md (recommended format for Claude Code)
@@ -270,16 +284,19 @@ async function handleInstall(packageSpec, options) {
270
284
  }
271
285
  // Determine version to install
272
286
  let tarballUrl;
287
+ let actualVersion;
273
288
  if (version === 'latest') {
274
289
  if (!pkg.latest_version) {
275
290
  throw new Error('No versions available for this package');
276
291
  }
277
292
  tarballUrl = pkg.latest_version.tarball_url;
293
+ actualVersion = pkg.latest_version.version;
278
294
  console.log(` šŸ“¦ Installing version ${pkg.latest_version.version}`);
279
295
  }
280
296
  else {
281
297
  const versionInfo = await client.getPackageVersion(packageId, version);
282
298
  tarballUrl = versionInfo.tarball_url;
299
+ actualVersion = version;
283
300
  console.log(` šŸ“¦ Installing version ${version}`);
284
301
  }
285
302
  // Download package in requested format
@@ -295,6 +312,7 @@ async function handleInstall(packageSpec, options) {
295
312
  // Track where files were saved for user feedback
296
313
  let destPath;
297
314
  let fileCount = 0;
315
+ let hookMetadata = undefined;
298
316
  // Special handling for CLAUDE.md format (goes in project root)
299
317
  if (format === 'claude-md') {
300
318
  if (extractedFiles.length !== 1) {
@@ -321,6 +339,10 @@ async function handleInstall(packageSpec, options) {
321
339
  if (effectiveFormat === 'claude' && effectiveSubtype === 'skill') {
322
340
  destPath = `${destDir}/SKILL.md`;
323
341
  }
342
+ else if (effectiveFormat === 'claude' && effectiveSubtype === 'hook') {
343
+ // Claude hooks are merged into settings.json
344
+ destPath = `${destDir}/settings.json`;
345
+ }
324
346
  else if (effectiveFormat === 'agents.md') {
325
347
  destPath = `${destDir}/${packageName}/AGENTS.md`;
326
348
  }
@@ -361,6 +383,62 @@ async function handleInstall(packageSpec, options) {
361
383
  mainFile = (0, claude_config_1.applyClaudeConfig)(mainFile, config.claude);
362
384
  }
363
385
  }
386
+ // Special handling for Claude hooks - merge into settings.json
387
+ if (effectiveFormat === 'claude' && effectiveSubtype === 'hook') {
388
+ const { readFile } = await Promise.resolve().then(() => __importStar(require('fs/promises')));
389
+ const { fileExists } = await Promise.resolve().then(() => __importStar(require('../core/filesystem')));
390
+ // Parse the hook configuration from the downloaded file
391
+ let hookConfig;
392
+ try {
393
+ hookConfig = JSON.parse(mainFile);
394
+ }
395
+ catch (err) {
396
+ throw new Error(`Invalid hook configuration: ${err}. Hook file must be valid JSON.`);
397
+ }
398
+ // Generate unique hook ID for this installation
399
+ const hookId = `${packageId}@${actualVersion || version}`;
400
+ // Read existing settings.json if it exists
401
+ let existingSettings = { hooks: {} };
402
+ if (await fileExists(destPath)) {
403
+ try {
404
+ const existingContent = await readFile(destPath, 'utf-8');
405
+ existingSettings = JSON.parse(existingContent);
406
+ if (!existingSettings.hooks) {
407
+ existingSettings.hooks = {};
408
+ }
409
+ }
410
+ catch (err) {
411
+ console.log(` āš ļø Warning: Could not parse existing settings.json, creating new one.`);
412
+ existingSettings = { hooks: {} };
413
+ }
414
+ }
415
+ // Track which events this hook adds to
416
+ const events = [];
417
+ // Merge the new hook configuration
418
+ // Assume the downloaded file contains a hooks object
419
+ if (hookConfig.hooks) {
420
+ for (const [event, eventHooks] of Object.entries(hookConfig.hooks)) {
421
+ if (!existingSettings.hooks[event]) {
422
+ existingSettings.hooks[event] = [];
423
+ }
424
+ // Add hook ID to each hook config for tracking
425
+ const hooksWithId = eventHooks.map(hook => ({
426
+ ...hook,
427
+ __prpm_hook_id: hookId, // Internal tracking ID
428
+ }));
429
+ // Add new hooks to the event
430
+ existingSettings.hooks[event] = [
431
+ ...existingSettings.hooks[event],
432
+ ...hooksWithId
433
+ ];
434
+ events.push(event);
435
+ }
436
+ console.log(` āœ“ Merged hook configuration into settings.json`);
437
+ // Store metadata for lockfile
438
+ hookMetadata = { events, hookId };
439
+ }
440
+ mainFile = JSON.stringify(existingSettings, null, 2);
441
+ }
364
442
  await (0, filesystem_1.saveFile)(destPath, mainFile);
365
443
  fileCount = 1;
366
444
  }
@@ -477,7 +555,6 @@ async function handleInstall(packageSpec, options) {
477
555
  }
478
556
  // Update or create lock file
479
557
  const updatedLockfile = lockfile || (0, lockfile_1.createLockfile)();
480
- const actualVersion = version === 'latest' ? pkg.latest_version?.version : version;
481
558
  (0, lockfile_1.addToLockfile)(updatedLockfile, packageId, {
482
559
  version: actualVersion || version,
483
560
  tarballUrl,
@@ -485,6 +562,7 @@ async function handleInstall(packageSpec, options) {
485
562
  subtype: pkg.subtype, // Preserve original package subtype
486
563
  installedPath: destPath,
487
564
  fromCollection: options.fromCollection,
565
+ hookMetadata, // Track hook installation metadata for uninstall
488
566
  });
489
567
  (0, lockfile_1.setPackageIntegrity)(updatedLockfile, packageId, tarball);
490
568
  await (0, lockfile_1.writeLockfile)(updatedLockfile);
@@ -52,6 +52,7 @@ const marketplace_converter_1 = require("../core/marketplace-converter");
52
52
  const schema_validator_1 = require("../core/schema-validator");
53
53
  const license_extractor_1 = require("../utils/license-extractor");
54
54
  const snippet_extractor_1 = require("../utils/snippet-extractor");
55
+ const script_executor_1 = require("../utils/script-executor");
55
56
  /**
56
57
  * Try to find and load manifest files
57
58
  * Checks for:
@@ -357,6 +358,26 @@ async function handlePublish(options) {
357
358
  // Read and validate manifests
358
359
  console.log('šŸ” Validating package manifest(s)...');
359
360
  const { manifests, collections, source } = await findAndLoadManifests();
361
+ // Execute prepublishOnly script if defined (for multi-package manifests)
362
+ // This runs before any packages are published
363
+ if (source === 'prpm.json (multi-package)' || source === 'prpm.json') {
364
+ try {
365
+ // Re-read the raw prpm.json to check for scripts
366
+ const prpmJsonPath = (0, path_1.join)(process.cwd(), 'prpm.json');
367
+ const prpmContent = await (0, promises_1.readFile)(prpmJsonPath, 'utf-8');
368
+ const prpmManifest = JSON.parse(prpmContent);
369
+ if (prpmManifest.scripts) {
370
+ await (0, script_executor_1.executePrepublishOnly)(prpmManifest.scripts);
371
+ }
372
+ }
373
+ catch (error) {
374
+ // If script execution fails, abort publish
375
+ if (error instanceof Error && error.message.includes('script')) {
376
+ throw error;
377
+ }
378
+ // Ignore other errors (e.g., file not found - shouldn't happen at this point)
379
+ }
380
+ }
360
381
  if (manifests.length > 1 || collections.length > 0) {
361
382
  if (manifests.length > 0) {
362
383
  console.log(` Found ${manifests.length} package(s) in ${source}`);
@@ -422,7 +422,7 @@ function createSearchCommand() {
422
422
  const limit = options.limit ? parseInt(options.limit, 10) : 20;
423
423
  const page = options.page ? parseInt(options.page, 10) : 1;
424
424
  const validFormats = ['cursor', 'claude', 'continue', 'windsurf', 'copilot', 'kiro', 'agents.md', 'generic', 'mcp'];
425
- const validSubtypes = ['rule', 'agent', 'skill', 'slash-command', 'prompt', 'collection', 'chatmode'];
425
+ const validSubtypes = ['rule', 'agent', 'skill', 'slash-command', 'prompt', 'collection', 'chatmode', 'tool', 'hook'];
426
426
  if (options.format && !validFormats.includes(format)) {
427
427
  console.error(`āŒ Format must be one of: ${validFormats.join(', ')}`);
428
428
  throw new errors_1.CLIError(`āŒ Format must be one of: ${validFormats.join(', ')}`, 1);
@@ -21,7 +21,46 @@ async function handleUninstall(name) {
21
21
  if (!pkg) {
22
22
  throw new errors_1.CLIError(`āŒ Package "${name}" not found`, 1);
23
23
  }
24
- // Get the installation path from lock file if available, otherwise calculate it
24
+ // Special handling for Claude hooks
25
+ if (pkg.format === 'claude' && pkg.subtype === 'hook' && pkg.hookMetadata) {
26
+ const settingsPath = pkg.installedPath || '.claude/settings.json';
27
+ try {
28
+ // Read settings.json
29
+ const settingsContent = await fs_1.promises.readFile(settingsPath, 'utf-8');
30
+ const settings = JSON.parse(settingsContent);
31
+ if (settings.hooks) {
32
+ let removedCount = 0;
33
+ // Remove hooks with matching hook ID from each event
34
+ for (const event of pkg.hookMetadata.events) {
35
+ if (settings.hooks[event]) {
36
+ const originalLength = settings.hooks[event].length;
37
+ settings.hooks[event] = settings.hooks[event].filter((hook) => hook.__prpm_hook_id !== pkg.hookMetadata.hookId);
38
+ const newLength = settings.hooks[event].length;
39
+ removedCount += originalLength - newLength;
40
+ // Clean up empty event arrays
41
+ if (settings.hooks[event].length === 0) {
42
+ delete settings.hooks[event];
43
+ }
44
+ }
45
+ }
46
+ // Write updated settings back
47
+ await fs_1.promises.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
48
+ console.log(` šŸŖ Removed ${removedCount} hook(s) from ${settingsPath}`);
49
+ }
50
+ }
51
+ catch (error) {
52
+ const err = error;
53
+ if (err.code === 'ENOENT') {
54
+ console.warn(` āš ļø Settings file not found: ${settingsPath}`);
55
+ }
56
+ else {
57
+ throw new Error(`Failed to remove hooks from settings: ${error}`);
58
+ }
59
+ }
60
+ console.log(`āœ… Successfully uninstalled ${name}`);
61
+ return;
62
+ }
63
+ // Standard file/directory uninstall for non-hook packages
25
64
  const packageName = (0, filesystem_1.stripAuthorNamespace)(name);
26
65
  let targetPath;
27
66
  if (pkg.installedPath) {
@@ -45,6 +45,9 @@ function getDestinationDir(format, subtype, name) {
45
45
  return '.claude/commands';
46
46
  if (subtype === 'agent')
47
47
  return '.claude/agents';
48
+ // Hooks are configured in settings.json, return .claude directory
49
+ if (subtype === 'hook')
50
+ return '.claude';
48
51
  return '.claude/agents'; // Default for claude
49
52
  case 'continue':
50
53
  // Continue has separate directories for prompts (slash commands) and rules
@@ -80,6 +80,7 @@ function addToLockfile(lockfile, packageId, packageInfo) {
80
80
  subtype: packageInfo.subtype,
81
81
  installedPath: packageInfo.installedPath,
82
82
  fromCollection: packageInfo.fromCollection,
83
+ hookMetadata: packageInfo.hookMetadata,
83
84
  };
84
85
  lockfile.generated = new Date().toISOString();
85
86
  }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Utility for executing package lifecycle scripts
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.executeScript = executeScript;
7
+ exports.executePrepublishOnly = executePrepublishOnly;
8
+ const child_process_1 = require("child_process");
9
+ const util_1 = require("util");
10
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
11
+ /**
12
+ * Execute a package lifecycle script
13
+ * @param script - The script command to execute
14
+ * @param scriptName - Name of the script (for logging)
15
+ * @param options - Execution options
16
+ * @throws Error if script fails
17
+ */
18
+ async function executeScript(script, scriptName, options = {}) {
19
+ const { cwd = process.cwd(), timeout = 5 * 60 * 1000, // 5 minutes default
20
+ maxBuffer = 10 * 1024 * 1024, // 10MB default
21
+ } = options;
22
+ console.log(`šŸ”§ Running ${scriptName} script...`);
23
+ console.log(` $ ${script}\n`);
24
+ try {
25
+ const { stdout, stderr } = await execAsync(script, {
26
+ cwd,
27
+ timeout,
28
+ maxBuffer,
29
+ // Inherit environment variables
30
+ env: process.env,
31
+ });
32
+ // Show output in real-time
33
+ if (stdout) {
34
+ process.stdout.write(stdout);
35
+ }
36
+ if (stderr) {
37
+ process.stderr.write(stderr);
38
+ }
39
+ console.log(`\nāœ“ ${scriptName} script completed successfully\n`);
40
+ return {
41
+ stdout,
42
+ stderr,
43
+ exitCode: 0,
44
+ };
45
+ }
46
+ catch (error) {
47
+ // Show error output
48
+ if (error.stdout) {
49
+ process.stdout.write(error.stdout);
50
+ }
51
+ if (error.stderr) {
52
+ process.stderr.write(error.stderr);
53
+ }
54
+ const errorMessage = error.code === 'ETIMEDOUT'
55
+ ? `${scriptName} script timed out after ${timeout}ms`
56
+ : error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
57
+ ? `${scriptName} script output exceeded maximum buffer size`
58
+ : `${scriptName} script failed with exit code ${error.code || 1}`;
59
+ throw new Error(errorMessage);
60
+ }
61
+ }
62
+ /**
63
+ * Execute prepublishOnly script if defined
64
+ * @param scripts - Package scripts object
65
+ * @param options - Execution options
66
+ */
67
+ async function executePrepublishOnly(scripts, options = {}) {
68
+ if (!scripts?.prepublishOnly) {
69
+ return; // No script defined, nothing to do
70
+ }
71
+ await executeScript(scripts.prepublishOnly, 'prepublishOnly', options);
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
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.3.13",
49
- "@pr-pm/types": "^0.2.14",
48
+ "@pr-pm/registry-client": "^1.3.14",
49
+ "@pr-pm/types": "^0.2.15",
50
50
  "ajv": "^8.17.1",
51
51
  "ajv-formats": "^3.0.1",
52
52
  "commander": "^11.1.0",
@@ -153,6 +153,29 @@
153
153
  "description": "Whether the package is private. Private packages are only accessible to the owner/organization members. Defaults to false (public).",
154
154
  "default": false
155
155
  },
156
+ "scripts": {
157
+ "type": "object",
158
+ "description": "Lifecycle scripts that run during package operations. Only applies to multi-package manifests (prpm.json with packages array).",
159
+ "properties": {
160
+ "prepublishOnly": {
161
+ "type": "string",
162
+ "description": "Script to run before publishing (recommended - only runs on 'prpm publish')",
163
+ "examples": [
164
+ "npm run build",
165
+ "cd packages/hooks && npm run build",
166
+ "npm test && npm run build"
167
+ ]
168
+ },
169
+ "prepublish": {
170
+ "type": "string",
171
+ "description": "Script to run before publishing and on npm install (not recommended - use prepublishOnly instead)",
172
+ "examples": [
173
+ "npm run build"
174
+ ]
175
+ }
176
+ },
177
+ "additionalProperties": false
178
+ },
156
179
  "tags": {
157
180
  "type": "array",
158
181
  "description": "Package tags for categorization",