@versatiles/release-tool 2.5.0 → 2.6.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 CHANGED
@@ -51,6 +51,7 @@ Node.js/TypeScript projects.
51
51
 
52
52
  Options:
53
53
  -h, --help display help for command
54
+ -v, --verbose Enable verbose output
54
55
 
55
56
  Commands:
56
57
  check Check repo for required scripts and other stuff.
@@ -61,7 +62,7 @@ Commands:
61
62
  doc-toc <readme> [heading] Generate a Table of Contents (TOC) in a Markdown file.
62
63
  doc-typescript [options] Generate documentation for a TypeScript project.
63
64
  help [command] display help for command
64
- release-npm [path] Publish an npm package from the specified path to the npm registry.
65
+ release-npm [options] [path] Publish an npm package from the specified path to the npm registry.
65
66
  ```
66
67
 
67
68
  ## Subcommand: `vrt check`
@@ -178,11 +179,12 @@ Usage: vrt release-npm [options] [path]
178
179
  Publish an npm package from the specified path to the npm registry.
179
180
 
180
181
  Arguments:
181
- path Root path of the Node.js project. Defaults to the current
182
- directory.
182
+ path Root path of the Node.js project. Defaults to the current
183
+ directory.
183
184
 
184
185
  Options:
185
- -h, --help display help for command
186
+ -h, --help display help for command
187
+ -n, --dry-run Show what would be done without making any changes
186
188
  ```
187
189
 
188
190
  # Development
@@ -220,6 +222,7 @@ end
220
222
  5-->4
221
223
  6-->4
222
224
  6-->7
225
+ 7-->4
223
226
  8-->9
224
227
  A-->4
225
228
  B-->9
@@ -234,6 +237,7 @@ E-->8
234
237
  E-->A
235
238
  E-->B
236
239
  E-->C
240
+ E-->4
237
241
 
238
242
  class 0,1,3 subgraphs;
239
243
  classDef subgraphs fill-opacity:0.1, fill:#888, color:#888, stroke:#888;
