keepchanges 0.0.3 → 1.0.0-beta.2

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.2";
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,33 +54,11 @@ 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",
61
+ tokenEnv: "GITEA_TOKEN",
195
62
  parse(source) {
196
63
  try {
197
64
  const url = new URL(source.replace(/^git\+/, ""));
@@ -199,7 +66,7 @@ const giteaProvider = {
199
66
  const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
200
67
  if (!path.includes("/")) return;
201
68
  return {
202
- provider: giteaProvider,
69
+ provider: giteaRepository,
203
70
  path,
204
71
  webUrl: `${url.origin}/${path}`
205
72
  };
@@ -217,6 +84,12 @@ const giteaProvider = {
217
84
  compareUrl(repository, from, to) {
218
85
  return `${repository.webUrl}/compare/${from}...${to}`;
219
86
  },
87
+ manualReleaseUrl(repository, release, action) {
88
+ if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
89
+ const url = new URL(`${repository.webUrl}/releases/new`);
90
+ url.search = new URLSearchParams({ tag: release.tag }).toString();
91
+ return url.toString();
92
+ },
220
93
  async resolveAuthors(commits, repository, token, fetch) {
221
94
  const headers = {
222
95
  accept: "application/json",
@@ -257,7 +130,8 @@ const giteaProvider = {
257
130
  tag_name: release.tag,
258
131
  name: release.name,
259
132
  body: release.body,
260
- prerelease: release.prerelease
133
+ prerelease: release.prerelease,
134
+ draft: release.draft
261
135
  })
262
136
  });
263
137
  if (!response.ok) throw new Error(`Gitea release publishing failed (${response.status})`);
@@ -268,14 +142,15 @@ const giteaProvider = {
268
142
  }
269
143
  };
270
144
  //#endregion
271
- //#region src/providers/github.ts
272
- const githubProvider = {
145
+ //#region src/repositories/github.ts
146
+ const githubRepository = {
273
147
  name: "GitHub",
148
+ tokenEnv: "GITHUB_TOKEN",
274
149
  parse(source) {
275
150
  const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
276
151
  if (!match) return;
277
152
  return {
278
- provider: githubProvider,
153
+ provider: githubRepository,
279
154
  path: match[1],
280
155
  webUrl: `https://github.com/${match[1]}`
281
156
  };
@@ -292,7 +167,8 @@ const githubProvider = {
292
167
  compareUrl(repository, from, to) {
293
168
  return `${repository.webUrl}/compare/${from}...${to}`;
294
169
  },
295
- manualReleaseUrl(repository, release) {
170
+ manualReleaseUrl(repository, release, action) {
171
+ if (action === "edit") return `${repository.webUrl}/releases/edit/${encodeURIComponent(release.tag)}`;
296
172
  const url = new URL(`${repository.webUrl}/releases/new`);
297
173
  url.search = new URLSearchParams({
298
174
  title: release.name,
@@ -340,7 +216,8 @@ const githubProvider = {
340
216
  tag_name: release.tag,
341
217
  name: release.name,
342
218
  body: release.body,
343
- prerelease: release.prerelease
219
+ prerelease: release.prerelease,
220
+ draft: release.draft
344
221
  })
345
222
  });
346
223
  if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
@@ -352,24 +229,28 @@ const githubProvider = {
352
229
  };
353
230
  //#endregion
354
231
  //#region src/repository.ts
355
- const providers$1 = {
356
- gitea: giteaProvider,
357
- github: githubProvider
232
+ const repositoryProviders = {
233
+ github: githubRepository,
234
+ gitea: giteaRepository
358
235
  };
359
- async function resolveRepository(cwd) {
236
+ async function resolveRepository(cwd, explicit) {
237
+ if (explicit) {
238
+ const repository = parseRepository(/^[^/:]+\/[^/]+$/.test(explicit) ? `https://github.com/${explicit}` : explicit, Object.values(repositoryProviders));
239
+ if (!repository) throw new Error(`Unsupported repository: ${explicit}`);
240
+ return repository;
241
+ }
360
242
  let providerName = "";
361
243
  let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
362
244
  const repository = JSON.parse(contents).repository;
363
245
  if (typeof repository === "object") providerName = repository?.provider?.toLowerCase() || "";
364
246
  return typeof repository === "string" ? repository : repository?.url || "";
365
247
  }).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]) {
248
+ if (!source) source = await git(cwd, "remote", "get-url", "origin").then((output) => output.trim()).catch(() => "");
249
+ if (providerName) return repositoryProviders[providerName]?.parse(source);
250
+ return parseRepository(source, [githubRepository]);
251
+ }
252
+ function parseRepository(source, providers) {
253
+ for (const provider of providers) {
373
254
  const repository = provider.parse(source);
374
255
  if (repository) return repository;
375
256
  }
@@ -398,22 +279,41 @@ async function updateVersion(cwd, version) {
398
279
  }
399
280
  }
400
281
  //#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) : [];
282
+ //#region src/cli/output.ts
283
+ function printChangesPreview(preview, stdout, colors) {
284
+ stdout([
285
+ colors.dim(`keep${colors.bold("changes")} v${version}`),
286
+ `${colors.cyan(preview.from)}${colors.dim(" -> ")}${colors.blue(preview.tag)}${colors.dim(` (${preview.commitCount} commits)`)}`,
287
+ colors.dim("--------------"),
288
+ "",
289
+ preview.body.replaceAll("&nbsp;", ""),
290
+ "",
291
+ colors.dim("--------------"),
292
+ ""
293
+ ].join("\n"));
294
+ }
295
+ function printManualReleaseUrl(repository, release, stdout, colors, action = "create") {
296
+ const url = repository.provider.manualReleaseUrl?.(repository, release, action);
297
+ if (url) stdout(`${colors.yellow(`Using the following link to ${action} it manually:`)}\n${colors.yellow(url)}\n`);
298
+ }
299
+ function printPublishedRelease(provider, result, stdout, colors) {
300
+ const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
301
+ stdout(`${colors.green(`${action} ${provider} release: ${result.url}`)}\n`);
302
+ }
303
+ //#endregion
304
+ //#region src/cli/createChanges.ts
305
+ async function createChanges(options, environment) {
306
+ if (options.commit && options.to !== defaultConfig.cli.to) {
307
+ 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())]);
308
+ if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
309
+ }
407
310
  const env = environment.env ?? process.env;
408
311
  const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
409
312
  const colors = environment.colors ?? ansis;
410
- const latestTag = await getLatestTag(environment.cwd);
411
- const repository = await resolveRepository(environment.cwd);
313
+ const tag = `v${options.version}`;
314
+ const repository = await resolveRepository(environment.cwd, options.repository);
412
315
  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
- }
316
+ validateReleaseSupport(options, repository);
417
317
  let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
418
318
  let releaseRef = taggedCommit ? tag : void 0;
419
319
  const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
@@ -426,84 +326,88 @@ async function runChangelog(options, environment) {
426
326
  taggedCommit = await getTagCommit(environment.cwd, tag);
427
327
  releaseRef = tag;
428
328
  }
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);
433
- 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);
329
+ const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : await getLatestTag(environment.cwd));
330
+ const to = releaseRef || options.to;
331
+ const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
332
+ const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
333
+ if (token && repository) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
334
+ const style = {
335
+ emoji: options.emoji,
336
+ capitalize: options.capitalize,
337
+ group: options.group
338
+ };
339
+ const { body, release } = generateChangelog({
340
+ version: options.version,
341
+ commits,
342
+ repository,
343
+ comparisonFrom
344
+ }, style);
435
345
  const repositoryRelease = {
436
346
  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;
347
+ name: options.name ?? tag,
348
+ body,
349
+ prerelease: options.prerelease ?? options.version.includes("-"),
350
+ draft: options.draft
457
351
  };
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`);
352
+ const preview = {
353
+ from: from || comparisonFrom,
354
+ tag,
355
+ commitCount: commits.length,
356
+ body
468
357
  };
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
358
  if (options.dry) {
359
+ printChangesPreview(preview, stdout, colors);
474
360
  if (options.release) {
475
- printReleasePreview();
476
361
  stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
477
- printManualReleaseUrl();
478
- } else stdout(release);
362
+ printManualReleaseUrl(repository, repositoryRelease, stdout, colors, taggedCommit ? "edit" : "create");
363
+ }
479
364
  return;
480
365
  }
481
366
  if (options.release && taggedCommit) {
482
367
  if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
483
- await publishRepositoryRelease();
368
+ await publishRelease(repository, repositoryRelease, token, preview, environment, "edit");
484
369
  return;
485
370
  }
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
- }
371
+ const outputPath = resolve(environment.cwd, options.output);
372
+ const currentChangelog = await readChangelog(outputPath);
373
+ const releaseExists = hasRelease(currentChangelog, options.version);
374
+ await writeChangelog(outputPath, insertRelease(currentChangelog, release));
375
+ const versionPath = await updateVersion(environment.cwd, options.version);
376
+ if (options.commit || options.release) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists);
497
377
  if (options.release) {
378
+ const gitIdentity = resolveGitIdentity(options.author);
498
379
  await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
499
380
  await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
500
- await publishRepositoryRelease();
381
+ await publishRelease(repository, repositoryRelease, token, preview, environment);
501
382
  }
502
383
  }
503
- function capitalize(value) {
504
- return value.charAt(0).toUpperCase() + value.slice(1);
384
+ function validateReleaseSupport(options, repository) {
385
+ if (!options.release) return;
386
+ if (!repository) throw new Error("A supported repository is required to release");
387
+ if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
388
+ }
389
+ async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists) {
390
+ const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
391
+ if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
392
+ await git(cwd, "add", "--", ...releasePaths);
393
+ const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
394
+ const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
395
+ await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
505
396
  }
506
- function resolveGitIdentity(author = defaultAuthor) {
397
+ async function publishRelease(repository, release, token, preview, environment, manualAction = "create") {
398
+ const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
399
+ const colors = environment.colors ?? ansis;
400
+ printChangesPreview(preview, stdout, colors);
401
+ if (!token || !repository.provider.publishRelease) {
402
+ if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
403
+ stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or ${repository.provider.tokenEnv} env. Release skipped.`)}\n\n`);
404
+ printManualReleaseUrl(repository, release, stdout, colors, manualAction);
405
+ return;
406
+ }
407
+ const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
408
+ printPublishedRelease(repository.provider.name, result, stdout, colors);
409
+ }
410
+ function resolveGitIdentity(author) {
507
411
  const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
508
412
  if (!match) throw new Error("Author must use the \"Name <email>\" format");
509
413
  return [
@@ -514,31 +418,43 @@ function resolveGitIdentity(author = defaultAuthor) {
514
418
  ];
515
419
  }
516
420
  //#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();
421
+ //#region src/cli/options.ts
422
+ function resolveOptions(versionArgument, options) {
423
+ if (!versionArgument) throw new Error("A release version is required");
424
+ const version = normalizeFull(versionArgument);
425
+ if (!version) throw new Error(`Invalid release version: ${versionArgument}`);
426
+ const commit = options.commit ?? defaultConfig.cli.commit;
427
+ const release = options.release ?? defaultConfig.cli.release;
428
+ if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
429
+ if (options.author !== void 0 && !commit && !release) throw new Error("--author requires --commit or --release");
430
+ if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0)) throw new Error("--name, --draft, and --prerelease require --release");
431
+ return {
432
+ version,
433
+ from: options.from,
434
+ to: options.to ?? defaultConfig.cli.to,
435
+ repository: options.repository,
436
+ output: options.output ?? defaultConfig.cli.output,
437
+ dry: options.dry ?? defaultConfig.cli.dry,
438
+ commit,
439
+ release,
440
+ author: options.author ?? defaultConfig.cli.author,
441
+ token: options.token,
442
+ name: options.name,
443
+ draft: options.draft ?? defaultConfig.cli.draft,
444
+ prerelease: options.prerelease,
445
+ emoji: options.emoji ?? defaultConfig.changelog.emoji,
446
+ capitalize: options.capitalize ?? defaultConfig.changelog.capitalize,
447
+ group: options.group ?? defaultConfig.changelog.group
448
+ };
538
449
  }
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;
450
+ //#endregion
451
+ //#region src/cli.ts
452
+ 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");
453
+ cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
454
+ await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
542
455
  });
456
+ cli.help();
457
+ cli.version(version);
458
+ cli.parse();
543
459
  //#endregion
544
- export { runCli };
460
+ export {};