@versatiles/release-tool 2.5.0 → 2.7.0

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.
@@ -1,34 +1,85 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  import { readFileSync, writeFileSync } from 'fs';
3
+ import { resolve } from 'path';
3
4
  import select from '@inquirer/select';
5
+ import { updateChangelog } from '../lib/changelog.js';
6
+ import { releaseError, validationError } from '../lib/errors.js';
7
+ import { COMMIT_TYPES, getGit, getSuggestedBump, groupCommitsByType, parseConventionalCommit, } from '../lib/git.js';
4
8
  import { check, info, panic, warn } from '../lib/log.js';
9
+ import { withRetry } from '../lib/retry.js';
5
10
  import { Shell } from '../lib/shell.js';
6
- import { getGit } from '../lib/git.js';
7
- import { resolve } from 'path';
8
- export async function release(directory, branch = 'main') {
11
+ /**
12
+ * Type guard to validate package.json structure.
13
+ */
14
+ function isValidPackageJson(pkg) {
15
+ if (typeof pkg !== 'object' || pkg === null)
16
+ return false;
17
+ if (!('version' in pkg) || typeof pkg.version !== 'string')
18
+ return false;
19
+ if (!('scripts' in pkg) || typeof pkg.scripts !== 'object' || pkg.scripts === null)
20
+ return false;
21
+ return true;
22
+ }
23
+ /**
24
+ * Executes the npm release process.
25
+ *
26
+ * This function performs a complete release workflow:
27
+ * 1. Validates git state (correct branch, no uncommitted changes)
28
+ * 2. Pulls latest changes from remote
29
+ * 3. Verifies npm authentication
30
+ * 4. Prompts for new version (with suggestion based on conventional commits)
31
+ * 5. Runs project checks
32
+ * 6. Updates package.json version
33
+ * 7. Updates CHANGELOG.md
34
+ * 8. Publishes to npm (if not private)
35
+ * 9. Creates git commit and tag
36
+ * 10. Pushes to remote and creates GitHub release
37
+ *
38
+ * @param directory - The project directory containing package.json
39
+ * @param branch - The git branch to release from (default: 'main')
40
+ * @param dryRun - If true, simulate the release without making changes
41
+ * @throws {VrtError} If any step in the release process fails
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // Standard release from main branch
46
+ * await release('/path/to/project');
47
+ *
48
+ * // Dry run to preview release
49
+ * await release('/path/to/project', 'main', true);
50
+ *
51
+ * // Release from a different branch
52
+ * await release('/path/to/project', 'release');
53
+ * ```
54
+ */
55
+ export async function release(directory, branch = 'main', dryRun = false) {
9
56
  const shell = new Shell(directory);
10
57
  const { getCommitsBetween, getCurrentGitHubCommit, getLastGitHubTag } = getGit(directory);
11
- info('starting release process');
58
+ info(dryRun ? 'starting release process (dry-run)' : 'starting release process');
12
59
  // git: check if in the correct branch
13
60
  const currentBranch = await check('get branch name', shell.stdout('git rev-parse --abbrev-ref HEAD'));
14
61
  if (currentBranch !== branch)
15
62
  panic(`current branch is "${currentBranch}" but should be "${branch}"`);
16
63
  // git: check if no changes
17
64
  await check('are all changes committed?', checkThatNoUncommittedChanges());
18
- // git: pull
19
- await check('git pull', shell.run('git pull -t'));
65
+ // git: pull (with retry for transient network failures)
66
+ await check('git pull', withRetry(() => shell.run('git pull -t'), {
67
+ onRetry: (attempt, error) => warn(`git pull failed (attempt ${attempt}): ${error.message}, retrying...`),
68
+ }));
20
69
  // check package.json
21
- const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'));
22
- if (typeof pkg !== 'object' || pkg === null)
70
+ const pkgRaw = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'));
71
+ if (!isValidPackageJson(pkgRaw))
23
72
  panic('package.json is not valid');
24
- if (!('version' in pkg) || (typeof pkg.version !== 'string'))
25
- panic('package.json is missing "version"');
26
- if (!('scripts' in pkg) || (typeof pkg.scripts !== 'object') || (pkg.scripts == null))
27
- panic('package.json is missing "scripts"');
28
- if (!('check' in pkg.scripts))
73
+ if (!('check' in pkgRaw.scripts))
29
74
  panic('missing npm script "check" in package.json');
30
- if (!('prepack' in pkg.scripts))
75
+ if (!('prepack' in pkgRaw.scripts))
31
76
  panic('missing npm script "prepack" in package.json');
77
+ const pkg = pkgRaw;
78
+ const isPrivatePackage = pkg.private === true;
79
+ // npm: verify authentication (skip for private packages)
80
+ if (!isPrivatePackage) {
81
+ await check('verify npm authentication', verifyNpmAuth());
82
+ }
32
83
  // get last version
33
84
  const tag = await check('get last github tag', getLastGitHubTag());
34
85
  const shaLast = tag?.sha;
@@ -38,37 +89,90 @@ export async function release(directory, branch = 'main') {
38
89
  warn(`versions differ in package.json (${versionLastPackage}) and last GitHub tag (${versionLastGithub})`);
39
90
  // get current sha
40
91
  const { sha: shaCurrent } = await check('get current github commit', getCurrentGitHubCommit());
41
- // handle version
42
- const nextVersion = await getNewVersion(versionLastPackage);
92
+ // get and parse commits for conventional commit support
93
+ const commits = await check('get commits since last release', getCommitsBetween(shaLast, shaCurrent));
94
+ const parsedCommits = commits.map(parseConventionalCommit);
95
+ // handle version (with suggested bump based on conventional commits)
96
+ const nextVersion = await getNewVersion(versionLastPackage, parsedCommits);
97
+ // prepare release notes (grouped by conventional commit type)
98
+ const releaseNotes = getReleaseNotes(nextVersion, parsedCommits);
99
+ info('prepared release notes');
100
+ if (dryRun) {
101
+ info('Dry-run mode - the following actions would be performed:');
102
+ info(` Version: ${versionLastPackage} -> ${nextVersion}`);
103
+ info(' Release notes:');
104
+ releaseNotes.split('\n').forEach((line) => info(` ${line}`));
105
+ info(' Commands that would be executed:');
106
+ info(' npm run check');
107
+ info(' npm i --package-lock-only');
108
+ info(' Update CHANGELOG.md');
109
+ if (!isPrivatePackage) {
110
+ info(' npm publish --access public');
111
+ }
112
+ info(' git add .');
113
+ info(` git commit -m "v${nextVersion}"`);
114
+ info(` git tag -f -a "v${nextVersion}" -m "new release: v${nextVersion}"`);
115
+ info(' git push --no-verify --follow-tags');
116
+ info(` gh release create/edit "v${nextVersion}"`);
117
+ info('Dry-run complete - no changes were made');
118
+ return;
119
+ }
43
120
  // test
44
121
  await check('run checks', shell.run('npm run check'));
45
122
  // update version
46
123
  await check('update version', setNextVersion(nextVersion));
47
- // prepare release notes
48
- const releaseNotes = await check('prepare release notes', getReleaseNotes(nextVersion, shaLast, shaCurrent));
49
- if (!('private' in pkg) || !pkg.private) {
50
- // npm publish
51
- await check('npm publish', shell.runInteractive('npm publish --access public'));
124
+ // update changelog
125
+ const changelogResult = updateChangelog(directory, nextVersion, parsedCommits);
126
+ if (changelogResult.created) {
127
+ info('created CHANGELOG.md');
128
+ }
129
+ else {
130
+ info('updated CHANGELOG.md');
131
+ }
132
+ if (!isPrivatePackage) {
133
+ // npm publish (with retry for transient network failures)
134
+ await check('npm publish', withRetry(() => shell.runInteractive('npm publish --access public'), {
135
+ onRetry: (attempt, error) => warn(`npm publish failed (attempt ${attempt}): ${error.message}, retrying...`),
136
+ }));
52
137
  }
53
138
  // git push
54
139
  await check('git add', shell.run('git add .'));
55
140
  await check('git commit', shell.run(`git commit -m "v${nextVersion}"`, false));
56
141
  await check('git tag', shell.run(`git tag -f -a "v${nextVersion}" -m "new release: v${nextVersion}"`));
57
- await check('git push', shell.run('git push --no-verify --follow-tags'));
58
- // github release
59
- const releaseNotesPipe = `echo -e '${releaseNotes.replace(/[^a-z0-9,.?!:_<> -]/gi, c => '\\x' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))}'`;
60
- if (await check('check github release', shell.ok('gh release view v' + nextVersion))) {
61
- await check('edit release', shell.run(`${releaseNotesPipe} | gh release edit "v${nextVersion}" -F -`));
142
+ await check('git push', withRetry(() => shell.run('git push --no-verify --follow-tags'), {
143
+ onRetry: (attempt, error) => warn(`git push failed (attempt ${attempt}): ${error.message}, retrying...`),
144
+ }));
145
+ // github release (with retry for transient network failures)
146
+ const releaseTag = `v${nextVersion}`;
147
+ if (await check('check github release', shell.ok(`gh release view ${releaseTag}`))) {
148
+ await check('edit release', withRetry(() => shell.exec('gh', ['release', 'edit', releaseTag, '--notes', releaseNotes]), {
149
+ onRetry: (attempt, error) => warn(`gh release edit failed (attempt ${attempt}): ${error.message}, retrying...`),
150
+ }));
62
151
  }
63
152
  else {
64
- await check('create release', shell.run(`${releaseNotesPipe} | gh release create "v${nextVersion}" -F -`));
153
+ await check('create release', withRetry(() => shell.exec('gh', ['release', 'create', releaseTag, '--notes', releaseNotes]), {
154
+ onRetry: (attempt, error) => warn(`gh release create failed (attempt ${attempt}): ${error.message}, retrying...`),
155
+ }));
65
156
  }
66
157
  info('Finished');
67
158
  return;
159
+ async function verifyNpmAuth() {
160
+ try {
161
+ const username = await shell.stdout('npm whoami');
162
+ if (!username || username.trim().length === 0) {
163
+ throw releaseError('npm authentication failed: no username returned');
164
+ }
165
+ info(`authenticated as npm user: ${username.trim()}`);
166
+ }
167
+ catch {
168
+ throw releaseError('npm authentication required. Please run "npm login" first.\n' +
169
+ 'If you are using a CI environment, ensure NPM_TOKEN is set.');
170
+ }
171
+ }
68
172
  async function checkThatNoUncommittedChanges() {
69
173
  if ((await shell.stdout('git status --porcelain')).length < 3)
70
174
  return;
71
- throw Error('please commit all changes before releasing');
175
+ throw releaseError('please commit all changes before releasing');
72
176
  }
73
177
  async function setNextVersion(version) {
74
178
  // set new version in package.json
@@ -78,34 +182,76 @@ export async function release(directory, branch = 'main') {
78
182
  // rebuild package.json
79
183
  await shell.run('npm i --package-lock-only');
80
184
  }
81
- async function getReleaseNotes(version, hashLast, hashCurrent) {
82
- const commits = await getCommitsBetween(hashLast, hashCurrent);
83
- let notes = commits.reverse()
84
- .map(commit => '- ' + commit.message.replace(/\s+/g, ' '))
85
- .join('\n');
86
- notes = `# Release v${version}\n\nchanges:\n${notes}\n\n`;
185
+ function getReleaseNotes(version, commits) {
186
+ const reversed = [...commits].reverse();
187
+ const grouped = groupCommitsByType(reversed);
188
+ let notes = `# Release v${version}\n\n`;
189
+ // Order: breaking changes first, then features, fixes, and others
190
+ const typeOrder = [
191
+ 'feat',
192
+ 'fix',
193
+ 'perf',
194
+ 'refactor',
195
+ 'docs',
196
+ 'test',
197
+ 'build',
198
+ 'ci',
199
+ 'chore',
200
+ 'style',
201
+ 'revert',
202
+ 'other',
203
+ ];
204
+ // Add breaking changes section if any
205
+ const breakingCommits = reversed.filter((c) => c.breaking);
206
+ if (breakingCommits.length > 0) {
207
+ notes += '## Breaking Changes\n\n';
208
+ for (const commit of breakingCommits) {
209
+ notes += `- ${commit.description.replace(/\s+/g, ' ')}\n`;
210
+ }
211
+ notes += '\n';
212
+ }
213
+ // Add grouped commits
214
+ for (const type of typeOrder) {
215
+ const typeCommits = grouped.get(type);
216
+ if (!typeCommits || typeCommits.length === 0)
217
+ continue;
218
+ // Skip commits already shown in breaking changes
219
+ const nonBreaking = typeCommits.filter((c) => !c.breaking);
220
+ if (nonBreaking.length === 0)
221
+ continue;
222
+ const label = type === 'other' ? 'Other Changes' : COMMIT_TYPES[type];
223
+ notes += `## ${label}\n\n`;
224
+ for (const commit of nonBreaking) {
225
+ const scope = commit.scope ? `**${commit.scope}:** ` : '';
226
+ notes += `- ${scope}${commit.description.replace(/\s+/g, ' ')}\n`;
227
+ }
228
+ notes += '\n';
229
+ }
87
230
  return notes;
88
231
  }
89
- async function getNewVersion(versionPackage) {
90
- // ask for new version
91
- const choices = [
92
- { value: versionPackage },
93
- { ...bump(2) },
94
- { ...bump(1) },
95
- { ...bump(0) }
96
- ];
97
- const versionNew = (await select({
232
+ async function getNewVersion(versionPackage, commits) {
233
+ // Determine suggested bump based on conventional commits
234
+ const suggestedBump = getSuggestedBump(commits);
235
+ // choices: [current, patch, minor, major] -> indices [0, 1, 2, 3]
236
+ const suggestedIndex = suggestedBump === 'major' ? 3 : suggestedBump === 'minor' ? 2 : 1;
237
+ const choices = [{ value: versionPackage }, { ...bump(2) }, { ...bump(1) }, { ...bump(0) }];
238
+ // Add recommendation label to suggested version
239
+ const suggestedChoice = choices[suggestedIndex];
240
+ if (suggestedChoice && 'name' in suggestedChoice) {
241
+ suggestedChoice.name += ' (recommended)';
242
+ }
243
+ const versionNew = await select({
98
244
  message: 'What should be the new version?',
99
245
  choices,
100
- default: choices[1].value,
101
- }));
246
+ default: suggestedChoice?.value ?? choices[1].value,
247
+ });
102
248
  if (!versionNew)
103
- throw Error();
249
+ throw releaseError('no version selected');
104
250
  return versionNew;
105
251
  function bump(index) {
106
- const p = versionPackage.split('.').map(v => parseInt(v, 10));
252
+ const p = versionPackage.split('.').map((v) => parseInt(v, 10));
107
253
  if (p.length !== 3)
108
- throw Error();
254
+ throw validationError('invalid version format, expected x.y.z');
109
255
  switch (index) {
110
256
  case 0:
111
257
  p[0]++;
@@ -120,7 +266,7 @@ export async function release(directory, branch = 'main') {
120
266
  p[2]++;
121
267
  break;
122
268
  }
123
- const name = p.map((n, i) => (i == index) ? `\x1b[1m${n}` : `${n}`).join('.') + '\x1b[22m';
269
+ const name = p.map((n, i) => (i == index ? `\x1b[1m${n}` : `${n}`)).join('.') + '\x1b[22m';
124
270
  const value = p.join('.');
125
271
  return { name, value };
126
272
  }
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { upgradeDependencies } from './commands/deps-upgrade.js';
11
11
  import { generateDependencyGraph } from './commands/deps-graph.js';
12
12
  import { check } from './commands/check.js';
13
13
  import { generateTypescriptDocs } from './commands/doc-typescript.js';
14
+ import { setVerbose } from './lib/log.js';
14
15
  /**
15
16
  * Main CLI program, configured with custom text styling for titles, commands, options, etc.
16
17
  */
@@ -26,12 +27,19 @@ program.configureHelp({
26
27
  });
27
28
  program
28
29
  .name('vrt')
29
- .description('CLI tool for releasing packages and generating documentation for Node.js/TypeScript projects.');
30
+ .description('CLI tool for releasing packages and generating documentation for Node.js/TypeScript projects.')
31
+ .option('-v, --verbose', 'Enable verbose output')
32
+ .hook('preAction', (thisCommand) => {
33
+ const opts = thisCommand.opts();
34
+ if (opts.verbose)
35
+ setVerbose(true);
36
+ });
30
37
  /**
31
38
  * Command: check-package
32
39
  * Checks that the project's package.json includes certain required scripts/fields.
33
40
  */
34
- program.command('check')
41
+ program
42
+ .command('check')
35
43
  .description('Check repo for required scripts and other stuff.')
36
44
  .action(() => {
37
45
  void check(process.cwd());
@@ -40,7 +48,8 @@ program.command('check')
40
48
  * Command: deps-graph
41
49
  * Analyzes the project’s files to produce a dependency graph (in Mermaid format).
42
50
  */
43
- program.command('deps-graph')
51
+ program
52
+ .command('deps-graph')
44
53
  .description('Analyze project files and output a dependency graph as Mermaid markup.')
45
54
  .action(() => {
46
55
  void generateDependencyGraph(process.cwd());
@@ -49,7 +58,8 @@ program.command('deps-graph')
49
58
  * Command: deps-upgrade
50
59
  * Upgrades project dependencies in package.json to their latest versions and reinstalls them.
51
60
  */
52
- program.command('deps-upgrade')
61
+ program
62
+ .command('deps-upgrade')
53
63
  .description('Upgrade all dependencies in the current project to their latest versions.')
54
64
  .action(() => {
55
65
  void upgradeDependencies(process.cwd());
@@ -58,7 +68,8 @@ program.command('deps-upgrade')
58
68
  * Command: doc-command
59
69
  * Generates Markdown documentation for a given CLI command.
60
70
  */
61
- program.command('doc-command')
71
+ program
72
+ .command('doc-command')
62
73
  .description('Generate Markdown documentation for a specified command and output the result.')
63
74
  .argument('<command>', 'Command to document (e.g., "npm run build").')
64
75
  .action(async (command) => {
@@ -70,7 +81,8 @@ program.command('doc-command')
70
81
  * Inserts Markdown content from stdin into a specified Markdown file under a given heading.
71
82
  * Optionally makes the inserted content foldable.
72
83
  */
73
- program.command('doc-insert')
84
+ program
85
+ .command('doc-insert')
74
86
  .description('Insert Markdown from stdin into a specified section of a Markdown file.')
75
87
  .argument('<readme>', 'Path to the target Markdown file (e.g., README.md).', checkFilename)
76
88
  .argument('[heading]', 'Heading in the Markdown file where content should be placed. Default is "# API".', '# API')
@@ -80,8 +92,7 @@ program.command('doc-insert')
80
92
  for await (const data of process.stdin) {
81
93
  buffers.push(data);
82
94
  }
83
- const mdContent = '<!--- This chapter is generated automatically --->\n'
84
- + Buffer.concat(buffers).toString();
95
+ const mdContent = '<!--- This chapter is generated automatically --->\n' + Buffer.concat(buffers).toString();
85
96
  let mdFile = readFileSync(mdFilename, 'utf8');
86
97
  mdFile = injectMarkdown(mdFile, mdContent, heading, foldable);
87
98
  writeFileSync(mdFilename, mdFile);
@@ -90,7 +101,8 @@ program.command('doc-insert')
90
101
  * Command: doc-toc
91
102
  * Updates or generates a Table of Contents in a Markdown file under a specified heading.
92
103
  */
93
- program.command('doc-toc')
104
+ program
105
+ .command('doc-toc')
94
106
  .description('Generate a Table of Contents (TOC) in a Markdown file.')
95
107
  .argument('<readme>', 'Path to the Markdown file (e.g., README.md).', checkFilename)
96
108
  .argument('[heading]', 'Heading in the Markdown file where TOC should be inserted. Default is "# Table of Content".', '# Table of Content')
@@ -104,7 +116,8 @@ program.command('doc-toc')
104
116
  * Generates documentation for a TypeScript project.
105
117
  * Allows specifying entry point and output location.
106
118
  */
107
- program.command('doc-typescript')
119
+ program
120
+ .command('doc-typescript')
108
121
  .description('Generate documentation for a TypeScript project.')
109
122
  .option('-i, --input <entryPoint>', 'Entry point of the TypeScript project. Default is "./src/index.ts".')
110
123
  .option('-o, --output <outputPath>', 'Output path for the generated documentation. Default is "./docs".')
@@ -116,11 +129,13 @@ program.command('doc-typescript')
116
129
  * Command: release-npm
117
130
  * Releases/publishes an npm package from a specified project path to the npm registry.
118
131
  */
119
- program.command('release-npm')
132
+ program
133
+ .command('release-npm')
120
134
  .description('Publish an npm package from the specified path to the npm registry.')
135
+ .option('-n, --dry-run', 'Show what would be done without making any changes')
121
136
  .argument('[path]', 'Root path of the Node.js project. Defaults to the current directory.')
122
- .action((path) => {
123
- void release(resolve(path ?? '.', process.cwd()), 'main');
137
+ .action((path, options) => {
138
+ void release(resolve(process.cwd(), path ?? '.'), 'main', options.dryRun ?? false);
124
139
  });
125
140
  if (process.env.NODE_ENV !== 'test') {
126
141
  await program.parseAsync();
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Performance benchmarking utilities for CLI operations.
3
+ *
4
+ * Provides timing measurement and optional logging for tracking
5
+ * execution duration of key operations.
6
+ */
7
+ /**
8
+ * Result of a benchmarked operation
9
+ */
10
+ export interface BenchmarkResult<T> {
11
+ /** The result returned by the benchmarked function */
12
+ result: T;
13
+ /** Execution duration in milliseconds */
14
+ durationMs: number;
15
+ /** Human-readable duration string */
16
+ durationFormatted: string;
17
+ }
18
+ /**
19
+ * Options for benchmark execution
20
+ */
21
+ export interface BenchmarkOptions {
22
+ /** Label for the operation being benchmarked */
23
+ label?: string;
24
+ /** Whether to log timing to console */
25
+ log?: boolean;
26
+ /** Custom logger function (defaults to console.log) */
27
+ logger?: (message: string) => void;
28
+ }
29
+ /**
30
+ * Formats a duration in milliseconds to a human-readable string.
31
+ *
32
+ * @param ms - Duration in milliseconds
33
+ * @returns Formatted duration string (e.g., "1.23s", "456ms")
34
+ */
35
+ export declare function formatDuration(ms: number): string;
36
+ /**
37
+ * Measures the execution time of a synchronous function.
38
+ *
39
+ * @param fn - The function to benchmark
40
+ * @param options - Optional benchmark configuration
41
+ * @returns The function result along with timing information
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const { result, durationMs } = benchmarkSync(() => {
46
+ * return heavyComputation();
47
+ * }, { label: 'Heavy computation', log: true });
48
+ * ```
49
+ */
50
+ export declare function benchmarkSync<T>(fn: () => T, options?: BenchmarkOptions): BenchmarkResult<T>;
51
+ /**
52
+ * Measures the execution time of an asynchronous function.
53
+ *
54
+ * @param fn - The async function to benchmark
55
+ * @param options - Optional benchmark configuration
56
+ * @returns Promise resolving to the function result along with timing information
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const { result, durationMs } = await benchmark(async () => {
61
+ * return await fetchData();
62
+ * }, { label: 'API fetch', log: true });
63
+ * ```
64
+ */
65
+ export declare function benchmark<T>(fn: () => Promise<T>, options?: BenchmarkOptions): Promise<BenchmarkResult<T>>;
66
+ /**
67
+ * Creates a timer for manual timing control.
68
+ *
69
+ * @returns Timer object with start, stop, and elapsed methods
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * const timer = createTimer();
74
+ * timer.start();
75
+ * // ... do work ...
76
+ * const elapsed = timer.stop();
77
+ * console.log(`Operation took ${elapsed.durationFormatted}`);
78
+ * ```
79
+ */
80
+ export declare function createTimer(): {
81
+ start: () => void;
82
+ stop: () => {
83
+ durationMs: number;
84
+ durationFormatted: string;
85
+ };
86
+ elapsed: () => number;
87
+ isRunning: () => boolean;
88
+ };
89
+ /**
90
+ * Aggregates multiple benchmark results for statistical analysis.
91
+ */
92
+ export interface BenchmarkStats {
93
+ /** Number of samples */
94
+ count: number;
95
+ /** Total duration in milliseconds */
96
+ totalMs: number;
97
+ /** Average duration in milliseconds */
98
+ avgMs: number;
99
+ /** Minimum duration in milliseconds */
100
+ minMs: number;
101
+ /** Maximum duration in milliseconds */
102
+ maxMs: number;
103
+ /** Formatted average duration */
104
+ avgFormatted: string;
105
+ }
106
+ /**
107
+ * Calculates statistics from an array of benchmark durations.
108
+ *
109
+ * @param durations - Array of duration values in milliseconds
110
+ * @returns Statistical summary of the benchmarks
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const durations = [100, 150, 120, 130, 110];
115
+ * const stats = calculateStats(durations);
116
+ * console.log(`Average: ${stats.avgFormatted}`);
117
+ * ```
118
+ */
119
+ export declare function calculateStats(durations: number[]): BenchmarkStats;