@prompd/cli 0.4.11 → 0.5.0-beta.10

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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/compile.d.ts.map +1 -1
  3. package/dist/commands/compile.js +8 -1
  4. package/dist/commands/compile.js.map +1 -1
  5. package/dist/commands/package.d.ts.map +1 -1
  6. package/dist/commands/package.js +319 -34
  7. package/dist/commands/package.js.map +1 -1
  8. package/dist/commands/registry.d.ts.map +1 -1
  9. package/dist/commands/registry.js +58 -4
  10. package/dist/commands/registry.js.map +1 -1
  11. package/dist/commands/run.d.ts.map +1 -1
  12. package/dist/commands/run.js +9 -5
  13. package/dist/commands/run.js.map +1 -1
  14. package/dist/commands/uninstall.d.ts.map +1 -1
  15. package/dist/commands/uninstall.js +52 -18
  16. package/dist/commands/uninstall.js.map +1 -1
  17. package/dist/commands/workflow.d.ts.map +1 -1
  18. package/dist/commands/workflow.js +10 -4
  19. package/dist/commands/workflow.js.map +1 -1
  20. package/dist/lib/auth.d.ts +0 -9
  21. package/dist/lib/auth.d.ts.map +1 -1
  22. package/dist/lib/auth.js +0 -14
  23. package/dist/lib/auth.js.map +1 -1
  24. package/dist/lib/commandExecutor.d.ts +2 -2
  25. package/dist/lib/commandExecutor.d.ts.map +1 -1
  26. package/dist/lib/commandExecutor.js +2 -2
  27. package/dist/lib/commandExecutor.js.map +1 -1
  28. package/dist/lib/compiler/file-system.d.ts.map +1 -1
  29. package/dist/lib/compiler/file-system.js +10 -0
  30. package/dist/lib/compiler/file-system.js.map +1 -1
  31. package/dist/lib/compiler/index.d.ts +1 -0
  32. package/dist/lib/compiler/index.d.ts.map +1 -1
  33. package/dist/lib/compiler/index.js +10 -1
  34. package/dist/lib/compiler/index.js.map +1 -1
  35. package/dist/lib/compiler/package-resolver.d.ts +38 -3
  36. package/dist/lib/compiler/package-resolver.d.ts.map +1 -1
  37. package/dist/lib/compiler/package-resolver.js +181 -40
  38. package/dist/lib/compiler/package-resolver.js.map +1 -1
  39. package/dist/lib/compiler/pipeline.d.ts.map +1 -1
  40. package/dist/lib/compiler/pipeline.js +9 -3
  41. package/dist/lib/compiler/pipeline.js.map +1 -1
  42. package/dist/lib/compiler/stages/semantic.d.ts +7 -0
  43. package/dist/lib/compiler/stages/semantic.d.ts.map +1 -1
  44. package/dist/lib/compiler/stages/semantic.js +78 -2
  45. package/dist/lib/compiler/stages/semantic.js.map +1 -1
  46. package/dist/lib/compiler/stages/template.d.ts +12 -0
  47. package/dist/lib/compiler/stages/template.d.ts.map +1 -1
  48. package/dist/lib/compiler/stages/template.js +117 -12
  49. package/dist/lib/compiler/stages/template.js.map +1 -1
  50. package/dist/lib/config.d.ts.map +1 -1
  51. package/dist/lib/config.js +20 -8
  52. package/dist/lib/config.js.map +1 -1
  53. package/dist/lib/executor.d.ts.map +1 -1
  54. package/dist/lib/executor.js +25 -14
  55. package/dist/lib/executor.js.map +1 -1
  56. package/dist/lib/index.d.ts +2 -0
  57. package/dist/lib/index.d.ts.map +1 -1
  58. package/dist/lib/index.js +5 -1
  59. package/dist/lib/index.js.map +1 -1
  60. package/dist/lib/nodeTypeRegistry.d.ts +1 -1
  61. package/dist/lib/nodeTypeRegistry.d.ts.map +1 -1
  62. package/dist/lib/providers/base.d.ts +6 -1
  63. package/dist/lib/providers/base.d.ts.map +1 -1
  64. package/dist/lib/providers/base.js +51 -8
  65. package/dist/lib/providers/base.js.map +1 -1
  66. package/dist/lib/providers/index.d.ts +1 -1
  67. package/dist/lib/providers/index.d.ts.map +1 -1
  68. package/dist/lib/providers/index.js.map +1 -1
  69. package/dist/lib/providers/types.d.ts +29 -0
  70. package/dist/lib/providers/types.d.ts.map +1 -1
  71. package/dist/lib/providers/types.js +26 -16
  72. package/dist/lib/providers/types.js.map +1 -1
  73. package/dist/lib/registry.d.ts +42 -3
  74. package/dist/lib/registry.d.ts.map +1 -1
  75. package/dist/lib/registry.js +451 -86
  76. package/dist/lib/registry.js.map +1 -1
  77. package/dist/lib/testHarness.d.ts +101 -0
  78. package/dist/lib/testHarness.d.ts.map +1 -0
  79. package/dist/lib/testHarness.js +45 -0
  80. package/dist/lib/testHarness.js.map +1 -0
  81. package/dist/lib/validation.d.ts.map +1 -1
  82. package/dist/lib/validation.js +10 -2
  83. package/dist/lib/validation.js.map +1 -1
  84. package/dist/lib/workflowExecutor.d.ts +7 -1
  85. package/dist/lib/workflowExecutor.d.ts.map +1 -1
  86. package/dist/lib/workflowExecutor.js +756 -346
  87. package/dist/lib/workflowExecutor.js.map +1 -1
  88. package/dist/lib/workflowTypes.d.ts +5 -1
  89. package/dist/lib/workflowTypes.d.ts.map +1 -1
  90. package/dist/lib/workflowTypes.js.map +1 -1
  91. package/dist/types/index.d.ts +54 -1
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +54 -1
  94. package/dist/types/index.js.map +1 -1
  95. package/package.json +125 -124
