changelog-machine 1.1.0 → 2.1.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.
Files changed (86) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +149 -75
  3. package/bin/cli.js +5 -1
  4. package/lib/changelog/contributors.d.ts +2 -0
  5. package/lib/changelog/contributors.js +31 -0
  6. package/lib/changelog/links.d.ts +31 -0
  7. package/lib/changelog/links.js +83 -0
  8. package/lib/changelog/render.d.ts +2 -0
  9. package/lib/changelog/render.js +69 -0
  10. package/lib/changelog/text.d.ts +5 -0
  11. package/lib/changelog/text.js +23 -0
  12. package/lib/changelog/update.d.ts +7 -0
  13. package/lib/changelog/update.js +119 -0
  14. package/lib/cli/files.d.ts +5 -0
  15. package/lib/cli/files.js +37 -0
  16. package/lib/cli/index.d.ts +1 -1
  17. package/lib/cli/index.js +68 -39
  18. package/lib/cli/options.d.ts +14 -0
  19. package/lib/cli/options.js +57 -0
  20. package/lib/cli/plans.d.ts +7 -0
  21. package/lib/cli/plans.js +67 -0
  22. package/lib/cli/progress.d.ts +2 -0
  23. package/lib/cli/progress.js +37 -0
  24. package/lib/commits/parse.d.ts +3 -0
  25. package/lib/commits/parse.js +87 -0
  26. package/lib/contributors/authors.d.ts +6 -0
  27. package/lib/contributors/authors.js +20 -0
  28. package/lib/contributors/privacy.d.ts +2 -0
  29. package/lib/contributors/privacy.js +9 -0
  30. package/lib/contributors/profile.d.ts +5 -0
  31. package/lib/contributors/profile.js +51 -0
  32. package/lib/contributors/repository.d.ts +7 -0
  33. package/lib/contributors/repository.js +36 -0
  34. package/lib/contributors/resolve.d.ts +10 -0
  35. package/lib/contributors/resolve.js +86 -0
  36. package/lib/git/command.d.ts +5 -0
  37. package/lib/git/command.js +39 -0
  38. package/lib/git/commits.d.ts +2 -0
  39. package/lib/git/commits.js +27 -0
  40. package/lib/git/history.d.ts +3 -0
  41. package/lib/git/history.js +41 -0
  42. package/lib/git/refs.d.ts +12 -0
  43. package/lib/git/refs.js +101 -0
  44. package/lib/git/remote.d.ts +1 -0
  45. package/lib/git/remote.js +59 -0
  46. package/lib/git/tags.d.ts +1 -0
  47. package/lib/git/tags.js +27 -0
  48. package/lib/index.d.ts +8 -1
  49. package/lib/index.js +6 -13
  50. package/lib/types.d.ts +31 -0
  51. package/lib/types.js +1 -0
  52. package/lib/version/command.d.ts +5 -0
  53. package/lib/version/command.js +52 -0
  54. package/lib/version/json.d.ts +5 -0
  55. package/lib/version/json.js +35 -0
  56. package/lib/version/plan.d.ts +11 -0
  57. package/lib/version/plan.js +41 -0
  58. package/package.json +33 -41
  59. package/lib/cli/commands/printHelp.d.ts +0 -2
  60. package/lib/cli/commands/printHelp.js +0 -15
  61. package/lib/cli/commands/printMarkdown.d.ts +0 -3
  62. package/lib/cli/commands/printMarkdown.js +0 -26
  63. package/lib/cli/commands/printVersion.d.ts +0 -2
  64. package/lib/cli/commands/printVersion.js +0 -12
  65. package/lib/cli/config/options.d.ts +0 -3
  66. package/lib/cli/config/options.js +0 -13
  67. package/lib/cli/config/readConfigFile.d.ts +0 -3
  68. package/lib/cli/config/readConfigFile.js +0 -25
  69. package/lib/core/async.d.ts +0 -9
  70. package/lib/core/async.js +0 -32
  71. package/lib/core/default.d.ts +0 -6
  72. package/lib/core/default.js +0 -9
  73. package/lib/core/git.d.ts +0 -8
  74. package/lib/core/git.js +0 -88
  75. package/lib/core/markdown.d.ts +0 -8
  76. package/lib/core/markdown.js +0 -85
  77. package/lib/core/test-helpers.d.ts +0 -6
  78. package/lib/core/test-helpers.js +0 -14
  79. package/lib/models/commit.d.ts +0 -6
  80. package/lib/models/commit.js +0 -2
  81. package/lib/models/config.d.ts +0 -30
  82. package/lib/models/config.js +0 -2
  83. package/lib/models/index.d.ts +0 -3
  84. package/lib/models/index.js +0 -15
  85. package/lib/models/tagGroup.d.ts +0 -7
  86. package/lib/models/tagGroup.js +0 -2
