@peiyanlu/release 0.0.3 → 0.0.5

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,21 +1,26 @@
1
1
  import path, { join, resolve } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { eol, execAsync, isGitRepo, parseGitHubRepo, readJsonFile, runGit, runNpm } from "@peiyanlu/cli-utils";
4
- import { dim, gray, green, red, rgb, underline, yellow } from "ansis";
3
+ import { bumpPackageVersion, coloredChangeset, eol, execAsync, getAllRemotes, getAuthenticatedUser, getCurrentBranch, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLog, getPackageInfo, getPackageUrl, getPublishedVersion, getRemote, getStatus, gitAddAll, gitAddTracked, gitCommit, gitTagAnnotated, hasWriteAccess, isGitRepo, isWorkingDirClean, parseGitHubRepo, pingRegistry, publishPackage, pushBranch, pushTag, readJsonFile, resolveChangelogRange, runGit } from "@peiyanlu/cli-utils";
4
+ import { dim, green, red, rgb, underline, yellow } from "ansis";
5
5
  import { program } from "commander";
6
+ import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
7
  import { cancel, confirm, group, intro, isCancel, log, outro, select, tasks, text } from "@clack/prompts";
7
- import { clean, coerce, gt, gte, inc, parse, prerelease, valid } from "semver";
8
+ import { publint } from "publint";
9
+ import { formatMessage } from "publint/utils";
10
+ import { coerce, eq, inc, lt, parse, prerelease, valid } from "semver";
8
11
  import { castArray, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
9
- import { createReadStream, existsSync, statSync } from "node:fs";
10
12
  import { spawnSync } from "node:child_process";
11
- import { inspect, styleText } from "node:util";
12
13
  import { Octokit } from "@octokit/rest";
13
14
  import { contentType } from "mime-types";
14
15
  import open from "open";
15
16
  import { glob } from "tinyglobby";
16
- import { readFile, writeFile } from "node:fs/promises";
17
+ import { inspect } from "node:util";
18
+ import { ConventionalChangelog } from "conventional-changelog";
19
+ import createPreset from "conventional-changelog-conventionalcommits";
20
+ import { writeFile } from "node:fs/promises";
21
+ import { finished } from "node:stream/promises";
17
22
 
18
- //#region node_modules/.pnpm/tsdown@0.18.4_typescript@5.9.3/node_modules/tsdown/esm-shims.js
23
+ //#region node_modules/.pnpm/tsdown@0.18.4_publint@0.3.17_typescript@5.9.3/node_modules/tsdown/esm-shims.js
19
24
  const getFilename = () => fileURLToPath(import.meta.url);
20
25
  const getDirname = () => path.dirname(getFilename());
21
26
  const __dirname = /* @__PURE__ */ getDirname();
@@ -26,10 +31,8 @@ const findConfigFile = (cwd = process.cwd()) => {
26
31
  for (const ext of [
27
32
  "ts",
28
33
  "mts",
29
- "cts",
30
34
  "js",
31
- "mjs",
32
- "cjs"
35
+ "mjs"
33
36
  ]) {
34
37
  const path$1 = resolve(cwd, `release.config.${ext}`);
35
38
  if (existsSync(path$1)) return path$1;
@@ -41,11 +44,16 @@ const loadConfig = async (file) => {
41
44
  const temp = mod.default ?? mod;
42
45
  return typeof temp === "function" ? temp() : temp;
43
46
  };
44
- const resolveConfig = async (cwd, log$1) => {
47
+ const resolveConfig = async (cwd) => {
45
48
  const configFile = findConfigFile(cwd);
46
- if (!configFile) return {};
47
- log$1?.(configFile);
48
- return loadConfig(configFile);
49
+ if (!configFile) return {
50
+ configPath: "",
51
+ config: {}
52
+ };
53
+ return {
54
+ configPath: configFile,
55
+ config: await loadConfig(configFile)
56
+ };
49
57
  };
50
58
  const isFunction = (v) => typeof v === "function";
51
59
  const mergeConfig = (defaults, overrides) => {
@@ -69,8 +77,8 @@ const mergeConfig = (defaults, overrides) => {
69
77
  const n = (b) => b ? "(dry run)" : "";
70
78
  const t = () => green(`${Math.floor(process.uptime())}s`);
71
79
  const MSG = {
72
- INTRO: (pkgName, dryRun) => `🚀 Starting release ${yellow(pkgName)} ${n(dryRun)}`,
73
- OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}.`,
80
+ INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
81
+ OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
74
82
  LOG: {
75
83
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
76
84
  SHOW_CHANGELOG: "🎉 Changelog collected",
@@ -80,11 +88,11 @@ const MSG = {
80
88
  CHECK: {
81
89
  GIT: {
82
90
  CHECKING: "Checking Git repository",
83
- CHECKED: (name, url) => `Git remote resolved: ${dim(name)} → ${underline(dim(url))}`
91
+ CHECKED: (name, url) => `Git remote resolved: ${url ? `${dim(name)} → ${underline(dim(url))}` : "not found"}`
84
92
  },
85
93
  NPM: {
86
94
  CHECKING: "Checking npm registry",
87
- CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))}${msg$1 ? ` ${msg$1}` : ""}`
95
+ CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg$1))}`
88
96
  }
89
97
  },
90
98
  TASK: {
@@ -110,6 +118,7 @@ const MSG = {
110
118
  }
111
119
  },
112
120
  PROMPT: {
121
+ SELECT_PACKAGE: "Select package release:",
113
122
  SELECT_VERSION: "Select version bump:",
114
123
  INPUT_VERSION: "Enter custom version:",
115
124
  NPM_PUBLISH: (pkg$1) => `Publish to npm ${underline(dim`(${pkg$1})`)}?`,
@@ -130,14 +139,20 @@ const MSG = {
130
139
  NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
131
140
  NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again.`,
132
141
  NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}.`
142
+ },
143
+ ABORT: {
144
+ CANCEL: "Operation cancelled",
145
+ MONOREPO_CI_NO_PACKAGE: "CI mode requires a target package in monorepo. Please specify it via \"--package <pkg>\"",
146
+ MONOREPO_NO_PACKAGES: "Monorepo detected, but no packages found. Please configure \"config.packages\""
147
+ },
148
+ INFO: {
149
+ TOOL: (name, version) => `${name} ${dim(`v${version}`)}`,
150
+ CONFIG: (path$1) => `config file: ${underline(dim(path$1 || "not found"))}`
133
151
  }
134
152
  };
135
153
 
136
154
  //#endregion
137
155
  //#region src/github/release.ts
138
- const getGithubUrl = (owner, repo) => {
139
- return `https://github.com/${owner}/${repo}`;
140
- };
141
156
  const truncateBody = (body) => {
142
157
  if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
143
158
  return body;
@@ -164,28 +179,28 @@ const uploadAssets = async (ctx, config) => {
164
179
  }));