@@ -45,7 +45,10 @@ const tar = __importStar(require("tar"));
45
45
  const events_1 = require("events");
46
46
  const security_1 = require("./security");
47
47
  const config_1 = require("./config");
48
+ const types_1 = require("../types");
48
49
  const file_system_1 = require("./compiler/file-system");
50
+ const package_resolver_1 = require("./compiler/package-resolver");
51
+ const validation_1 = require("./validation");
49
52
  /**
50
53
  * Package Registry Client
51
54
  */
@@ -75,10 +78,25 @@ class RegistryClient extends events_1.EventEmitter {
75
78
  this.ensureCacheDir();
76
79
  }
77
80
  get registryUrl() {
78
- return this.registryConfig.url;
81
+ return this.registryConfig.url.replace(/\/+$/, '');
82
+ }
83
+ /**
84
+ * Encode a package name for safe URL usage.
85
+ * Preserves the @ prefix and / separator in scoped packages (e.g. @scope/name),
86
+ * but encodes all other special characters in each segment.
87
+ */
88
+ encodePackageName(packageName) {
89
+ // Handle scoped packages: @scope/name
90
+ if (packageName.startsWith('@') && packageName.includes('/')) {
91
+ const slashIndex = packageName.indexOf('/');
92
+ const scope = packageName.substring(1, slashIndex);
93
+ const name = packageName.substring(slashIndex + 1);
94
+ return `@${encodeURIComponent(scope)}/${encodeURIComponent(name)}`;
95
+ }
96
+ return encodeURIComponent(packageName);
79
97
  }
80
98
  get authToken() {
81
- return this.registryConfig.token;
99
+ return this.registryConfig.api_key || this.registryConfig.token;
82
100
  }
