prpm 0.0.10 → 0.0.12

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.
@@ -115,13 +115,17 @@ class RegistryClient {
115
115
  /**
116
116
  * Publish a package (requires authentication)
117
117
  */
118
- async publish(manifest, tarball) {
118
+ async publish(manifest, tarball, options) {
119
119
  if (!this.token) {
120
120
  throw new Error('Authentication required. Run `prpm login` first.');
121
121
  }
122
122
  const formData = new FormData();
123
123
  formData.append('manifest', JSON.stringify(manifest));
124
124
  formData.append('tarball', new Blob([tarball]), 'package.tar.gz');
125
+ // Add org_id if provided
126
+ if (options?.orgId) {
127
+ formData.append('org_id', options.orgId);
128
+ }
125
129
  const response = await this.fetch('/api/v1/packages', {
126
130
  method: 'POST',
127
131
  body: formData,
@@ -205,6 +209,12 @@ class RegistryClient {
205
209
  */
206
210
  async fetch(path, options = {}, retries = 3) {
207
211
  const url = `${this.baseUrl}${path}`;
212
+ // Debug logging
213
+ if (process.env.DEBUG || process.env.PRPM_DEBUG) {
214
+ console.error(`[DEBUG] Fetching: ${url}`);
215
+ console.error(`[DEBUG] Method: ${options.method || 'GET'}`);
216
+ console.error(`[DEBUG] Has token: ${!!this.token}`);
217
+ }
208
218
  const headers = {
209
219
  'Content-Type': 'application/json',
210
220
  ...options.headers,
@@ -215,6 +225,9 @@ class RegistryClient {
215
225
  let lastError = null;
216
226
  for (let attempt = 0; attempt < retries; attempt++) {
217
227
  try {
228
+ if (process.env.DEBUG || process.env.PRPM_DEBUG) {
229
+ console.error(`[DEBUG] Attempt ${attempt + 1}/${retries}`);
230
+ }
218
231
  const response = await fetch(url, {
219
232
  ...options,
220
233
  headers,
@@ -242,21 +255,35 @@ class RegistryClient {
242
255
  }
243
256
  catch (error) {
244
257
  lastError = error instanceof Error ? error : new Error(String(error));
258
+ if (process.env.DEBUG || process.env.PRPM_DEBUG) {
259
+ console.error(`[DEBUG] Error on attempt ${attempt + 1}:`, lastError.message);
260
+ console.error(`[DEBUG] Error type:`, lastError.constructor.name);
261
+ console.error(`[DEBUG] Full error:`, lastError);
262
+ }
245
263
  // Network errors - retry with exponential backoff
246
264
  if (attempt < retries - 1 && (lastError.message.includes('fetch failed') ||
247
265
  lastError.message.includes('ECONNREFUSED') ||
248
266
  lastError.message.includes('ETIMEDOUT'))) {
249
267
  const waitTime = Math.pow(2, attempt) * 1000;
268
+ if (process.env.DEBUG || process.env.PRPM_DEBUG) {
269
+ console.error(`[DEBUG] Retrying after ${waitTime}ms...`);
270
+ }
250
271
  await new Promise(resolve => setTimeout(resolve, waitTime));
251
272
  continue;
252
273
  }
253
- // If it's not a retryable error or we're out of retries, throw
274
+ // If it's not a retryable error or we're out of retries, throw with more context
254
275
  if (attempt === retries - 1) {
255
- throw lastError;
276
+ const enhancedError = new Error(`Failed to connect to registry at ${url}\n` +
277
+ `Original error: ${lastError.message}\n\n` +
278
+ `💡 Possible causes:\n` +
279
+ ` - Registry server is not running\n` +
280
+ ` - Network connection issue\n` +
281
+ ` - Incorrect PRPM_REGISTRY_URL (currently: ${this.baseUrl})`);
282
+ throw enhancedError;
256
283
  }
257
284
  }
258
285
  }
259
- throw lastError || new Error('Request failed after retries');
286
+ throw lastError || new Error(`Request failed after ${retries} retries to ${url}`);
260
287
  }
261
288
  }
262
289
  exports.RegistryClient = RegistryClient;
@@ -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
  */
@@ -3,3 +3,10 @@
3
3
  * Registry API types for CLI
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isMultiPackageManifest = isMultiPackageManifest;
7
+ /**
8
+ * Type guard to check if manifest is multi-package
9
+ */
10
+ function isMultiPackageManifest(manifest) {
11
+ return 'packages' in manifest && Array.isArray(manifest.packages);
12
+ }
package/dist/types.js CHANGED
@@ -3,3 +3,33 @@
3
3
  * Core types for the Prompt Package Manager
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SUBTYPES = exports.FORMATS = void 0;
7
+ /**
8
+ * Available formats as a constant array
9
+ * Useful for CLI prompts, validation, etc.
10
+ */
11
+ exports.FORMATS = [
12
+ 'cursor',
13
+ 'claude',
14
+ 'continue',
15
+ 'windsurf',
16
+ 'copilot',
17
+ 'kiro',
18
+ 'agents.md',
19
+ 'generic',
20
+ 'mcp',
21
+ ];
22
+ /**
23
+ * Available subtypes as a constant array
24
+ * Useful for CLI prompts, validation, etc.
25
+ */
26
+ exports.SUBTYPES = [
27
+ 'rule',
28
+ 'agent',
29
+ 'skill',
30
+ 'slash-command',
31
+ 'prompt',
32
+ 'collection',
33
+ 'chatmode',
34
+ 'tool',
35
+ ];
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ /**
3
+ * License extraction utilities
4
+ * Extracts license information from LICENSE files for proper attribution
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.extractLicenseInfo = extractLicenseInfo;
8
+ exports.validateLicenseInfo = validateLicenseInfo;
9
+ const promises_1 = require("fs/promises");
10
+ const path_1 = require("path");
11
+ const fs_1 = require("fs");
12
+ /**
13
+ * Common license file names to search for
14
+ */
15
+ const LICENSE_FILE_PATTERNS = [
16
+ 'LICENSE',
17
+ 'LICENSE.md',
18
+ 'LICENSE.txt',
19
+ 'LICENCE',
20
+ 'LICENCE.md',
21
+ 'LICENCE.txt',
22
+ 'LICENSE-MIT',
23
+ 'LICENSE-APACHE',
24
+ 'COPYING',
25
+ 'COPYING.txt',
26
+ ];
27
+ /**
28
+ * License type detection patterns
29
+ * Ordered by specificity - more specific patterns first
30
+ */
31
+ const LICENSE_PATTERNS = [
32
+ { pattern: /MIT License/i, type: 'MIT' },
33
+ { pattern: /Apache License[\s\S]*Version 2\.0/i, type: 'Apache-2.0' },
34
+ { pattern: /GNU GENERAL PUBLIC LICENSE[\s\S]*Version 3/i, type: 'GPL-3.0' },
35
+ { pattern: /GNU GENERAL PUBLIC LICENSE[\s\S]*Version 2/i, type: 'GPL-2.0' },
36
+ { pattern: /GNU LESSER GENERAL PUBLIC LICENSE[\s\S]*Version 3/i, type: 'LGPL-3.0' },
37
+ { pattern: /GNU LESSER GENERAL PUBLIC LICENSE[\s\S]*Version 2/i, type: 'LGPL-2.1' },
38
+ { pattern: /BSD 3-Clause License/i, type: 'BSD-3-Clause' },
39
+ { pattern: /BSD 2-Clause License/i, type: 'BSD-2-Clause' },
40
+ { pattern: /Mozilla Public License[\s\S]*Version 2\.0/i, type: 'MPL-2.0' },
41
+ { pattern: /ISC License/i, type: 'ISC' },
42
+ { pattern: /The Unlicense/i, type: 'Unlicense' },
43
+ { pattern: /Creative Commons Zero[\s\S]*1\.0/i, type: 'CC0-1.0' },
44
+ ];
45
+ /**
46
+ * Detect license type from license text
47
+ */
48
+ function detectLicenseType(text) {
49
+ for (const { pattern, type } of LICENSE_PATTERNS) {
50
+ if (pattern.test(text)) {
51
+ return type;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Generate GitHub raw URL for license file
58
+ */
59
+ function generateLicenseUrl(repositoryUrl, fileName) {
60
+ if (!repositoryUrl) {
61
+ return null;
62
+ }
63
+ // Extract owner/repo from GitHub URL
64
+ const githubMatch = repositoryUrl.match(/github\.com[/:]([\w-]+)\/([\w-]+)/);
65
+ if (!githubMatch) {
66
+ return null;
67
+ }
68
+ const [, owner, repo] = githubMatch;
69
+ // Use raw.githubusercontent.com for direct file access
70
+ return `https://raw.githubusercontent.com/${owner}/${repo}/main/${fileName}`;
71
+ }
72
+ /**
73
+ * Find and extract license information from the current directory
74
+ */
75
+ async function extractLicenseInfo(repositoryUrl) {
76
+ const cwd = process.cwd();
77
+ // Try each license file pattern
78
+ for (const fileName of LICENSE_FILE_PATTERNS) {
79
+ const filePath = (0, path_1.join)(cwd, fileName);
80
+ try {
81
+ // Check if file exists
82
+ await (0, promises_1.access)(filePath, fs_1.constants.R_OK);
83
+ // Read license file
84
+ const text = await (0, promises_1.readFile)(filePath, 'utf-8');
85
+ // Detect license type
86
+ const type = detectLicenseType(text);
87
+ // Generate license URL if repository is provided
88
+ const url = generateLicenseUrl(repositoryUrl, fileName);
89
+ return {
90
+ type,
91
+ text,
92
+ url,
93
+ fileName,
94
+ };
95
+ }
96
+ catch {
97
+ // File doesn't exist or can't be read, try next pattern
98
+ continue;
99
+ }
100
+ }
101
+ // No license file found
102
+ return {
103
+ type: null,
104
+ text: null,
105
+ url: null,
106
+ fileName: null,
107
+ };
108
+ }
109
+ /**
110
+ * Display license information if found
111
+ */
112
+ function validateLicenseInfo(licenseInfo, packageName) {
113
+ if (licenseInfo.text && licenseInfo.type) {
114
+ console.log(` License: ${licenseInfo.type} (${licenseInfo.fileName})`);
115
+ }
116
+ else if (licenseInfo.text && !licenseInfo.type) {
117
+ console.log(` License: Found (${licenseInfo.fileName})`);
118
+ }
119
+ else {
120
+ console.log(` License: Not found (package will be published without license)`);
121
+ }
122
+ }
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-package manifest utilities
4
+ * Handles merging root-level fields with package-level fields
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.mergePackageFields = mergePackageFields;
8
+ exports.getPackagesWithInheritance = getPackagesWithInheritance;
9
+ exports.validateMultiPackageManifest = validateMultiPackageManifest;
10
+ exports.filterPackages = filterPackages;
11
+ /**
12
+ * Fields that can be inherited from root to packages
13
+ */
14
+ const INHERITABLE_FIELDS = [
15
+ 'author',
16
+ 'license',
17
+ 'repository',
18
+ 'homepage',
19
+ 'documentation',
20
+ 'organization',
21
+ 'keywords',
22
+ 'tags',
23
+ ];
24
+ /**
25
+ * Merge root-level fields into a package manifest
26
+ * Package-level fields take precedence over root-level fields
27
+ */
28
+ function mergePackageFields(root, pkg) {
29
+ const merged = { ...pkg };
30
+ // Inherit each inheritable field if not defined in package
31
+ for (const field of INHERITABLE_FIELDS) {
32
+ if (merged[field] === undefined && root[field] !== undefined) {
33
+ // @ts-ignore - dynamic field access
34
+ merged[field] = root[field];
35
+ }
36
+ }
37
+ return merged;
38
+ }
39
+ /**
40
+ * Get all packages from a multi-package manifest with inherited fields
41
+ */
42
+ function getPackagesWithInheritance(manifest) {
43
+ return manifest.packages.map(pkg => mergePackageFields(manifest, pkg));
44
+ }
45
+ /**
46
+ * Validate multi-package manifest
47
+ */
48
+ function validateMultiPackageManifest(manifest) {
49
+ const errors = [];
50
+ // Check packages array exists and is not empty
51
+ if (!manifest.packages || !Array.isArray(manifest.packages)) {
52
+ errors.push('packages field must be an array');
53
+ return { valid: false, errors };
54
+ }
55
+ if (manifest.packages.length === 0) {
56
+ errors.push('packages array must contain at least one package');
57
+ return { valid: false, errors };
58
+ }
59
+ // Check for duplicate package names
60
+ const names = new Set();
61
+ for (let i = 0; i < manifest.packages.length; i++) {
62
+ const pkg = manifest.packages[i];
63
+ if (names.has(pkg.name)) {
64
+ errors.push(`Duplicate package name: ${pkg.name}`);
65
+ }
66
+ names.add(pkg.name);
67
+ }
68
+ // Validate each package has required fields
69
+ for (let i = 0; i < manifest.packages.length; i++) {
70
+ const pkg = manifest.packages[i];
71
+ const pkgPrefix = `Package ${i} (${pkg.name || 'unnamed'})`;
72
+ if (!pkg.name) {
73
+ errors.push(`${pkgPrefix}: missing required field 'name'`);
74
+ }
75
+ if (!pkg.version) {
76
+ errors.push(`${pkgPrefix}: missing required field 'version'`);
77
+ }
78
+ if (!pkg.description) {
79
+ errors.push(`${pkgPrefix}: missing required field 'description'`);
80
+ }
81
+ if (!pkg.format) {
82
+ errors.push(`${pkgPrefix}: missing required field 'format'`);
83
+ }
84
+ if (!pkg.files || pkg.files.length === 0) {
85
+ errors.push(`${pkgPrefix}: must have at least one file in 'files' array`);
86
+ }
87
+ }
88
+ return {
89
+ valid: errors.length === 0,
90
+ errors,
91
+ };
92
+ }
93
+ /**
94
+ * Filter packages by name or pattern
95
+ */
96
+ function filterPackages(packages, filter) {
97
+ // If filter is a number, treat as index
98
+ if (typeof filter === 'number') {
99
+ if (filter < 0 || filter >= packages.length) {
100
+ throw new Error(`Package index ${filter} out of range (0-${packages.length - 1})`);
101
+ }
102
+ return [packages[filter]];
103
+ }
104
+ // If exact match, return that package
105
+ const exactMatch = packages.find(pkg => pkg.name === filter);
106
+ if (exactMatch) {
107
+ return [exactMatch];
108
+ }
109
+ // Try as glob pattern
110
+ const pattern = filter.replace(/\*/g, '.*');
111
+ const regex = new RegExp(`^${pattern}$`);
112
+ const matches = packages.filter(pkg => regex.test(pkg.name));
113
+ if (matches.length === 0) {
114
+ throw new Error(`No packages match filter: ${filter}`);
115
+ }
116
+ return matches;
117
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ /**
3
+ * Parallel publishing utilities with concurrency control
4
+ * Optimizes multi-package publishing performance
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.publishInParallel = publishInParallel;
8
+ exports.withRetry = withRetry;
9
+ exports.formatDuration = formatDuration;
10
+ exports.calculateStats = calculateStats;
11
+ /**
12
+ * Execute tasks in parallel with concurrency limit
13
+ */
14
+ async function publishInParallel(tasks, options = {}) {
15
+ const { concurrency = 5, continueOnError = false, onProgress, onSuccess, onError, } = options;
16
+ const results = new Array(tasks.length);
17
+ let completed = 0;
18
+ let hasError = false;
19
+ let taskIndex = 0;
20
+ async function executeTask(task, index) {
21
+ const startTime = Date.now();
22
+ try {
23
+ const result = await task.execute();
24
+ const duration = Date.now() - startTime;
25
+ results[index] = {
26
+ success: true,
27
+ name: task.name,
28
+ result,
29
+ duration,
30
+ };
31
+ completed++;
32
+ onProgress?.(completed, tasks.length, task.name);
33
+ onSuccess?.(task.name, result);
34
+ }
35
+ catch (error) {
36
+ const duration = Date.now() - startTime;
37
+ const err = error instanceof Error ? error : new Error(String(error));
38
+ results[index] = {
39
+ success: false,
40
+ name: task.name,
41
+ error: err,
42
+ duration,
43
+ };
44
+ completed++;
45
+ hasError = true;
46
+ onProgress?.(completed, tasks.length, task.name);
47
+ onError?.(task.name, err);
48
+ // If not continuing on error, mark hasError to skip remaining tasks
49
+ if (!continueOnError) {
50
+ throw err;
51
+ }
52
+ }
53
+ }
54
+ // Execute tasks with concurrency control
55
+ const executing = new Set();
56
+ while (taskIndex < tasks.length || executing.size > 0) {
57
+ // Fill up to concurrency limit
58
+ while (taskIndex < tasks.length && executing.size < concurrency) {
59
+ // If in strict mode and we've encountered an error, skip remaining tasks
60
+ if (!continueOnError && hasError) {
61
+ results[taskIndex] = {
62
+ success: false,
63
+ name: tasks[taskIndex].name,
64
+ error: new Error('Skipped due to previous failure'),
65
+ duration: 0,
66
+ };
67
+ taskIndex++;
68
+ continue;
69
+ }
70
+ const currentIndex = taskIndex;
71
+ const currentTask = tasks[taskIndex];
72
+ taskIndex++;
73
+ const promise = executeTask(currentTask, currentIndex)
74
+ .catch(() => {
75
+ // Errors already handled in executeTask
76
+ })
77
+ .finally(() => {
78
+ executing.delete(promise);
79
+ });
80
+ executing.add(promise);
81
+ }
82
+ // Wait for at least one task to complete
83
+ if (executing.size > 0) {
84
+ await Promise.race(executing);
85
+ }
86
+ }
87
+ return results;
88
+ }
89
+ /**
90
+ * Retry a task with exponential backoff
91
+ */
92
+ async function withRetry(fn, options = {}) {
93
+ const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000, backoffFactor = 2, } = options;
94
+ let lastError;
95
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
96
+ try {
97
+ return await fn();
98
+ }
99
+ catch (error) {
100
+ lastError = error instanceof Error ? error : new Error(String(error));
101
+ // Don't retry on last attempt
102
+ if (attempt === maxRetries - 1) {
103
+ break;
104
+ }
105
+ // Calculate delay with exponential backoff
106
+ const delay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
107
+ await sleep(delay);
108
+ }
109
+ }
110
+ throw lastError || new Error('Max retries exceeded');
111
+ }
112
+ /**
113
+ * Sleep utility
114
+ */
115
+ function sleep(ms) {
116
+ return new Promise(resolve => setTimeout(resolve, ms));
117
+ }
118
+ /**
119
+ * Format duration in human-readable format
120
+ */
121
+ function formatDuration(ms) {
122
+ if (ms < 1000) {
123
+ return `${ms}ms`;
124
+ }
125
+ const seconds = (ms / 1000).toFixed(1);
126
+ return `${seconds}s`;
127
+ }
128
+ function calculateStats(results) {
129
+ const succeeded = results.filter(r => r.success && r.result !== undefined).length;
130
+ const failed = results.filter(r => !r.success && r.error && r.error.message !== 'Skipped due to previous failure').length;
131
+ const skipped = results.filter(r => r.error?.message === 'Skipped due to previous failure').length;
132
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
133
+ const completedCount = succeeded + failed;
134
+ const avgDuration = completedCount > 0 ? totalDuration / completedCount : 0;
135
+ return {
136
+ total: results.length,
137
+ succeeded,
138
+ failed,
139
+ skipped,
140
+ totalDuration,
141
+ avgDuration,
142
+ successRate: results.length > 0 ? succeeded / results.length : 0,
143
+ };
144
+ }