keepchanges 0.0.3 → 1.0.0-beta.1

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/dist/cli.mjs CHANGED
@@ -1,122 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from "node:fs";
2
+ import { a as insertRelease, c as defaultConfig, i as hasRelease, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog } from "./commit-FX77u6Sj.mjs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { normalizeFull } from "verkit";
3
5
  import process from "node:process";
4
- import { fileURLToPath } from "node:url";
5
6
  import { cac } from "cac";
6
7
  import { resolve } from "node:path";
7
8
  import ansis from "ansis";
8
- import { readFile, writeFile } from "node:fs/promises";
9
- import { normalizeFull } from "verkit";
10
9
  import { x } from "tinyexec";
11
10
  //#region package.json
12
- var version = "0.0.3";
13
- //#endregion
14
- //#region src/changelog.ts
15
- function createChangelog(version, commits, repository, comparisonFrom) {
16
- const body = [
17
- ...renderSection(commits.filter((commit) => commit.isBreaking), "🚨 Breaking Changes", repository),
18
- ...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "feat"), "🚀 Features", repository),
19
- ...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "fix"), "🐞 Bug Fixes", repository),
20
- ...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "perf"), "🏎 Performance", repository),
21
- ...repository && comparisonFrom ? ["", `#####     [View changes on ${repository.provider.name}](${repository.provider.compareUrl(repository, comparisonFrom, `v${version}`)})`] : []
22
- ].join("\n").trim();
23
- return {
24
- body,
25
- release: [
26
- `## v${version}`,
27
- "",
28
- body,
29
- ""
30
- ].join("\n")
31
- };
32
- }
33
- async function readChangelog(path) {
34
- return readFile(path, "utf8").catch((error) => {
35
- if (error.code === "ENOENT") return "# Changelog\n";
36
- throw error;
37
- });
38
- }
39
- function writeChangelog(path, changelog) {
40
- return writeFile(path, changelog);
41
- }
42
- function hasRelease(changelog, version) {
43
- const releaseVersion = normalizeFull(version);
44
- return releaseHeadings(changelog).some((heading) => heading.version === releaseVersion);
45
- }
46
- function insertRelease(changelog, release) {
47
- const releaseVersion = releaseHeadings(release)[0]?.version;
48
- const headings = releaseHeadings(changelog);
49
- const existingReleaseIndex = headings.findIndex((heading) => heading.version === releaseVersion);
50
- if (existingReleaseIndex !== -1) {
51
- const start = headings[existingReleaseIndex].index;
52
- const end = headings[existingReleaseIndex + 1]?.index ?? changelog.length;
53
- changelog = changelog.slice(0, start) + changelog.slice(end);
54
- }
55
- const firstRelease = releaseHeadings(changelog)[0];
56
- if (!firstRelease) return `${changelog.trimEnd()}\n\n${release.trim()}\n`;
57
- const preamble = changelog.slice(0, firstRelease.index).trimEnd();
58
- const history = changelog.slice(firstRelease.index).trim();
59
- return `${preamble}\n\n${release.trim()}\n\n${history}\n`;
60
- }
61
- function releaseHeadings(changelog) {
62
- return [...changelog.matchAll(/^##[^\S\r\n]+(\S+)/gm)].flatMap((heading) => {
63
- const version = normalizeFull(heading[1]);
64
- return version ? [{
65
- index: heading.index,
66
- version
67
- }] : [];
68
- });
69
- }
70
- function renderSection(commits, title, repository) {
71
- const renderedCommits = commits.map((commit) => {
72
- const reference = repository ? `[<samp>(${commit.hash.slice(0, 5)})</samp>](${repository.provider.commitUrl(repository, commit.hash)})` : "";
73
- const authorNames = commit.authors.map((author) => author.login ? `@${author.login}` : `**${escapeHtml(author.name)}**`);
74
- const authors = authorNames.length > 1 ? `${authorNames.slice(0, -1).join(", ")} and ${authorNames.at(-1)}` : authorNames[0] || "";
75
- const pullRequests = commit.pullRequests.map((pullRequest) => repository ? `[${pullRequest}](${repository.provider.pullRequestUrl(repository, pullRequest)})` : pullRequest).join(", ");
76
- const details = [
77
- authors ? `by ${authors}` : "",
78
- pullRequests ? `in ${pullRequests}` : "",
79
- reference
80
- ].filter(Boolean).join(" ");
81
- const suffix = details ? ` &nbsp;-&nbsp; ${details}` : "";
82
- return {
83
- scope: commit.scope,
84
- line: `${escapeHtml(capitalize$1(commit.description))}${suffix}`
85
- };
86
- });
87
- if (!renderedCommits.length) return [];
88
- const commitsByScope = /* @__PURE__ */ new Map();
89
- for (const { scope, line } of renderedCommits) {
90
- const lines = commitsByScope.get(scope) || [];
91
- lines.push(line);
92
- commitsByScope.set(scope, lines);
93
- }
94
- const lines = [...commitsByScope].some(([scope, lines]) => Boolean(scope) && lines.length > 1) ? [...commitsByScope.keys()].sort().flatMap((scope) => {
95
- const scopedLines = (commitsByScope.get(scope) || []).reverse().map((line) => `${scope ? " " : ""}- ${line}`);
96
- return scope ? [`- **${escapeHtml(scope)}**:`, ...scopedLines] : scopedLines;
97
- }) : renderedCommits.reverse().map(({ scope, line }) => {
98
- return `- ${scope ? `**${escapeHtml(scope)}**: ` : ""}${line}`;
99
- });
100
- return [
101
- "",
102
- `### ${title}`,
103
- "",
104
- ...lines
105
- ];
106
- }
107
- function capitalize$1(value) {
108
- return value.charAt(0).toUpperCase() + value.slice(1);
109
- }
110
- function escapeHtml(value) {
111
- const htmlEntities = {
112
- "&": "&amp;",
113
- "<": "&lt;",
114
- ">": "&gt;",
115
- "\"": "&quot;",
116
- "'": "&#39;"
117
- };
118
- return value.replace(/[&<>"']/g, (character) => htmlEntities[character]);
119
- }
11
+ var version = "1.0.0-beta.1";
120
12
  //#endregion
121
13
  //#region src/git.ts
122
14
  async function git(cwd, ...args) {
@@ -145,11 +37,8 @@ async function getRemoteTagCommit(cwd, tag) {
145
37
  const refs = (await git(cwd, "ls-remote", "--tags", "origin", `refs/tags/${tag}`, `refs/tags/${tag}^{}`)).trim().split("\n").filter(Boolean);
146
38
  return (refs.find((line) => line.endsWith("^{}")) || refs[0])?.split(/\s+/)[0];
147
39
  }
148
- async function readCommits(cwd, from, to) {
149
- return parseGitLog(await git(cwd, "log", from ? `${from}..${to}` : to, "--format=%h%x00%an%x00%ae%x00%s%x00%b%x00")).map((commit) => parseCommit(commit.hash, commit.subject, commit.body, commit.author)).filter((commit) => commit !== null);
150
- }
151
- function parseGitLog(log) {
152
- const fields = log.split("\0");
40
+ async function readGitCommits(cwd, from, to) {
41
+ const fields = (await git(cwd, "log", from ? `${from}..${to}` : to, "--format=%h%x00%an%x00%ae%x00%s%x00%b%x00")).split("\0");
153
42
  const commits = [];
154
43
  for (let index = 0; index + 4 < fields.length; index += 5) {
155
44
  const subject = fields[index + 3].trim();
@@ -165,32 +54,9 @@ function parseGitLog(log) {
165
54
  }
166
55
  return commits;
167
56
  }
168
- function parseCommit(hash, subject, body, author) {
169
- const match = /^(?<type>[a-z]+)(?:\((?<scope>[^()\r\n]+)\))?(?<breaking>!)?: (?<description>.+)$/i.exec(subject);
170
- if (!match?.groups) return null;
171
- const pullRequestPattern = /\([ a-z]*(#\d+)\s*\)/gi;
172
- const pullRequests = [...new Set([...match.groups.description.matchAll(pullRequestPattern)].map((reference) => reference[1]))];
173
- const authors = [author];
174
- for (const coAuthor of body.matchAll(/^Co-Authored-By:([^<\r\n]+)<([^>\r\n]+)>[^\S\r\n]*$/gim)) {
175
- const email = coAuthor[2].trim();
176
- if (!authors.some((author) => author.email === email)) authors.push({
177
- name: coAuthor[1].trim(),
178
- email
179
- });
180
- }
181
- return {
182
- hash,
183
- authors: authors.filter((author) => !/\[bot\]|dependabot|\(bot\)/i.test(author.name)),
184
- type: match.groups.type.toLowerCase(),
185
- scope: match.groups.scope || "",
186
- description: match.groups.description.replace(pullRequestPattern, "").trim(),
187
- pullRequests,
188
- isBreaking: Boolean(match.groups.breaking) || /^BREAKING(?: |-)CHANGE:/im.test(body)
189
- };
190
- }
191
57
  //#endregion
192
- //#region src/providers/gitea.ts
193
- const giteaProvider = {
58
+ //#region src/repositories/gitea.ts
59
+ const giteaRepository = {
194
60
  name: "Gitea",
195
61
  parse(source) {
196
62
  try {
@@ -199,7 +65,7 @@ const giteaProvider = {
199
65
  const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
200
66
  if (!path.includes("/")) return;
201
67
  return {
202
- provider: giteaProvider,
68
+ provider: giteaRepository,
203
69
  path,
204
70
  webUrl: `${url.origin}/${path}`
205
71
  };
@@ -217,6 +83,11 @@ const giteaProvider = {
217
83
  compareUrl(repository, from, to) {
218
84
  return `${repository.webUrl}/compare/${from}...${to}`;
219
85
  },
86
+ manualReleaseUrl(repository, release) {
87
+ const url = new URL(`${repository.webUrl}/releases/new`);
88
+ url.search = new URLSearchParams({ tag: release.tag }).toString();
89
+ return url.toString();
90
+ },
220
91
  async resolveAuthors(commits, repository, token, fetch) {
221
92
  const headers = {
222
93
  accept: "application/json",
@@ -257,7 +128,8 @@ const giteaProvider = {
257
128
  tag_name: release.tag,
258
129
  name: release.name,
259
130
  body: release.body,
260
- prerelease: release.prerelease
131
+ prerelease: release.prerelease,
132
+ draft: release.draft
261
133
  })
262
134
  });
263
135
  if (!response.ok) throw new Error(`Gitea release publishing failed (${response.status})`);
@@ -268,14 +140,14 @@ const giteaProvider = {
268
140
  }
269
141
  };
270
142
  //#endregion
271
- //#region src/providers/github.ts
272
- const githubProvider = {
143
+ //#region src/repositories/github.ts
144
+ const githubRepository = {
273
145
  name: "GitHub",
274
146
  parse(source) {
275
147
  const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
276
148
  if (!match) return;
277
149
  return {
278
- provider: githubProvider,
150
+ provider: githubRepository,
279
151
  path: match[1],
280
152
  webUrl: `https://github.com/${match[1]}`
281
153
  };
@@ -340,7 +212,8 @@ const githubProvider = {
340
212
  tag_name: release.tag,
341
213
  name: release.name,
342
214
  body: release.body,
343
- prerelease: release.prerelease
215
+ prerelease: release.prerelease,
216
+ draft: release.draft
344
217
  })
345
218
  });
346
219
  if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
@@ -352,24 +225,28 @@ const githubProvider = {
352
225
  };
353
226
  //#endregion
354
227
  //#region src/repository.ts
355
- const providers$1 = {
356
- gitea: giteaProvider,
357
- github: githubProvider
228
+ const repositoryProviders = {
229
+ github: githubRepository,
230
+ gitea: giteaRepository
358
231
  };
359
- async function resolveRepository(cwd) {
232
+ async function resolveRepository(cwd, explicit) {
233
+ if (explicit) {
234
+ const repository = parseRepository(/^[^/:]+\/[^/]+$/.test(explicit) ? `https://github.com/${explicit}` : explicit, Object.values(repositoryProviders));
235
+ if (!repository) throw new Error(`Unsupported repository: ${explicit}`);
236
+ return repository;
237
+ }
360
238
  let providerName = "";
361
239
  let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
362
240
  const repository = JSON.parse(contents).repository;
363
241
  if (typeof repository === "object") providerName = repository?.provider?.toLowerCase() || "";
364
242
  return typeof repository === "string" ? repository : repository?.url || "";
365
243
  }).catch(() => "");
366
- if (!source) source = (await x("git", [
367
- "remote",
368
- "get-url",
369
- "origin"
370
- ], { nodeOptions: { cwd } })).stdout.trim();
371
- if (providerName) return providers$1[providerName]?.parse(source);
372
- for (const provider of [githubProvider]) {
244
+ if (!source) source = await git(cwd, "remote", "get-url", "origin").then((output) => output.trim()).catch(() => "");
245
+ if (providerName) return repositoryProviders[providerName]?.parse(source);
246
+ return parseRepository(source, [githubRepository]);
247
+ }
248
+ function parseRepository(source, providers) {
249
+ for (const provider of providers) {
373
250
  const repository = provider.parse(source);
374
251
  if (repository) return repository;
375
252
  }
@@ -398,22 +275,41 @@ async function updateVersion(cwd, version) {
398
275
  }
399
276
  }
400
277
  //#endregion
401
- //#region src/run.ts
402
- const defaultAuthor = "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>";
403
- async function runChangelog(options, environment) {
404
- const { version: version$1 } = options;
405
- const tag = `v${version$1}`;
406
- const gitIdentity = options.commit || options.release ? resolveGitIdentity(options.author) : [];
278
+ //#region src/cli/output.ts
279
+ function printChangesPreview(preview, stdout, colors) {
280
+ stdout([
281
+ colors.dim(`keep${colors.bold("changes")} v${version}`),
282
+ `${colors.cyan(preview.from)}${colors.dim(" -> ")}${colors.blue(preview.tag)}${colors.dim(` (${preview.commitCount} commits)`)}`,
283
+ colors.dim("--------------"),
284
+ "",
285
+ preview.body.replaceAll("&nbsp;", ""),
286
+ "",
287
+ colors.dim("--------------"),
288
+ ""
289
+ ].join("\n"));
290
+ }
291
+ function printManualReleaseUrl(repository, release, stdout, colors) {
292
+ const url = repository.provider.manualReleaseUrl?.(repository, release);
293
+ if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
294
+ }
295
+ function printPublishedRelease(provider, result, stdout, colors) {
296
+ const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
297
+ stdout(`${colors.green(`${action} ${provider} release: ${result.url}`)}\n`);
298
+ }
299
+ //#endregion
300
+ //#region src/cli/createChanges.ts
301
+ async function createChanges(options, environment) {
302
+ if (options.commit && options.to !== defaultConfig.cli.to) {
303
+ const [toCommit, headCommit] = await Promise.all([git(environment.cwd, "rev-parse", options.to).then((value) => value.trim()), git(environment.cwd, "rev-parse", "HEAD").then((value) => value.trim())]);
304
+ if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
305
+ }
407
306
  const env = environment.env ?? process.env;
408
307
  const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
409
308
  const colors = environment.colors ?? ansis;
410
- const latestTag = await getLatestTag(environment.cwd);
411
- const repository = await resolveRepository(environment.cwd);
309
+ const tag = `v${options.version}`;
310
+ const repository = await resolveRepository(environment.cwd, options.repository);
412
311
  const token = repository?.provider.token(options.token, env);
413
- if (options.release) {
414
- if (!repository) throw new Error("A supported repository is required to release");
415
- if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
416
- }
312
+ validateReleaseSupport(options, repository);
417
313
  let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
418
314
  let releaseRef = taggedCommit ? tag : void 0;
419
315
  const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
@@ -426,84 +322,88 @@ async function runChangelog(options, environment) {
426
322
  taggedCommit = await getTagCommit(environment.cwd, tag);
427
323
  releaseRef = tag;
428
324
  }
429
- const from = taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : latestTag;
430
- const to = releaseRef || "HEAD";
431
- const comparisonFrom = from || (repository ? (await git(environment.cwd, "rev-list", "--max-parents=0", to)).trim() : "");
432
- const commits = await readCommits(environment.cwd, from, to);
325
+ const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : await getLatestTag(environment.cwd));
326
+ const to = releaseRef || options.to;
327
+ const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
328
+ const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
433
329
  if (token && repository && !(options.release && options.dry)) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
434
- const { body: releaseBody, release } = createChangelog(version$1, commits, repository, comparisonFrom);
330
+ const style = {
331
+ emoji: options.emoji,
332
+ capitalize: options.capitalize,
333
+ group: options.group
334
+ };
335
+ const { body, release } = generateChangelog({
336
+ version: options.version,
337
+ commits,
338
+ repository,
339
+ comparisonFrom
340
+ }, style);
435
341
  const repositoryRelease = {
436
342
  tag,
437
- name: tag,
438
- body: releaseBody,
439
- prerelease: version$1.includes("-")
440
- };
441
- const printReleasePreview = () => {
442
- stdout([
443
- colors.dim(`keep${colors.bold("changes")} v${version}`),
444
- `${colors.cyan(from || comparisonFrom)}${colors.dim(" -> ")}${colors.blue(tag)}${colors.dim(` (${commits.length} commits)`)}`,
445
- colors.dim("--------------"),
446
- "",
447
- releaseBody.replaceAll("&nbsp;", ""),
448
- "",
449
- colors.dim("--------------"),
450
- ""
451
- ].join("\n"));
452
- };
453
- const printManualReleaseUrl = () => {
454
- const url = repository.provider.manualReleaseUrl?.(repository, repositoryRelease);
455
- if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
456
- return url;
343
+ name: options.name ?? tag,
344
+ body,
345
+ prerelease: options.prerelease ?? options.version.includes("-"),
346
+ draft: options.draft
457
347
  };
458
- const publishRepositoryRelease = async () => {
459
- printReleasePreview();
460
- if (!token || !repository.provider.publishRelease) {
461
- if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
462
- stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via GITHUB_TOKEN env. Release skipped.`)}\n\n`);
463
- printManualReleaseUrl();
464
- return;
465
- }
466
- const result = await repository.provider.publishRelease(repository, repositoryRelease, token, environment.fetch ?? globalThis.fetch);
467
- stdout(`${colors.green(`${capitalize(result.action)} ${repository.provider.name} release: ${result.url}`)}\n`);
348
+ const preview = {
349
+ from: from || comparisonFrom,
350
+ tag,
351
+ commitCount: commits.length,
352
+ body
468
353
  };
469
- const outputPath = resolve(environment.cwd, options.output || "CHANGELOG.md");
470
- const currentChangelog = await readChangelog(outputPath);
471
- const releaseExists = hasRelease(currentChangelog, version$1);
472
- const changelog = insertRelease(currentChangelog, release);
473
354
  if (options.dry) {
355
+ printChangesPreview(preview, stdout, colors);
474
356
  if (options.release) {
475
- printReleasePreview();
476
357
  stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
477
- printManualReleaseUrl();
478
- } else stdout(release);
358
+ printManualReleaseUrl(repository, repositoryRelease, stdout, colors);
359
+ }
479
360
  return;
480
361
  }
481
362
  if (options.release && taggedCommit) {
482
363
  if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
483
- await publishRepositoryRelease();
364
+ await publishRelease(repository, repositoryRelease, token, preview, environment);
484
365
  return;
485
366
  }
486
- await writeChangelog(outputPath, changelog);
487
- const versionPath = await updateVersion(environment.cwd, version$1);
488
- if (options.commit || options.release) {
489
- const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
490
- if ((await git(environment.cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) {
491
- await git(environment.cwd, "add", "--", ...releasePaths);
492
- const versionChanges = versionPath ? await git(environment.cwd, "status", "--porcelain", "--", versionPath) : "";
493
- const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${version$1} release notes` : `chore(release): v${version$1}`;
494
- await git(environment.cwd, ...gitIdentity, "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
495
- }
496
- }
367
+ const outputPath = resolve(environment.cwd, options.output);
368
+ const currentChangelog = await readChangelog(outputPath);
369
+ const releaseExists = hasRelease(currentChangelog, options.version);
370
+ await writeChangelog(outputPath, insertRelease(currentChangelog, release));
371
+ const versionPath = await updateVersion(environment.cwd, options.version);
372
+ if (options.commit || options.release) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists);
497
373
  if (options.release) {
374
+ const gitIdentity = resolveGitIdentity(options.author);
498
375
  await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
499
376
  await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
500
- await publishRepositoryRelease();
377
+ await publishRelease(repository, repositoryRelease, token, preview, environment);
501
378
  }
502
379
  }
503
- function capitalize(value) {
504
- return value.charAt(0).toUpperCase() + value.slice(1);
380
+ function validateReleaseSupport(options, repository) {
381
+ if (!options.release) return;
382
+ if (!repository) throw new Error("A supported repository is required to release");
383
+ if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
384
+ }
385
+ async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists) {
386
+ const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
387
+ if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
388
+ await git(cwd, "add", "--", ...releasePaths);
389
+ const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
390
+ const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
391
+ await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
505
392
  }
506
- function resolveGitIdentity(author = defaultAuthor) {
393
+ async function publishRelease(repository, release, token, preview, environment) {
394
+ const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
395
+ const colors = environment.colors ?? ansis;
396
+ printChangesPreview(preview, stdout, colors);
397
+ if (!token || !repository.provider.publishRelease) {
398
+ if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
399
+ stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or environment variable. Release skipped.`)}\n\n`);
400
+ printManualReleaseUrl(repository, release, stdout, colors);
401
+ return;
402
+ }
403
+ const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
404
+ printPublishedRelease(repository.provider.name, result, stdout, colors);
405
+ }
406
+ function resolveGitIdentity(author) {
507
407
  const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
508
408
  if (!match) throw new Error("Author must use the \"Name <email>\" format");
509
409
  return [
@@ -514,31 +414,43 @@ function resolveGitIdentity(author = defaultAuthor) {
514
414
  ];
515
415
  }
516
416
  //#endregion
517
- //#region src/cli.ts
518
- async function runCli(args, environment) {
519
- const cli = cac("changelog").version(version).option("--output <path>", "Changelog file path").option("--dry", "Print the changelog without writing it").option("--commit", "Commit the changelog").option("--release", "Publish a repository release").option("--author <author>", "Commit author in \"Name <email>\" format").option("--token <token>", "Repository token for resolving authors").help();
520
- cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
521
- if (!versionArgument) throw new Error("A release version is required");
522
- await runChangelog({
523
- version: versionArgument.replace(/^v/, ""),
524
- output: options.output,
525
- dry: options.dry,
526
- commit: options.commit,
527
- release: options.release,
528
- author: options.author,
529
- token: options.token
530
- }, environment);
531
- });
532
- cli.parse([
533
- "node",
534
- "changelog",
535
- ...args
536
- ], { run: false });
537
- await cli.runMatchedCommand();
417
+ //#region src/cli/options.ts
418
+ function resolveOptions(versionArgument, options) {
419
+ if (!versionArgument) throw new Error("A release version is required");
420
+ const version = normalizeFull(versionArgument);
421
+ if (!version) throw new Error(`Invalid release version: ${versionArgument}`);
422
+ const commit = options.commit ?? defaultConfig.cli.commit;
423
+ const release = options.release ?? defaultConfig.cli.release;
424
+ if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
425
+ if (options.author !== void 0 && !commit && !release) throw new Error("--author requires --commit or --release");
426
+ if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0)) throw new Error("--name, --draft, and --prerelease require --release");
427
+ return {
428
+ version,
429
+ from: options.from,
430
+ to: options.to ?? defaultConfig.cli.to,
431
+ repository: options.repository,
432
+ output: options.output ?? defaultConfig.cli.output,
433
+ dry: options.dry ?? defaultConfig.cli.dry,
434
+ commit,
435
+ release,
436
+ author: options.author ?? defaultConfig.cli.author,
437
+ token: options.token,
438
+ name: options.name,
439
+ draft: options.draft ?? defaultConfig.cli.draft,
440
+ prerelease: options.prerelease,
441
+ emoji: options.emoji ?? defaultConfig.changelog.emoji,
442
+ capitalize: options.capitalize ?? defaultConfig.changelog.capitalize,
443
+ group: options.group ?? defaultConfig.changelog.group
444
+ };
538
445
  }
539
- if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) runCli(process.argv.slice(2), { cwd: process.cwd() }).catch((error) => {
540
- console.error(error instanceof Error ? error.message : String(error));
541
- process.exitCode = 1;
446
+ //#endregion
447
+ //#region src/cli.ts
448
+ const cli = cac("keepchanges").option("--from <ref>", "Start Git reference").option("--to <ref>", "End Git reference").option("--repository <source>", "Repository slug or URL").option("--output <path>", "Changelog file path").option("--dry", "Preview without modifying files or remotes").option("--commit", "Commit the changelog and version update").option("--release", "Publish a repository release").option("--author <author>", "Commit author in \"Name <email>\" format").option("-t, --token <token>", "Repository token").option("--name <name>", "Repository release name").option("-d, --draft", "Create a draft repository release").option("--prerelease", "Mark the repository release as prerelease").option("--emoji", "Use emojis in changelog section titles").option("--capitalize", "Capitalize changelog entries").option("--group", "Group repeated commit scopes");
449
+ cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
450
+ await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
542
451
  });
452
+ cli.help();
453
+ cli.version(version);
454
+ cli.parse();
543
455
  //#endregion
544
- export { runCli };
456
+ export {};