@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.
- package/README.md +44 -30
- package/dist/commands/check.d.ts +24 -0
- package/dist/commands/check.js +27 -3
- package/dist/commands/deps-graph.d.ts +22 -0
- package/dist/commands/deps-graph.js +23 -1
- package/dist/commands/doc-command.js +6 -6
- package/dist/commands/doc-typescript.d.ts +39 -3
- package/dist/commands/doc-typescript.js +25 -5
- package/dist/commands/markdown.d.ts +9 -1
- package/dist/commands/markdown.js +167 -38
- package/dist/commands/release-npm.d.ts +44 -1
- package/dist/commands/release-npm.js +195 -49
- package/dist/index.js +28 -13
- package/dist/lib/benchmark.d.ts +119 -0
- package/dist/lib/benchmark.js +148 -0
- package/dist/lib/changelog.d.ts +23 -0
- package/dist/lib/changelog.js +117 -0
- package/dist/lib/errors.d.ts +32 -0
- package/dist/lib/errors.js +47 -0
- package/dist/lib/git.d.ts +92 -0
- package/dist/lib/git.js +120 -8
- package/dist/lib/log.d.ts +61 -1
- package/dist/lib/log.js +76 -3
- package/dist/lib/retry.d.ts +24 -0
- package/dist/lib/retry.js +44 -0
- package/dist/lib/shell.d.ts +131 -10
- package/dist/lib/shell.js +142 -28
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.js +43 -2
- package/package.json +27 -14
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
22
|
-
if (
|
|
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 (!('
|
|
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
|
|
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
|
-
//
|
|
42
|
-
const
|
|
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
|
-
//
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
];
|
|
97
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?? '.'
|
|
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;
|