@peiyanlu/release 0.0.2 → 0.0.4

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,23 +1,29 @@
1
- import path, { dirname, join, resolve } from "node:path";
1
+ import path, { join, resolve } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { eol, execAsync, isGitRepo, parseGitHubRepo, readJsonFile, runGit, runNpm } from "@peiyanlu/cli-utils";
4
- import { blue, dim, gray, green, red, rgb, underline, yellow } from "ansis";
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 { createReadStream, existsSync, statSync } from "node:fs";
9
- import { readFile, writeFile } from "node:fs/promises";
8
+ import { publint } from "publint";
9
+ import { formatMessage } from "publint/utils";
10
+ import { clean, coerce, eq, gt, inc, lt, parse, prerelease, valid } from "semver";
11
+ import { castArray, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
12
+ import { spawnSync } from "node:child_process";
13
+ import { inspect } from "node:util";
10
14
  import { Octokit } from "@octokit/rest";
11
15
  import { contentType } from "mime-types";
12
16
  import open from "open";
13
17
  import { glob } from "tinyglobby";
14
- import { inspect, styleText } from "node:util";
15
- import { castArray, isUndefined } from "@peiyanlu/ts-utils";
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";
16
22
 
17
- //#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
18
24
  const getFilename = () => fileURLToPath(import.meta.url);
19
25
  const getDirname = () => path.dirname(getFilename());
20
- const __dirname$1 = /* @__PURE__ */ getDirname();
26
+ const __dirname = /* @__PURE__ */ getDirname();
21
27
 
22
28
  //#endregion
23
29
  //#region src/config.ts
@@ -25,10 +31,8 @@ const findConfigFile = (cwd = process.cwd()) => {
25
31
  for (const ext of [
26
32
  "ts",
27
33
  "mts",
28
- "cts",
29
34
  "js",
30
- "mjs",
31
- "cjs"
35
+ "mjs"
32
36
  ]) {
33
37
  const path$1 = resolve(cwd, `release.config.${ext}`);
34
38
  if (existsSync(path$1)) return path$1;
@@ -40,15 +44,18 @@ const loadConfig = async (file) => {
40
44
  const temp = mod.default ?? mod;
41
45
  return typeof temp === "function" ? temp() : temp;
42
46
  };
43
- const resolveConfig = async (cwd, log$1) => {
47
+ const resolveConfig = async (cwd) => {
44
48
  const configFile = findConfigFile(cwd);
45
- if (!configFile) return {};
46
- log$1?.(configFile);
47
- return loadConfig(configFile);
49
+ if (!configFile) return {
50
+ configPath: "",
51
+ config: {}
52
+ };
53
+ return {
54
+ configPath: configFile,
55
+ config: await loadConfig(configFile)
56
+ };
48
57
  };
49
- const toString = (self) => Object.prototype.toString.call(self);
50
- const isPlainObject = (v) => toString(v) === "[object Object]";
51
- const isFunction = (v) => toString(v) === "[object Function]";
58
+ const isFunction = (v) => typeof v === "function";
52
59
  const mergeConfig = (defaults, overrides) => {
53
60
  if (!overrides) return structuredClone(defaults);
54
61
  if (Object.keys(defaults).length === 0) return structuredClone(overrides);
@@ -67,15 +74,11 @@ const mergeConfig = (defaults, overrides) => {
67
74
 
68
75
  //#endregion
69
76
  //#region src/messages.ts
70
- const n = (b) => {
71
- return b ? "(dry run)" : "";
72
- };
73
- const t = () => {
74
- return green(Math.floor(process.uptime()));
75
- };
77
+ const n = (b) => b ? "(dry run)" : "";
78
+ const t = () => green(`${Math.floor(process.uptime())}s`);
76
79
  const MSG = {
77
- INTRO: (pkgName, dryRun) => `🚀 Starting release ${yellow(pkgName)} ${n(dryRun)}`,
78
- OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()}s ${n(dryRun)} `,
80
+ INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
81
+ OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
79
82
  LOG: {
80
83
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
81
84
  SHOW_CHANGELOG: "🎉 Changelog collected",
@@ -85,21 +88,25 @@ const MSG = {
85
88
  CHECK: {
86
89
  GIT: {
87
90
  CHECKING: "Checking Git repository",
88
- 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"}`
89
92
  },
90
93
  NPM: {
91
94
  CHECKING: "Checking npm registry",
92
- 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))}`
93
96
  }
94
97
  },
95
98
  TASK: {
96
99
  VERSION: {
97
100
  START: "Bumping version",
98
- END: (version) => `Version bumped to ${green(version)}`
101
+ END: (to) => `Version bumped: ${underline(dim(to))}`
102
+ },
103
+ CHANGELOG: {
104
+ START: "Generating changelog",
105
+ END: `Changelog generated`
99
106
  },
100
107
  NPM: {
101
108
  START: "Publishing npm package",
102
- END: "NPM package published"
109
+ END: (url) => `NPM package published: ${underline(dim(url))}`
103
110
  },
104
111
  GIT: {
105
112
  START: "Creating git commit, tag & push",
@@ -107,10 +114,11 @@ const MSG = {
107
114
  },
108
115
  GITHUB: {
109
116
  START: "Creating GitHub release",
110
- END: "GitHub release created"
117
+ END: (url) => `GitHub release created: ${underline(dim(url))}`
111
118
  }
112
119
  },
113
120
  PROMPT: {
121
+ SELECT_PACKAGE: "Select package release:",
114
122
  SELECT_VERSION: "Select version bump:",
115
123
  INPUT_VERSION: "Enter custom version:",
116
124
  NPM_PUBLISH: (pkg$1) => `Publish to npm ${underline(dim`(${pkg$1})`)}?`,
@@ -121,6 +129,7 @@ const MSG = {
121
129
  GIT_PUSH: "Push commits and tags to remote?"
122
130
  },
123
131
  ERROR: {
132
+ GIT_WORKDIR: `[git] Working dir must be clean`,
124
133
  GIT_REGISTRY: (name) => `[git] Repository not found for ${dim(name)}`,
125
134
  GIT_REMOTE: (name) => `[git] Remote "${dim(name)}" not found`,
126
135
  GITHUB_TAG_EXIT: (tag) => `[github] Release or tag "${tag}" already exists`,
@@ -130,326 +139,173 @@ 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
- //#region src/prompts.ts
138
- const abortTask = (msg$1) => {
139
- cancel(`${red("✕")} ${msg$1}`);
140
- process.exit(0);
141
- };
142
- const abortOnCancel = (value) => {
143
- if (isCancel(value)) abortTask("Operation cancelled.");
144
- };
145
- const abortOnError = (err) => {
146
- const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
147
- log.message(`\n${msg$1}\n`);
148
- process.exit(1);
149
- };
150
- const taskEnd = (msg$1) => {
151
- outro(msg$1);
152
- process.exit(0);
153
- };
154
- const msg = (prefix, msg$1) => {
155
- log.message(gray`${rgb(33, 91, 184)(`[${prefix}]`)} ${msg$1}`);
156
- };
157
- const prompt = (msg$1) => {
158
- return `${blue`?`} ${msg$1}`;
159
- };
160
-
161
- //#endregion
162
- //#region src/version/bump.ts
163
- const isPreRelease = (version) => {
164
- return Boolean(prerelease(version));
165
- };
166
- const isValid = (version) => {
167
- return Boolean(valid(version));
155
+ //#region src/github/release.ts
156
+ const getGithubUrl = (owner, repo) => {
157
+ return `https://github.com/${owner}/${repo}`;
168
158
  };
169
- const parseVersion = (raw) => {
170
- const next = isValid(raw) ? raw : coerce(raw)?.toString();
171
- if (!next) return {};
172
- const { prerelease: prerelease$1 } = parse(next);
173
- const toPreRelease = Boolean(prerelease$1.length);
174
- const [preId, preBase] = prerelease$1.map(String);
175
- return {
176
- next,
177
- toPreRelease,
178
- preId,
179
- preBase
180
- };
159
+ const truncateBody = (body) => {
160
+ if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
161
+ return body;
181
162
  };
182
- const getCIVersion = (current) => {
183
- return inc(current, Release.PATCH);
163
+ const uploadAssets = async (ctx, config) => {
164
+ const { github: { assets } } = config;
165
+ const { github: { isReleased, owner, repo, uploadUrl, releaseId, token }, pkg: { name } } = ctx;
166
+ if (!assets?.length || !isReleased) return;
167
+ const octokit = new Octokit({ auth: token });
168
+ const files = await glob(assets);
169
+ await Promise.all(files.map((file) => {
170
+ return octokit.repos.uploadReleaseAsset({
171
+ owner,
172
+ repo,
173
+ name,
174
+ url: uploadUrl,
175
+ release_id: releaseId,
176
+ data: createReadStream(file),
177
+ headers: {
178
+ "content-type": contentType(file) || "application/octet-stream",
179
+ "content-length": statSync(file).size
180
+ }
181
+ });
182
+ }));
184
183
  };
185
-
186
- //#endregion
187
- //#region src/version/prompts.ts
188
- let Release = /* @__PURE__ */ function(Release$1) {
189
- Release$1["MAJOR"] = "major";
190
- Release$1["MINOR"] = "minor";
191
- Release$1["PATCH"] = "patch";
192
- return Release$1;
193
- }({});
194
- let PreRelease = /* @__PURE__ */ function(PreRelease$1) {
195
- PreRelease$1["MAJOR"] = "premajor";
196
- PreRelease$1["MINOR"] = "preminor";
197
- PreRelease$1["PATCH"] = "prepatch";
198
- PreRelease$1["RELEASE"] = "prerelease";
199
- return PreRelease$1;
200
- }({});
201
- let PreId = /* @__PURE__ */ function(PreId$1) {
202
- PreId$1["ALPHA"] = "alpha";
203
- PreId$1["BETA"] = "beta";
204
- PreId$1["RC"] = "rc";
205
- PreId$1["NET"] = "next";
206
- PreId$1["CANARY"] = "canary";
207
- PreId$1["NIGHT"] = "nightly";
208
- PreId$1["DEV"] = "dev";
209
- return PreId$1;
210
- }({});
211
- const diff = (from, to) => {
212
- const a = from.split(".");
213
- return to.split(".").map((v, i) => v === a[i] ? dim(v) : green(v)).join(".");
184
+ const createWebRelease = async (ctx, config) => {
185
+ const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
186
+ const { github: { prerelease: prerelease$1 } } = config;
187
+ const github = getGithubUrl(owner, repo);
188
+ const url = new URL(`${github}/releases/new`);
189
+ url.searchParams.set("tag", currentTag);
190
+ url.searchParams.set("title", releaseName);
191
+ url.searchParams.set("body", truncateBody(changelog));
192
+ url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
193
+ const isWindows = process.platform === "win32";
194
+ await open(url.toString(), { wait: isWindows });
214
195
  };
215
- const runVersionPrompts = async (ctx, config) => {
216
- const { pkg: { current, fromPreRelease } } = ctx;
217
- const selectVersion = async () => {
218
- const patch = inc(current, Release.PATCH);
219
- const minor = inc(current, Release.MINOR);
220
- const major = inc(current, Release.MAJOR);
221
- const prerelease$1 = inc(current, PreRelease.RELEASE);
222
- const prepatch = inc(current, PreRelease.PATCH, PreId.BETA);
223
- const preminor = inc(current, PreRelease.MINOR, PreId.BETA);
224
- const premajor = inc(current, PreRelease.MAJOR, PreId.BETA);
225
- const pr = [{
226
- label: "Pre-Release",
227
- value: prerelease$1,
228
- hint: prerelease$1
229
- }];
230
- const pre = [
231
- {
232
- label: "Pre-Patch",
233
- value: prepatch,
234
- hint: prepatch
235
- },
236
- {
237
- label: "Pre-Minor",
238
- value: preminor,
239
- hint: preminor
240
- },
241
- {
242
- label: "Pre-Major",
243
- value: premajor,
244
- hint: premajor
245
- }
246
- ];
247
- const release = [
248
- {
249
- label: "Patch",
250
- value: patch,
251
- hint: patch
252
- },
253
- {
254
- label: "Minor",
255
- value: minor,
256
- hint: minor
257
- },
258
- {
259
- label: "Major",
260
- value: major,
261
- hint: major
262
- }
263
- ];
264
- const custom = [{
265
- label: "As-Is",
266
- value: "ignore",
267
- hint: current
268
- }, {
269
- label: "Custom",
270
- value: "custom",
271
- hint: "custom specified"
272
- }];
273
- const version = await select({
274
- message: prompt(MSG.PROMPT.SELECT_VERSION),
275
- options: [
276
- ...fromPreRelease ? pr : [],
277
- ...release,
278
- ...fromPreRelease ? [] : pre,
279
- ...custom
280
- ],
281
- initialValue: fromPreRelease ? prerelease$1 : patch
196
+ const createCiRelease = async (ctx, config) => {
197
+ const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
198
+ const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
199
+ const octokit = new Octokit({ auth: token });
200
+ try {
201
+ const { data } = await octokit.repos.createRelease({
202
+ owner,
203
+ repo,
204
+ tag_name: currentTag,
205
+ target_commitish: void 0,
206
+ name: releaseName,
207
+ body: autoGenerate ? "" : truncateBody(changelog),
208
+ draft: draft ?? false,
209
+ prerelease: (toPreRelease || prerelease$1) ?? false,
210
+ discussion_category_name: void 0,
211
+ generate_release_notes: autoGenerate,
212
+ make_latest: "true"
282
213
  });
283
- abortOnCancel(version);
284
- return version;
285
- };
286
- const inputVersion = async () => {
287
- const version = await text({
288
- message: prompt(MSG.PROMPT.INPUT_VERSION),
289
- initialValue: current,
290
- validate: (val) => {
291
- if (!isValid(val)) return "Invalid version";
292
- }
214
+ const { html_url, upload_url, id, discussion_url } = data;
215
+ Object.assign(ctx.github, {
216
+ isReleased: true,
217
+ releaseId: id,
218
+ releaseUrl: html_url,
219
+ uploadUrl: upload_url,
220
+ discussionUrl: discussion_url
293
221
  });
294
- abortOnCancel(version);
295
- return version;
296
- };
297
- const type = await selectVersion();
298
- const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
299
- const parsed = parseVersion(next);
300
- Object.assign(ctx.pkg, { ...parsed });
301
- ctx.isIncrement = gte(next, current);
302
- log.message(gray`from ${dim(current)}${eol()} to ${diff(current, next)}`);
303
- return { next };
222
+ } catch (e) {
223
+ if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
224
+ throw e;
225
+ }
304
226
  };
305
-
306
- //#endregion
307
- //#region src/defaults.ts
308
- const createDefaultContext = () => {
309
- return {
310
- ci: false,
311
- release: false,
312
- dryRun: false,
313
- changelog: false,
314
- increment: "",
315
- isIncrement: false,
316
- configFileExists: false,
317
- pkg: {
318
- current: "",
319
- next: "",
320
- name: "",
321
- isPrivate: false,
322
- fromPreRelease: false,
323
- toPreRelease: false,
324
- publishConfig: {
325
- access: "public",
326
- registry: "https://registry.npmjs.org"
327
- },
328
- preId: PreId.BETA,
329
- preBase: "0"
330
- },
331
- npm: {
332
- username: "",
333
- otp: ""
334
- },
335
- git: {
336
- remoteName: "origin",
337
- remoteUrl: "",
338
- latestTag: "",
339
- previousTag: "",
340
- currentTag: "",
341
- isCommitted: false,
342
- isTagged: false
343
- },
344
- github: {
345
- username: "",
346
- repo: "",
347
- changelog: "",
348
- owner: "",
349
- isWeb: false,
350
- skipChecks: false,
351
- isReleased: false,
352
- releaseId: 0,
353
- releaseUrl: "",
354
- uploadUrl: "",
355
- discussionUrl: "",
356
- token: ""
357
- }
358
- };
227
+ const parseGithubUrl = (ctx) => {
228
+ const { git: { remoteUrl } } = ctx;
229
+ const [owner, repo] = parseGitHubRepo(remoteUrl);
230
+ Object.assign(ctx.github, {
231
+ owner,
232
+ repo
233
+ });
359
234
  };
360
- const createDefaultConfig = (isCI) => {
361
- return {
362
- hooks: {
363
- "before:bump": "",
364
- "after:bump": "",
365
- "before:publish": "",
366
- "after:publish": "",
367
- "before:push": "",
368
- "after:push": "",
369
- "before:release": "",
370
- "after:release": ""
371
- },
372
- git: {
373
- commit: isCI,
374
- tag: isCI,
375
- push: isCI,
376
- commitMessage: "chore(release): ${version}",
377
- tagMessage: "Release ${version}",
378
- tagName: "${version}"
379
- },
380
- npm: {
381
- publish: isCI,
382
- tag: "",
383
- allowSameVersion: false
384
- },
385
- github: {
386
- release: isCI,
387
- releaseName: "Release ${version}",
388
- prerelease: false,
389
- draft: false,
390
- tokenRef: "GITHUB_TOKEN",
391
- assets: []
235
+ const createRelease = async (ctx, config) => {
236
+ const { github: { owner, repo }, isCI } = ctx;
237
+ const { github: { tokenRef, release, skipChecks } } = config;
238
+ if (!release) return;
239
+ const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
240
+ if (!token) {
241
+ Object.assign(ctx.github, { isWeb: true });
242
+ if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
243
+ }
244
+ Object.assign(ctx.github, { token });
245
+ const octokit = new Octokit({ auth: token });
246
+ if (token && !skipChecks) if (process.env.GITHUB_ACTIONS) Object.assign(ctx.github, { username: process.env.GITHUB_ACTOR });
247
+ else {
248
+ try {
249
+ const { data: { login: username } } = await octokit.users.getAuthenticated();
250
+ Object.assign(ctx.github, { username });
251
+ } catch (e) {
252
+ throw new Error(MSG.ERROR.GITHUB_AUTH);
253
+ }
254
+ try {
255
+ const { github: { username } } = ctx;
256
+ await octokit.repos.checkCollaborator({
257
+ owner,
258
+ repo,
259
+ username
260
+ });
261
+ } catch (e) {
262
+ throw new Error(MSG.ERROR.GITHUB_USER);
392
263
  }
393
- };
394
- };
395
-
396
- //#endregion
397
- //#region src/utils.ts
398
- const __filename = fileURLToPath(import.meta.url);
399
- const __dirname = dirname(__filename);
400
- const info = (msg$1) => {
401
- console.log(`${rgb(33, 91, 184)(`ℹ`)} ${gray(msg$1)}`);
402
- };
403
- const success = (msg$1, dryRun) => {
404
- return `${(dryRun ? rgb(123, 115, 66) : green)("✔")} ${msg$1}`;
405
- };
406
- const runLifeCycleHook = async (hooks, key, dryRun) => {
407
- const handler = hooks[key];
408
- if (typeof handler === "function") {
409
- if (!dryRun) await handler();
410
- log.success(success(`${dim`run`} ${inspect(handler)}`, dryRun));
411
- return;
412
264
  }
413
- return Promise.allSettled(castArray(handler).filter(Boolean).map(async (hook) => {
414
- if (!dryRun) await execAsync(hook, { shell: true }).catch(console.error);
415
- log.success(success(hook.replace("echo", dim("echo")), dryRun));
416
- }));
265
+ const { github: { isWeb }, dryRun } = ctx;
266
+ if (dryRun) return true;
267
+ if (isWeb) await createWebRelease(ctx, config);
268
+ else if (isCI) {
269
+ await createCiRelease(ctx, config);
270
+ await uploadAssets(ctx, config);
271
+ }
417
272
  };
418
- const format = (str, ctx) => {
419
- const { pkg: { next } } = ctx;
420
- return str.replace("${version}", next);
273
+ const getGithubReleaseUrl = (ctx) => {
274
+ const { github: { owner, repo }, git: { currentTag } } = ctx;
275
+ return `${getGithubUrl(owner, repo)}/releases/tag/${normalizeTag(currentTag)}`;
421
276
  };
422
277
 
423
278
  //#endregion
424
279
  //#region src/git/commit.ts
425
- const runGitProxy = (...args) => {
426
- if (process.env["dryRun"]) log.message(`${dim("$")} ${gray(["git", ...args[0]].join(" "))}`);
427
- return runGit(...args);
428
- };
429
- const getBranchName = () => {
430
- return runGitProxy([
431
- "rev-parse",
432
- "--abbrev-ref",
433
- "HEAD"
434
- ]);
280
+ const getBranchName = async () => {
281
+ let branch = await runGit(["branch", "--show-current"]);
282
+ if (!branch) {
283
+ branch = await runGit([
284
+ "rev-parse",
285
+ "--abbrev-ref",
286
+ "HEAD"
287
+ ]);
288
+ if (branch === "HEAD") return void 0;
289
+ }
290
+ return branch;
435
291
  };
436
292
  const getRemoteForBranch = (branch) => {
437
- return runGitProxy([
293
+ return runGit([
438
294
  "config",
439
295
  "--get",
440
296
  `branch.${branch}.remote`
441
297
  ]);
442
298
  };
299
+ const getAllRemotes = async () => {
300
+ return (await runGit(["remote"]))?.split("\n").filter(Boolean) ?? [];
301
+ };
443
302
  const getStatus = () => {
444
- return runGitProxy([
303
+ return runGit([
445
304
  "status",
446
305
  "--short",
447
306
  "--untracked-files=no"
448
307
  ], { trim: false });
449
308
  };
450
- const gitRestore = () => {
451
- return runGitProxy(["restore", "."]);
452
- };
453
309
  const coloredChangeset = (log$1) => {
454
310
  const colorStatusChar = (ch) => {
455
311
  switch (ch) {
@@ -463,7 +319,7 @@ const coloredChangeset = (log$1) => {
463
319
  return log$1.split("\n").filter(Boolean).map((line) => {
464
320
  const status = line.slice(0, 2);
465
321
  const file = line.slice(3);
466
- return `${colorStatus(status)} ${styleText(["dim"], file)}`;
322
+ return `${colorStatus(status)} ${dim(file)}`;
467
323
  }).join("\n");
468
324
  };
469
325
  const getRemote = async () => {
@@ -471,11 +327,11 @@ const getRemote = async () => {
471
327
  const remoteName = branch && await getRemoteForBranch(branch) || "origin";
472
328
  return {
473
329
  remoteName,
474
- remoteUrl: await runGitProxy([
330
+ remoteUrl: await runGit([
475
331
  "remote",
476
332
  "get-url",
477
333
  remoteName
478
- ]).catch((_) => runGitProxy([
334
+ ]).catch((_) => runGit([
479
335
  "config",
480
336
  "--get",
481
337
  `remote.${remoteName}.url`
@@ -483,7 +339,7 @@ const getRemote = async () => {
483
339
  };
484
340
  };
485
341
  const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
486
- return runGitProxy([
342
+ return runGit([
487
343
  "describe",
488
344
  "--tags",
489
345
  "--abbrev=0",
@@ -492,11 +348,11 @@ const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
492
348
  ]);
493
349
  };
494
350
  const getPreviousTag = async (current) => {
495
- return runGitProxy([
351
+ return runGit([
496
352
  "describe",
497
353
  "--tags",
498
354
  "--abbrev=0",
499
- `${await runGitProxy([
355
+ `${await runGit([
500
356
  "rev-list",
501
357
  "--tags",
502
358
  current || "--skip=1",
@@ -504,7 +360,7 @@ const getPreviousTag = async (current) => {
504
360
  ])}^`
505
361
  ]);
506
362
  };
507
- const resolveChangelogRange = async (isIncrement) => {
363
+ const resolveChangelogRange = async (isIncrement = true) => {
508
364
  const latestTag = await getLatestTag();
509
365
  const previousTag = await getPreviousTag(latestTag);
510
366
  if (!latestTag) return {
@@ -520,226 +376,476 @@ const resolveChangelogRange = async (isIncrement) => {
520
376
  to: "HEAD"
521
377
  };
522
378
  };
523
- const getLog = async (print, isIncrement) => {
524
- const cmd = ["log", `--pretty=format:${print ? "* %s (%h)" : "%s %h %H"}`];
525
- const { from, to } = await resolveChangelogRange(isIncrement);
379
+ const getLog = async (from = "", to = "HEAD", scope) => {
380
+ const cmd = ["log", `--pretty=format:* %s (%h)`];
526
381
  if (from) cmd.push(`${from}...${to}`);
527
- return runGitProxy(cmd, { trim: false });
382
+ if (scope) cmd.push(...["--", scope]);
383
+ return runGit(cmd, { trim: false });
528
384
  };
529
- const normalizeTag = (tag) => clean(tag) ?? tag;
530
- const resolveTag$1 = async (ctx, config) => {
531
- const { git: { tagName: tagTemplate } } = config;
532
- const latestTag = await getLatestTag() ?? "";
533
- const currentTag = format(tagTemplate || ((latestTag || "").match(/^v/) ? "v${version}" : "${version}"), ctx);
534
- Object.assign(ctx.git, {
535
- latestTag,
536
- currentTag
537
- });
538
- return true;
539
- };
540
- const gitCheck = async (ctx, config) => {
541
- const { pkg: { name } } = ctx;
542
- if (!await isGitRepo()) throw new Error(MSG.ERROR.GIT_REGISTRY(name));
543
- const { remoteName, remoteUrl } = await getRemote();
544
- if (!remoteUrl) throw new Error(MSG.ERROR.GIT_REMOTE(name));
545
- Object.assign(ctx.git, {
546
- remoteName,
547
- remoteUrl
548
- });
549
- parseGithubUrl(ctx);
385
+ const isWorkingDirClean = async () => {
386
+ return (await runGit(["status", "-s"]))?.length === 0;
550
387
  };
551
388
  const hasUpstreamBranch = async () => {
552
- const branch = await runGitProxy([
389
+ const branch = await runGit([
553
390
  "for-each-ref",
554
391
  "--format=%(upstream:short)",
555
- await runGitProxy(["symbolic-ref", "HEAD"])
392
+ await runGit(["symbolic-ref", "HEAD"])
556
393
  ]);
557
394
  return Boolean(branch);
558
395
  };
559
396
  const getUpstreamArgs = async (remoteName) => {
560
397
  const hasUpstream = await hasUpstreamBranch();
561
398
  const branch = await getBranchName();
562
- if (!hasUpstream) return remoteName ? [
399
+ if (!hasUpstream) return [
563
400
  "--set-upstream",
564
401
  remoteName,
565
402
  branch
566
- ] : [
567
- "--set-upstream",
568
- "origin",
569
- branch
570
403
  ];
571
- if (remoteName) return [remoteName];
572
- return [];
404
+ return [remoteName, branch];
405
+ };
406
+ const normalizeTag = (tag) => clean(tag) ?? tag;
407
+ const gitCheck = async (ctx, config) => {
408
+ const { pkg: { name } } = ctx;
409
+ const { git: { requireRepository, requireRemote, requireWorkDirClean } } = config;
410
+ if (!await isGitRepo()) if (requireRepository) throw new Error(MSG.ERROR.GIT_REGISTRY(name));
411
+ else {
412
+ ctx.noGit = true;
413
+ return;
414
+ }
415
+ if (requireWorkDirClean && !await isWorkingDirClean()) throw new Error(MSG.ERROR.GIT_WORKDIR);
416
+ const { remoteName, remoteUrl } = await getRemote();
417
+ if (!remoteUrl) if (requireRemote) throw new Error(MSG.ERROR.GIT_REMOTE(name));
418
+ else {
419
+ ctx.noGitHub = true;
420
+ return;
421
+ }
422
+ Object.assign(ctx.git, {
423
+ remoteName,
424
+ remoteUrl
425
+ });
426
+ parseGithubUrl(ctx);
573
427
  };
574
428
  const commitAndTag = async (ctx, config) => {
575
- const { git: { currentTag, remoteName }, dryRun } = ctx;
576
- const { git: { commit, tag, push, tagMessage, commitMessage } } = config;
429
+ const { git: { remoteName, currentTag, commitMessage, tagMessage }, dryRun } = ctx;
430
+ const { git: { commit, tag, push, addUntrackedFiles, commitArgs, tagArgs, pushArgs } } = config;
577
431
  if (!commit) return;
578
432
  const args = dryRun ? ["--dry-run"] : [];
579
- await runGitProxy([
433
+ await runGit([
580
434
  "add",
581
435
  ".",
582
- "--all",
436
+ addUntrackedFiles ? "--all" : "--update",
583
437
  ...args
584
438
  ]);
585
- await runGitProxy([
439
+ await runGit([
586
440
  "commit",
587
441
  "--message",
588
- format(commitMessage, ctx),
442
+ commitMessage,
443
+ ...commitArgs,
589
444
  ...args
590
445
  ]);
591
446
  if (!dryRun) Object.assign(ctx.git, { isCommitted: true });
592
447
  if (tag) {
593
- await runGitProxy([
448
+ await runGit([
594
449
  "tag",
595
450
  "--annotate",
596
451
  "--message",
597
- format(tagMessage, ctx),
452
+ tagMessage,
598
453
  currentTag,
454
+ ...tagArgs,
599
455
  ...args
600
456
  ]);
601
457
  if (!dryRun) Object.assign(ctx.git, { isTagged: true });
602
458
  }
603
- if (push) await runGitProxy([
604
- "push",
605
- ...await getUpstreamArgs(remoteName),
606
- "--follow-tags",
607
- ...args
459
+ if (push) {
460
+ const remotes = await getAllRemotes();
461
+ for (const remoteName$1 of remotes) {
462
+ const upstreamArgs = await getUpstreamArgs(remoteName$1);
463
+ await runGit([
464
+ "push",
465
+ remoteName$1,
466
+ `refs/tags/${currentTag}`,
467
+ ...args
468
+ ]);
469
+ await runGit([
470
+ "push",
471
+ ...upstreamArgs,
472
+ ...pushArgs,
473
+ ...args
474
+ ]);
475
+ }
476
+ if (!dryRun) Object.assign(ctx.git, { isPushed: true });
477
+ }
478
+ };
479
+ const gitRollback$1 = (ctx) => {
480
+ const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
481
+ if (isPushed) return;
482
+ spawnSync("git", ["restore", "."]);
483
+ if (isTagged) spawnSync("git", [
484
+ "tag",
485
+ "--delete",
486
+ currentTag
608
487
  ]);
488
+ spawnSync("git", [
489
+ "reset",
490
+ "--hard",
491
+ isCommitted ? "HEAD~1" : "HEAD"
492
+ ]);
493
+ };
494
+
495
+ //#endregion
496
+ //#region src/utils.ts
497
+ const info = (msg$1) => {
498
+ console.log(`${rgb(33, 91, 184)(`i`)} ${msg$1}`);
499
+ };
500
+ const msg = (prefix, msg$1) => {
501
+ log.message(`${rgb(33, 91, 184)(`[${prefix}]`)} ${dim(msg$1)}`);
502
+ };
503
+ const question = (msg$1, type) => {
504
+ return `${{
505
+ package: yellow,
506
+ version: green,
507
+ git: rgb(220, 94, 62),
508
+ npm: rgb(186, 70, 61),
509
+ github: rgb(64, 110, 228)
510
+ }[type]`?`} ${msg$1}`;
511
+ };
512
+ const success = (msg$1, dryRun) => {
513
+ return `${(dryRun ? rgb(123, 115, 66) : green)("✔")} ${msg$1}`;
514
+ };
515
+ const runLifeCycleHook = async (hooks, key, dryRun) => {
516
+ const handler = hooks[key];
517
+ if (typeof handler === "function") {
518
+ if (!dryRun) await handler(log);
519
+ log.success(success(`${dim`run`} ${inspect(handler)}`, dryRun));
520
+ return;
521
+ }
522
+ return Promise.allSettled(castArray(handler).filter(Boolean).map(async (hook) => {
523
+ if (!dryRun) await execAsync(hook).catch(log.error);
524
+ log.success(success(hook.replace(/^echo$/, dim("echo")), dryRun));
525
+ }));
609
526
  };
610
- const gitRollback = async (ctx) => {
527
+ const gitRollback = (ctx) => {
611
528
  const { git: { currentTag, isCommitted, isTagged } } = ctx;
612
- await gitRestore();
613
- if (isTagged) await runGitProxy([
529
+ spawnSync("git", ["restore", "."]);
530
+ if (isTagged) spawnSync("git", [
614
531
  "tag",
615
532
  "--delete",
616
533
  currentTag
617
534
  ]);
618
- await runGitProxy([
535
+ spawnSync("git", [
619
536
  "reset",
620
537
  "--hard",
621
538
  isCommitted ? "HEAD~1" : "HEAD"
622
539
  ]);
623
540
  };
541
+ const diff = (from, to, separator = ".") => {
542
+ const a = from.split(separator);
543
+ return to.split(separator).map((v, i) => v === a[i] ? dim(v) : green(v)).join(separator);
544
+ };
545
+ const formatTemplate = async (ctx, config) => {
546
+ const { pkg: { next }, selectedPkg } = ctx;
547
+ const { git: { commitMessage: cm, tagMessage: tm }, github: { releaseName: rm }, toTag } = config;
548
+ const f = (s, v) => s.replace("${tag}", v);
549
+ const latestTag = await getLatestTag() ?? "";
550
+ const currentTag = toTag(selectedPkg, next);
551
+ const commitMessage = f(cm, currentTag);
552
+ const tagMessage = f(tm, currentTag);
553
+ const releaseName = f(rm, currentTag);
554
+ Object.assign(ctx.git, {
555
+ latestTag,
556
+ currentTag,
557
+ commitMessage,
558
+ tagMessage
559
+ });
560
+ Object.assign(ctx.github, { releaseName });
561
+ };
562
+ const getPackageInfo = (pkgName, getPkgDir = (pkg$1) => `packages/${pkg$1}`) => {
563
+ const pkgDir = resolve(getPkgDir(pkgName));
564
+ const pkgPath = resolve(pkgDir, "package.json");
565
+ return {
566
+ pkg: readJsonFile(pkgPath),
567
+ pkgDir,
568
+ pkgPath
569
+ };
570
+ };
624
571
 
625
572
  //#endregion
626
- //#region src/github/release.ts
627
- const getGithubUrl = (owner, repo) => {
628
- return `https://github.com/${owner}/${repo}`;
573
+ //#region src/prompts.ts
574
+ const abortTask = (msg$1) => {
575
+ cancel(`${red("✕")} ${msg$1}`);
576
+ process.exit(0);
629
577
  };
630
- const truncateBody = (body) => {
631
- if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
632
- return body;
578
+ const abortSinglePrompt = (value) => {
579
+ if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
633
580
  };
634
- const uploadAssets = async (ctx, config) => {
635
- const { github: { assets } } = config;
636
- const { github: { isReleased, owner, repo, uploadUrl, releaseId, token }, pkg: { name } } = ctx;
637
- if (!assets?.length || !isReleased) return;
638
- const octokit = new Octokit({ auth: token });
639
- const files = await glob(assets);
640
- await Promise.all(files.map((file) => {
641
- return octokit.repos.uploadReleaseAsset({
642
- owner,
643
- repo,
644
- name,
645
- url: uploadUrl,
646
- release_id: releaseId,
647
- data: createReadStream(file),
648
- headers: {
649
- "content-type": contentType(file) || "application/octet-stream",
650
- "content-length": statSync(file).size
651
- }
652
- });
653
- }));
581
+ const abortGroupPrompt = (ctx) => {
582
+ ctx && gitRollback(ctx);
583
+ abortTask(MSG.ABORT.CANCEL);
654
584
  };
655
- const createWebRelease = async (ctx, config) => {
656
- const { pkg: { next, toPreRelease }, github: { owner, repo, changelog } } = ctx;
657
- const { github: { prerelease: prerelease$1, releaseName } } = config;
658
- const github = getGithubUrl(owner, repo);
659
- const url = new URL(`${github}/releases/new`);
660
- url.searchParams.set("tag", next);
661
- url.searchParams.set("title", format(releaseName, ctx) ?? `Release ${next}`);
662
- url.searchParams.set("body", truncateBody(changelog));
663
- url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
664
- const isWindows = process.platform === "win32";
665
- await open(url.toString(), { wait: isWindows });
585
+ const abortOnError = (err, ctx) => {
586
+ ctx && gitRollback(ctx);
587
+ const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
588
+ log.message(`\n${msg$1}\n`);
589
+ process.exit(1);
666
590
  };
667
- const createCiRelease = async (ctx, config) => {
668
- const { pkg: { next, toPreRelease }, github: { owner, repo, token, changelog } } = ctx;
669
- const { github: { prerelease: prerelease$1, releaseName, draft } } = config;
670
- const octokit = new Octokit({ auth: token });
671
- try {
672
- const { data } = await octokit.repos.createRelease({
673
- owner,
674
- repo,
675
- tag_name: next,
676
- name: format(releaseName, ctx) ?? `Release ${next}`,
677
- body: truncateBody(changelog),
678
- draft: draft ?? false,
679
- prerelease: (toPreRelease || prerelease$1) ?? false
591
+ const taskEnd = (msg$1) => {
592
+ outro(msg$1);
593
+ process.exit(0);
594
+ };
595
+
596
+ //#endregion
597
+ //#region src/version/bump.ts
598
+ const isPreRelease = (version) => {
599
+ return Boolean(prerelease(version));
600
+ };
601
+ const isValid = (version) => {
602
+ return Boolean(valid(version));
603
+ };
604
+ const parseVersion = (raw) => {
605
+ const next = isValid(raw) ? raw : coerce(raw)?.toString();
606
+ if (!next) return {};
607
+ const { prerelease: prerelease$1 } = parse(next);
608
+ const toPreRelease = Boolean(prerelease$1.length);
609
+ const [preId, preBase] = prerelease$1.map(String);
610
+ return {
611
+ next,
612
+ toPreRelease,
613
+ preId,
614
+ preBase
615
+ };
616
+ };
617
+ const getCIVersion = (current) => {
618
+ return inc(current, Release.PATCH) ?? "";
619
+ };
620
+
621
+ //#endregion
622
+ //#region src/version/prompts.ts
623
+ let Release = /* @__PURE__ */ function(Release$1) {
624
+ Release$1["MAJOR"] = "major";
625
+ Release$1["MINOR"] = "minor";
626
+ Release$1["PATCH"] = "patch";
627
+ return Release$1;
628
+ }({});
629
+ let PreRelease = /* @__PURE__ */ function(PreRelease$1) {
630
+ PreRelease$1["MAJOR"] = "premajor";
631
+ PreRelease$1["MINOR"] = "preminor";
632
+ PreRelease$1["PATCH"] = "prepatch";
633
+ PreRelease$1["RELEASE"] = "prerelease";
634
+ return PreRelease$1;
635
+ }({});
636
+ let PreId = /* @__PURE__ */ function(PreId$1) {
637
+ PreId$1["ALPHA"] = "alpha";
638
+ PreId$1["BETA"] = "beta";
639
+ PreId$1["RC"] = "rc";
640
+ PreId$1["NET"] = "next";
641
+ PreId$1["CANARY"] = "canary";
642
+ PreId$1["NIGHT"] = "nightly";
643
+ PreId$1["DEV"] = "dev";
644
+ return PreId$1;
645
+ }({});
646
+ const runVersionPrompts = async (ctx, config) => {
647
+ const { pkg: { current, fromPreRelease } } = ctx;
648
+ const selectVersion = async () => {
649
+ const patch = inc(current, Release.PATCH);
650
+ const minor = inc(current, Release.MINOR);
651
+ const major = inc(current, Release.MAJOR);
652
+ const prerelease$1 = inc(current, PreRelease.RELEASE);
653
+ const prepatch = inc(current, PreRelease.PATCH, PreId.BETA);
654
+ const preminor = inc(current, PreRelease.MINOR, PreId.BETA);
655
+ const premajor = inc(current, PreRelease.MAJOR, PreId.BETA);
656
+ const pr = [{
657
+ label: "Pre-Release",
658
+ value: prerelease$1,
659
+ hint: prerelease$1
660
+ }];
661
+ const pre = [
662
+ {
663
+ label: "Pre-Patch",
664
+ value: prepatch,
665
+ hint: prepatch
666
+ },
667
+ {
668
+ label: "Pre-Minor",
669
+ value: preminor,
670
+ hint: preminor
671
+ },
672
+ {
673
+ label: "Pre-Major",
674
+ value: premajor,
675
+ hint: premajor
676
+ }
677
+ ];
678
+ const release = [
679
+ {
680
+ label: "Patch",
681
+ value: patch,
682
+ hint: patch
683
+ },
684
+ {
685
+ label: "Minor",
686
+ value: minor,
687
+ hint: minor
688
+ },
689
+ {
690
+ label: "Major",
691
+ value: major,
692
+ hint: major
693
+ }
694
+ ];
695
+ const custom = [{
696
+ label: "As-Is",
697
+ value: "ignore",
698
+ hint: current
699
+ }, {
700
+ label: "Custom",
701
+ value: "custom",
702
+ hint: "custom specified"
703
+ }];
704
+ const version = await select({
705
+ message: question(MSG.PROMPT.SELECT_VERSION, "version"),
706
+ options: [
707
+ ...fromPreRelease ? pr : [],
708
+ ...release,
709
+ ...fromPreRelease ? [] : pre,
710
+ ...custom
711
+ ],
712
+ initialValue: fromPreRelease ? prerelease$1 : patch
680
713
  });
681
- const { html_url, upload_url, id, discussion_url } = data;
682
- Object.assign(ctx.github, {
683
- isReleased: true,
684
- releaseId: id,
685
- releaseUrl: html_url,
686
- uploadUrl: upload_url,
687
- discussionUrl: discussion_url
714
+ abortSinglePrompt(version);
715
+ return version;
716
+ };
717
+ const inputVersion = async () => {
718
+ const version = await text({
719
+ message: question(MSG.PROMPT.INPUT_VERSION, "version"),
720
+ initialValue: current,
721
+ validate: (val) => {
722
+ if (!isValid(val)) return "Invalid version";
723
+ }
688
724
  });
689
- } catch (e) {
690
- if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(next));
691
- throw e;
692
- }
693
- };
694
- const parseGithubUrl = (ctx) => {
695
- const { git: { remoteUrl } } = ctx;
696
- const [owner, repo] = parseGitHubRepo(remoteUrl);
697
- Object.assign(ctx.github, {
698
- owner,
699
- repo
700
- });
725
+ abortSinglePrompt(version);
726
+ return version;
727
+ };
728
+ const type = await selectVersion();
729
+ const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
730
+ ctx.isIncrement = !eq(next, current);
731
+ return { next };
701
732
  };
702
- const createRelease = async (ctx, config) => {
703
- const { github: { owner, repo, skipChecks }, ci } = ctx;
704
- const { github: { tokenRef, release } } = config;
705
- if (!release) return;
706
- const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
707
- if (!token) {
708
- Object.assign(ctx.github, { isWeb: true });
709
- if (ci && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
710
- }
711
- Object.assign(ctx.github, { token });
712
- const octokit = new Octokit({ auth: token });
713
- if (token && !skipChecks) if (process.env.GITHUB_ACTIONS) Object.assign(ctx.github, { username: process.env.GITHUB_ACTOR });
714
- else {
715
- try {
716
- const { data: { login: username } } = await octokit.users.getAuthenticated();
717
- Object.assign(ctx.github, { username });
718
- } catch (e) {
719
- throw new Error(MSG.ERROR.GITHUB_AUTH);
720
- }
721
- try {
722
- const { github: { username } } = ctx;
723
- await octokit.repos.checkCollaborator({
724
- owner,
725
- repo,
726
- username
727
- });
728
- } catch (e) {
729
- throw new Error(MSG.ERROR.GITHUB_USER);
733
+
734
+ //#endregion
735
+ //#region src/defaults.ts
736
+ const createDefaultContext = () => {
737
+ return {
738
+ isCI: false,
739
+ release: false,
740
+ showRelease: false,
741
+ showChangelog: false,
742
+ dryRun: false,
743
+ noGit: false,
744
+ noNpm: false,
745
+ noGitHub: false,
746
+ increment: "",
747
+ isIncrement: false,
748
+ configFileExists: false,
749
+ selectedPkg: "",
750
+ pkg: {
751
+ current: "",
752
+ next: "",
753
+ name: "",
754
+ isPrivate: false,
755
+ fromPreRelease: false,
756
+ toPreRelease: false,
757
+ publishConfig: {
758
+ access: "public",
759
+ registry: "https://registry.npmjs.org"
760
+ },
761
+ preId: PreId.BETA,
762
+ preBase: "0"
763
+ },
764
+ npm: {
765
+ username: "",
766
+ otp: "",
767
+ tag: "latest"
768
+ },
769
+ git: {
770
+ remoteName: "origin",
771
+ remoteUrl: "",
772
+ latestTag: "",
773
+ previousTag: "",
774
+ currentTag: "",
775
+ isCommitted: false,
776
+ isTagged: false,
777
+ isPushed: false,
778
+ commitMessage: "",
779
+ tagMessage: ""
780
+ },
781
+ github: {
782
+ username: "",
783
+ repo: "",
784
+ changelog: "",
785
+ owner: "",
786
+ isWeb: false,
787
+ isReleased: false,
788
+ releaseId: 0,
789
+ releaseUrl: "",
790
+ uploadUrl: "",
791
+ discussionUrl: "",
792
+ token: "",
793
+ releaseName: ""
730
794
  }
731
- }
732
- const { github: { isWeb }, dryRun } = ctx;
733
- if (dryRun) return true;
734
- if (isWeb) await createWebRelease(ctx, config);
735
- else if (ci) {
736
- await createCiRelease(ctx, config);
737
- await uploadAssets(ctx, config);
738
- }
795
+ };
739
796
  };
740
- const getGithubReleaseUrl = (ctx) => {
741
- const { github: { owner, repo }, git: { currentTag } } = ctx;
742
- return `${getGithubUrl(owner, repo)}/releases/tag/${normalizeTag(currentTag)}`;
797
+ const createDefaultConfig = (isCI) => {
798
+ return {
799
+ hooks: {
800
+ "before:bump": "",
801
+ "after:bump": "",
802
+ "before:publish": "",
803
+ "after:publish": "",
804
+ "before:push": "",
805
+ "after:push": "",
806
+ "before:release": "",
807
+ "after:release": ""
808
+ },
809
+ isMonorepo: false,
810
+ packages: [],
811
+ getPkgDir(pkg$1) {
812
+ return ".";
813
+ },
814
+ toTag(pkg$1, version) {
815
+ return `v${version}`;
816
+ },
817
+ changelogTagPrefix: void 0,
818
+ git: {
819
+ commit: isCI,
820
+ tag: isCI,
821
+ push: isCI,
822
+ commitMessage: "chore(release): ${tag}",
823
+ commitArgs: [],
824
+ tagMessage: "${tag}",
825
+ tagArgs: [],
826
+ pushArgs: [],
827
+ addUntrackedFiles: false,
828
+ requireRemote: true,
829
+ requireRepository: true,
830
+ requireWorkDirClean: true
831
+ },
832
+ npm: {
833
+ publish: isCI,
834
+ publishPath: ".",
835
+ pushArgs: [],
836
+ skipChecks: false
837
+ },
838
+ github: {
839
+ release: isCI,
840
+ releaseName: "${tag}",
841
+ autoGenerate: false,
842
+ prerelease: false,
843
+ draft: false,
844
+ tokenRef: "GITHUB_TOKEN",
845
+ assets: [],
846
+ skipChecks: false
847
+ }
848
+ };
743
849
  };
744
850
 
745
851
  //#endregion
@@ -750,15 +856,25 @@ const defaultTypes = [
750
856
  section: "✨ 新功能",
751
857
  description: "新增功能"
752
858
  },
859
+ {
860
+ type: "feature",
861
+ section: "✨ 新功能",
862
+ description: "新增功能"
863
+ },
753
864
  {
754
865
  type: "fix",
755
866
  section: "🐛 Bug 修复",
756
867
  description: "修复 bug"
757
868
  },
758
869
  {
759
- type: "chore",
760
- section: "🎫 其他更新",
761
- description: "非功能性更改"
870
+ type: "perf",
871
+ section: "⚡ 性能优化",
872
+ description: "提升性能"
873
+ },
874
+ {
875
+ type: "revert",
876
+ section: "⏪ 回退",
877
+ description: "回退到之前版本"
762
878
  },
763
879
  {
764
880
  type: "docs",
@@ -770,26 +886,21 @@ const defaultTypes = [
770
886
  section: "💄 样式调整",
771
887
  description: "样式或格式修改"
772
888
  },
889
+ {
890
+ type: "chore",
891
+ section: "🎫 其他更新",
892
+ description: "非功能性更改"
893
+ },
773
894
  {
774
895
  type: "refactor",
775
896
  section: "♻ 代码重构",
776
897
  description: "重构代码"
777
898
  },
778
- {
779
- type: "perf",
780
- section: "⚡ 性能优化",
781
- description: "提升性能"
782
- },
783
899
  {
784
900
  type: "test",
785
901
  section: "✅ 测试",
786
902
  description: "测试更新"
787
903
  },
788
- {
789
- type: "revert",
790
- section: "⏪ 回退",
791
- description: "回退到之前版本"
792
- },
793
904
  {
794
905
  type: "build",
795
906
  section: "👷‍ 构建系统",
@@ -829,103 +940,103 @@ const defaultTypes = [
829
940
  type: "hotfix",
830
941
  section: "🔥 热修复",
831
942
  description: "紧急修复"
832
- },
833
- {
834
- type: "break",
835
- section: "💥 破坏性变更",
836
- description: "破坏性变更"
837
943
  }
838
944
  ];
839
- const findType = (type) => {
840
- return defaultTypes.find((item) => item.type === type)?.section ?? type;
841
- };
842
945
 
843
946
  //#endregion
844
947
  //#region src/git/changelog.ts
845
- const getCommits = async (isIncrement) => {
846
- return (await getLog(false, isIncrement))?.split("\n").filter(Boolean);
847
- };
848
- const parseCommit = (message) => {
849
- const match = message.match(/^(\w+):\s(.+)\s(\w{7})\s(\w{40})$/);
850
- if (!match) return;
851
- return {
852
- type: match[1],
853
- description: match[2],
854
- hash: match[3],
855
- fullHash: match[4]
856
- };
857
- };
858
- const classify = (commits) => {
859
- const sections = {};
860
- for (const msg$1 of commits) {
861
- const parsed = parseCommit(msg$1);
862
- if (!parsed) continue;
863
- const { type, description, hash, fullHash } = parsed;
864
- (sections[type] ??= []).push([
865
- description,
866
- hash,
867
- fullHash
868
- ]);
869
- }
870
- return sections;
871
- };
872
- const renderChangelog = async (ctx, sections) => {
873
- const { pkg: { next: to }, github: { owner, repo }, git: { latestTag } } = ctx;
874
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
875
- const github = getGithubUrl(owner, repo);
876
- let md;
877
- if (!latestTag) md = `## ${to} (${date})${eol(2)}`;
878
- else md = `## [${to}](${github}/compare/${latestTag}...${to}) (${date})${eol(2)}`;
879
- for (const [type, items] of Object.entries(sections)) {
880
- if (!items.length) continue;
881
- md += `### ${findType(type)}${eol(2)}`;
882
- for (const [desc, hash, fullHash] of items) md += `* **${type}** ${desc} [${hash}](${github}/commit/${fullHash})${eol(1)}`;
883
- md += eol(2);
884
- }
885
- if (latestTag) {
886
- md += `**Full Changelog**: ${github}/compare/${latestTag}...${to}`;
887
- md += eol(3);
948
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
949
+ const pkgDir = getPkgDir();
950
+ const infile = join(pkgDir, "CHANGELOG.md");
951
+ if (!existsSync(infile)) await writeFile(infile, "");
952
+ const originalChangelog = readFileSync(infile, "utf-8");
953
+ const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
954
+ ...t$1,
955
+ hidden: false
956
+ })) });
957
+ preset.writer ??= {};
958
+ preset.writer.headerPartial = `
959
+ ## {{#if isPatch~}} <small> {{~/if~}}
960
+ {{#if @root.linkCompare~}}
961
+ [{{version}}](
962
+ {{~#if @root.repository~}}
963
+ {{~#if @root.host}}
964
+ {{~@root.host}}/
965
+ {{~/if}}
966
+ {{~#if @root.owner}}
967
+ {{~@root.owner}}/
968
+ {{~/if}}
969
+ {{~@root.repository}}
970
+ {{~else}}
971
+ {{~@root.repoUrl}}
972
+ {{~/if~}}
973
+ /compare/{{previousTag}}...{{currentTag}})
974
+ {{~else}}
975
+ {{~version}}
976
+ {{~/if}}
977
+ {{~#if title}} "{{title}}"
978
+ {{~/if}}
979
+ {{~#if date}} ({{date}})
980
+ {{~/if}}
981
+ {{~#if isPatch~}} </small> {{~/if}}
982
+ `.trim() + eol();
983
+ preset.writer.mainTemplate = `
984
+ {{> header}}
985
+ {{#if noteGroups}}
986
+ {{#each noteGroups}}
987
+
988
+ ### ⚠ {{title}}
989
+
990
+ {{#each notes}}
991
+ * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
992
+ {{/each}}
993
+ {{/each}}
994
+ {{/if}}
995
+ {{#each commitGroups}}
996
+
997
+ {{#if title}}
998
+ ### {{title}}
999
+
1000
+ {{/if}}
1001
+ {{#each commits}}
1002
+ {{> commit root=@root}}
1003
+ {{/each}}
1004
+ {{/each}}`.trim() + eol(2);
1005
+ const generator = new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
1006
+ const writeStream = createWriteStream(infile);
1007
+ let changelog = "";
1008
+ for await (const chunk of generator.write()) {
1009
+ changelog += chunk;
1010
+ writeStream.write(chunk);
888
1011
  }
889
- return md;
890
- };
891
- const generateChangelog = async (ctx) => {
892
- const { isIncrement } = ctx;
893
- const commits = await getCommits(isIncrement);
894
- if (!commits) return "";
895
- const changelog = await renderChangelog(ctx, classify(commits));
896
- Object.assign(ctx.github, { changelog });
1012
+ writeStream.write(originalChangelog);
1013
+ writeStream.end();
1014
+ await finished(writeStream);
897
1015
  return changelog;
898
1016
  };
899
- const updateChangelog = async (newContent) => {
900
- const file = "CHANGELOG.md";
901
- const title = `# Changelog${eol(3)}`;
902
- await writeFile(file, `${(existsSync(file) && await readFile(file, "utf-8") || title).replace(/* @__PURE__ */ new RegExp(`^${title}`), `${title}${newContent}`).trim()}${eol()}`);
903
- await runGitProxy(["add", file]);
904
- };
905
1017
 
906
1018
  //#endregion
907
1019
  //#region src/git/prompts.ts
908
1020
  const runGitPrompts = async (ctx, config) => {
909
- const { git: { currentTag }, configFileExists: cfe, dryRun } = ctx;
910
- const { git: { commit, tag, push, commitMessage } } = config;
1021
+ const { git: { currentTag, commitMessage }, configFileExists: cfe, dryRun } = ctx;
1022
+ const { git: { commit, tag, push } } = config;
911
1023
  const logs = [];
912
- const cMsg = format(commitMessage, ctx);
913
1024
  const shouldPrompt = (val) => isUndefined(val) || dryRun;
914
1025
  const { log: _, ...reset } = await group({
915
1026
  commit: () => {
916
- return shouldPrompt(commit) ? confirm({ message: prompt(MSG.PROMPT.GIT_COMMIT(cMsg)) }) : (() => {
1027
+ return shouldPrompt(commit) ? confirm({ message: question(MSG.PROMPT.GIT_COMMIT(commitMessage), "git") }) : (() => {
917
1028
  logs.push(yellow`git.commit`);
918
1029
  return Promise.resolve(commit);
919
1030
  })();
920
1031
  },
921
1032
  tag: () => {
922
- return shouldPrompt(tag) ? confirm({ message: prompt(MSG.PROMPT.GIT_TAG(currentTag)) }) : (() => {
1033
+ return shouldPrompt(tag) ? confirm({ message: question(MSG.PROMPT.GIT_TAG(currentTag), "git") }) : (() => {
923
1034
  logs.push(yellow`git.tag`);
924
1035
  return Promise.resolve(tag);
925
1036
  })();
926
1037
  },
927
1038
  push: () => {
928
- return shouldPrompt(push) ? confirm({ message: prompt(MSG.PROMPT.GIT_PUSH) }) : (() => {
1039
+ return shouldPrompt(push) ? confirm({ message: question(MSG.PROMPT.GIT_PUSH, "git") }) : (() => {
929
1040
  logs.push(yellow`git.push`);
930
1041
  return Promise.resolve(push);
931
1042
  })();
@@ -934,9 +1045,8 @@ const runGitPrompts = async (ctx, config) => {
934
1045
  if (logs.length) msg("GIT", `Using ${cfe ? "user" : "default"} config: ${logs.join(", ")}`);
935
1046
  return Promise.resolve();
936
1047
  }
937
- }, { onCancel: async ({ results }) => {
938
- await gitRollback(ctx);
939
- abortTask("Operation cancelled.");
1048
+ }, { onCancel: ({ results }) => {
1049
+ abortGroupPrompt(ctx);
940
1050
  } });
941
1051
  Object.assign(config.git, reset);
942
1052
  };
@@ -944,13 +1054,12 @@ const runGitPrompts = async (ctx, config) => {
944
1054
  //#endregion
945
1055
  //#region src/github/prompts.ts
946
1056
  const runGithubPrompts = async (ctx, config) => {
947
- const { configFileExists: cfe, dryRun } = ctx;
948
- const { github: { release, releaseName } } = config;
1057
+ const { configFileExists: cfe, dryRun, github: { releaseName } } = ctx;
1058
+ const { github: { release } } = config;
949
1059
  const logs = [];
950
- const title = format(releaseName, ctx);
951
1060
  const shouldPrompt = (val) => isUndefined(val) || dryRun;
952
1061
  const res = await group({
953
- release: () => shouldPrompt(release) ? confirm({ message: prompt(MSG.PROMPT.GITHUB_RELEASE(title)) }) : (() => {
1062
+ release: () => shouldPrompt(release) ? confirm({ message: question(MSG.PROMPT.GITHUB_RELEASE(releaseName), "github") }) : (() => {
954
1063
  logs.push(yellow`github.release`);
955
1064
  return Promise.resolve(release);
956
1065
  })(),
@@ -958,9 +1067,8 @@ const runGithubPrompts = async (ctx, config) => {
958
1067
  if (logs.length) msg("GITHUB", `Using ${cfe ? "user" : "default"} config: ${logs.join(", ")}`);
959
1068
  return Promise.resolve();
960
1069
  }
961
- }, { onCancel: async ({ results }) => {
962
- await gitRollback(ctx);
963
- abortTask("Operation cancelled.");
1070
+ }, { onCancel: ({ results }) => {
1071
+ abortGroupPrompt(ctx);
964
1072
  } });
965
1073
  Object.assign(config.github, { release: res.release });
966
1074
  return res;
@@ -975,7 +1083,7 @@ const runNpmPublishPrompts = async (ctx, config) => {
975
1083
  const target = `${name}@${next}`;
976
1084
  const shouldPrompt = (val) => isUndefined(val) || dryRun;
977
1085
  const res = await group({
978
- publish: () => shouldPrompt(publish) ? confirm({ message: prompt(MSG.PROMPT.NPM_PUBLISH(target)) }) : (() => {
1086
+ publish: () => shouldPrompt(publish) ? confirm({ message: question(MSG.PROMPT.NPM_PUBLISH(target), "npm") }) : (() => {
979
1087
  logs.push(yellow`npm.publish`);
980
1088
  return Promise.resolve(publish);
981
1089
  })(),
@@ -984,16 +1092,14 @@ const runNpmPublishPrompts = async (ctx, config) => {
984
1092
  return Promise.resolve();
985
1093
  }
986
1094
  }, { onCancel: async ({ results }) => {
987
- await gitRollback(ctx);
988
- abortTask("Operation cancelled.");
1095
+ abortGroupPrompt(ctx);
989
1096
  } });
990
1097
  Object.assign(config.npm, { publish: res.publish });
991
1098
  return res;
992
1099
  };
993
1100
  const runNpmOptPrompts = async (ctx, config) => {
994
- const res = await group({ otp: () => text({ message: MSG.PROMPT.NPM_OTP }) }, { onCancel: async ({ results }) => {
995
- await gitRollback(ctx);
996
- abortTask("Operation cancelled.");
1101
+ const res = await group({ otp: () => text({ message: MSG.PROMPT.NPM_OTP }) }, { onCancel: ({ results }) => {
1102
+ abortGroupPrompt(ctx);
997
1103
  } });
998
1104
  Object.assign(ctx.npm, { otp: res.otp });
999
1105
  return res;
@@ -1001,65 +1107,67 @@ const runNpmOptPrompts = async (ctx, config) => {
1001
1107
 
1002
1108
  //#endregion
1003
1109
  //#region src/npm/publish.ts
1004
- const runNpmProxy = (...args) => {
1005
- if (process.env["dryRun"]) log.message(`${dim("$")} ${gray(["npm", ...args[0]].join(" "))}`);
1006
- return runNpm(...args);
1007
- };
1008
- const DEFAULT_TAG = "latest";
1009
- const DEFAULT_TAG_PRERELEASE = "next";
1010
- const getRegistry = (publishConfig = {}) => {
1011
- return (publishConfig.registry ? [publishConfig.registry] : Object.keys(publishConfig).filter((key) => key.endsWith("registry")).map((key) => publishConfig[key]))[0];
1110
+ const resolvePublishRegistry = (publishConfig = {}) => {
1111
+ if (publishConfig.registry) return publishConfig.registry;
1112
+ return Object.keys(publishConfig).filter((k) => k.endsWith("registry")).map((k) => publishConfig[k])[0];
1012
1113
  };
1013
- const isRegistryUp = async (registry) => {
1014
- return void 0 !== await runNpmProxy(["ping", ...registry ? [`--registry ${registry}`] : []]);
1114
+ const pingRegistry = async () => {
1115
+ return void 0 !== await runNpm(["ping"]);
1015
1116
  };
1016
- const isAuthenticated = (registry) => {
1017
- return runNpmProxy(["whoami", ...registry ? [`--registry ${registry}`] : []]);
1117
+ const getAuthenticatedUser = () => {
1118
+ return runNpm(["whoami"]);
1018
1119
  };
1019
- const getLatestRegistryVersion = (name, tag, registry) => {
1020
- const registryArg = registry ? [`--registry ${registry}`] : [];
1021
- return runNpmProxy([
1120
+ const getVersionByTag = (name, tag) => {
1121
+ return runNpm([
1022
1122
  "show",
1023
1123
  `${name}@${tag}`,
1024
- "version",
1025
- ...registryArg
1124
+ "version"
1026
1125
  ]);
1027
1126
  };
1028
- const getRegistryTags = async (name, registry) => {
1029
- const res = await runNpmProxy([
1030
- "view",
1127
+ const getPublishedVersion = (name) => {
1128
+ return runNpm([
1129
+ "info",
1031
1130
  name,
1032
- "dist-tags",
1033
- "--json",
1034
- ...registry ? [`--registry ${registry}`] : []
1131
+ "version"
1035
1132
  ]);
1036
- return Object.keys(JSON.parse(res || "{}"));
1037
1133
  };
1038
- const bump = (version, allowSameVersion, args = []) => {
1039
- allowSameVersion && args.push(`--allow-same-version`);
1040
- return runNpmProxy([
1134
+ const resolvePublishTag = async (pkgName, version) => {
1135
+ const { toPreRelease, preId } = parseVersion(version);
1136
+ if (!toPreRelease) {
1137
+ const active = await getPublishedVersion(pkgName);
1138
+ return active && lt(version, active) ? "previous" : "latest";
1139
+ } else return preId || "prerelease";
1140
+ };
1141
+ const bumpPackageVersion = (version, args = [], cwd = ".") => {
1142
+ return runNpm([
1041
1143
  "version",
1042
1144
  version,
1043
1145
  "--no-git-tag-version",
1044
1146
  "--workspaces=false",
1147
+ "--allow-same-version",
1045
1148
  ...args
1046
- ]);
1047
- };
1048
- const guessPreReleaseTag = async (name) => {
1049
- return (await getRegistryTags(name)).filter((tag) => DEFAULT_TAG !== tag)[0] ?? DEFAULT_TAG_PRERELEASE;
1149
+ ], { cwd });
1150
+ };
1151
+ const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
1152
+ const publishPackage = (pkg$1 = ".", tag = "latest", args = []) => {
1153
+ return runNpm([
1154
+ "publish",
1155
+ "--access",
1156
+ "public",
1157
+ "--tag",
1158
+ tag,
1159
+ "--workspaces=false",
1160
+ ...args
1161
+ ], { cwd: pkg$1 });
1050
1162
  };
1051
- const resolveTag = async (ctx, config) => {
1052
- const { npm: { tag } } = config;
1053
- const { pkg: { name, current } } = ctx;
1054
- const { toPreRelease, preId } = parseVersion(current);
1055
- if (!toPreRelease) return DEFAULT_TAG;
1056
- else return tag || preId || await guessPreReleaseTag(name);
1163
+ const getPackageUrl = (name, next) => {
1164
+ return `https://www.npmjs.com/package/${name}/v/${next}`;
1057
1165
  };
1058
1166
  const isCollaborator = async (ctx) => {
1059
1167
  const { pkg: { name, publishConfig }, npm: { username } } = ctx;
1060
- const registry = getRegistry(publishConfig);
1061
- const registryArg = registry ? [`--registry ${registry}`] : [];
1062
- let npmVersion = await runNpmProxy(["--version"]);
1168
+ const registry = resolvePublishRegistry(publishConfig);
1169
+ const registryArg = registry ? ["--registry", registry] : [];
1170
+ let npmVersion = await runNpm(["--version"]);
1063
1171
  let accessCommand;
1064
1172
  if (gt(npmVersion, "9.0.0")) accessCommand = [
1065
1173
  "access",
@@ -1068,73 +1176,92 @@ const isCollaborator = async (ctx) => {
1068
1176
  "--json"
1069
1177
  ];
1070
1178
  else accessCommand = ["access", "ls-collaborators"];
1071
- const res = await runNpmProxy([
1179
+ const res = await runNpm([
1072
1180
  ...accessCommand,
1073
1181
  name,
1074
1182
  ...registryArg
1075
1183
  ]);
1076
- if (res) {
1077
- const permissions = JSON.parse(res ?? "{}")[username];
1078
- return permissions && permissions.includes("write");
1079
- }
1080
- log.error(`Unable to verify if user ${username} is a collaborator for ${name}.`);
1081
- };
1082
- const getPackageUrl = (ctx) => {
1083
- const { pkg: { name, next } } = ctx;
1084
- return `https://www.npmjs.com/package/${name}/v/${next}`;
1184
+ if (res) return JSON.parse(res ?? "{}")[username]?.includes("write");
1185
+ return false;
1085
1186
  };
1086
1187
  const npmCheck = async (ctx, config) => {
1087
- const { pkg: { name, publishConfig }, npm: { username } } = ctx;
1088
- const registry = getRegistry(publishConfig);
1089
- const tag = await resolveTag(ctx, config);
1188
+ const { pkg: { name, current, publishConfig: { registry } }, npm: { username } } = ctx;
1189
+ const { npm: { skipChecks } } = config;
1190
+ const tag = await resolvePublishTag(name, current);
1191
+ Object.assign(ctx.npm, { tag });
1192
+ if (skipChecks) return "";
1193
+ if (!await pingRegistry()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1090
1194
  const getUser = async () => {
1091
- const username$1 = await isAuthenticated(registry);
1195
+ const username$1 = await getAuthenticatedUser();
1092
1196
  if (!username$1) return false;
1093
1197
  Object.assign(ctx.npm, { username: username$1 });
1094
1198
  return true;
1095
1199
  };
1096
- if (!await isRegistryUp()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1097
1200
  if (!await getUser()) throw new Error(MSG.ERROR.NPM_AUTH);
1098
- const latestPublish = await getLatestRegistryVersion(name, tag, registry);
1099
- if (latestPublish) {
1201
+ const latest = await getVersionByTag(name, tag);
1202
+ if (latest) {
1100
1203
  if (!await isCollaborator(ctx)) throw new Error(MSG.ERROR.NPM_USER(username, name));
1101
1204
  }
1102
- return dim(latestPublish ? `(latest is ${latestPublish})` : "(new publish)");
1205
+ return latest ? `(${tag} → ${latest})` : `(${tag} → no published version)`;
1103
1206
  };
1104
- const isOtpError = (err) => /one-time password|otp/i.test(err.message);
1105
1207
  const publishNpm = async (ctx, config) => {
1106
- const { dryRun, npm: { otp } } = ctx;
1107
- const { npm: { publish, tag } } = config;
1208
+ const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
1209
+ const { npm: { publish, pushArgs }, getPkgDir } = config;
1108
1210
  if (!publish) return;
1109
- const args = ["publish"];
1110
- if (tag) args.push("--tag", tag);
1111
- if (otp) args.push("--otp", otp);
1112
- if (dryRun) args.push("--dry-run");
1113
- return runNpmProxy(args);
1211
+ const otpArgs = otp ? ["--otp", otp] : [];
1212
+ const dryRunArg = dryRun ? ["--dry-run"] : [];
1213
+ return publishPackage(getPkgDir(selectedPkg), tag, [
1214
+ ...otpArgs,
1215
+ ...pushArgs,
1216
+ ...dryRunArg
1217
+ ]);
1114
1218
  };
1115
1219
 
1116
1220
  //#endregion
1117
1221
  //#region src/action.ts
1118
1222
  var Action = class {
1119
1223
  async handle(cmdArgs, options) {
1120
- const { version: cVersion, name: cName } = readJsonFile(join(__dirname$1, "../package.json"));
1121
- info(`${cName} ${dim(`v${cVersion}`)} `);
1122
- const { otp, ...others } = options;
1224
+ const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
1225
+ const { otp, package: defPkg, ...others } = options;
1123
1226
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
1124
1227
  process.env["dryRun"] = String(dryRun);
1125
- const { version: pkgVersion, private: pkgPrivate, name: pkgName, publishConfig = {} } = readJsonFile("package.json");
1126
- const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1127
- const local = await resolveConfig(process.cwd(), (path$1) => {
1128
- info(`config file: ${underline(dim(path$1))}`);
1129
- });
1228
+ const { configPath, config: local } = await resolveConfig(process.cwd());
1130
1229
  const config = mergeConfig(createDefaultConfig(ci ? true : void 0), local);
1230
+ info(MSG.INFO.TOOL(cName, cVersion));
1231
+ info(MSG.INFO.CONFIG(configPath));
1232
+ console.log();
1233
+ intro(MSG.INTRO(dryRun));
1234
+ const { isMonorepo, packages, getPkgDir } = config;
1235
+ if (isMonorepo) {
1236
+ if (ci) {
1237
+ if (!defPkg) abortTask(MSG.ABORT.MONOREPO_CI_NO_PACKAGE);
1238
+ } else if (packages.length < 1) abortTask(MSG.ABORT.MONOREPO_NO_PACKAGES);
1239
+ }
1240
+ const selectPackage = async () => {
1241
+ const pkg$2 = await select({
1242
+ message: question(MSG.PROMPT.SELECT_PACKAGE, "package"),
1243
+ options: packages.map((pkg$3) => ({
1244
+ label: pkg$3,
1245
+ value: pkg$3
1246
+ }))
1247
+ });
1248
+ abortSinglePrompt(pkg$2);
1249
+ return pkg$2;
1250
+ };
1251
+ const selectedPkg = !isMonorepo ? "" : ci ? defPkg : packages.length === 1 ? packages[0] : await selectPackage();
1252
+ const { pkg: pkg$1, pkgDir } = getPackageInfo(selectedPkg, getPkgDir);
1253
+ const { version: pkgVersion, private: pkgPrivate = false, name: pkgName, publishConfig = {} } = pkg$1;
1254
+ const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1131
1255
  const ctx = mergeConfig(createDefaultContext(), {
1256
+ selectedPkg,
1132
1257
  configFileExists: Object.keys(local).length > 0,
1133
1258
  dryRun,
1259
+ showRelease,
1260
+ showChangelog,
1134
1261
  npm: { otp: String(otp) },
1135
1262
  increment: cmdArgs,
1136
1263
  isIncrement: true,
1137
- ci: [
1264
+ isCI: [
1138
1265
  ci,
1139
1266
  showChangelog,
1140
1267
  showRelease,
@@ -1146,14 +1273,28 @@ var Action = class {
1146
1273
  isPrivate: pkgPrivate,
1147
1274
  fromPreRelease: isPreRelease(pkgVersion),
1148
1275
  current: pkgVersion,
1149
- next: "",
1276
+ next: nextVersion,
1150
1277
  toPreRelease: false,
1151
1278
  publishConfig: { ...publishConfig }
1152
- }
1279
+ },
1280
+ noNpm: pkgPrivate
1153
1281
  });
1154
- console.log();
1155
- intro(MSG.INTRO(pkgName, dryRun));
1156
- const { hooks } = config;
1282
+ const { messages } = await publint({ pkgDir });
1283
+ for (const message of messages) {
1284
+ const formated = formatMessage(message, pkg$1);
1285
+ formated && msg("PKG", formated);
1286
+ }
1287
+ await this.checkTask(ctx, config);
1288
+ await this.bumpTask(ctx, config);
1289
+ await this.changelogTask(ctx, config);
1290
+ await this.gitTask(ctx, config);
1291
+ await this.npmTask(ctx, config);
1292
+ await this.githubTask(ctx, config);
1293
+ if (dryRun) gitRollback$1(ctx);
1294
+ taskEnd(MSG.OUTRO(dryRun));
1295
+ }
1296
+ async checkTask(ctx, config) {
1297
+ const { noNpm, dryRun } = ctx;
1157
1298
  await tasks([{
1158
1299
  title: MSG.CHECK.GIT.CHECKING,
1159
1300
  task: async () => {
@@ -1167,73 +1308,65 @@ var Action = class {
1167
1308
  const msg$1 = await npmCheck(ctx, config);
1168
1309
  const { pkg: { publishConfig: { registry } } } = ctx;
1169
1310
  return success(MSG.CHECK.NPM.CHECKED(registry, msg$1), dryRun);
1170
- }
1311
+ },
1312
+ enabled: !noNpm
1171
1313
  }]).catch(abortOnError);
1172
- {
1173
- const { pkg: { current, next }, isIncrement, ci: ci$1 } = ctx;
1174
- if (ci$1) {
1175
- if (!next && isIncrement) {
1176
- const next$1 = nextVersion || getCIVersion(current);
1177
- if (next$1) {
1178
- const parsed = parseVersion(next$1);
1179
- Object.assign(ctx.pkg, { ...parsed });
1180
- }
1181
- }
1182
- if (showRelease) {
1183
- const { pkg: { next: next$1 } } = ctx;
1184
- taskEnd(MSG.LOG.SHOW_VERSION(next$1));
1185
- }
1186
- }
1187
- }
1188
- {
1189
- const logStr = await getLog(true, ctx.isIncrement);
1190
- if (logStr) {
1191
- msg("GIT", `Changelog:${eol(2)}` + dim(logStr));
1192
- if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1193
- } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1194
- }
1195
- {
1196
- const { pkg: { next }, isIncrement } = ctx;
1197
- if (!next && isIncrement) await runVersionPrompts(ctx, config);
1198
- await runLifeCycleHook(hooks, "before:bump", dryRun);
1199
- await tasks([{
1200
- title: MSG.TASK.VERSION.START,
1201
- task: async () => {
1202
- const { pkg: { next: next$1 }, isIncrement: isIncrement$1 } = ctx;
1203
- await bump(next$1, isIncrement$1);
1204
- await resolveTag$1(ctx, config);
1205
- await updateChangelog(await generateChangelog(ctx));
1206
- return success(MSG.TASK.VERSION.END(next$1), dryRun);
1207
- }
1208
- }]).catch(abortOnError);
1209
- await runLifeCycleHook(hooks, "after:bump", dryRun);
1210
- }
1211
- {
1212
- const changeset = await getStatus();
1213
- if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1214
- else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1314
+ }
1315
+ async bumpTask(ctx, config) {
1316
+ const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
1317
+ const { hooks, getPkgDir } = config;
1318
+ const need = (ctx$1) => {
1319
+ const { pkg: { next: next$1 }, isIncrement: isIncrement$1 } = ctx$1;
1320
+ return !next$1 && isIncrement$1;
1321
+ };
1322
+ if (need(ctx)) {
1323
+ const parsed = parseVersion(isCI && getCIVersion(current) || (await runVersionPrompts(ctx, config)).next);
1324
+ Object.assign(ctx.pkg, { ...parsed });
1215
1325
  }
1216
- {
1217
- const { pkg: { isPrivate }, ci: ci$1 } = ctx;
1218
- if (!isPrivate) {
1219
- await runNpmPublishPrompts(ctx, config);
1220
- await runLifeCycleHook(hooks, "before:publish", dryRun);
1221
- await tasks([{
1222
- title: MSG.TASK.NPM.START,
1223
- task: async () => {
1224
- await publishNpm(ctx, config).catch(async (err) => {
1225
- if (isOtpError(err) && !ci$1) {
1226
- await runNpmOptPrompts(ctx, config);
1227
- await publishNpm(ctx, config);
1228
- } else throw err;
1229
- });
1230
- const url = getPackageUrl(ctx);
1231
- return success(MSG.TASK.NPM.END + `: ${url}`, dryRun);
1232
- }
1233
- }]).catch(abortOnError);
1234
- await runLifeCycleHook(hooks, "after:publish", dryRun);
1326
+ const { pkg: { next }, isIncrement } = ctx;
1327
+ if (showRelease) taskEnd(MSG.LOG.SHOW_VERSION(next));
1328
+ await runLifeCycleHook(hooks, "before:bump", dryRun);
1329
+ await tasks([{
1330
+ title: MSG.TASK.VERSION.START,
1331
+ task: async () => {
1332
+ await bumpPackageVersion(next, [], getPkgDir(selectedPkg));
1333
+ await formatTemplate(ctx, config);
1334
+ const to$1 = `(${current}...${diff(current, next)})`;
1335
+ return success(MSG.TASK.VERSION.END(to$1), dryRun);
1235
1336
  }
1236
- }
1337
+ }]).catch((err) => abortOnError(err, ctx));
1338
+ await runLifeCycleHook(hooks, "after:bump", dryRun);
1339
+ const { from, to } = await resolveChangelogRange(isIncrement);
1340
+ const logStr = await getLog(from, to, getPkgDir(selectedPkg));
1341
+ if (logStr) {
1342
+ msg("GIT", `Changelog:${eol(2)}` + logStr);
1343
+ if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1344
+ } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1345
+ }
1346
+ async changelogTask(ctx, config) {
1347
+ const { isIncrement, dryRun, selectedPkg } = ctx;
1348
+ const { getPkgDir, changelogTagPrefix } = config;
1349
+ await tasks([{
1350
+ title: MSG.TASK.CHANGELOG.START,
1351
+ task: async () => {
1352
+ const changelog = await generateChangelog({
1353
+ getPkgDir: () => getPkgDir(selectedPkg),
1354
+ tagPrefix: changelogTagPrefix?.(selectedPkg)
1355
+ });
1356
+ await runGit(["add", `${getPkgDir(selectedPkg)}/CHANGELOG.md`]);
1357
+ Object.assign(ctx.github, { changelog });
1358
+ return success(MSG.TASK.CHANGELOG.END, dryRun);
1359
+ },
1360
+ enabled: isIncrement
1361
+ }]).catch((err) => abortOnError(err, ctx));
1362
+ const changeset = await getStatus();
1363
+ if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1364
+ else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1365
+ }
1366
+ async gitTask(ctx, config) {
1367
+ const { noGit, dryRun } = ctx;
1368
+ const { hooks } = config;
1369
+ if (noGit) return;
1237
1370
  await runGitPrompts(ctx, config);
1238
1371
  await runLifeCycleHook(hooks, "before:push", dryRun);
1239
1372
  await tasks([{
@@ -1242,8 +1375,34 @@ var Action = class {
1242
1375
  await commitAndTag(ctx, config);
1243
1376
  return success(MSG.TASK.GIT.END, dryRun);
1244
1377
  }
1245
- }]).catch(abortOnError);
1378
+ }]).catch((err) => abortOnError(err, ctx));
1246
1379
  await runLifeCycleHook(hooks, "after:push", dryRun);
1380
+ }
1381
+ async npmTask(ctx, config) {
1382
+ const { noNpm, dryRun, isCI, pkg: { name, next } } = ctx;
1383
+ const { hooks } = config;
1384
+ if (noNpm) return;
1385
+ await runNpmPublishPrompts(ctx, config);
1386
+ await runLifeCycleHook(hooks, "before:publish", dryRun);
1387
+ await tasks([{
1388
+ title: MSG.TASK.NPM.START,
1389
+ task: async () => {
1390
+ await publishNpm(ctx, config).catch(async (err) => {
1391
+ if (isOtpError(err) && !isCI) {
1392
+ await runNpmOptPrompts(ctx, config);
1393
+ await publishNpm(ctx, config);
1394
+ } else throw err;
1395
+ });
1396
+ const url = getPackageUrl(name, next);
1397
+ return success(MSG.TASK.NPM.END(url), dryRun);
1398
+ }
1399
+ }]).catch((err) => abortOnError(err, ctx));
1400
+ await runLifeCycleHook(hooks, "after:publish", dryRun);
1401
+ }
1402
+ async githubTask(ctx, config) {
1403
+ const { noGitHub, dryRun } = ctx;
1404
+ const { hooks } = config;
1405
+ if (noGitHub) return;
1247
1406
  await runGithubPrompts(ctx, config);
1248
1407
  await runLifeCycleHook(hooks, "before:release", dryRun);
1249
1408
  await tasks([{
@@ -1251,21 +1410,43 @@ var Action = class {
1251
1410
  task: async () => {
1252
1411
  await createRelease(ctx, config);
1253
1412
  const url = getGithubReleaseUrl(ctx);
1254
- return success(MSG.TASK.GITHUB.END + `: ${url}`, dryRun);
1413
+ return success(MSG.TASK.GITHUB.END(url), dryRun);
1255
1414
  }
1256
- }]).catch(abortOnError);
1415
+ }]).catch((err) => abortOnError(err, ctx));
1257
1416
  await runLifeCycleHook(hooks, "after:release", dryRun);
1258
- if (dryRun) await gitRollback(ctx);
1259
- taskEnd(MSG.OUTRO(dryRun));
1260
1417
  }
1261
1418
  };
1262
1419
 
1263
1420
  //#endregion
1264
1421
  //#region src/cli.ts
1265
- const pkg = readJsonFile(join(__dirname$1, "..", "package.json"));
1266
- 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) => {
1267
- await new Action().handle(argName, options);
1268
- }).helpOption("-h, --help", "Output usage information.").parse(process.argv);
1422
+ const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1423
+ 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) => {
1424
+ await new Action().handle(releaseType, options);
1425
+ });
1426
+ 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) => {
1427
+ const { force = false, monorepo = false } = options;
1428
+ const cwd = process.cwd();
1429
+ const { type } = readJsonFile(join(cwd, "package.json"));
1430
+ const isESM = type === "module";
1431
+ const isTS = existsSync(join(cwd, "tsconfig.json"));
1432
+ const configPath = join(cwd, `release.config.${isESM ? "" : "m"}${isTS ? "ts" : "js"}`);
1433
+ if (existsSync(configPath) && !force) {
1434
+ console.error(`${red`Error:`} ${yellow`release.config.ts`} already exists.\nUse ${green`--force`} to overwrite.`);
1435
+ process.exit(1);
1436
+ }
1437
+ const monoConfigToString = () => `{
1438
+ isMonorepo: true,
1439
+ packages: [ 'demo-a', 'demo-b' ],
1440
+ getPkgDir: (pkg) => \`packages/\${ pkg }\`,
1441
+ toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1442
+ changelogTagPrefix: (pkg) => \`\${ pkg }@\`,
1443
+ }`;
1444
+ writeFileSync(configPath, `import { defineConfig } from '@peiyanlu/release'
1445
+
1446
+ export default defineConfig(${monorepo ? monoConfigToString() : "{}"})`, "utf-8");
1447
+ console.log(`${green`✔ release.config.ts`} created successfully`);
1448
+ });
1449
+ program.parse(process.argv);
1269
1450
  program.on("command:*", () => {
1270
1451
  console.error(`\n${red`Error`} Invalid command: ${red`%s`}`, program.args.join(" "));
1271
1452
  console.log(`See ${red`--help`} for a list of available commands.\n`);