@@ -0,0 +1,5 @@
1
+ export type FilePlan = {
2
+ path: string;
3
+ content: string;
4
+ };
5
+ export declare function apply(files: FilePlan[], beforeReplace?: () => Promise<void>): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ async function fileMode(path) {
5
+ try {
6
+ const info = await stat(path);
7
+ if (!info.isFile())
8
+ throw new Error(`Not a regular file: ${path}`);
9
+ return info.mode;
10
+ }
11
+ catch (error) {
12
+ if (error.code !== 'ENOENT')
13
+ throw error;
14
+ return undefined;
15
+ }
16
+ }
17
+ export async function apply(files, beforeReplace) {
18
+ const staged = [];
19
+ try {
20
+ for (const file of files) {
21
+ await mkdir(dirname(file.path), { recursive: true });
22
+ const mode = await fileMode(file.path);
23
+ const temporary = `${file.path}.${randomUUID()}.tmp`;
24
+ staged.push({ path: file.path, temporary });
25
+ await writeFile(temporary, file.content, { flag: 'wx', mode });
26
+ }
27
+ // Validate and stage changelog destinations before running a mutating command.
28
+ await beforeReplace?.();
29
+ for (const file of staged)
30
+ await rename(file.temporary, file.path);
31
+ }
32
+ finally {
33
+ await Promise.all(staged.map((file) => rm(file.temporary, { force: true })));
34
+ }
35
+ for (const file of files)
36
+ console.error(`Updated ${file.path}`);
37
+ }
@@ -1 +1 @@
1
- export {};
1
+ export declare function run(args?: string[]): Promise<void>;
package/lib/cli/index.js CHANGED
@@ -1,43 +1,72 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- const minimist_1 = __importDefault(require("minimist"));
16
- const default_1 = require("../core/default");
17
- const printHelp_1 = require("./commands/printHelp");
18
- const printMarkdown_1 = require("./commands/printMarkdown");
19
- const printVersion_1 = require("./commands/printVersion");
20
- const options_1 = require("./config/options");
21
- const readConfigFile_1 = require("./config/readConfigFile");
22
- const async_1 = require("../core/async");
23
- const run = () => __awaiter(void 0, void 0, void 0, function* () {
24
- if (!(0, async_1.checkPackageJsonExists)()) {
25
- console.error('Error: package.json does not exist.');
26
- process.exit(1);
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parseChangelog } from '../changelog/update.js';
3
+ import { getLatestReleaseTag, getReleases } from '../git/history.js';
4
+ import { defaultBumpCommand, formatBumpCommand, runBumpCommand, } from '../version/command.js';
5
+ import { apply } from './files.js';
6
+ import { parseFlags, showInformation } from './options.js';
7
+ import { changelogPath, planBump, planChangelog } from './plans.js';
8
+ import { withProgress } from './progress.js';
9
+ async function readChangelog(path) {
10
+ try {
11
+ return await readFile(path, 'utf8');
27
12
  }
28
- const args = (0, minimist_1.default)(process.argv.slice(2), options_1.flags);
29
- if (args.version) {
30
- (0, printVersion_1.printVersion)();
31
- return;
13
+ catch (error) {
14
+ if (error.code === 'ENOENT')
15
+ return undefined;
16
+ throw error;
32
17
  }
33
- if (args.help) {
34
- (0, printHelp_1.printHelp)();
18
+ }
19
+ async function readExistingChangelog(flags, cwd) {
20
+ const existing = flags.full
21
+ ? undefined
22
+ : await readChangelog(changelogPath(flags, cwd));
23
+ // Only regenerate history newer than the latest documented release.
24
+ const boundary = existing === undefined ? undefined : parseChangelog(existing).boundaryTag;
25
+ return { existing, boundary };
26
+ }
27
+ async function prepareBump(flags, cwd) {
28
+ if (!flags.bump)
29
+ return undefined;
30
+ const tag = await getLatestReleaseTag(cwd);
31
+ return planBump(await getReleases(cwd, tag), cwd, tag);
32
+ }
33
+ function promoteUnreleased(releases, next, boundary) {
34
+ if (!next)
35
+ return releases;
36
+ // Incremental history may expose the previous tag only through Unreleased.
37
+ const latestTag = releases.find((release) => release.tag !== 'Unreleased')?.tag ??
38
+ releases.find((release) => release.tag === 'Unreleased')?.previousTag ??
39
+ boundary;
40
+ return releases.map((release) => release.tag === 'Unreleased'
41
+ ? { ...release, tag: `v${next}`, previousTag: latestTag }
42
+ : release);
43
+ }
44
+ async function prepareFiles(flags, cwd, existing, boundary, bump) {
45
+ const releases = await withProgress('Reading Git releases', () => getReleases(cwd, boundary));
46
+ return [
47
+ ...(bump?.files ?? []),
48
+ ...(await planChangelog(promoteUnreleased(releases, bump?.next, boundary), flags, cwd, existing)),
49
+ ];
50
+ }
51
+ async function applyOrPreview(files, flags, cwd, next) {
52
+ if (flags['dry-run']) {
53
+ if (next) {
54
+ console.error(`Would run: ${formatBumpCommand(flags['bump-command'] ?? defaultBumpCommand, next)}`);
55
+ }
35
56
  return;
36
57
  }
37
- let config = default_1.defaultConfig;
38
- if (args.config) {
39
- config = Object.assign(Object.assign({}, default_1.defaultConfig), (yield (0, readConfigFile_1.readConfigFile)(args.config)));
40
- }
41
- yield (0, printMarkdown_1.printMarkdown)(config);
42
- });
43
- run();
58
+ // Stage the changelog first; update metadata before replacing it.
59
+ await apply(files, next ? () => runBumpCommand(next, cwd, flags['bump-command']) : undefined);
60
+ }
61
+ export async function run(args = process.argv.slice(2)) {
62
+ const flags = parseFlags(args);
63
+ if (await showInformation(flags))
64
+ return;
65
+ const cwd = process.cwd();
66
+ const { existing, boundary } = await readExistingChangelog(flags, cwd);
67
+ const bump = await prepareBump(flags, cwd);
68
+ if (flags.bump && !bump?.next)
69
+ return;
70
+ const files = await prepareFiles(flags, cwd, existing, boundary, bump);
71
+ await applyOrPreview(files, flags, cwd, bump?.next);
72
+ }
@@ -0,0 +1,14 @@
1
+ export type Flags = ReturnType<typeof parseFlags>;
2
+ export declare function parseFlags(args: string[]): {
3
+ full?: boolean | undefined;
4
+ bump?: boolean | undefined;
5
+ 'bump-command'?: string | undefined;
6
+ 'dry-run'?: boolean | undefined;
7
+ output?: string | undefined;
8
+ 'repo-url'?: string | undefined;
9
+ 'resolve-contributors'?: boolean | undefined;
10
+ 'include-non-conventional'?: boolean | undefined;
11
+ help?: boolean | undefined;
12
+ version?: boolean | undefined;
13
+ };
14
+ export declare function showInformation(flags: Flags): Promise<boolean>;
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parseArgs } from 'node:util';
3
+ import { validateBumpCommand } from '../version/command.js';
4
+ const help = `Usage: changelog-machine [options]
5
+
6
+ Updates missing releases and refreshes Unreleased in CHANGELOG.md by default.
7
+ Creates a complete changelog when the file does not exist.
8
+
9
+ --full Replace the entire CHANGELOG.md from Git history
10
+ --bump Infer version; update package metadata and changelog
11
+ --bump-command <command> Trusted shell command containing {version} (requires --bump)
12
+ --dry-run Preview without writing any files
13
+ --output <path> Changelog destination (default: CHANGELOG.md)
14
+ --repo-url <url> Repository URL for links
15
+ --resolve-contributors Look up verified hosting profiles (uses network)
16
+ --include-non-conventional Include other commits under Other Changes
17
+ --help Show help
18
+ --version Show this tool's version
19
+
20
+ Requires Node 24+ and Git. Never fetches, commits, tags, pushes or publishes.
21
+ `;
22
+ export function parseFlags(args) {
23
+ const flags = parseArgs({
24
+ args,
25
+ options: {
26
+ full: { type: 'boolean' },
27
+ bump: { type: 'boolean' },
28
+ 'bump-command': { type: 'string' },
29
+ 'dry-run': { type: 'boolean' },
30
+ output: { type: 'string' },
31
+ 'repo-url': { type: 'string' },
32
+ 'resolve-contributors': { type: 'boolean' },
33
+ 'include-non-conventional': { type: 'boolean' },
34
+ help: { type: 'boolean', short: 'h' },
35
+ version: { type: 'boolean', short: 'v' },
36
+ },
37
+ allowPositionals: false,
38
+ }).values;
39
+ if (flags['bump-command'] !== undefined) {
40
+ if (!flags.bump)
41
+ throw new Error('--bump-command requires --bump');
42
+ validateBumpCommand(flags['bump-command']);
43
+ }
44
+ return flags;
45
+ }
46
+ export async function showInformation(flags) {
47
+ if (flags.help) {
48
+ process.stdout.write(help);
49
+ return true;
50
+ }
51
+ if (flags.version) {
52
+ const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
53
+ console.log(pkg.version);
54
+ return true;
55
+ }
56
+ return false;
57
+ }
@@ -0,0 +1,7 @@
1
+ import type { Release } from '../types.js';
2
+ import type { VersionPlan } from '../version/plan.js';
3
+ import type { FilePlan } from './files.js';
4
+ import type { Flags } from './options.js';
5
+ export declare function planBump(releases: Release[], cwd: string, latestTag?: string): Promise<VersionPlan>;
6
+ export declare function changelogPath(flags: Flags, cwd: string): string;
7
+ export declare function planChangelog(releases: Release[], flags: Flags, cwd: string, existing?: string): Promise<FilePlan[]>;
@@ -0,0 +1,67 @@
1
+ import { resolve } from 'node:path';
2
+ import { renderMarkdown } from '../changelog/render.js';
3
+ import { mergeChangelog, parseChangelog } from '../changelog/update.js';
4
+ import { parseCommit } from '../commits/parse.js';
5
+ import { resolveContributors } from '../contributors/resolve.js';
6
+ import { getRepositoryUrl } from '../git/remote.js';
7
+ import { planVersion } from '../version/plan.js';
8
+ import { withProgress } from './progress.js';
9
+ export async function planBump(releases, cwd, latestTag) {
10
+ const unreleased = releases.find((release) => release.tag === 'Unreleased');
11
+ const plan = await planVersion(unreleased?.commits ?? [], latestTag, cwd);
12
+ console.error(plan.next
13
+ ? `${plan.current} → ${plan.next} (${plan.bump})`
14
+ : `No version bump (${plan.current})`);
15
+ return plan;
16
+ }
17
+ async function repositoryUrl(flags, cwd) {
18
+ const repository = flags['repo-url'] ?? (await getRepositoryUrl(cwd));
19
+ if (repository) {
20
+ const url = new URL(repository);
21
+ if (url.protocol !== 'https:' ||
22
+ url.username ||
23
+ url.password ||
24
+ url.search ||
25
+ url.hash) {
26
+ throw new Error('--repo-url must be an HTTPS repository URL without credentials, query or fragment');
27
+ }
28
+ }
29
+ return repository;
30
+ }
31
+ function changelogReleases(releases, flags) {
32
+ return releases.filter((release) => {
33
+ if (release.tag !== 'Unreleased')
34
+ return true;
35
+ return release.commits.some((commit) => parseCommit(commit, flags['include-non-conventional']));
36
+ });
37
+ }
38
+ export function changelogPath(flags, cwd) {
39
+ const path = resolve(cwd, flags.output ?? 'CHANGELOG.md');
40
+ if (['package.json', 'package-lock.json', 'npm-shrinkwrap.json'].some((name) => path === resolve(cwd, name))) {
41
+ throw new Error('Changelog output must not overwrite package metadata');
42
+ }
43
+ return path;
44
+ }
45
+ export async function planChangelog(releases, flags, cwd, existing) {
46
+ const selectedReleases = changelogReleases(releases, flags);
47
+ const url = await repositoryUrl(flags, cwd);
48
+ const commits = selectedReleases
49
+ .flatMap((release) => release.commits)
50
+ .filter((commit) => parseCommit(commit, flags['include-non-conventional']));
51
+ const contributors = flags['resolve-contributors']
52
+ ? await withProgress('Resolving contributor profiles', (update) => resolveContributors(commits, url, (progress) => update(`Contributors: ${progress.completed}/${progress.total} checked, ${progress.resolved} resolved, ${progress.requests} requests`)))
53
+ : undefined;
54
+ const markdown = renderMarkdown(selectedReleases, {
55
+ repositoryUrl: url,
56
+ contributors,
57
+ includeNonConventional: flags['include-non-conventional'],
58
+ });
59
+ const updated = mergeChangelog(existing === undefined ? undefined : parseChangelog(existing), markdown);
60
+ if (flags['dry-run']) {
61
+ process.stdout.write(updated);
62
+ return [];
63
+ }
64
+ return updated === existing
65
+ ? []
66
+ : [{ path: changelogPath(flags, cwd), content: updated }];
67
+ }
@@ -0,0 +1,2 @@
1
+ import type { WriteStream } from 'node:tty';
2
+ export declare function withProgress<T>(label: string, task: (update: (label: string) => void) => Promise<T>, output?: WriteStream): Promise<T>;
@@ -0,0 +1,37 @@
1
+ import { clearLine, cursorTo } from 'node:readline';
2
+ const frames = ['|', '/', '-', '\\'];
3
+ const refreshIntervalMs = 100;
4
+ const displayDelayMs = 200;
5
+ export async function withProgress(label, task, output = process.stderr) {
6
+ if (!output.isTTY)
7
+ return task(() => { });
8
+ const update = (message) => {
9
+ label = message;
10
+ };
11
+ let frame = 0;
12
+ let visible = false;
13
+ let animation;
14
+ const render = () => {
15
+ cursorTo(output, 0);
16
+ clearLine(output, 0);
17
+ output.write(`${frames[frame++ % frames.length]} ${label}`);
18
+ visible = true;
19
+ };
20
+ const delay = setTimeout(() => {
21
+ render();
22
+ animation = setInterval(render, refreshIntervalMs);
23
+ animation.unref();
24
+ }, displayDelayMs);
25
+ delay.unref();
26
+ try {
27
+ return await task(update);
28
+ }
29
+ finally {
30
+ clearTimeout(delay);
31
+ clearInterval(animation);
32
+ if (visible) {
33
+ cursorTo(output, 0);
34
+ clearLine(output, 0);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,3 @@
1
+ import type { Commit, ParsedCommit } from '../types.js';
2
+ export declare function parseCommit(commit: Commit, includeNonConventional?: boolean): ParsedCommit | undefined;
3
+ export declare function recommendedBump(commits: Commit[]): 'major' | 'minor' | 'patch' | undefined;
@@ -0,0 +1,87 @@
1
+ import { isVersionTag } from '../git/tags.js';
2
+ const conventionalHeaderPattern = /^([A-Za-z][A-Za-z0-9-]*)(?:\(([^()\r\n]+)\))?(!)?:\s+(.+)$/;
3
+ const breakingNotePattern = /^BREAKING[ -]CHANGE:\s*(.*)$/;
4
+ const breakingFooterPattern = /^BREAKING[ -]CHANGE:/m;
5
+ const footerTokenPattern = /^[A-Za-z][A-Za-z0-9-]*/;
6
+ const footerSeparatorPattern = /^(?::\s|\s+#)/;
7
+ const pullRequestSuffixPattern = /\s+\(#(\d+)\)$/;
8
+ const lineBreakPattern = /\r?\n/;
9
+ function isFooter(line) {
10
+ const token = footerTokenPattern.exec(line);
11
+ return Boolean(token && footerSeparatorPattern.test(line.slice(token[0].length)));
12
+ }
13
+ function breakingNotes(body) {
14
+ const notes = [];
15
+ let noteLines;
16
+ const finishNote = () => {
17
+ if (noteLines) {
18
+ const note = noteLines.join('\n').trim();
19
+ if (note)
20
+ notes.push(note);
21
+ }
22
+ noteLines = undefined;
23
+ };
24
+ for (const line of body.split(lineBreakPattern)) {
25
+ const breakingMatch = breakingNotePattern.exec(line);
26
+ if (breakingMatch) {
27
+ finishNote();
28
+ noteLines = [breakingMatch[1]];
29
+ }
30
+ else if (isFooter(line)) {
31
+ finishNote();
32
+ }
33
+ else if (noteLines) {
34
+ noteLines.push(line);
35
+ }
36
+ }
37
+ finishNote();
38
+ return notes;
39
+ }
40
+ function isReleaseCommit(type, scope, description) {
41
+ const version = description.startsWith('release ')
42
+ ? description.slice('release '.length)
43
+ : description;
44
+ const releaseType = type === 'release';
45
+ const releaseChore = type === 'chore' &&
46
+ (scope === 'release' || description.startsWith('release '));
47
+ return (releaseType || releaseChore) && isVersionTag(version);
48
+ }
49
+ export function parseCommit(commit, includeNonConventional = false) {
50
+ const headerMatch = conventionalHeaderPattern.exec(commit.subject.trim());
51
+ if (!headerMatch && !includeNonConventional)
52
+ return undefined;
53
+ let description = headerMatch ? headerMatch[4] : commit.subject.trim();
54
+ const pullRequestMatch = pullRequestSuffixPattern.exec(description);
55
+ if (pullRequestMatch)
56
+ description = description.slice(0, pullRequestMatch.index).trimEnd();
57
+ if (headerMatch &&
58
+ isReleaseCommit(headerMatch[1].toLowerCase(), headerMatch[2]?.toLowerCase(), description))
59
+ return undefined;
60
+ const notes = breakingNotes(commit.body);
61
+ return {
62
+ ...commit,
63
+ type: headerMatch ? headerMatch[1].toLowerCase() : 'other',
64
+ ...(headerMatch?.[2] ? { scope: headerMatch[2] } : {}),
65
+ description,
66
+ breaking: Boolean(headerMatch?.[3]) ||
67
+ notes.length > 0 ||
68
+ breakingFooterPattern.test(commit.body),
69
+ breakingNotes: notes,
70
+ ...(pullRequestMatch ? { pr: pullRequestMatch[1] } : {}),
71
+ };
72
+ }
73
+ export function recommendedBump(commits) {
74
+ let bump;
75
+ for (const commit of commits) {
76
+ const parsed = parseCommit(commit);
77
+ if (!parsed)
78
+ continue;
79
+ if (parsed.breaking)
80
+ return 'major';
81
+ if (parsed.type === 'feat')
82
+ bump = 'minor';
83
+ else if ((parsed.type === 'fix' || parsed.type === 'perf') && !bump)
84
+ bump = 'patch';
85
+ }
86
+ return bump;
87
+ }
@@ -0,0 +1,6 @@
1
+ export interface AuthorCommit {
2
+ hash: string;
3
+ name?: string;
4
+ email?: string;
5
+ }
6
+ export declare function groupAuthorCommits(commits: readonly AuthorCommit[]): string[][];
@@ -0,0 +1,20 @@
1
+ const validHash = /^[a-f\d]{7,64}$/i;
2
+ function authorIdentity(commit) {
3
+ if (commit.email?.trim())
4
+ return `email:${commit.email.trim().toLowerCase()}`;
5
+ if (commit.name?.trim())
6
+ return `name:${commit.name.trim()}`;
7
+ return `commit:${commit.hash}`;
8
+ }
9
+ export function groupAuthorCommits(commits) {
10
+ const authors = new Map();
11
+ for (const commit of commits) {
12
+ if (!validHash.test(commit.hash))
13
+ continue;
14
+ const identity = authorIdentity(commit);
15
+ const hashes = authors.get(identity) ?? new Set();
16
+ hashes.add(commit.hash);
17
+ authors.set(identity, hashes);
18
+ }
19
+ return [...authors.values()].map((hashes) => [...hashes]);
20
+ }
@@ -0,0 +1,2 @@
1
+ export declare function redactEmails(value: string): string;
2
+ export declare function authorName(value: string): string;
@@ -0,0 +1,9 @@
1
+ const emailAddressPattern = /[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+(?:@|%40)[A-Z0-9-]+(?:(?:\.|%2e)[A-Z0-9-]+)+/gi;
2
+ const unknownContributor = 'Unknown contributor';
3
+ export function redactEmails(value) {
4
+ return value.replace(emailAddressPattern, '[redacted email]');
5
+ }
6
+ export function authorName(value) {
7
+ const name = value.trim();
8
+ return !name || redactEmails(name) !== name ? unknownContributor : name;
9
+ }
@@ -0,0 +1,5 @@
1
+ import type { ContributorProfile } from '../types.js';
2
+ export type { ContributorProfile } from '../types.js';
3
+ export declare function validateProfile(username: unknown, urlValue: unknown, host: string): ContributorProfile | undefined;
4
+ export declare function readGitHubProfile(data: unknown): ContributorProfile | undefined;
5
+ export declare function readBitbucketProfile(data: unknown): ContributorProfile | undefined;
@@ -0,0 +1,51 @@
1
+ function objectValue(value) {
2
+ return value !== null && typeof value === 'object'
3
+ ? value
4
+ : undefined;
5
+ }
6
+ export function validateProfile(username, urlValue, host) {
7
+ if (typeof username !== 'string' ||
8
+ !username.trim() ||
9
+ username.includes('@') ||
10
+ /[<>\r\n]/.test(username))
11
+ return undefined;
12
+ if (host === 'github.com' && /\s/.test(username))
13
+ return undefined;
14
+ if (typeof urlValue !== 'string')
15
+ return undefined;
16
+ try {
17
+ const url = new URL(urlValue);
18
+ const path = decodeURIComponent(url.pathname);
19
+ if (url.protocol !== 'https:' ||
20
+ url.hostname.toLowerCase() !== host ||
21
+ url.username ||
22
+ url.password ||
23
+ url.port ||
24
+ url.search ||
25
+ url.hash ||
26
+ !path ||
27
+ path === '/' ||
28
+ path.includes('@') ||
29
+ /%40/i.test(url.pathname))
30
+ return undefined;
31
+ return { username: username.trim(), url: url.href };
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ export function readGitHubProfile(data) {
38
+ const author = objectValue(objectValue(data)?.author);
39
+ return validateProfile(author?.login, author?.html_url, 'github.com');
40
+ }
41
+ export function readBitbucketProfile(data) {
42
+ const response = objectValue(data);
43
+ const author = objectValue(response?.author);
44
+ const user = objectValue(author?.user);
45
+ if (!user)
46
+ return undefined;
47
+ const links = objectValue(user.links);
48
+ const html = objectValue(links?.html);
49
+ const username = user.nickname ?? user.display_name;
50
+ return validateProfile(username, html?.href, 'bitbucket.org');
51
+ }
@@ -0,0 +1,7 @@
1
+ export interface Repository {
2
+ host: 'github.com' | 'bitbucket.org';
3
+ path: string;
4
+ }
5
+ export declare function parseRepository(repositoryUrl?: string): Repository | undefined;
6
+ export declare function getCommitEndpoint(repository: Repository, hash: string): string;
7
+ export declare function getRepositoryToken(repository: Repository): string | undefined;
@@ -0,0 +1,36 @@
1
+ export function parseRepository(repositoryUrl) {
2
+ if (!repositoryUrl)
3
+ return undefined;
4
+ try {
5
+ const url = new URL(repositoryUrl);
6
+ if (url.protocol !== 'https:' ||
7
+ url.username ||
8
+ url.password ||
9
+ url.port ||
10
+ url.search ||
11
+ url.hash)
12
+ return undefined;
13
+ const host = url.hostname.toLowerCase();
14
+ if (host !== 'github.com' && host !== 'bitbucket.org')
15
+ return undefined;
16
+ const path = url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '');
17
+ const segments = path.split('/');
18
+ if (segments.length !== 2 ||
19
+ segments.some((part) => !/^[a-z\d_.-]+$/i.test(part) || part === '.' || part === '..'))
20
+ return undefined;
21
+ return { host, path };
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ export function getCommitEndpoint(repository, hash) {
28
+ return repository.host === 'github.com'
29
+ ? `https://api.github.com/repos/${repository.path}/commits/${hash}`
30
+ : `https://api.bitbucket.org/2.0/repositories/${repository.path}/commit/${hash}`;
31
+ }
32
+ export function getRepositoryToken(repository) {
33
+ return repository.host === 'github.com'
34
+ ? process.env.GH_TOKEN || process.env.GITHUB_TOKEN
35
+ : process.env.BITBUCKET_TOKEN;
36
+ }
@@ -0,0 +1,10 @@
1
+ import type { AuthorCommit } from './authors.js';
2
+ import type { ContributorProfile } from './profile.js';
3
+ export type { ContributorProfile } from './profile.js';
4
+ export interface ContributorProgress {
5
+ completed: number;
6
+ total: number;
7
+ resolved: number;
8
+ requests: number;
9
+ }
10
+ export declare function resolveContributors(commits: readonly AuthorCommit[], repositoryUrl?: string, onProgress?: (progress: ContributorProgress) => void): Promise<Record<string, ContributorProfile>>;