@@ -17,8 +17,8 @@ export function checkPackage(directory) {
17
17
  info('scripts.doc is recommended');
18
18
  if (!scripts.build)
19
19
  warn('scripts.build is required');
20
- else if (!scripts.build.includes("npm run doc")) {
21
- warn(`scripts.build should include "npm run doc-graph", but is "${scripts.build}"`);
20
+ else if (!scripts.build.includes('npm run doc')) {
21
+ warn(`scripts.build should include "npm run doc", but is "${scripts.build}"`);
22
22
  }
23
23
  if (!scripts.check)
24
24
  warn('scripts.check is required');
@@ -6,7 +6,7 @@ export async function generateDependencyGraph(directory) {
6
6
  cruiseResult = await cruise([directory], {
7
7
  includeOnly: '^src',
8
8
  outputType: 'mermaid',
9
- exclude: ["\\.(test|d)\\.ts$", "node_modules", "__mocks__/"],
9
+ exclude: ['\\.(test|d)\\.ts$', 'node_modules', '__mocks__/'],
10
10
  });
11
11
  }
12
12
  catch (pError) {
@@ -36,18 +36,18 @@ async function getCommandResults(command) {
36
36
  NODE_ENV: undefined,
37
37
  NODE_DISABLE_COLORS: '1',
38
38
  NO_COLORS: '1',
39
- FORCE_COLOR: '0'
39
+ FORCE_COLOR: '0',
40
40
  };
41
41
  // Spawn a child process to run the command with the '--help' flag.
42
42
  const childProcess = cp.spawn('npm', ['--offline', 'exec', '--', ...command.split(' '), '--help'], { env });
43
43
  let output = '';
44
44
  // Collect output data from the process.
45
- childProcess.stdout.on('data', data => output += String(data));
46
- childProcess.stderr.on('data', data => {
45
+ childProcess.stdout.on('data', (data) => (output += String(data)));
46
+ childProcess.stderr.on('data', (data) => {
47
47
  console.error(`stderr: ${data}`);
48
48
  });
49
49
  // Handle process errors.
50
- childProcess.on('error', error => {
50
+ childProcess.on('error', (error) => {
51
51
  reject(new Error(`Failed to start subprocess: ${error.message}`));
52
52
  });
53
53
  // Handle process exit.
@@ -72,8 +72,8 @@ async function getCommandResults(command) {
72
72
  */
73
73
  function extractSubcommands(result) {
74
74
  return result
75
- .replace(/.*\nCommands:/msgi, '') // Remove everything before the "Commands:" section.
76
- .replace(/\n[a-z]+:.*/msi, '') // Remove everything after the subcommands list.
75
+ .replace(/.*\nCommands:/gims, '') // Remove everything before the "Commands:" section.
76
+ .replace(/\n[a-z]+:.*/ims, '') // Remove everything after the subcommands list.
77
77
  .split('\n') // Split by newline to process each line.
78
78
  .flatMap((line) => {
79
79
  // Extract subcommand names from each line.
@@ -16,11 +16,7 @@ export async function generateTypescriptDocs(options) {
16
16
  logLevel: quiet ? 'Warn' : 'Info',
17
17
  highlightLanguages: ['typescript', 'javascript', 'json', 'shell', 'bash', 'sh', 'css', 'html'],
18
18
  groupOrder: ['Classes', 'Variables', 'Functions', '*'],
19
- }, [
20
- new td.TypeDocReader(),
21
- new td.PackageJsonReader(),
22
- new td.TSConfigReader(),
23
- ]);
19
+ }, [new td.TypeDocReader(), new td.PackageJsonReader(), new td.TSConfigReader()]);
24
20
  app.options.setValue('readme', 'none');
25
21
  if (isMarkdown) {
26
22
  app.options.setValue('hidePageHeader', true);
@@ -2,6 +2,18 @@ import { remark } from 'remark';
2
2
  import remarkGfm from 'remark-gfm';
3
3
  import remarkStringify from 'remark-stringify';
4
4
  import { getErrorMessage } from '../lib/utils.js';
5
+ // Custom blockquote handler that preserves GitHub alert syntax
6
+ function blockquoteHandler(node, _parent, state, info) {
7
+ const exit = state.enter('blockquote');
8
+ const tracker = state.createTracker(info);
9
+ tracker.move('> ');
10
+ tracker.shift(2);
11
+ let value = state.indentLines(state.containerFlow(node, tracker.current()), (line, _index, blank) => '>' + (blank ? '' : ' ') + line);
12
+ exit();
13
+ // Unescape GitHub alert markers that remark-stringify escapes
14
+ value = value.replace(/^(>\s*)\\\[!(NOTE|WARNING|TIP|IMPORTANT|CAUTION)\]/m, '$1[!$2]');
15
+ return value;
16
+ }
5
17
  /**
6
18
  * Injects a Markdown segment under a specified heading in a Markdown document.
7
19
  * Optionally, the injected segment can be made foldable for better readability.
@@ -45,6 +57,9 @@ export function injectMarkdown(document, segment, heading, foldable) {
45
57
  .use(remarkStringify, {
46
58
  bullet: '-',
47
59
  rule: '-',
60
+ handlers: {
61
+ blockquote: blockquoteHandler,
62
+ },
48
63
  })
49
64
  .stringify(documentAst);
50
65
  return result;
@@ -62,7 +77,7 @@ export function updateTOC(main, heading) {
62
77
  const headingText = extractTextFromMDAsHTML(parseMarkdown(heading));
63
78
  // Build the TOC by iterating over each heading in the document.
64
79
  const toc = mainAst.children
65
- .flatMap(c => {
80
+ .flatMap((c) => {
66
81
  // Skip non-heading nodes and the specified heading.
67
82
  if (c.type !== 'heading' || extractTextFromMDAsHTML(c) === headingText)
68
83
  return [];
@@ -89,12 +104,12 @@ function findSegmentStartIndex(mainAst, headingAst) {
89
104
  if (headingAst.children.length !== 1)
90
105
  throw Error('headingAst.children.length !== 1');
91
106
  if (headingAst.children[0].type !== 'heading')
92
- throw Error('headingAst.children[0].type !== \'heading\'');
107
+ throw Error("headingAst.children[0].type !== 'heading'");
93
108
  const sectionDepth = headingAst.children[0].depth;
94
109
  const sectionText = extractTextFromMDAsHTML(headingAst);
95
110
  // Search for the index of the heading in the main document AST.
96
111
  const indexes = mainAst.children.flatMap((c, index) => {
97
- if ((c.type === 'heading') && (c.depth === sectionDepth) && extractTextFromMDAsHTML(c).startsWith(sectionText)) {
112
+ if (c.type === 'heading' && c.depth === sectionDepth && extractTextFromMDAsHTML(c).startsWith(sectionText)) {
98
113
  return [index];
99
114
  }
100
115
  return [];
@@ -133,7 +148,7 @@ function findNextHeadingIndex(mainAst, startIndex, depth) {
133
148
  function getHeadingDepth(mainAst, index) {
134
149
  const node = mainAst.children[index];
135
150
  if (node.type !== 'heading')
136
- throw Error('node.type !== \'heading\'');
151
+ throw Error("node.type !== 'heading'");
137
152
  return node.depth;
138
153
  }
139
154
  /**
@@ -142,9 +157,9 @@ function getHeadingDepth(mainAst, index) {
142
157
  * @param depth The depth to which the segment should be indented.
143
158
  */
144
159
  function indentSegmentToDepth(segmentAst, depth) {
145
- segmentAst.children.forEach(node => {
160
+ segmentAst.children.forEach((node) => {
146
161
  if (node.type == 'heading')
147
- return node.depth += depth;
162
+ return (node.depth += depth);
148
163
  });
149
164
  }
150
165
  /**
@@ -174,7 +189,6 @@ function extractTextFromMDAsHTML(node) {
174
189
  case 'html':
175
190
  return '';
176
191
  default:
177
- console.log(node);
178
192
  throw Error('unknown type: ' + node.type);
179
193
  }
180
194
  }
@@ -200,12 +214,12 @@ function getMDAnchor(node) {
200
214
  text += c.value;
201
215
  break;
202
216
  default:
203
- console.log(c);
204
217
  throw Error('unknown type: ' + c.type);
205
218
  }
206
219
  }
207
220
  // Format the text to create a suitable anchor ID.
208
- text = text.toLowerCase()
221
+ text = text
222
+ .toLowerCase()
209
223
  .replace(/[()]+/g, '')
210
224
  .replace(/[^a-z0-9]+/g, '-')
211
225
  .replace(/^-+|-+$/g, '');
@@ -238,7 +252,7 @@ function convertToFoldable(ast) {
238
252
  closeDetails(0);
239
253
  ast.children = children;
240
254
  function closeDetails(depth) {
241
- while ((openDetails.length > 0) && (openDetails[0] >= depth)) {
255
+ while (openDetails.length > 0 && openDetails[0] >= depth) {
242
256
  children.push({ type: 'html', value: '</details>' });
243
257
  openDetails.shift();
244
258
  }
@@ -273,11 +287,13 @@ export function nodeToHtml(node) {
273
287
  attributes.push(`title="${node.title}"`);
274
288
  return `<img ${attributes.join(' ')} />`;
275
289
  }
276
- case 'footnoteReference': throw new Error('Not implemented yet: "footnoteReference" case');
277
- case 'imageReference': throw new Error('Not implemented yet: "imageReference" case');
278
- case 'linkReference': throw new Error('Not implemented yet: "linkReference" case');
290
+ case 'footnoteReference':
291
+ throw new Error('Not implemented yet: "footnoteReference" case');
292
+ case 'imageReference':
293
+ throw new Error('Not implemented yet: "imageReference" case');
294
+ case 'linkReference':
295
+ throw new Error('Not implemented yet: "linkReference" case');
279
296
  default:
280
- console.log(node);
281
297
  throw Error('unknown type');
282
298
  }
283
299
  }
@@ -285,7 +301,7 @@ function nodesToHtml(children) {
285
301
  return children.map(nodeToHtml).join('');
286
302
  }
287
303
  function textToHtml(text) {
288
- return text.replace(/[^a-z0-9 ,.\-:_?@äöüß]/gi, c => `&#${c.charCodeAt(0)};`);
304
+ return text.replace(/[^a-z0-9 ,.\-:_?@äöüß]/gi, (c) => `&#${c.charCodeAt(0)};`);
289
305
  }
290
306
  export function parseMarkdown(document) {
291
307
  return remark().use(remarkGfm).parse(document);
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env npx tsx
2
- export declare function release(directory: string, branch?: string): Promise<void>;
2
+ export declare function release(directory: string, branch?: string, dryRun?: boolean): Promise<void>;
@@ -5,10 +5,19 @@ import { check, info, panic, warn } from '../lib/log.js';
5
5
  import { Shell } from '../lib/shell.js';
6
6
  import { getGit } from '../lib/git.js';
7
7
  import { resolve } from 'path';
8
- export async function release(directory, branch = 'main') {
8
+ function isValidPackageJson(pkg) {
9
+ if (typeof pkg !== 'object' || pkg === null)
10
+ return false;
11
+ if (!('version' in pkg) || typeof pkg.version !== 'string')
12
+ return false;
13
+ if (!('scripts' in pkg) || typeof pkg.scripts !== 'object' || pkg.scripts === null)
14
+ return false;
15
+ return true;
16
+ }
17
+ export async function release(directory, branch = 'main', dryRun = false) {
9
18
  const shell = new Shell(directory);
10
19
  const { getCommitsBetween, getCurrentGitHubCommit, getLastGitHubTag } = getGit(directory);
11
- info('starting release process');
20
+ info(dryRun ? 'starting release process (dry-run)' : 'starting release process');
12
21
  // git: check if in the correct branch
13
22
  const currentBranch = await check('get branch name', shell.stdout('git rev-parse --abbrev-ref HEAD'));
14
23
  if (currentBranch !== branch)
@@ -18,17 +27,14 @@ export async function release(directory, branch = 'main') {
18
27
  // git: pull
19
28
  await check('git pull', shell.run('git pull -t'));
20
29
  // check package.json
21
- const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'));
22
- if (typeof pkg !== 'object' || pkg === null)
30
+ const pkgRaw = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'));
31
+ if (!isValidPackageJson(pkgRaw))
23
32
  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))
33
+ if (!('check' in pkgRaw.scripts))
29
34
  panic('missing npm script "check" in package.json');
30
- if (!('prepack' in pkg.scripts))
35
+ if (!('prepack' in pkgRaw.scripts))
31
36
  panic('missing npm script "prepack" in package.json');
37
+ const pkg = pkgRaw;
32
38
  // get last version
33
39
  const tag = await check('get last github tag', getLastGitHubTag());
34
40
  const shaLast = tag?.sha;
@@ -40,12 +46,31 @@ export async function release(directory, branch = 'main') {
40
46
  const { sha: shaCurrent } = await check('get current github commit', getCurrentGitHubCommit());
41
47
  // handle version
42
48
  const nextVersion = await getNewVersion(versionLastPackage);
49
+ // prepare release notes
50
+ const releaseNotes = await check('prepare release notes', getReleaseNotes(nextVersion, shaLast, shaCurrent));
51
+ if (dryRun) {
52
+ info('Dry-run mode - the following actions would be performed:');
53
+ info(` Version: ${versionLastPackage} -> ${nextVersion}`);
54
+ info(' Release notes:');
55
+ releaseNotes.split('\n').forEach((line) => info(` ${line}`));
56
+ info(' Commands that would be executed:');
57
+ info(' npm run check');
58
+ info(' npm i --package-lock-only');
59
+ if (!('private' in pkg) || !pkg.private) {
60
+ info(' npm publish --access public');
61
+ }
62
+ info(' git add .');
63
+ info(` git commit -m "v${nextVersion}"`);
64
+ info(` git tag -f -a "v${nextVersion}" -m "new release: v${nextVersion}"`);
65
+ info(' git push --no-verify --follow-tags');
66
+ info(` gh release create/edit "v${nextVersion}"`);
67
+ info('Dry-run complete - no changes were made');
68
+ return;
69
+ }
43
70
  // test
44
71
  await check('run checks', shell.run('npm run check'));
45
72
  // update version
46
73
  await check('update version', setNextVersion(nextVersion));
47
- // prepare release notes
48
- const releaseNotes = await check('prepare release notes', getReleaseNotes(nextVersion, shaLast, shaCurrent));
49
74
  if (!('private' in pkg) || !pkg.private) {
50
75
  // npm publish
51
76
  await check('npm publish', shell.runInteractive('npm publish --access public'));
@@ -56,12 +81,12 @@ export async function release(directory, branch = 'main') {
56
81
  await check('git tag', shell.run(`git tag -f -a "v${nextVersion}" -m "new release: v${nextVersion}"`));
57
82
  await check('git push', shell.run('git push --no-verify --follow-tags'));
58
83
  // 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 -`));
84
+ const releaseTag = `v${nextVersion}`;
85
+ if (await check('check github release', shell.ok(`gh release view ${releaseTag}`))) {
86
+ await check('edit release', shell.exec('gh', ['release', 'edit', releaseTag, '--notes', releaseNotes]));
62
87
  }
63
88
  else {
64
- await check('create release', shell.run(`${releaseNotesPipe} | gh release create "v${nextVersion}" -F -`));
89
+ await check('create release', shell.exec('gh', ['release', 'create', releaseTag, '--notes', releaseNotes]));
65
90
  }
66
91
  info('Finished');
67
92
  return;
@@ -80,30 +105,26 @@ export async function release(directory, branch = 'main') {
80
105
  }
81
106
  async function getReleaseNotes(version, hashLast, hashCurrent) {
82
107
  const commits = await getCommitsBetween(hashLast, hashCurrent);
83
- let notes = commits.reverse()
84
- .map(commit => '- ' + commit.message.replace(/\s+/g, ' '))
108
+ let notes = commits
109
+ .reverse()
110
+ .map((commit) => '- ' + commit.message.replace(/\s+/g, ' '))
85
111
  .join('\n');
86
112
  notes = `# Release v${version}\n\nchanges:\n${notes}\n\n`;
87
113
  return notes;
88
114
  }
89
115
  async function getNewVersion(versionPackage) {
90
116
  // 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({
117
+ const choices = [{ value: versionPackage }, { ...bump(2) }, { ...bump(1) }, { ...bump(0) }];
118
+ const versionNew = await select({
98
119
  message: 'What should be the new version?',
99
120
  choices,
100
121
  default: choices[1].value,
101
- }));
122
+ });
102
123
  if (!versionNew)
103
124
  throw Error();
104
125
  return versionNew;
105
126
  function bump(index) {
106
- const p = versionPackage.split('.').map(v => parseInt(v, 10));
127
+ const p = versionPackage.split('.').map((v) => parseInt(v, 10));
107
128
  if (p.length !== 3)
108
129
  throw Error();
109
130
  switch (index) {
@@ -120,7 +141,7 @@ export async function release(directory, branch = 'main') {
120
141
  p[2]++;
121
142
  break;
122
143
  }
123
- const name = p.map((n, i) => (i == index) ? `\x1b[1m${n}` : `${n}`).join('.') + '\x1b[22m';
144
+ const name = p.map((n, i) => (i == index ? `\x1b[1m${n}` : `${n}`)).join('.') + '\x1b[22m';
124
145
  const value = p.join('.');
125
146
  return { name, value };
126
147
  }
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();
package/dist/lib/git.js CHANGED
@@ -9,19 +9,19 @@ export function getGit(cwd) {
9
9
  async function getLastGitHubTag() {
10
10
  const commits = await getAllCommits();
11
11
  const result = commits
12
- .map(commit => ({
12
+ .map((commit) => ({
13
13
  sha: commit.sha,
14
- version: commit.tag?.match(/^v(\d+\.\d+\.\d+)$/)?.[1],
14
+ version: commit.tag?.match(/^v(\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?)$/)?.[1],
15
15
  }))
16
- .find(r => r.version);
16
+ .find((r) => r.version);
17
17
  return result;
18
18
  }
19
19
  async function getAllCommits() {
20
- const result = await shell.stdout('git log --pretty=format:\'⍃%H⍄%s⍄%D⍄\'');
20
+ const result = await shell.stdout("git log --pretty=format:'⍃%H⍄%s⍄%D'");
21
21
  return result
22
22
  .split('⍃')
23
- .filter(line => line.length > 2)
24
- .map(line => {
23
+ .filter((line) => line.length > 2)
24
+ .map((line) => {
25
25
  const obj = line.split('⍄');
26
26
  return {
27
27
  sha: obj[0],
@@ -35,10 +35,10 @@ export function getGit(cwd) {
35
35
  }
36
36
  async function getCommitsBetween(shaLast, shaCurrent) {
37
37
  let commits = await getAllCommits();
38
- const start = commits.findIndex(commit => commit.sha === shaCurrent);
38
+ const start = commits.findIndex((commit) => commit.sha === shaCurrent);
39
39
  if (start >= 0)
40
40
  commits = commits.slice(start);
41
- const end = commits.findIndex(commit => commit.sha === shaLast);
41
+ const end = commits.findIndex((commit) => commit.sha === shaLast);
42
42
  if (end >= 0)
43
43
  commits = commits.slice(0, end);
44
44
  return commits;
package/dist/lib/log.d.ts CHANGED
@@ -1,5 +1,8 @@
1
+ export declare function setVerbose(enabled: boolean): void;
2
+ export declare function isVerbose(): boolean;
1
3
  export declare function panic(text: string): never;
2
4
  export declare function warn(text: string): void;
3
5
  export declare function info(text: string): void;
6
+ export declare function debug(text: string): void;
4
7
  export declare function abort(): never;
5
- export declare function check<T>(message: string, promise: (Promise<T>) | (() => Promise<T>)): Promise<T>;
8
+ export declare function check<T>(message: string, promise: Promise<T> | (() => Promise<T>)): Promise<T>;
package/dist/lib/log.js CHANGED
@@ -1,3 +1,10 @@
1
+ let verboseMode = false;
2
+ export function setVerbose(enabled) {
3
+ verboseMode = enabled;
4
+ }
5
+ export function isVerbose() {
6
+ return verboseMode;
7
+ }
1
8
  export function panic(text) {
2
9
  process.stderr.write(`\x1b[1;31m! ERROR: ${text}\x1b[0m\n`);
3
10
  abort();
@@ -8,9 +15,14 @@ export function warn(text) {
8
15
  export function info(text) {
9
16
  process.stderr.write(`\x1b[0mi ${text}\n`);
10
17
  }
18
+ export function debug(text) {
19
+ if (verboseMode) {
20
+ process.stderr.write(`\x1b[0;90m ${text}\x1b[0m\n`);
21
+ }
22
+ }
11
23
  export function abort() {
12
24
  info('abort');
13
- process.exit();
25
+ process.exit(1);
14
26
  }
15
27
  export async function check(message, promise) {
16
28
  process.stderr.write(`\x1b[0;90m\u2B95 ${message}\x1b[0m`);
@@ -22,7 +34,6 @@ export async function check(message, promise) {
22
34
  catch (error) {
23
35
  process.stderr.write(`\r\x1b[0;91m\u2718 ${message}\x1b[0m\n`);
24
36
  panic(error.message);
25
- throw error;
26
37
  }
27
38
  }
28
39
  //# sourceMappingURL=log.js.map
@@ -7,6 +7,12 @@ export declare class Shell {
7
7
  stdout: string;
8
8
  stderr: string;
9
9
  }>;
10
+ exec(command: string, args: string[], errorOnCodeNonZero?: boolean, skipLog?: boolean): Promise<{
11
+ code: number | null;
12
+ signal: string | null;
13
+ stdout: string;
14
+ stderr: string;
15
+ }>;
10
16
  runInteractive(command: string, errorOnCodeNonZero?: boolean): Promise<{
11
17
  code: number | null;
12
18
  signal: string | null;
package/dist/lib/shell.js CHANGED
@@ -1,38 +1,48 @@
1
1
  import { spawn } from 'child_process';
2
+ import { debug, isVerbose } from './log.js';
2
3
  export class Shell {
3
4
  cwd;
4
5
  constructor(cwd) {
5
6
  this.cwd = cwd;
6
7
  }
7
8
  async run(command, errorOnCodeNonZero = true) {
8
- try {
9
- return await new Promise((resolve, reject) => {
10
- const stdout = [];
11
- const stderr = [];
12
- const cp = spawn('bash', ['-c', command], { cwd: this.cwd })
13
- .on('error', error => reject(error))
14
- .on('close', (code, signal) => {
15
- const result = {
16
- code,
17
- signal,
18
- stdout: Buffer.concat(stdout).toString(),
19
- stderr: Buffer.concat(stderr).toString(),
20
- };
21
- if (errorOnCodeNonZero && code !== 0) {
22
- reject(result);
23
- }
24
- else {
25
- resolve(result);
26
- }
27
- });
28
- cp.stdout.on('data', chunk => stdout.push(chunk));
29
- cp.stderr.on('data', chunk => stderr.push(chunk));
30
- });
31
- }
32
- catch (error) {
33
- console.error(error);
34
- throw error;
9
+ debug(`$ ${command}`);
10
+ return this.exec('bash', ['-c', command], errorOnCodeNonZero, true);
11
+ }
12
+ // Execute a command with arguments directly, avoiding shell escaping issues
13
+ async exec(command, args, errorOnCodeNonZero = true, skipLog = false) {
14
+ if (!skipLog) {
15
+ debug(`$ ${command} ${args.join(' ')}`);
35
16
  }
17
+ return await new Promise((resolve, reject) => {
18
+ const stdout = [];
19
+ const stderr = [];
20
+ const cp = spawn(command, args, { cwd: this.cwd })
21
+ .on('error', (error) => reject(error))
22
+ .on('close', (code, signal) => {
23
+ const result = {
24
+ code,
25
+ signal,
26
+ stdout: Buffer.concat(stdout).toString(),
27
+ stderr: Buffer.concat(stderr).toString(),
28
+ };
29
+ if (isVerbose()) {
30
+ if (result.stdout)
31
+ result.stdout.split('\n').forEach((line) => debug(` stdout: ${line}`));
32
+ if (result.stderr)
33
+ result.stderr.split('\n').forEach((line) => debug(` stderr: ${line}`));
34
+ debug(` exit code: ${code}`);
35
+ }
36
+ if (errorOnCodeNonZero && code !== 0) {
37
+ reject(result);
38
+ }
39
+ else {
40
+ resolve(result);
41
+ }
42
+ });
43
+ cp.stdout.on('data', (chunk) => stdout.push(chunk));
44
+ cp.stderr.on('data', (chunk) => stderr.push(chunk));
45
+ });
36
46
  }
37
47
  // Runs a command interactively, so the user can interact with stdin/stdout/stderr directly.
38
48
  async runInteractive(command, errorOnCodeNonZero = true) {
package/dist/lib/utils.js CHANGED
@@ -24,10 +24,22 @@ export function prettyStyleJSON(inputData) {
24
24
  return singleLine(data);
25
25
  if (typeof data === 'object') {
26
26
  if (Array.isArray(data)) {
27
- return '[\n\t' + prefix + data.map((value) => recursive(value, prefix + '\t', path + '[]')).join(',\n\t' + prefix) + '\n' + prefix + ']';
27
+ return ('[\n\t' +
28
+ prefix +
29
+ data.map((value) => recursive(value, prefix + '\t', path + '[]')).join(',\n\t' + prefix) +
30
+ '\n' +
31
+ prefix +
32
+ ']');
28
33
  }
29
34
  if (data) {
30
- return '{\n\t' + prefix + Object.entries(data).map(([key, value]) => '"' + key + '": ' + recursive(value, prefix + '\t', path + '.' + key)).join(',\n\t' + prefix) + '\n' + prefix + '}';
35
+ return ('{\n\t' +
36
+ prefix +
37
+ Object.entries(data)
38
+ .map(([key, value]) => '"' + key + '": ' + recursive(value, prefix + '\t', path + '.' + key))
39
+ .join(',\n\t' + prefix) +
40
+ '\n' +
41
+ prefix +
42
+ '}');
31
43
  }
32
44
  }
33
45
  return singleLine(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatiles/release-tool",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "VersaTiles release and documentation tools",
5
5
  "bin": {
6
6
  "vrt": "./dist/index.js"
@@ -12,11 +12,13 @@
12
12
  "scripts": {
13
13
  "build": "npm run build-node && npm run doc",
14
14
  "build-node": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/index.js",
15
- "check": "npm run lint && npm run build && npm run test",
15
+ "check": "npm run lint && npm run build && npm run test && npm run format:check",
16
16
  "dev": "tsx src/index.ts",
17
17
  "doc": "npm run doc-command && npm run doc-graph",
18
18
  "doc-command": "tsx src/index.ts doc-command vrt | tsx src/index.ts doc-insert README.md '# Command'",
19
19
  "doc-graph": "tsx src/index.ts deps-graph | tsx src/index.ts doc-insert README.md '## Dependency Graph'",
20
+ "format": "prettier --write .",
21
+ "format:check": "prettier --check .",
20
22
  "lint": "eslint . --color",
21
23
  "prepack": "npm run build",
22
24
  "release": "tsx src/index.ts release-npm",
@@ -27,6 +29,9 @@
27
29
  "author": "Michael Kreil <versatiles@michael-kreil.de>",
28
30
  "license": "Unlicense",
29
31
  "type": "module",
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
30
35
  "repository": {
31
36
  "type": "git",
32
37
  "url": "git+https://github.com/versatiles-org/node-release-tool.git"
@@ -34,24 +39,25 @@
34
39
  "homepage": "https://github.com/versatiles-org/node-release-tool",
35
40
  "devDependencies": {
36
41
  "@schemastore/package": "^0.0.10",
37
- "@types/node": "^24.10.1",
38
- "@typescript-eslint/eslint-plugin": "^8.48.1",
39
- "@typescript-eslint/parser": "^8.48.1",
40
- "@vitest/coverage-v8": "^4.0.15",
41
- "eslint": "^9.39.1",
42
+ "@types/node": "^25.1.0",
43
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
44
+ "@typescript-eslint/parser": "^8.54.0",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "eslint": "^9.39.2",
47
+ "prettier": "^3.8.1",
42
48
  "tsx": "^4.21.0",
43
49
  "typescript": "^5.9.3",
44
- "typescript-eslint": "^8.48.1",
45
- "vitest": "^4.0.15"
50
+ "typescript-eslint": "^8.54.0",
51
+ "vitest": "^4.0.18"
46
52
  },
47
53
  "dependencies": {
48
- "@inquirer/select": "^5.0.2",
54
+ "@inquirer/select": "^5.0.4",
49
55
  "commander": "^14.0.2",
50
- "dependency-cruiser": "^17.3.2",
51
- "npm-check-updates": "^19.1.2",
56
+ "dependency-cruiser": "^17.3.7",
57
+ "npm-check-updates": "^19.3.2",
52
58
  "remark": "^15.0.1",
53
59
  "remark-gfm": "^4.0.1",
54
- "typedoc": "^0.28.15",
60
+ "typedoc": "^0.28.16",
55
61
  "typedoc-github-theme": "^0.3.1",
56
62
  "typedoc-github-wiki-theme": "^2.1.0",
57
63
  "typedoc-plugin-markdown": "^4.9.0"