165
180
  };
166
181
  const createWebRelease = async (ctx, config) => {
167
- const { pkg: { next, toPreRelease }, github: { owner, repo, changelog, releaseName } } = ctx;
182
+ const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
168
183
  const { github: { prerelease: prerelease$1 } } = config;
169
184
  const github = getGithubUrl(owner, repo);
170
185
  const url = new URL(`${github}/releases/new`);
171
- url.searchParams.set("tag", next);
172
- url.searchParams.set("title", releaseName ?? `Release ${next}`);
186
+ url.searchParams.set("tag", currentTag);
187
+ url.searchParams.set("title", releaseName);
173
188
  url.searchParams.set("body", truncateBody(changelog));
174
189
  url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
175
190
  const isWindows = process.platform === "win32";
176
191
  await open(url.toString(), { wait: isWindows });
177
192
  };
178
193
  const createCiRelease = async (ctx, config) => {
179
- const { pkg: { next, toPreRelease }, github: { owner, repo, token, changelog, releaseName } } = ctx;
194
+ const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
180
195
  const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
181
196
  const octokit = new Octokit({ auth: token });
182
197
  try {
183
198
  const { data } = await octokit.repos.createRelease({
184
199
  owner,
185
200
  repo,
186
- tag_name: next,
201
+ tag_name: currentTag,
187
202
  target_commitish: void 0,
188
- name: releaseName ?? `Release ${next}`,
203
+ name: releaseName,
189
204
  body: autoGenerate ? "" : truncateBody(changelog),
190
205
  draft: draft ?? false,
191
206
  prerelease: (toPreRelease || prerelease$1) ?? false,
@@ -202,7 +217,7 @@ const createCiRelease = async (ctx, config) => {
202
217
  discussionUrl: discussion_url
203
218
  });
204
219
  } catch (e) {
205
- if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(next));
220
+ if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
206
221
  throw e;
207
222
  }
208
223
  };
@@ -252,133 +267,9 @@ const createRelease = async (ctx, config) => {
252
267
  await uploadAssets(ctx, config);
253
268
  }
254
269
  };
255
- const getGithubReleaseUrl = (ctx) => {
256
- const { github: { owner, repo }, git: { currentTag } } = ctx;
257
- return `${getGithubUrl(owner, repo)}/releases/tag/${normalizeTag(currentTag)}`;
258
- };
259
270
 
260
271
  //#endregion
261
272
  //#region src/git/commit.ts