83
101
  get cacheDir() {
84
102
  return path.join(require('os').homedir(), '.prompd', 'cache');
@@ -101,7 +119,17 @@ class RegistryClient extends events_1.EventEmitter {
101
119
  const path = require('path');
102
120
  const configPath = path.join(os.homedir(), '.prompd', 'config.yaml');
103
121
  // Use env var for registry URL if set (useful for local development)
104
- const registryUrl = process.env.PROMPD_REGISTRY_URL || 'https://registry.prompdhub.ai';
122
+ const defaultUrl = 'https://registry.prompdhub.ai';
123
+ let registryUrl = defaultUrl;
124
+ const envRegistryUrl = process.env.PROMPD_REGISTRY_URL;
125
+ if (envRegistryUrl) {
126
+ if ((0, validation_1.validateRegistryUrl)(envRegistryUrl)) {
127
+ registryUrl = envRegistryUrl;
128
+ }
129
+ else {
130
+ console.warn(`Warning: PROMPD_REGISTRY_URL value is invalid, falling back to default: ${defaultUrl}`);
131
+ }
132
+ }
105
133
  const defaultConfig = {
106
134
  apiKeys: {},
107
135
  customProviders: {},
@@ -121,7 +149,11 @@ class RegistryClient extends events_1.EventEmitter {
121
149
  try {
122
150
  if (fs.existsSync(configPath)) {
123
151
  const configContent = fs.readFileSync(configPath, 'utf-8');
124
- const fileConfig = yaml.parse(configContent);
152
+ const fileConfig = yaml.parse(configContent, { strict: true, maxAliasCount: 64 });
153
+ // Validate parsed config is a plain object
154
+ if (!fileConfig || typeof fileConfig !== 'object' || Array.isArray(fileConfig)) {
155
+ return defaultConfig;
156
+ }
125
157
  // Merge with default config
126
158
  const mergedConfig = { ...defaultConfig, ...fileConfig };
127
159
  // Ensure registry structure exists
@@ -234,8 +266,6 @@ class RegistryClient extends events_1.EventEmitter {
234
266
  */
235
267
  async install(packageName, options = {}) {
236
268
  this.emit('installStart', { packageName, options });
237
- console.log('[RegistryClient.install] Starting install:', packageName);
238
- console.log('[RegistryClient.install] Options:', JSON.stringify(options));
239
269
  try {
240
270
  // Parse package reference if it includes @version
241
271
  // Format: @namespace/package@version
@@ -248,16 +278,18 @@ class RegistryClient extends events_1.EventEmitter {
248
278
  name = packageName.substring(0, lastAtIndex);
249
279
  versionSpec = packageName.substring(lastAtIndex + 1);
250
280
  }
251
- console.log('[RegistryClient.install] Parsed name:', name, 'version:', versionSpec);
252
281
  // Resolve version
253
282
  const resolvedVersion = await this.resolveVersion(name, versionSpec);
254
- console.log('[RegistryClient.install] Resolved version:', resolvedVersion);
255
283
  // Check cache first
256
284
  const cacheKey = `${name}@${resolvedVersion}`;
257
285
  if (!options.skipCache && !options.force) {
258
286
  const cachedPath = await this.getCachedPackage(cacheKey);
259
287
  if (cachedPath) {
260
288
  await this.installFromCache(cachedPath, name, resolvedVersion, options);
289
+ if (!options.global) {
290
+ await this.addWorkspaceDependency(name, resolvedVersion, options.workspaceRoot);
291
+ }
292
+ this.emit('installComplete', { name, version: resolvedVersion });
261
293
  return;
262
294
  }
263
295
  }
@@ -280,16 +312,17 @@ class RegistryClient extends events_1.EventEmitter {
280
312
  }
281
313
  }
282
314
  // Extract and install package
283
- console.log('[RegistryClient.install] Extracting package to workspace...');
284
315
  await this.extractAndInstallPackage(packageData, name, resolvedVersion, options);
285
- console.log('[RegistryClient.install] Extraction complete');
286
316
  // Cache package
287
317
  await this.cachePackage(cacheKey, packageData);
318
+ // Update workspace prompd.json dependencies (skip for global installs)
319
+ if (!options.global) {
320
+ await this.addWorkspaceDependency(name, resolvedVersion, options.workspaceRoot);
321
+ }
288
322
  this.emit('installComplete', {
289
323
  name: name,
290
324
  version: resolvedVersion
291
325
  });
292
- console.log('[RegistryClient.install] Install complete for', name, '@', resolvedVersion);
293
326
  }
294
327
  catch (error) {
295
328
  this.emit('installError', { packageName, error });
@@ -303,11 +336,13 @@ class RegistryClient extends events_1.EventEmitter {
303
336
  try {
304
337
  const searchParams = new URLSearchParams();
305
338
  if (query.query)
306
- searchParams.set('q', query.query);
339
+ searchParams.set('search', query.query);
307
340
  if (query.category)
308
341
  searchParams.set('category', query.category);
309
- if (query.type)
310
- searchParams.set('type', query.type);
342
+ if (query.type) {
343
+ const typeValue = Array.isArray(query.type) ? query.type.join(',') : query.type;
344
+ searchParams.set('type', typeValue);
345
+ }
311
346
  if (query.tags)
312
347
  searchParams.set('tags', query.tags.join(','));
313
348
  if (query.author)
@@ -318,8 +353,7 @@ class RegistryClient extends events_1.EventEmitter {
318
353
  searchParams.set('offset', query.offset.toString());
319
354
  if (query.sort)
320
355
  searchParams.set('sort', query.sort);
321
- // Registry uses /packages?search= endpoint, not /search?q=
322
- const response = await fetch(`${this.registryUrl}/packages?search=${encodeURIComponent(query.query || '')}`, {
356
+ const response = await fetch(`${this.registryUrl}/packages?${searchParams.toString()}`, {
323
357
  headers: this.getAuthHeaders()
324
358
  });
325
359
  if (!response.ok) {
@@ -339,9 +373,10 @@ class RegistryClient extends events_1.EventEmitter {
339
373
  */
340
374
  async getPackageInfo(packageName, version) {
341
375
  try {
376
+ const encodedName = this.encodePackageName(packageName);
342
377
  const url = version
343
- ? `${this.registryUrl}/packages/${packageName}/${version}`
344
- : `${this.registryUrl}/packages/${packageName}`;
378
+ ? `${this.registryUrl}/packages/${encodedName}/${encodeURIComponent(version)}`
379
+ : `${this.registryUrl}/packages/${encodedName}`;
345
380
  const response = await fetch(url, {
346
381
  headers: this.getAuthHeaders()
347
382
  });
@@ -363,7 +398,7 @@ class RegistryClient extends events_1.EventEmitter {
363
398
  */
364
399
  async getPackageVersions(packageName) {
365
400
  try {
366
- const response = await fetch(`${this.registryUrl}/packages/${packageName}/versions`, {
401
+ const response = await fetch(`${this.registryUrl}/packages/${this.encodePackageName(packageName)}/versions`, {
367
402
  headers: this.getAuthHeaders()
368
403
  });
369
404
  if (!response.ok) {
@@ -423,7 +458,7 @@ class RegistryClient extends events_1.EventEmitter {
423
458
  metadata.keywords = metadata.keywords || [];
424
459
  metadata.dependencies = metadata.dependencies || {};
425
460
  metadata.files = metadata.files || ['**/*'];
426
- metadata.type = metadata.type || 'collection';
461
+ metadata.type = metadata.type || 'package';
427
462
  metadata.category = metadata.category || 'general';
428
463
  metadata.tags = metadata.tags || [];
429
464
  metadata.prmdVersion = '0.2.3';
@@ -444,7 +479,7 @@ class RegistryClient extends events_1.EventEmitter {
444
479
  }
445
480
  // Check for required files
446
481
  const requiredFiles = [];
447
- if (metadata.type === 'prompt' && metadata.main) {
482
+ if (metadata.type === 'package' && metadata.main) {
448
483
  requiredFiles.push(metadata.main);
449
484
  }
450
485
  for (const file of requiredFiles) {
@@ -484,8 +519,8 @@ class RegistryClient extends events_1.EventEmitter {
484
519
  formData.append('metadata', JSON.stringify(metadata));
485
520
  formData.append('access', options.access);
486
521
  formData.append('tag', options.tag);
487
- const response = await fetch(`${this.registryUrl}/publish`, {
488
- method: 'POST',
522
+ const response = await fetch(`${this.registryUrl}/packages/${this.encodePackageName(metadata.name)}`, {
523
+ method: 'PUT',
489
524
  headers: this.getAuthHeaders(),
490
525
  body: formData
491
526
  });
@@ -499,8 +534,9 @@ class RegistryClient extends events_1.EventEmitter {
499
534
  const packageInfo = await this.getPackageInfo(packageName);
500
535
  return packageInfo.version;
501
536
  }
502
- if (semver.valid(versionSpec)) {
503
- return versionSpec;
537
+ const cleaned = semver.valid(versionSpec);
538
+ if (cleaned) {
539
+ return cleaned;
504
540
  }
505
541
  // Resolve version range
506
542
  const versions = await this.getPackageVersions(packageName);
@@ -523,7 +559,7 @@ class RegistryClient extends events_1.EventEmitter {
523
559
  }
524
560
  async downloadPackage(packageName, version) {
525
561
  // Registry endpoint format: /packages/@scope/name/download/version
526
- const response = await fetch(`${this.registryUrl}/packages/${packageName}/download/${version}`, {
562
+ const response = await fetch(`${this.registryUrl}/packages/${this.encodePackageName(packageName)}/download/${encodeURIComponent(version)}`, {
527
563
  headers: this.getAuthHeaders()
528
564
  });
529
565
  if (!response.ok) {
@@ -533,7 +569,13 @@ class RegistryClient extends events_1.EventEmitter {
533
569
  const tarballBuffer = Buffer.from(arrayBuffer);
534
570
  // Extract metadata from the .pdpkg (ZIP) file instead of calling getPackageInfo
535
571
  // Try prompd.json first, fall back to manifest.json for older packages
536
- const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
572
+ let AdmZip;
573
+ try {
574
+ AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
575
+ }
576
+ catch {
577
+ throw new Error('adm-zip package is required for package installation. Run: npm install adm-zip');
578
+ }
537
579
  const zip = new AdmZip(tarballBuffer);
538
580
  // Check for prompd.json first (newer format), then manifest.json (legacy)
539
581
  let manifestEntry = zip.getEntry('prompd.json');
@@ -545,6 +587,11 @@ class RegistryClient extends events_1.EventEmitter {
545
587
  }
546
588
  const manifestContent = manifestEntry.getData().toString('utf8');
547
589
  const metadata = JSON.parse(manifestContent);
590
+ // Prevent prototype pollution from untrusted package manifests
591
+ const metadataObj = metadata;
592
+ delete metadataObj['__proto__'];
593
+ delete metadataObj['constructor'];
594
+ delete metadataObj['prototype'];
548
595
  return {
549
596
  tarball: tarballBuffer,
550
597
  metadata
@@ -557,36 +604,154 @@ class RegistryClient extends events_1.EventEmitter {
557
604
  }
558
605
  }
559
606
  async extractAndInstallPackage(packageData, packageName, version, options) {
560
- // Use workspace root from options if provided, otherwise use cwd
561
- const workspaceRoot = options.workspaceRoot || process.cwd();
562
- console.log('[RegistryClient.extractAndInstallPackage] packageName:', packageName);
563
- console.log('[RegistryClient.extractAndInstallPackage] version:', version);
564
- console.log('[RegistryClient.extractAndInstallPackage] workspaceRoot:', workspaceRoot);
565
- console.log('[RegistryClient.extractAndInstallPackage] global:', options.global);
566
- const installDir = options.global
567
- ? path.join(this.cacheDir, 'global', 'packages', packageName, version)
568
- : path.join(workspaceRoot, '.prompd', 'cache', packageName, version);
569
- console.log('[RegistryClient.extractAndInstallPackage] installDir:', installDir);
570
- await fs.ensureDir(installDir);
571
- console.log('[RegistryClient.extractAndInstallPackage] Directory ensured');
572
- // Extract .pdpkg (ZIP archive) using adm-zip
573
- const AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
607
+ const workspaceRoot = options.workspaceRoot || (0, package_resolver_1.findProjectRoot)();
608
+ // Determine and validate package type: manifest > options hint > default 'package'
609
+ const rawType = packageData.metadata?.type || options.type || 'package';
610
+ if (!(0, types_1.isValidPackageType)(rawType)) {
611
+ throw new Error(`Invalid package type '${rawType}' in ${packageName}@${version}. Valid types: package, workflow, skill, node-template`);
612
+ }
613
+ const packageType = rawType;
614
+ const typeDir = (0, types_1.getInstallDirForType)(packageType);
615
+ // Load adm-zip for archive operations
616
+ let AdmZip;
617
+ try {
618
+ AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
619
+ }
620
+ catch {
621
+ throw new Error('adm-zip package is required for package installation. Run: npm install adm-zip');
622
+ }
574
623
  const zip = new AdmZip(packageData.tarball);
575
- // Log zip contents
624
+ // Validate ZIP entries: file sizes, decompression bomb, path traversal, null bytes, symlinks
576
625
  const zipEntries = zip.getEntries();
577
- console.log('[RegistryClient.extractAndInstallPackage] ZIP contains', zipEntries.length, 'entries:');
578
- zipEntries.forEach(entry => {
579
- console.log(' -', entry.entryName);
580
- });
626
+ let cumulativeDecompressedSize = 0;
627
+ for (const entry of zipEntries) {
628
+ // Individual file size limit
629
+ if (entry.header.size > RegistryClient.MAX_FILE_SIZE_IN_ZIP) {
630
+ throw new Error(`File too large in package: ${entry.entryName} (${entry.header.size} bytes, max: ${RegistryClient.MAX_FILE_SIZE_IN_ZIP})`);
631
+ }
632
+ // Cumulative decompressed size limit (decompression bomb protection)
633
+ cumulativeDecompressedSize += entry.header.size;
634
+ if (cumulativeDecompressedSize > RegistryClient.MAX_TOTAL_EXTRACTED_SIZE) {
635
+ throw new Error(`Package total decompressed size exceeds limit (${RegistryClient.MAX_TOTAL_EXTRACTED_SIZE} bytes). Possible decompression bomb.`);
636
+ }
637
+ // Compression ratio check per file (decompression bomb detection)
638
+ const compressedSize = entry.header.compressedSize || 1;
639
+ if (compressedSize > 0 && entry.header.size / compressedSize > RegistryClient.MAX_COMPRESSION_RATIO) {
640
+ throw new Error(`Suspicious compression ratio for ${entry.entryName}: ${Math.round(entry.header.size / compressedSize)}:1 (max: ${RegistryClient.MAX_COMPRESSION_RATIO}:1)`);
641
+ }
642
+ // Null byte check in entry names
643
+ if (entry.entryName.includes('\0')) {
644
+ throw new Error(`Security violation: null byte in entry name: ${entry.entryName}`);
645
+ }
646
+ // Symlink check - reject symlink entries
647
+ // In ZIP, external attributes can indicate symlinks (Unix mode with S_IFLNK = 0xA000)
648
+ const externalAttrs = entry.header.attr;
649
+ if (externalAttrs) {
650
+ const unixMode = (externalAttrs >>> 16) & 0xFFFF;
651
+ if ((unixMode & 0xF000) === 0xA000) {
652
+ throw new Error(`Security violation: symlink detected in archive: ${entry.entryName}`);
653
+ }
654
+ }
655
+ // ZIP slip protection: reject path traversal
656
+ const normalized = path.normalize(entry.entryName);
657
+ if (normalized.includes('..') || path.isAbsolute(entry.entryName)) {
658
+ throw new Error(`Security violation: path traversal detected in ${entry.entryName}`);
659
+ }
660
+ }
661
+ const os = require('os');
662
+ // Node-templates: install as .pdpkg archive (not extracted) so template
663
+ // handlers can scan uniformly for .pdpkg files in the templates directory.
664
+ if (packageType === 'node-template') {
665
+ const templatesDir = options.global
666
+ ? path.join(os.homedir(), '.prompd', typeDir)
667
+ : path.join(workspaceRoot, '.prompd', typeDir);
668
+ await fs.ensureDir(templatesDir);
669
+ // Slugify package name for filename: @scope/name -> scope-name
670
+ const slugName = packageName
671
+ .toLowerCase()
672
+ .replace(/[@/]+/g, '-')
673
+ .replace(/[^a-z0-9-]+/g, '-')
674
+ .replace(/^-+|-+$/g, '');
675
+ const pdpkgFileName = `${slugName}-${version}.pdpkg`;
676
+ await fs.writeFile(path.join(templatesDir, pdpkgFileName), packageData.tarball);
677
+ return;
678
+ }
679
+ const installDir = options.global
680
+ ? path.join(os.homedir(), '.prompd', typeDir, packageName, version)
681
+ : path.join(workspaceRoot, '.prompd', typeDir, packageName, version);
682
+ await fs.ensureDir(installDir);
581
683
  zip.extractAllTo(installDir, true);
582
- console.log('[RegistryClient.extractAndInstallPackage] ZIP extracted to:', installDir);
583
- // Verify extraction
584
- const extractedFiles = await fs.readdir(installDir);
585
- console.log('[RegistryClient.extractAndInstallPackage] Extracted files:', extractedFiles);
586
684
  // Write package metadata for cache tracking
587
685
  const metadataPath = path.join(installDir, '.prmdmeta');
588
686
  await fs.writeJson(metadataPath, packageData.metadata, { spaces: 2 });
589
- console.log('[RegistryClient.extractAndInstallPackage] Wrote .prmdmeta');
687
+ // Deploy to tool-native directories if --tools was specified
688
+ if (options.tools && options.tools.length > 0) {
689
+ if (packageType !== 'skill') {
690
+ throw new Error(`--tools flag is only valid for skills, but package type is '${packageType}'`);
691
+ }
692
+ // Track successful deployments for rollback on failure
693
+ const deployedDirs = [];
694
+ try {
695
+ for (const toolName of options.tools) {
696
+ const deployedDir = await this.deploySkillToTool(installDir, packageName, toolName);
697
+ deployedDirs.push(deployedDir);
698
+ }
699
+ }
700
+ catch (deployError) {
701
+ // Rollback successful deployments
702
+ for (const dir of deployedDirs) {
703
+ await fs.remove(dir).catch(() => { });
704
+ }
705
+ throw deployError;
706
+ }
707
+ }
708
+ }
709
+ /**
710
+ * Deploy skill files to a tool-native directory (e.g., ~/.claude/skills/).
711
+ * Copies skill files and writes a reference marker back to the prompd source location.
712
+ * Returns the deployed directory path for rollback support.
713
+ */
714
+ async deploySkillToTool(skillDir, packageName, toolName) {
715
+ const deployDir = (0, types_1.resolveToolDeployDir)(toolName);
716
+ if (!deployDir) {
717
+ throw new Error(`Unknown tool '${toolName}'. Supported tools: ${Object.keys(types_1.TOOL_DEPLOY_DIRS).join(', ')}`);
718
+ }
719
+ // Verify skill source directory exists before copying
720
+ if (!await fs.pathExists(skillDir)) {
721
+ throw new Error(`Skill source directory not found: ${skillDir}`);
722
+ }
723
+ // Create a subdirectory for this skill within the tool's skills directory
724
+ const skillDeployDir = path.join(deployDir, packageName);
725
+ await fs.ensureDir(skillDeployDir);
726
+ // Pre-copy check: reject if source tree contains symlinks (prevent symlink-based attacks)
727
+ await this.rejectSymlinks(skillDir);
728
+ // Copy all files from the install dir to the tool deploy dir (dereference: false to not follow symlinks)
729
+ await fs.copy(skillDir, skillDeployDir, { overwrite: true, dereference: false });
730
+ // Write a reference marker so we know this was deployed by prompd
731
+ const markerPath = path.join(skillDeployDir, '.prompd-source');
732
+ await fs.writeJson(markerPath, {
733
+ source: skillDir,
734
+ deployedAt: new Date().toISOString(),
735
+ tool: toolName,
736
+ }, { spaces: 2 });
737
+ return skillDeployDir;
738
+ }
739
+ /**
740
+ * Recursively walk a directory and reject if any symlinks are found.
741
+ * Prevents symlink-based path traversal attacks during skill deployment.
742
+ */
743
+ async rejectSymlinks(dir) {
744
+ const entries = await fs.readdir(dir);
745
+ for (const entry of entries) {
746
+ const fullPath = path.join(dir, entry);
747
+ const lstat = await fs.lstat(fullPath);
748
+ if (lstat.isSymbolicLink()) {
749
+ throw new Error(`Security violation: symlink detected in package: ${fullPath}`);
750
+ }
751
+ if (lstat.isDirectory()) {
752
+ await this.rejectSymlinks(fullPath);
753
+ }
754
+ }
590
755
  }
591
756
  getAuthHeaders() {
592
757
  const headers = {
@@ -622,18 +787,33 @@ class RegistryClient extends events_1.EventEmitter {
622
787
  return { size: totalSize, files: totalFiles };
623
788
  }
624
789
  matchesFilePatterns(filePath, patterns) {
625
- // Simple glob matching - in production would use proper glob library
626
790
  for (const pattern of patterns) {
627
791
  if (pattern === '**/*' || pattern === '*') {
628
792
  return true;
629
793
  }
630
794
  if (pattern.includes('*')) {
631
- const regex = new RegExp(pattern.replace(/\*/g, '.*'));
632
- if (regex.test(filePath)) {
633
- return true;
795
+ // Safely convert glob pattern to regex:
796
+ // 1. Escape all regex metacharacters EXCEPT *
797
+ // 2. Replace ** with a full-path wildcard, and * with single-segment wildcard
798
+ const escaped = pattern.replace(/([.+?^${}()|[\]\\])/g, '\\$1');
799
+ const regexStr = escaped
800
+ .replace(/\*\*/g, '\u0000') // Temporary placeholder for **
801
+ .replace(/\*/g, '[^/]*') // * matches within a single path segment
802
+ .replace(/\u0000/g, '.*'); // ** matches across path segments
803
+ try {
804
+ const regex = new RegExp(`^${regexStr}$`);
805
+ if (regex.test(filePath)) {
806
+ return true;
807
+ }
808
+ }
809
+ catch {
810
+ // If regex construction fails, fall back to exact match
811
+ if (filePath === pattern) {
812
+ return true;
813
+ }
634
814
  }
635
815
  }
636
- else if (filePath === pattern) {
816
+ else if (filePath === pattern || filePath.endsWith('/' + pattern)) {
637
817
  return true;
638
818
  }
639
819
  }
@@ -644,38 +824,191 @@ class RegistryClient extends events_1.EventEmitter {
644
824
  return await fs.pathExists(cachePath) ? cachePath : null;
645
825
  }
646
826
  async installFromCache(cachePath, packageName, version, options) {
647
- console.log('[RegistryClient.installFromCache] Installing from cache:', cachePath);
648
827
  this.emit('installingFromCache', { name: packageName, version });
649
- // Read the cached tarball
650
828
  const tarballBuffer = await fs.readFile(cachePath);
651
- console.log('[RegistryClient.installFromCache] Read tarball:', tarballBuffer.length, 'bytes');
652
- // Extract to the workspace
653
- const packageData = { tarball: tarballBuffer, metadata: { name: packageName, version } };
829
+ // Try to read full metadata from the cached .meta sidecar file
830
+ const metadataPath = cachePath + '.meta';
831
+ let metadata = { name: packageName, version };
832
+ if (await fs.pathExists(metadataPath)) {
833
+ try {
834
+ metadata = await fs.readJson(metadataPath);
835
+ }
836
+ catch {
837
+ // Fall back to minimal metadata if .meta file is corrupt
838
+ }
839
+ }
840
+ // If metadata lacks type (old cache entry), extract it from the ZIP's prompd.json/manifest.json
841
+ if (!metadata.type) {
842
+ try {
843
+ let AdmZip;
844
+ AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
845
+ const zip = new AdmZip(tarballBuffer);
846
+ const manifestEntry = zip.getEntry('prompd.json') || zip.getEntry('manifest.json');
847
+ if (manifestEntry) {
848
+ const manifest = JSON.parse(manifestEntry.getData().toString('utf8'));
849
+ if (manifest.type) {
850
+ metadata.type = manifest.type;
851
+ }
852
+ // Backfill the .meta sidecar so future installs don't need to re-extract
853
+ await fs.writeJson(metadataPath, { ...metadata, ...manifest }, { spaces: 2 }).catch(() => { });
854
+ }
855
+ }
856
+ catch {
857
+ // Non-fatal: type will fall back to options.type or 'package'
858
+ }
859
+ }
860
+ const packageData = { tarball: tarballBuffer, metadata };
654
861
  await this.extractAndInstallPackage(packageData, packageName, version, options);
655
- console.log('[RegistryClient.installFromCache] Extraction complete');
656
862
  }
657
863
  async cachePackage(cacheKey, packageData) {
658
864
  const cachePath = path.join(this.cacheDir, 'packages', cacheKey);
659
865
  await fs.ensureDir(path.dirname(cachePath));
660
866
  await fs.writeFile(cachePath, packageData.tarball);
867
+ // Save full metadata alongside the tarball so cache installs preserve package type
868
+ await fs.writeJson(cachePath + '.meta', packageData.metadata, { spaces: 2 });
869
+ }
870
+ /**
871
+ * Add a dependency to the workspace prompd.json file.
872
+ */
873
+ async addWorkspaceDependency(name, version, workspaceRoot) {
874
+ const root = workspaceRoot || (0, package_resolver_1.findProjectRoot)();
875
+ const prompdJsonPath = path.join(root, 'prompd.json');
876
+ try {
877
+ let prompdJson = {};
878
+ if (await fs.pathExists(prompdJsonPath)) {
879
+ const content = await fs.readFile(prompdJsonPath, 'utf8');
880
+ if (content && content.trim() !== '') {
881
+ prompdJson = JSON.parse(content);
882
+ }
883
+ }
884
+ if (!prompdJson.dependencies || typeof prompdJson.dependencies !== 'object') {
885
+ prompdJson.dependencies = {};
886
+ }
887
+ prompdJson.dependencies[name] = version;
888
+ await fs.writeFile(prompdJsonPath, JSON.stringify(prompdJson, null, 2) + '\n');
889
+ }
890
+ catch {
891
+ // Non-fatal: dependency tracking failure shouldn't block install
892
+ }
893
+ }
894
+ /**
895
+ * Remove a dependency from the workspace prompd.json file.
896
+ */
897
+ async removeWorkspaceDependency(name, workspaceRoot) {
898
+ const root = workspaceRoot || (0, package_resolver_1.findProjectRoot)();
899
+ const prompdJsonPath = path.join(root, 'prompd.json');
900
+ try {
901
+ if (!await fs.pathExists(prompdJsonPath))
902
+ return;
903
+ const content = await fs.readFile(prompdJsonPath, 'utf8');
904
+ if (!content || content.trim() === '')
905
+ return;
906
+ const prompdJson = JSON.parse(content);
907
+ if (!prompdJson.dependencies || typeof prompdJson.dependencies !== 'object')
908
+ return;
909
+ delete prompdJson.dependencies[name];
910
+ await fs.writeFile(prompdJsonPath, JSON.stringify(prompdJson, null, 2) + '\n');
911
+ }
912
+ catch {
913
+ // Non-fatal
914
+ }
915
+ }
916
+ /**
917
+ * Uninstall a package by name, removing installed files and the prompd.json dependency entry.
918
+ * Scans type directories to find the installed location.
919
+ */
920
+ async uninstall(packageName, options = {}) {
921
+ const workspaceRoot = options.workspaceRoot || (0, package_resolver_1.findProjectRoot)();
922
+ const os = require('os');
923
+ const installBase = options.global
924
+ ? path.join(os.homedir(), '.prompd')
925
+ : path.join(workspaceRoot, '.prompd');
926
+ // Parse embedded version from ref (e.g. @scope/name@1.0.0)
927
+ let name = packageName;
928
+ const lastAtIndex = packageName.lastIndexOf('@');
929
+ if (lastAtIndex > 0) {
930
+ name = packageName.substring(0, lastAtIndex);
931
+ }
932
+ let removed = false;
933
+ // Scan all type directories for this package
934
+ for (const [type, dir] of Object.entries(types_1.PACKAGE_TYPE_DIRS)) {
935
+ if (type === 'node-template') {
936
+ // Node-templates are stored as .pdpkg files at the type root
937
+ const templatesDir = path.join(installBase, dir);
938
+ if (!await fs.pathExists(templatesDir))
939
+ continue;
940
+ const entries = await fs.readdir(templatesDir);
941
+ for (const entry of entries) {
942
+ if (!entry.endsWith('.pdpkg'))
943
+ continue;
944
+ // Read manifest from archive to match by name
945
+ const pkgPath = path.join(templatesDir, entry);
946
+ try {
947
+ let AdmZip;
948
+ AdmZip = (await Promise.resolve().then(() => __importStar(require('adm-zip')))).default;
949
+ const zip = new AdmZip(pkgPath);
950
+ const manifestEntry = zip.getEntry('prompd.json') || zip.getEntry('manifest.json');
951
+ if (!manifestEntry)
952
+ continue;
953
+ const manifest = JSON.parse(manifestEntry.getData().toString('utf8'));
954
+ if (manifest.name === name) {
955
+ await fs.remove(pkgPath);
956
+ removed = true;
957
+ }
958
+ }
959
+ catch {
960
+ // Skip unreadable archives
961
+ }
962
+ }
963
+ }
964
+ else {
965
+ // Standard packages: stored in @scope/name/ or name/ directories
966
+ const pkgDir = path.join(installBase, dir, name);
967
+ if (await fs.pathExists(pkgDir)) {
968
+ await fs.remove(pkgDir);
969
+ removed = true;
970
+ }
971
+ }
972
+ }
973
+ if (!removed) {
974
+ throw new Error(`Package '${name}' is not installed`);
975
+ }
976
+ // Remove from workspace prompd.json dependencies
977
+ if (!options.global) {
978
+ await this.removeWorkspaceDependency(name, workspaceRoot);
979
+ }
980
+ this.emit('uninstallComplete', { name });
661
981
  }
662
982
  /**
663
983
  * Load manifest.json from file system abstraction.
664
984
  * Supports both disk-based and in-memory file systems.
665
985
  */
666
986
  async loadManifestFromFS(packagePath, fileSystem) {
987
+ // Try prompd.json first (current format), fall back to manifest.json (legacy)
988
+ const prompdJsonPath = fileSystem.join(packagePath, 'prompd.json');
667
989
  const manifestPath = fileSystem.join(packagePath, 'manifest.json');
668
- const exists = await Promise.resolve(fileSystem.exists(manifestPath));
669
- if (!exists) {
670
- throw new Error('No manifest.json file found');
990
+ let resolvedPath;
991
+ if (await Promise.resolve(fileSystem.exists(prompdJsonPath))) {
992
+ resolvedPath = prompdJsonPath;
993
+ }
994
+ else if (await Promise.resolve(fileSystem.exists(manifestPath))) {
995
+ resolvedPath = manifestPath;
996
+ }
997
+ else {
998
+ throw new Error('No prompd.json (or legacy manifest.json) file found');
671
999
  }
672
- const content = await Promise.resolve(fileSystem.readFile(manifestPath));
1000
+ const content = await Promise.resolve(fileSystem.readFile(resolvedPath));
673
1001
  const manifest = JSON.parse(content);
1002
+ // Prevent prototype pollution from untrusted manifests
1003
+ const manifestObj = manifest;
1004
+ delete manifestObj['__proto__'];
1005
+ delete manifestObj['constructor'];
1006
+ delete manifestObj['prototype'];
674
1007
  // Validate required fields
675
1008
  const required = ['name', 'version', 'description', 'author'];
676
1009
  for (const field of required) {
677
1010
  if (!manifest[field]) {
678
- throw new Error(`Missing required field in manifest.json: ${field}`);
1011
+ throw new Error(`Missing required field in ${path.basename(resolvedPath)}: ${field}`);
679
1012
  }
680
1013
  }
681
1014
  // Validate version format
@@ -687,7 +1020,7 @@ class RegistryClient extends events_1.EventEmitter {
687
1020
  manifest.keywords = manifest.keywords || [];
688
1021
  manifest.dependencies = manifest.dependencies || {};
689
1022
  manifest.files = manifest.files || ['**/*.prmd'];
690
- manifest.type = manifest.type || 'collection';
1023
+ manifest.type = manifest.type || 'package';
691
1024
  manifest.category = manifest.category || 'general';
692
1025
  manifest.tags = manifest.tags || [];
693
1026
  manifest.prompdVersion = manifest.prompdVersion || '0.3.3';
@@ -746,41 +1079,73 @@ class RegistryClient extends events_1.EventEmitter {
746
1079
  }
747
1080
  /**
748
1081
  * Upload package Buffer to registry.
1082
+ * Uses form-data's submit() which handles Content-Length, transport, and piping.
749
1083
  */
750
1084
  async uploadPackageBuffer(tarballBuffer, metadata, options) {
751
1085
  const FormData = require('form-data');
752
1086
  const formData = new FormData();
753
1087
  formData.append('package', tarballBuffer, {
754
1088
  filename: `${metadata.name}-${metadata.version}.pdpkg`,
755
- contentType: 'application/gzip'
1089
+ contentType: 'application/zip'
756
1090
  });
757
1091
  formData.append('metadata', JSON.stringify(metadata));
758
1092
  formData.append('access', options.access);
759
1093
  formData.append('tag', options.tag);
760
- const response = await fetch(`${this.registryUrl}/publish`, {
761
- method: 'POST',
762
- headers: {
763
- ...this.getAuthHeaders(),
764
- ...formData.getHeaders()
765
- },
766
- body: formData
1094
+ const token = options.authToken || this.authToken;
1095
+ const url = new URL(`${this.registryUrl}/packages/${this.encodePackageName(metadata.name)}`);
1096
+ const response = await new Promise((resolve, reject) => {
1097
+ formData.submit({
1098
+ protocol: url.protocol,
1099
+ hostname: url.hostname,
1100
+ port: url.port || undefined,
1101
+ path: url.pathname,
1102
+ method: 'PUT',
1103
+ headers: {
1104
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
1105
+ 'User-Agent': 'prompd-cli/0.5.0'
1106
+ }
1107
+ }, (err, res) => {
1108
+ if (err)
1109
+ return reject(err);
1110
+ const chunks = [];
1111
+ res.on('data', (chunk) => chunks.push(chunk));
1112
+ res.on('error', reject);
1113
+ res.on('end', () => {
1114
+ resolve({ statusCode: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') });
1115
+ });
1116
+ });
767
1117
  });
768
- if (!response.ok) {
769
- const errorText = await response.text();
770
- throw new Error(`Publish failed: ${response.status} ${response.statusText} - ${errorText}`);
1118
+ if (response.statusCode < 200 || response.statusCode >= 300) {
1119
+ throw new Error(`Publish failed: ${response.statusCode} - ${response.body}`);
771
1120
  }
772
1121
  }
773
1122
  }
774
1123
  exports.RegistryClient = RegistryClient;
1124
+ RegistryClient.MAX_FILE_SIZE_IN_ZIP = 10 * 1024 * 1024; // 10MB per file
1125
+ RegistryClient.MAX_TOTAL_EXTRACTED_SIZE = 500 * 1024 * 1024; // 500MB total
1126
+ RegistryClient.MAX_COMPRESSION_RATIO = 100; // 100:1 max ratio per file
775
1127
  /**
776
1128
  * Default registry configuration
777
1129
  */
778
- const createDefaultRegistryConfig = () => ({
779
- registryUrl: process.env.PROMPD_REGISTRY_URL || 'https://registry.prompdhub.ai',
780
- authToken: process.env.PROMPD_AUTH_TOKEN,
781
- cacheDir: path.join(require('os').homedir(), '.prmd', 'cache'),
782
- timeout: 30000,
783
- maxPackageSize: 50 * 1024 * 1024 // 50MB
784
- });
1130
+ const createDefaultRegistryConfig = () => {
1131
+ const defaultUrl = 'https://registry.prompdhub.ai';
1132
+ const envUrl = process.env.PROMPD_REGISTRY_URL;
1133
+ let registryUrl = defaultUrl;
1134
+ if (envUrl) {
1135
+ if ((0, validation_1.validateRegistryUrl)(envUrl)) {
1136
+ registryUrl = envUrl;
1137
+ }
1138
+ else {
1139
+ console.warn(`Warning: PROMPD_REGISTRY_URL value is invalid, falling back to default: ${defaultUrl}`);
1140
+ }
1141
+ }
1142
+ return {
1143
+ registryUrl,
1144
+ authToken: process.env.PROMPD_AUTH_TOKEN,
1145
+ cacheDir: path.join(require('os').homedir(), '.prmd', 'cache'),
1146
+ timeout: 30000,
1147
+ maxPackageSize: 50 * 1024 * 1024 // 50MB
1148
+ };
1149
+ };
785
1150
  exports.createDefaultRegistryConfig = createDefaultRegistryConfig;
786
1151
  //# sourceMappingURL=registry.js.map