262
- const getBranchName = () => {
263
- return runGit([
264
- "rev-parse",
265
- "--abbrev-ref",
266
- "HEAD"
267
- ]);
268
- };
269
- const getRemoteForBranch = (branch) => {
270
- return runGit([
271
- "config",
272
- "--get",
273
- `branch.${branch}.remote`
274
- ]);
275
- };
276
- const getStatus = () => {
277
- return runGit([
278
- "status",
279
- "--short",
280
- "--untracked-files=no"
281
- ], { trim: false });
282
- };
283
- const coloredChangeset = (log$1) => {
284
- const colorStatusChar = (ch) => {
285
- switch (ch) {
286
- case "M": return `\x1b[33m${ch}\x1b[0m`;
287
- case "A": return `\x1b[32m${ch}\x1b[0m`;
288
- case "D": return `\x1b[31m${ch}\x1b[0m`;
289
- default: return ch;
290
- }
291
- };
292
- const colorStatus = (status) => status.split("").map(colorStatusChar).join("");
293
- return log$1.split("\n").filter(Boolean).map((line) => {
294
- const status = line.slice(0, 2);
295
- const file = line.slice(3);
296
- return `${colorStatus(status)} ${styleText(["dim"], file)}`;
297
- }).join("\n");
298
- };
299
- const getRemote = async () => {
300
- const branch = await getBranchName();
301
- const remoteName = branch && await getRemoteForBranch(branch) || "origin";
302
- return {
303
- remoteName,
304
- remoteUrl: await runGit([
305
- "remote",
306
- "get-url",
307
- remoteName
308
- ]).catch((_) => runGit([
309
- "config",
310
- "--get",
311
- `remote.${remoteName}.url`
312
- ]))
313
- };
314
- };
315
- const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
316
- return runGit([
317
- "describe",
318
- "--tags",
319
- "--abbrev=0",
320
- `--match=${match}`,
321
- `--exclude=${exclude}`
322
- ]);
323
- };
324
- const getPreviousTag = async (current) => {
325
- return runGit([
326
- "describe",
327
- "--tags",
328
- "--abbrev=0",
329
- `${await runGit([
330
- "rev-list",
331
- "--tags",
332
- current || "--skip=1",
333
- "--max-count=1"
334
- ])}^`
335
- ]);
336
- };
337
- const resolveChangelogRange = async (isIncrement) => {
338
- const latestTag = await getLatestTag();
339
- const previousTag = await getPreviousTag(latestTag);
340
- if (!latestTag) return {
341
- from: "",
342
- to: "HEAD"
343
- };
344
- if (!isIncrement && previousTag) return {
345
- from: previousTag,
346
- to: `${latestTag}^1`
347
- };
348
- return {
349
- from: latestTag,
350
- to: "HEAD"
351
- };
352
- };
353
- const getLog = async (print, isIncrement) => {
354
- const cmd = ["log", `--pretty=format:${print ? "* %s (%h)" : "%s %h %H"}`];
355
- const { from, to } = await resolveChangelogRange(isIncrement);
356
- if (from) cmd.push(`${from}...${to}`);
357
- return runGit(cmd, { trim: false });
358
- };
359
- const isWorkingDirClean = async () => {
360
- return (await runGit(["status", "-s"]))?.length === 0;
361
- };
362
- const hasUpstreamBranch = async () => {
363
- const branch = await runGit([
364
- "for-each-ref",
365
- "--format=%(upstream:short)",
366
- await runGit(["symbolic-ref", "HEAD"])
367
- ]);
368
- return Boolean(branch);
369
- };
370
- const getUpstreamArgs = async (remoteName) => {
371
- const hasUpstream = await hasUpstreamBranch();
372
- const branch = await getBranchName();
373
- if (!hasUpstream) return [
374
- "--set-upstream",
375
- remoteName || "origin",
376
- branch
377
- ];
378
- if (remoteName) return [remoteName];
379
- return [];
380
- };
381
- const normalizeTag = (tag) => clean(tag) ?? tag;
382
273
  const gitCheck = async (ctx, config) => {
383
274
  const { pkg: { name } } = ctx;
384
275
  const { git: { requireRepository, requireRemote, requireWorkDirClean } } = config;
@@ -405,42 +296,25 @@ const commitAndTag = async (ctx, config) => {
405
296
  const { git: { commit, tag, push, addUntrackedFiles, commitArgs, tagArgs, pushArgs } } = config;
406
297
  if (!commit) return;
407
298
  const args = dryRun ? ["--dry-run"] : [];
408
- await runGit([
409
- "add",
410
- ".",
411
- addUntrackedFiles ? "--all" : "--update",
412
- ...args
413
- ]);
414
- await runGit([
415
- "commit",
416
- "--message",
417
- commitMessage,
418
- ...commitArgs,
419
- ...args
420
- ]);
299
+ addUntrackedFiles ? await gitAddAll() : await gitAddTracked(args);
300
+ await gitCommit(commitMessage, [...commitArgs, ...args]);
421
301
  if (!dryRun) Object.assign(ctx.git, { isCommitted: true });
422
302
  if (tag) {
423
- await runGit([
424
- "tag",
425
- "--annotate",
426
- "--message",
427
- tagMessage,
428
- currentTag,
429
- ...tagArgs,
430
- ...args
431
- ]);
303
+ await gitTagAnnotated(currentTag, tagMessage, [...tagArgs, ...args]);
432
304
  if (!dryRun) Object.assign(ctx.git, { isTagged: true });
433
305
  }
434
- if (push) await runGit([
435
- "push",
436
- ...await getUpstreamArgs(remoteName),
437
- "--follow-tags",
438
- ...pushArgs,
439
- ...args
440
- ]);
306
+ if (push) {
307
+ const remotes = await getAllRemotes();
308
+ for (const remoteName$1 of remotes) {
309
+ await pushTag(remoteName$1, currentTag, args);
310
+ await pushBranch(remoteName$1, await getCurrentBranch(), [...pushArgs, ...args]);
311
+ }
312
+ if (!dryRun) Object.assign(ctx.git, { isPushed: true });
313
+ }
441
314
  };
442
- const gitRollback$1 = (ctx) => {
443
- const { git: { currentTag, isCommitted, isTagged } } = ctx;
315
+ const gitRollback = (ctx) => {
316
+ const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
317
+ if (isPushed) return;
444
318
  spawnSync("git", ["restore", "."]);
445
319
  if (isTagged) spawnSync("git", [
446
320
  "tag",
@@ -454,16 +328,41 @@ const gitRollback$1 = (ctx) => {
454
328
  ]);
455
329
  };
456
330
 
331
+ //#endregion
332
+ //#region src/prompts.ts
333
+ const abortTask = (msg$1) => {
334
+ cancel(`${red("✕")} ${msg$1}`);
335
+ process.exit(0);
336
+ };
337
+ const abortSinglePrompt = (value) => {
338
+ if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
339
+ };
340
+ const abortGroupPrompt = (ctx) => {
341
+ ctx && gitRollback(ctx);
342
+ abortTask(MSG.ABORT.CANCEL);
343
+ };
344
+ const abortOnError = (err, ctx) => {
345
+ ctx && gitRollback(ctx);
346
+ const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
347
+ log.message(`\n${msg$1}\n`);
348
+ process.exit(1);
349
+ };
350
+ const taskEnd = (msg$1) => {
351
+ outro(msg$1);
352
+ process.exit(0);
353
+ };
354
+
457
355
  //#endregion
458
356
  //#region src/utils.ts
459
357
  const info = (msg$1) => {
460
- console.log(`${rgb(33, 91, 184)(`i`)} ${gray(msg$1)}`);
358
+ console.log(`${rgb(33, 91, 184)(`i`)} ${msg$1}`);
461
359
  };
462
360
  const msg = (prefix, msg$1) => {
463
- log.message(gray`${rgb(33, 91, 184)(`[${prefix}]`)} ${msg$1}`);
361
+ log.message(`${rgb(33, 91, 184)(`[${prefix}]`)} ${dim(msg$1)}`);
464
362
  };
465
363
  const question = (msg$1, type) => {
466
364
  return `${{
365
+ package: yellow,
467
366
  version: green,
468
367
  git: rgb(220, 94, 62),
469
368
  npm: rgb(186, 70, 61),
@@ -476,7 +375,7 @@ const success = (msg$1, dryRun) => {
476
375
  const runLifeCycleHook = async (hooks, key, dryRun) => {
477
376
  const handler = hooks[key];
478
377
  if (typeof handler === "function") {
479
- if (!dryRun) await handler();
378
+ if (!dryRun) await handler(log);
480
379
  log.success(success(`${dim`run`} ${inspect(handler)}`, dryRun));
481
380
  return;
482
381
  }
@@ -485,33 +384,19 @@ const runLifeCycleHook = async (hooks, key, dryRun) => {
485
384
  log.success(success(hook.replace(/^echo$/, dim("echo")), dryRun));
486
385
  }));
487
386
  };
488
- const gitRollback = (ctx) => {
489
- const { git: { currentTag, isCommitted, isTagged } } = ctx;
490
- spawnSync("git", ["restore", "."]);
491
- if (isTagged) spawnSync("git", [
492
- "tag",
493
- "--delete",
494
- currentTag
495
- ]);
496
- spawnSync("git", [
497
- "reset",
498
- "--hard",
499
- isCommitted ? "HEAD~1" : "HEAD"
500
- ]);
501
- };
502
387
  const diff = (from, to, separator = ".") => {
503
388
  const a = from.split(separator);
504
389
  return to.split(separator).map((v, i) => v === a[i] ? dim(v) : green(v)).join(separator);
505
390
  };
506
391
  const formatTemplate = async (ctx, config) => {
507
- const { pkg: { next } } = ctx;
508
- const { git: { commitMessage: cm, tagMessage: tm, tagName }, github: { releaseName: rm } } = config;
509
- const f = (s, v) => s.replace("${version}", v);
392
+ const { pkg: { next }, selectedPkg } = ctx;
393
+ const { git: { commitMessage: cm, tagMessage: tm }, github: { releaseName: rm }, toTag } = config;
394
+ const f = (s, v) => s.replace("${tag}", v);
510
395
  const latestTag = await getLatestTag() ?? "";
511
- const currentTag = f(tagName || (latestTag.match(/^v/) ? "v${version}" : "${version}"), next);
512
- const commitMessage = f(cm, next);
513
- const tagMessage = f(tm, next);
514
- const releaseName = f(rm, next);
396
+ const currentTag = toTag(selectedPkg, next);
397
+ const commitMessage = f(cm, currentTag);
398
+ const tagMessage = f(tm, currentTag);
399
+ const releaseName = f(rm, currentTag);
515
400
  Object.assign(ctx.git, {
516
401
  latestTag,
517
402
  currentTag,
@@ -521,30 +406,6 @@ const formatTemplate = async (ctx, config) => {
521
406
  Object.assign(ctx.github, { releaseName });
522
407
  };
523
408
 
524
- //#endregion
525
- //#region src/prompts.ts
526
- const abortTask = (msg$1) => {
527
- cancel(`${red("✕")} ${msg$1}`);
528
- process.exit(0);
529
- };
530
- const abortSinglePrompt = (value) => {
531
- if (isCancel(value)) abortTask("Operation cancelled.");
532
- };
533
- const abortGroupPrompt = (ctx) => {
534
- ctx && gitRollback(ctx);
535
- abortTask("Operation cancelled.");
536
- };
537
- const abortOnError = (err, ctx) => {
538
- ctx && gitRollback(ctx);
539
- const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
540
- log.message(`\n${msg$1}\n`);
541
- process.exit(1);
542
- };
543
- const taskEnd = (msg$1) => {
544
- outro(msg$1);
545
- process.exit(0);
546
- };
547
-
548
409
  //#endregion
549
410
  //#region src/version/bump.ts
550
411
  const isPreRelease = (version) => {
@@ -679,7 +540,7 @@ const runVersionPrompts = async (ctx, config) => {
679
540
  };
680
541
  const type = await selectVersion();
681
542
  const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
682
- ctx.isIncrement = gte(next, current);
543
+ ctx.isIncrement = !eq(next, current);
683
544
  return { next };
684
545
  };
685
546
 
@@ -698,6 +559,7 @@ const createDefaultContext = () => {
698
559
  increment: "",
699
560
  isIncrement: false,
700
561
  configFileExists: false,
562
+ selectedPkg: "",
701
563
  pkg: {
702
564
  current: "",
703
565
  next: "",
@@ -725,9 +587,9 @@ const createDefaultContext = () => {
725
587
  currentTag: "",
726
588
  isCommitted: false,
727
589
  isTagged: false,
590
+ isPushed: false,
728
591
  commitMessage: "",
729
- tagMessage: "",
730
- tagName: ""
592
+ tagMessage: ""
731
593
  },
732
594
  github: {
733
595
  username: "",
@@ -757,14 +619,22 @@ const createDefaultConfig = (isCI) => {
757
619
  "before:release": "",
758
620
  "after:release": ""
759
621
  },
622
+ isMonorepo: false,
623
+ packages: [],
624
+ getPkgDir(pkg$1) {
625
+ return ".";
626
+ },
627
+ toTag(pkg$1, version) {
628
+ return `v${version}`;
629
+ },
630
+ tagPrefix: void 0,
760
631
  git: {
761
632
  commit: isCI,
762
633
  tag: isCI,
763
634
  push: isCI,
764
- commitMessage: "chore(release): ${version}",
635
+ commitMessage: "chore(release): ${tag}",
765
636
  commitArgs: [],
766
- tagMessage: "Release ${version}",
767
- tagName: "${version}",
637
+ tagMessage: "${tag}",
768
638
  tagArgs: [],
769
639
  pushArgs: [],
770
640
  addUntrackedFiles: false,
@@ -780,17 +650,13 @@ const createDefaultConfig = (isCI) => {
780
650
  },
781
651
  github: {
782
652
  release: isCI,
783
- releaseName: "Release ${version}",
653
+ releaseName: "${tag}",
784
654
  autoGenerate: false,
785
655
  prerelease: false,
786
656
  draft: false,
787
657
  tokenRef: "GITHUB_TOKEN",
788
658
  assets: [],
789
659
  skipChecks: false
790
- },
791
- changelog: {
792
- infile: "CHANGELOG.md",
793
- types: []
794
660
  }
795
661
  };
796
662
  };
@@ -803,15 +669,25 @@ const defaultTypes = [
803
669
  section: "✨ 新功能",
804
670
  description: "新增功能"
805
671
  },
672
+ {
673
+ type: "feature",
674
+ section: "✨ 新功能",
675
+ description: "新增功能"
676
+ },
806
677
  {
807
678
  type: "fix",
808
679
  section: "🐛 Bug 修复",
809
680
  description: "修复 bug"
810
681
  },
811
682
  {
812
- type: "chore",
813
- section: "🎫 其他更新",
814
- description: "非功能性更改"
683
+ type: "perf",
684
+ section: "⚡ 性能优化",
685
+ description: "提升性能"
686
+ },
687
+ {
688
+ type: "revert",
689
+ section: "⏪ 回退",
690
+ description: "回退到之前版本"
815
691
  },
816
692
  {
817
693
  type: "docs",
@@ -823,26 +699,21 @@ const defaultTypes = [
823
699
  section: "💄 样式调整",
824
700
  description: "样式或格式修改"
825
701
  },
702
+ {
703
+ type: "chore",
704
+ section: "🎫 其他更新",
705
+ description: "非功能性更改"
706
+ },
826
707
  {
827
708
  type: "refactor",
828
709
  section: "♻ 代码重构",
829
710
  description: "重构代码"
830
711
  },
831
- {
832
- type: "perf",
833
- section: "⚡ 性能优化",
834
- description: "提升性能"
835
- },
836
712
  {
837
713
  type: "test",
838
714
  section: "✅ 测试",
839
715
  description: "测试更新"
840
716
  },
841
- {
842
- type: "revert",
843
- section: "⏪ 回退",
844
- description: "回退到之前版本"
845
- },
846
717
  {
847
718
  type: "build",
848
719
  section: "👷‍ 构建系统",
@@ -882,94 +753,80 @@ const defaultTypes = [
882
753
  type: "hotfix",
883
754
  section: "🔥 热修复",
884
755
  description: "紧急修复"
885
- },
886
- {
887
- type: "break",
888
- section: "💥 破坏性变更",
889
- description: "破坏性变更"
890
756
  }
891
757
  ];
892
- const mergeTypes = (types) => {
893
- types.forEach((item) => {
894
- const index = defaultTypes.findIndex((it) => it.type === item.type);
895
- if (index >= 0) defaultTypes.splice(index, 1, { ...item });
896
- else defaultTypes.push({ ...item });
897
- });
898
- };
899
- const findType = (type) => {
900
- return defaultTypes.find((item) => item.type === type)?.section ?? type;
901
- };
902
758
 
903
759
  //#endregion
904
760
  //#region src/git/changelog.ts
905
- const getCommits = async (isIncrement) => {
906
- return (await getLog(false, isIncrement))?.split("\n").filter(Boolean);
907
- };
908
- const parseCommit = (message) => {
909
- const match = message.match(/^(\w+):\s(.+)\s(\w{7})\s(\w{40})$/);
910
- if (!match) return;
911
- return {
912
- type: match[1],
913
- description: match[2],
914
- hash: match[3],
915
- fullHash: match[4]
916
- };
917
- };
918
- const classify = (commits) => {
919
- const sections = {};
920
- for (const msg$1 of commits) {
921
- const parsed = parseCommit(msg$1);
922
- if (!parsed) continue;
923
- const { type, description, hash, fullHash } = parsed;
924
- (sections[type] ??= []).push([
925
- description,
926
- hash,
927
- fullHash
928
- ]);
761
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
762
+ const pkgDir = getPkgDir();
763
+ const infile = join(pkgDir, "CHANGELOG.md");
764
+ if (!existsSync(infile)) await writeFile(infile, "");
765
+ const originalChangelog = readFileSync(infile, "utf-8");
766
+ const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
767
+ ...t$1,
768
+ hidden: false
769
+ })) });
770
+ preset.writer ??= {};
771
+ preset.writer.headerPartial = `
772
+ ## {{#if isPatch~}} <small> {{~/if~}}
773
+ {{#if @root.linkCompare~}}
774
+ [{{version}}](
775
+ {{~#if @root.repository~}}
776
+ {{~#if @root.host}}
777
+ {{~@root.host}}/
778
+ {{~/if}}
779
+ {{~#if @root.owner}}
780
+ {{~@root.owner}}/
781
+ {{~/if}}
782
+ {{~@root.repository}}
783
+ {{~else}}
784
+ {{~@root.repoUrl}}
785
+ {{~/if~}}
786
+ /compare/{{previousTag}}...{{currentTag}})
787
+ {{~else}}
788
+ {{~version}}
789
+ {{~/if}}
790
+ {{~#if title}} "{{title}}"
791
+ {{~/if}}
792
+ {{~#if date}} ({{date}})
793
+ {{~/if}}
794
+ {{~#if isPatch~}} </small> {{~/if}}
795
+ `.trim() + eol();
796
+ preset.writer.mainTemplate = `
797
+ {{> header}}
798
+ {{#if noteGroups}}
799
+ {{#each noteGroups}}
800
+
801
+ ### ⚠ {{title}}
802
+
803
+ {{#each notes}}
804
+ * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
805
+ {{/each}}
806
+ {{/each}}
807
+ {{/if}}
808
+ {{#each commitGroups}}
809
+
810
+ {{#if title}}
811
+ ### {{title}}
812
+
813
+ {{/if}}
814
+ {{#each commits}}
815
+ {{> commit root=@root}}
816
+ {{/each}}
817
+ {{/each}}`.trim() + eol(2);
818
+ const generator = new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
819
+ const writeStream = createWriteStream(infile);
820
+ let changelog = "";
821
+ for await (const chunk of generator.write()) {
822
+ changelog += chunk;
823
+ writeStream.write(chunk);
929
824
  }
930
- return sections;
931
- };
932
- const renderChangelog = async (ctx, sections) => {
933
- const { github: { owner, repo }, git: { latestTag, currentTag } } = ctx;
934
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
935
- const github = getGithubUrl(owner, repo);
936
- return [
937
- latestTag ? `## [${currentTag}](${github}/compare/${latestTag}...${currentTag}) (${date})` : `## ${currentTag} (${date})`,
938
- Object.entries(sections).map((entry) => {
939
- const [type, items] = entry;
940
- return [`### ${findType(type)}`, items.map((item) => {
941
- const [desc, hash, fullHash] = item;
942
- return `* **${type}** ${desc} [${hash}](${github}/commit/${fullHash})`;
943
- }).join(eol(1))].join(eol(2));
944
- }).join(eol(2)),
945
- latestTag ? `**Full Changelog**: ${github}/compare/${latestTag}...${currentTag}` : ""
946
- ].join(eol(3));
947
- };
948
- const generateChangelog = async (ctx) => {
949
- const { isIncrement } = ctx;
950
- const commits = await getCommits(isIncrement);
951
- if (!commits) return "";
952
- const changelog = await renderChangelog(ctx, classify(commits));
953
- Object.assign(ctx.github, { changelog });
825
+ writeStream.write(originalChangelog);
826
+ writeStream.end();
827
+ await finished(writeStream);
954
828
  return changelog;
955
829
  };
956
- const updateChangelog = async (file, newContent) => {
957
- const infile = file || "CHANGELOG.md";
958
- const target = join(process.cwd(), infile);
959
- const SHORT_TITLE = "# Changelog";
960
- const exist = (existsSync(target) ? await readFile(target, "utf-8") : "").replace(/\r\n/g, "\n");
961
- await writeFile(target, [
962
- SHORT_TITLE,
963
- newContent,
964
- exist.startsWith(SHORT_TITLE) ? exist.slice(11) : exist
965
- ].map((k) => k.trim()).join(eol(4)) + eol());
966
- await runGit(["add", infile]);
967
- };
968
- const writeChangelog = async (ctx, config) => {
969
- const { changelog: { infile, types } } = config;
970
- mergeTypes(types);
971
- await updateChangelog(infile, await generateChangelog(ctx));
972
- };
973
830
 
974
831
  //#endregion
975
832
  //#region src/git/prompts.ts
@@ -1063,108 +920,45 @@ const runNpmOptPrompts = async (ctx, config) => {
1063
920
 
1064
921
  //#endregion
1065
922
  //#region src/npm/publish.ts
1066
- const getRegistry = (publishConfig = {}) => {
1067
- return (publishConfig.registry ? [publishConfig.registry] : Object.keys(publishConfig).filter((key) => key.endsWith("registry")).map((key) => publishConfig[key]))[0];
1068
- };
1069
- const isRegistryUp = async (registry) => {
1070
- return void 0 !== await runNpm(["ping", ...registry ? ["--registry", registry] : []]);
1071
- };
1072
- const isAuthenticated = (registry) => {
1073
- return runNpm(["whoami", ...registry ? ["--registry", registry] : []]);
1074
- };
1075
- const getLatestRegistryVersion = (name, tag, registry) => {
1076
- const registryArg = registry ? ["--registry", registry] : [];
1077
- return runNpm([
1078
- "show",
1079
- `${name}@${tag}`,
1080
- "version",
1081
- ...registryArg
1082
- ]);
1083
- };
1084
- const bump = (version, allowSameVersion, args = []) => {
1085
- allowSameVersion && args.push(`--allow-same-version`);
1086
- return runNpm([
1087
- "version",
1088
- version,
1089
- "--no-git-tag-version",
1090
- "--workspaces=false",
1091
- ...args
1092
- ]);
1093
- };
1094
- const resolveTag = async (version) => {
923
+ const resolvePublishTag = async (pkgName, version) => {
1095
924
  const { toPreRelease, preId } = parseVersion(version);
1096
- if (!toPreRelease) return "latest";
1097
- else return preId || "next";
1098
- };
1099
- const isCollaborator = async (ctx) => {
1100
- const { pkg: { name, publishConfig }, npm: { username } } = ctx;
1101
- const registry = getRegistry(publishConfig);
1102
- const registryArg = registry ? ["--registry", registry] : [];
1103
- let npmVersion = await runNpm(["--version"]);
1104
- let accessCommand;
1105
- if (gt(npmVersion, "9.0.0")) accessCommand = [
1106
- "access",
1107
- "list",
1108
- "collaborators",
1109
- "--json"
1110
- ];
1111
- else accessCommand = ["access", "ls-collaborators"];
1112
- const res = await runNpm([
1113
- ...accessCommand,
1114
- name,
1115
- ...registryArg
1116
- ]);
1117
- if (res) {
1118
- const permissions = JSON.parse(res ?? "{}")[username];
1119
- return permissions && permissions.includes("write");
1120
- }
1121
- log.error(`Unable to verify if user ${username} is a collaborator for ${name}.`);
1122
- };
1123
- const getPackageUrl = (ctx) => {
1124
- const { pkg: { name, next } } = ctx;
1125
- return `https://www.npmjs.com/package/${name}/v/${next}`;
925
+ if (!toPreRelease) {
926
+ const active = await getPublishedVersion(pkgName);
927
+ return active && lt(version, active) ? "previous" : "latest";
928
+ } else return preId || "prerelease";
1126
929
  };
930
+ const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
1127
931
  const npmCheck = async (ctx, config) => {
1128
- const { pkg: { name, current, publishConfig }, npm: { username } } = ctx;
932
+ const { pkg: { name, current, publishConfig: { registry } } } = ctx;
1129
933
  const { npm: { skipChecks } } = config;
1130
- const registry = getRegistry(publishConfig);
1131
- const tag = await resolveTag(current);
934
+ const tag = await resolvePublishTag(name, current);
1132
935
  Object.assign(ctx.npm, { tag });
1133
936
  if (skipChecks) return "";
1134
- if (!await isRegistryUp()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1135
- const getUser = async () => {
1136
- const username$1 = await isAuthenticated(registry);
1137
- if (!username$1) return false;
1138
- Object.assign(ctx.npm, { username: username$1 });
1139
- return true;
1140
- };
1141
- if (!await getUser()) throw new Error(MSG.ERROR.NPM_AUTH);
1142
- const latestPublish = await getLatestRegistryVersion(name, tag, registry);
1143
- if (latestPublish) {
1144
- if (!await isCollaborator(ctx)) throw new Error(MSG.ERROR.NPM_USER(username, name));
937
+ const [pinged, username] = await Promise.all([pingRegistry(registry), getAuthenticatedUser(registry)]);
938
+ if (!pinged) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
939
+ if (!username) throw new Error(MSG.ERROR.NPM_AUTH);
940
+ Object.assign(ctx.npm, { username });
941
+ const latest = await getPublishedVersion(`${name}@${tag}`);
942
+ if (latest) {
943
+ if (!await hasWriteAccess(name, username, registry)) throw new Error(MSG.ERROR.NPM_USER(username, name));
1145
944
  }
1146
- return underline(dim(latestPublish ? `(latest is ${latestPublish})` : "(new publish)"));
945
+ return latest ? `(${tag} → ${latest})` : `(${tag} → no published version)`;
1147
946
  };
1148
- const isOtpError = (err) => /one-time password|otp/i.test(err.message);
1149
947
  const publishNpm = async (ctx, config) => {
1150
- const { dryRun, npm: { otp, tag }, pkg: { publishConfig } } = ctx;
1151
- const { npm: { publish, publishPath, pushArgs } } = config;
1152
- const registry = getRegistry(publishConfig);
948
+ const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
949
+ const { npm: { publish, pushArgs }, getPkgDir } = config;
1153
950
  if (!publish) return;
1154
951
  const otpArgs = otp ? ["--otp", otp] : [];
1155
952
  const dryRunArg = dryRun ? ["--dry-run"] : [];
1156
- const registryArg = registry ? ["--registry", registry] : [];
1157
- return runNpm([
1158
- "publish",
1159
- publishPath,
1160
- "--tag",
953
+ return publishPackage({
1161
954
  tag,
1162
- "--workspaces=false",
1163
- ...otpArgs,
1164
- ...pushArgs,
1165
- ...registryArg,
1166
- ...dryRunArg
1167
- ]);
955
+ args: [
956
+ ...otpArgs,
957
+ ...pushArgs,
958
+ ...dryRunArg
959
+ ],
960
+ cwd: getPkgDir(selectedPkg)
961
+ });
1168
962
  };
1169
963
 
1170
964
  //#endregion
@@ -1172,17 +966,38 @@ const publishNpm = async (ctx, config) => {
1172
966
  var Action = class {
1173
967
  async handle(cmdArgs, options) {
1174
968
  const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
1175
- info(`${cName} ${dim(`v${cVersion}`)} `);
1176
- const { otp, ...others } = options;
969
+ const { otp, package: defPkg, ...others } = options;
1177
970
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
1178
971
  process.env["dryRun"] = String(dryRun);
1179
- const { version: pkgVersion, private: pkgPrivate, name: pkgName, publishConfig = {} } = readJsonFile("package.json");
1180
- const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1181
- const local = await resolveConfig(process.cwd(), (path$1) => {
1182
- info(`config file: ${underline(dim(path$1))}`);
1183
- });
972
+ const { configPath, config: local } = await resolveConfig(process.cwd());
1184
973
  const config = mergeConfig(createDefaultConfig(ci ? true : void 0), local);
974
+ info(MSG.INFO.TOOL(cName, cVersion));
975
+ info(MSG.INFO.CONFIG(configPath));
976
+ console.log();
977
+ intro(MSG.INTRO(dryRun));
978
+ const { isMonorepo, packages, getPkgDir } = config;
979
+ if (isMonorepo) {
980
+ if (ci) {
981
+ if (!defPkg) abortTask(MSG.ABORT.MONOREPO_CI_NO_PACKAGE);
982
+ } else if (packages.length < 1) abortTask(MSG.ABORT.MONOREPO_NO_PACKAGES);
983
+ }
984
+ const selectPackage = async () => {
985
+ const pkg$2 = await select({
986
+ message: question(MSG.PROMPT.SELECT_PACKAGE, "package"),
987
+ options: packages.map((pkg$3) => ({
988
+ label: pkg$3,
989
+ value: pkg$3
990
+ }))
991
+ });
992
+ abortSinglePrompt(pkg$2);
993
+ return pkg$2;
994
+ };
995
+ const selectedPkg = !isMonorepo ? "" : ci ? defPkg : packages.length === 1 ? packages[0] : await selectPackage();
996
+ const { pkg: pkg$1, pkgDir } = getPackageInfo(selectedPkg, getPkgDir);
997
+ const { version: pkgVersion, private: pkgPrivate = false, name: pkgName, publishConfig = {} } = pkg$1;
998
+ const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1185
999
  const ctx = mergeConfig(createDefaultContext(), {
1000
+ selectedPkg,
1186
1001
  configFileExists: Object.keys(local).length > 0,
1187
1002
  dryRun,
1188
1003
  showRelease,
@@ -1208,15 +1023,18 @@ var Action = class {
1208
1023
  },
1209
1024
  noNpm: pkgPrivate
1210
1025
  });
1211
- console.log();
1212
- intro(MSG.INTRO(pkgName, dryRun));
1026
+ const { messages } = await publint({ pkgDir });
1027
+ for (const message of messages) {
1028
+ const formated = formatMessage(message, pkg$1);
1029
+ formated && msg("PKG", formated);
1030
+ }
1213
1031
  await this.checkTask(ctx, config);
1214
1032
  await this.bumpTask(ctx, config);
1215
1033
  await this.changelogTask(ctx, config);
1216
1034
  await this.gitTask(ctx, config);
1217
1035
  await this.npmTask(ctx, config);
1218
1036
  await this.githubTask(ctx, config);
1219
- if (dryRun) gitRollback$1(ctx);
1037
+ if (dryRun) gitRollback(ctx);
1220
1038
  taskEnd(MSG.OUTRO(dryRun));
1221
1039
  }
1222
1040
  async checkTask(ctx, config) {
@@ -1239,8 +1057,8 @@ var Action = class {
1239
1057
  }]).catch(abortOnError);
1240
1058
  }
1241
1059
  async bumpTask(ctx, config) {
1242
- const { pkg: { current }, dryRun, isCI, showRelease, showChangelog } = ctx;
1243
- const { hooks } = config;
1060
+ const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
1061
+ const { isMonorepo, hooks, getPkgDir, tagPrefix } = config;
1244
1062
  const need = (ctx$1) => {
1245
1063
  const { pkg: { next: next$1 }, isIncrement: isIncrement$1 } = ctx$1;
1246
1064
  return !next$1 && isIncrement$1;
@@ -1255,32 +1073,39 @@ var Action = class {
1255
1073
  await tasks([{
1256
1074
  title: MSG.TASK.VERSION.START,
1257
1075
  task: async () => {
1258
- await bump(next, isIncrement);
1076
+ await bumpPackageVersion(next, [], getPkgDir(selectedPkg));
1259
1077
  await formatTemplate(ctx, config);
1260
- const to = `(${current}...${diff(current, next)})`;
1261
- return success(MSG.TASK.VERSION.END(to), dryRun);
1078
+ const to$1 = `(${current}...${diff(current, next)})`;
1079
+ return success(MSG.TASK.VERSION.END(to$1), dryRun);
1262
1080
  }
1263
1081
  }]).catch((err) => abortOnError(err, ctx));
1264
1082
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1265
- const logStr = (await getLog(true, isIncrement))?.split("\n").filter((line) => /^(\*\s+)?(\w+):\s(.+)\s\((\w{7})\)$/.test(line)).join("\n");
1083
+ const { from, to } = await resolveChangelogRange(isIncrement, isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*");
1084
+ const logStr = await getLog(from, to, getPkgDir(selectedPkg));
1266
1085
  if (logStr) {
1267
- msg("GIT", `Changelog:${eol(2)}` + dim(logStr));
1086
+ msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + logStr);
1268
1087
  if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1269
1088
  } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1270
- const changeset = await getStatus();
1271
- if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1272
- else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1273
1089
  }
1274
1090
  async changelogTask(ctx, config) {
1275
- const { isIncrement, dryRun } = ctx;
1091
+ const { isIncrement, dryRun, selectedPkg } = ctx;
1092
+ const { getPkgDir, tagPrefix } = config;
1276
1093
  await tasks([{
1277
1094
  title: MSG.TASK.CHANGELOG.START,
1278
1095
  task: async () => {
1279
- await writeChangelog(ctx, config);
1096
+ const changelog = await generateChangelog({
1097
+ getPkgDir: () => getPkgDir(selectedPkg),
1098
+ tagPrefix: tagPrefix?.(selectedPkg)
1099
+ });
1100
+ await runGit(["add", `${getPkgDir(selectedPkg)}/CHANGELOG.md`]);
1101
+ Object.assign(ctx.github, { changelog });
1280
1102
  return success(MSG.TASK.CHANGELOG.END, dryRun);
1281
1103
  },
1282
1104
  enabled: isIncrement
1283
1105
  }]).catch((err) => abortOnError(err, ctx));
1106
+ const changeset = await getStatus();
1107
+ if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1108
+ else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1284
1109
  }
1285
1110
  async gitTask(ctx, config) {
1286
1111
  const { noGit, dryRun } = ctx;
@@ -1298,7 +1123,7 @@ var Action = class {
1298
1123
  await runLifeCycleHook(hooks, "after:push", dryRun);
1299
1124
  }
1300
1125
  async npmTask(ctx, config) {
1301
- const { noNpm, dryRun, isCI } = ctx;
1126
+ const { noNpm, dryRun, isCI, pkg: { name, next } } = ctx;
1302
1127
  const { hooks } = config;
1303
1128
  if (noNpm) return;
1304
1129
  await runNpmPublishPrompts(ctx, config);
@@ -1312,7 +1137,7 @@ var Action = class {
1312
1137
  await publishNpm(ctx, config);
1313
1138
  } else throw err;
1314
1139
  });
1315
- const url = getPackageUrl(ctx);
1140
+ const url = getPackageUrl(name, next);
1316
1141
  return success(MSG.TASK.NPM.END(url), dryRun);
1317
1142
  }
1318
1143
  }]).catch((err) => abortOnError(err, ctx));
@@ -1327,8 +1152,9 @@ var Action = class {
1327
1152
  await tasks([{
1328
1153
  title: MSG.TASK.GITHUB.START,
1329
1154
  task: async () => {
1155
+ const { github: { owner, repo }, git: { currentTag } } = ctx;
1330
1156
  await createRelease(ctx, config);
1331
- const url = getGithubReleaseUrl(ctx);
1157
+ const url = getGithubReleaseUrl(owner, repo, currentTag);
1332
1158
  return success(MSG.TASK.GITHUB.END(url), dryRun);
1333
1159
  }
1334
1160
  }]).catch((err) => abortOnError(err, ctx));
@@ -1339,9 +1165,33 @@ var Action = class {
1339
1165
  //#endregion
1340
1166
  //#region src/cli.ts
1341
1167
  const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1342
- program.name(pkg.name).description(pkg.description).version(pkg.version, "-v, --version", "Output the current version.").usage("[ARGUMENT] [OPTION]...").argument("[argument]", "patch|minor|major").option("-n, --dry-run", "Report actions that would be performed without writing out results.", false).option("--ci", "CI mode", false).option("--otp", "npm publish otp", "").option("--show-changelog", "Skip npm publish", false).option("--show-release", "Skip github release", false).action(async (argName, options) => {
1343
- await new Action().handle(argName, options);
1344
- }).helpOption("-h, --help", "Output usage information.").parse(process.argv);
1168
+ program.name("release").description(pkg.description).version(pkg.version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", "Version bump type: patch | minor | major").option("-n, --dry-run", "Run in dry mode: show what would be released without making any changes.", false).option("-p, --package <pkg>", "Specify package name for mono-repo release (CI only).", "").option("--otp <code>", "One-time password for npm publish (used for 2FA).", "").option("--ci", "Enable CI mode: disable prompts and fail on missing required options.", false).option("--show-changelog", "Print the generated changelog and exit.", false).option("--show-release", "Print the version that would be released and exit.", false).helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1169
+ await new Action().handle(releaseType, options);
1170
+ });
1171
+ program.command("init").description("Create a release configuration file").option("-f, --force", "Overwrite existing config file", false).option("-m, --monorepo", "Mono-repo project", false).action((options) => {
1172
+ const { force = false, monorepo = false } = options;
1173
+ const cwd = process.cwd();
1174
+ const { type } = readJsonFile(join(cwd, "package.json"));
1175
+ const isESM = type === "module";
1176
+ const isTS = existsSync(join(cwd, "tsconfig.json"));
1177
+ const configPath = join(cwd, `release.config.${isESM ? "" : "m"}${isTS ? "ts" : "js"}`);
1178
+ if (existsSync(configPath) && !force) {
1179
+ console.error(`${red`Error:`} ${yellow`release.config.ts`} already exists.\nUse ${green`--force`} to overwrite.`);
1180
+ process.exit(1);
1181
+ }
1182
+ const monoConfigToString = () => `{
1183
+ isMonorepo: true,
1184
+ packages: [ 'demo-a', 'demo-b' ],
1185
+ getPkgDir: (pkg) => \`packages/\${ pkg }\`,
1186
+ toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1187
+ tagPrefix: (pkg) => \`\${ pkg }@\`,
1188
+ }`;
1189
+ writeFileSync(configPath, `import { defineConfig } from '@peiyanlu/release'
1190
+
1191
+ export default defineConfig(${monorepo ? monoConfigToString() : "{}"})`, "utf-8");
1192
+ console.log(`${green`✔ release.config.ts`} created successfully`);
1193
+ });
1194
+ program.parse(process.argv);
1345
1195
  program.on("command:*", () => {
1346
1196
  console.error(`\n${red`Error`} Invalid command: ${red`%s`}`, program.args.join(" "));
1347
1197
  console.log(`See ${red`--help`} for a list of available commands.\n`);