@quiteer/scripts 0.0.1 → 0.0.3

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/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  //#region src/index.d.ts
2
3
  declare function setupCli(): Promise<void>;
3
4
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,100 +1,63 @@
1
+ #!/usr/bin/env node
1
2
  import cac from "cac";
2
- import { bgRed, blue, gray, green, lightGreen, red, yellow } from "kolorist";
3
- import { rimraf } from "rimraf";
4
- import { access, writeFile } from "node:fs/promises";
3
+ import { bgGreen, bgRed, blue, gray, green, lightBlue, lightCyan, lightGreen, red, white, yellow } from "kolorist";
4
+ import { access, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
- import { prompt } from "enquirer";
7
+ import { execa } from "execa";
8
+ import { rimraf } from "rimraf";
8
9
  import { loadConfig } from "c12";
9
10
  import { readFileSync } from "node:fs";
11
+ import enquirer from "enquirer";
10
12
  import { versionBump } from "bumpp";
11
13
 
12
14
  //#region package.json
13
- var version = "0.0.1";
14
-
15
- //#endregion
16
- //#region src/commands/cleanup.ts
17
- async function cleanup(paths) {
18
- await rimraf(paths, { glob: true });
19
- }
15
+ var version = "0.0.2";
20
16
 
21
17
  //#endregion
22
- //#region src/config/index.ts
23
- const defaultOptions = {
24
- cwd: process.cwd(),
25
- cleanupDirs: [
26
- "**/dist",
27
- "**/node_modules",
28
- "!node_modules/**"
29
- ],
30
- lang: "zh-cn",
31
- ncuCommandArgs: ["--deep", "-u"],
32
- gitCommitVerifyIgnores: [
33
- /^((Merge pull request)|(Merge (.*?) into (.*)|(Merge branch (.*)))(?:\r?\n)*$)/m,
34
- /^(Merge tag (.*))(?:\r?\n)*$/m,
35
- /^(R|r)evert (.*)/,
36
- /^(amend|fixup|squash)!/,
37
- /^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
38
- /^Merge remote-tracking branch(\s*)(.*)/,
39
- /^Automatic merge(.*)/,
40
- /^Auto-merged (.*?) into (.*)/
41
- ]
42
- };
43
- async function loadCliOptions(overrides, cwd = process.cwd()) {
44
- const { config } = await loadConfig({
45
- name: "quiteer",
46
- defaults: defaultOptions,
47
- overrides,
48
- cwd,
49
- packageJson: true
50
- });
51
- return config;
52
- }
53
-
54
- //#endregion
55
- //#region src/commands/generate-cfg.ts
18
+ //#region src/locales/changelog.ts
56
19
  /**
57
- * 函数:在项目根目录生成 quiteer 配置文件
58
- * 作用:若已有配置则询问是否覆盖;按默认配置生成 `quiteer.config.ts`
20
+ * 获取 changelog 类型到分组标题的映射
21
+ * @param lang 输出语言
22
+ * @returns 类型分组标题映射
59
23
  */
60
- async function generateConfig() {
61
- const cwd = process.cwd();
62
- const configPath = path.join(cwd, "quiteer.config.ts");
63
- let exists = true;
64
- try {
65
- await access(configPath);
66
- } catch {
67
- exists = false;
68
- }
69
- if (exists) {
70
- const { overwrite } = await prompt([{
71
- name: "overwrite",
72
- type: "confirm",
73
- message: `检测到已存在配置文件 ${path.basename(configPath)},是否覆盖?`
74
- }]);
75
- if (!overwrite) return;
76
- }
77
- const regexList = defaultOptions.gitCommitVerifyIgnores.map((r) => r.toString());
78
- const indentJson = (val) => {
79
- return JSON.stringify(val, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
24
+ function getChangelogHeadingMap(lang) {
25
+ if (lang === "en-us") return {
26
+ "feat": "Features",
27
+ "feat-wip": "Features",
28
+ "fix": "Fixes",
29
+ "docs": "Docs",
30
+ "typo": "Typo",
31
+ "style": "Style",
32
+ "refactor": "Refactor",
33
+ "perf": "Performance",
34
+ "optimize": "Optimization",
35
+ "test": "Tests",
36
+ "build": "Build",
37
+ "ci": "CI",
38
+ "chore": "Chore",
39
+ "revert": "Revert"
40
+ };
41
+ return {
42
+ "feat": "新功能",
43
+ "feat-wip": "新功能",
44
+ "fix": "修复",
45
+ "docs": "文档",
46
+ "typo": "勘误",
47
+ "style": "代码风格",
48
+ "refactor": "重构",
49
+ "perf": "性能优化",
50
+ "optimize": "质量优化",
51
+ "test": "测试",
52
+ "build": "构建",
53
+ "ci": "CI",
54
+ "chore": "其他",
55
+ "revert": "回滚"
80
56
  };
81
- await writeFile(configPath, [
82
- "// 自动生成的 quiteer 配置文件",
83
- "",
84
- "export default {",
85
- ` cleanupDirs: ${indentJson(defaultOptions.cleanupDirs)},`,
86
- ` lang: ${JSON.stringify(defaultOptions.lang)},`,
87
- ` ncuCommandArgs: ${indentJson(defaultOptions.ncuCommandArgs)},`,
88
- " gitCommitVerifyIgnores: [",
89
- ...regexList.map((r, idx, arr) => ` ${r}${idx < arr.length - 1 ? "," : ""}`),
90
- " ]",
91
- "}",
92
- ""
93
- ].join("\n"), "utf8");
94
57
  }
95
58
 
96
59
  //#endregion
97
- //#region src/locales/index.ts
60
+ //#region src/locales/commit.ts
98
61
  const locales = {
99
62
  "zh-cn": {
100
63
  gitCommitMessages: {
@@ -173,83 +136,469 @@ const locales = {
173
136
  //#endregion
174
137
  //#region src/shared/index.ts
175
138
  async function execCommand(cmd, args, options) {
176
- const { execa } = await import("execa");
177
139
  return ((await execa(cmd, args, options))?.stdout)?.trim() || "";
178
140
  }
179
141
 
180
142
  //#endregion
181
- //#region src/commands/git-commit.ts
143
+ //#region src/commands/changelog.ts
182
144
  /**
183
- * 函数:交互式添加变更文件到暂存区
184
- * 作用:先询问是否添加全部;若否,列出变更文件供多选后执行 git add
145
+ * 获取分组标题映射(使用 locales/changelog 聚合配置)
146
+ * @param lang 输出语言
185
147
  */
186
- async function gitCommitAdd() {
187
- const { confirm } = await prompt([{
188
- name: "confirm",
189
- type: "confirm",
190
- message: "是否添加所有变更文件到暂存区?"
191
- }]);
192
- if (!confirm) {
193
- const files = (await execCommand("git", ["diff", "--name-only"])).split("\n").filter(Boolean);
194
- if (files.length === 0) return;
195
- const { selected } = await prompt([{
196
- name: "selected",
197
- type: "multiselect",
198
- message: "选择需要添加到暂存区的文件(空格选择,回车确认)",
199
- choices: files.map((f) => ({
200
- name: f,
201
- value: f
202
- }))
203
- }]);
204
- if (!selected?.length) return;
205
- await execCommand("git", ["add", ...selected], { stdio: "inherit" });
148
+ function getHeadingMap(lang) {
149
+ return getChangelogHeadingMap(lang);
150
+ }
151
+ function parseCommit(line) {
152
+ const [hash, short, date, author, email, subject] = line.split("|");
153
+ const m = subject.match(/(?<type>[a-z]+)(?:\((?<scope>.+)\))?!?: (?<description>.+)/i);
154
+ if (!m?.groups) return null;
155
+ return {
156
+ type: m.groups.type.toLowerCase(),
157
+ scope: m.groups.scope,
158
+ description: m.groups.description.trim(),
159
+ hash,
160
+ short,
161
+ author,
162
+ email,
163
+ date,
164
+ added: 0,
165
+ deleted: 0
166
+ };
167
+ }
168
+ function groupByType(items, lang) {
169
+ const map = /* @__PURE__ */ new Map();
170
+ const headingMap = getHeadingMap(lang);
171
+ for (const it of items) {
172
+ const heading = headingMap[it.type] || (lang === "en-us" ? "Other" : "其他");
173
+ const arr = map.get(heading) || [];
174
+ arr.push(it);
175
+ map.set(heading, arr);
176
+ }
177
+ return map;
178
+ }
179
+ /** 获取类型图标映射 */
180
+ function getTypeIcon(type) {
181
+ return {
182
+ "feat": "✨ ",
183
+ "feat-wip": "🧪 ",
184
+ "fix": "🐛 ",
185
+ "docs": "📝 ",
186
+ "style": "🎨 ",
187
+ "refactor": "♻️ ",
188
+ "perf": "⚡ ",
189
+ "optimize": "🧹 ",
190
+ "test": "✅ ",
191
+ "build": "🏗️ ",
192
+ "ci": "⚙️ ",
193
+ "chore": "🔧 ",
194
+ "deps": "📦 ",
195
+ "revert": "⏪ ",
196
+ "typo": "✏️ "
197
+ }[type] || "";
198
+ }
199
+ async function getTags() {
200
+ return (await execCommand("git", [
201
+ "tag",
202
+ "--list",
203
+ "--sort=-version:refname"
204
+ ])).split("\n").filter(Boolean);
205
+ }
206
+ async function getRootCommit() {
207
+ return await execCommand("git", [
208
+ "rev-list",
209
+ "--max-parents=0",
210
+ "HEAD"
211
+ ]);
212
+ }
213
+ async function getDateForRef(ref) {
214
+ return await execCommand("git", [
215
+ "log",
216
+ "-1",
217
+ "--date=short",
218
+ "--format=%ad",
219
+ ref
220
+ ]) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
221
+ }
222
+ async function getCommitsInRange(range) {
223
+ return (await execCommand("git", [
224
+ "log",
225
+ range,
226
+ "--no-merges",
227
+ "--pretty=format:%H|%h|%ad|%an|%ae|%s",
228
+ "--date=format:%Y-%m-%d %H:%M %z"
229
+ ])).split("\n").filter(Boolean).map(parseCommit).filter((x) => !!x);
230
+ }
231
+ /**
232
+ * 读取 package homepage 以生成提交链接
233
+ * @returns 仓库主页 URL
234
+ */
235
+ async function readHomepage() {
236
+ try {
237
+ const content = await readFile(path.join(process.cwd(), "scripts", "package.json"), "utf8");
238
+ const json = JSON.parse(content);
239
+ return typeof json.homepage === "string" ? json.homepage.replace(/\/$/, "") : void 0;
240
+ } catch {
206
241
  return;
207
242
  }
208
- await execCommand("git", ["add", "."], { stdio: "inherit" });
243
+ }
244
+ async function prependFile(filePath, content) {
245
+ try {
246
+ await access(filePath);
247
+ const prev = await readFile(filePath, "utf8");
248
+ await writeFile(filePath, [
249
+ content.trim(),
250
+ "",
251
+ prev.trim()
252
+ ].join("\n"), "utf8");
253
+ } catch {
254
+ await writeFile(filePath, `${content.trim()}\n`, "utf8");
255
+ }
209
256
  }
210
257
  /**
211
- * Git commit with Conventional Commits standard
212
- *
213
- * @param lang
258
+ * 为每个提交补充变更文件与统计信息
259
+ * @param items 提交项
214
260
  */
215
- async function gitCommit(lang = "en-us") {
216
- const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
217
- const typesChoices = gitCommitTypes.map(([value, msg]) => {
218
- return {
219
- name: value,
220
- message: `${`${value}:`.padEnd(12)}${msg}`
221
- };
261
+ async function enrichCommit(items) {
262
+ const results = [];
263
+ for (const it of items) {
264
+ const out = await execCommand("git", [
265
+ "show",
266
+ it.hash,
267
+ "--numstat",
268
+ "--pretty=format:"
269
+ ]);
270
+ let added = 0;
271
+ let deleted = 0;
272
+ for (const line of out.split("\n").filter(Boolean)) {
273
+ const parts = line.split(" ");
274
+ if (parts.length === 3) {
275
+ const ai = Number.parseInt(parts[0], 10);
276
+ const di = Number.parseInt(parts[1], 10);
277
+ if (!Number.isNaN(ai)) added += ai;
278
+ if (!Number.isNaN(di)) deleted += di;
279
+ }
280
+ }
281
+ const statusOut = await execCommand("git", [
282
+ "show",
283
+ it.hash,
284
+ "--name-status",
285
+ "--pretty=format:"
286
+ ]);
287
+ const filesAdded = [];
288
+ const filesModified = [];
289
+ const filesDeleted = [];
290
+ for (const sLine of statusOut.split("\n").filter(Boolean)) {
291
+ const [status, file] = sLine.split(" ");
292
+ if (status === "A") filesAdded.push(file);
293
+ else if (status === "M") filesModified.push(file);
294
+ else if (status === "D") filesDeleted.push(file);
295
+ else if (status?.startsWith("R") && file) filesModified.push(file);
296
+ }
297
+ results.push({
298
+ ...it,
299
+ added,
300
+ deleted,
301
+ filesAdded,
302
+ filesModified,
303
+ filesDeleted
304
+ });
305
+ }
306
+ return results;
307
+ }
308
+ function formatSection(title, date, items, repoUrl, lang) {
309
+ const groups = groupByType(items, lang);
310
+ const lines = [];
311
+ lines.push("## 变更日志");
312
+ lines.push("");
313
+ for (const [heading, arr] of groups) {
314
+ if (!arr.length) continue;
315
+ lines.push(`### ${heading}`);
316
+ const dayMap = groupByDay(arr);
317
+ for (const [day, list] of dayMap) {
318
+ const dayAdded = list.reduce((s, c) => s + (c.filesAdded?.length || 0), 0);
319
+ const dayMod = list.reduce((s, c) => s + (c.filesModified?.length || 0), 0);
320
+ const dayDel = list.reduce((s, c) => s + (c.filesDeleted?.length || 0), 0);
321
+ lines.push(`#### ${day} \`✏️ ${dayMod}+\` \`➕ ${dayAdded}+\` \`🗑️ ${dayDel}+\``);
322
+ for (const it of list) {
323
+ const scopeFmt = it.scope ? `\`${it.scope}\`` : "";
324
+ const link = repoUrl ? ` ([\`${it.short}\`](${repoUrl}/commit/${it.hash}))` : "";
325
+ const email = ` <${it.email}>`;
326
+ const typeIcon = getTypeIcon(it.type);
327
+ const typeLabel = it.type === "chore" ? "**chore**" : `**${it.type}**`;
328
+ const time = getTime(it.date);
329
+ lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
330
+ lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
331
+ lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
332
+ for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
333
+ for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
334
+ for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
335
+ }
336
+ lines.push("");
337
+ }
338
+ }
339
+ return lines.join("\n");
340
+ }
341
+ function formatTimeline(items, repoUrl) {
342
+ const lines = [];
343
+ lines.push("## 变更日志");
344
+ lines.push("");
345
+ const dayMap = groupByDay(items);
346
+ for (const [day, list] of dayMap) {
347
+ const dayAdded = list.reduce((s, c) => s + (c.filesAdded?.length || 0), 0);
348
+ const dayMod = list.reduce((s, c) => s + (c.filesModified?.length || 0), 0);
349
+ const dayDel = list.reduce((s, c) => s + (c.filesDeleted?.length || 0), 0);
350
+ lines.push(`### ${day} \`✏️ ${dayMod}+\` \`➕ ${dayAdded}+\` \`🗑️ ${dayDel}+\``);
351
+ for (const it of list) {
352
+ const link = repoUrl ? ` ([\`${it.short}\`](${repoUrl}/commit/${it.hash}))` : "";
353
+ const email = ` <${it.email}>`;
354
+ const typeIcon = getTypeIcon(it.type);
355
+ const time = getTime(it.date);
356
+ const scopeFmt = it.scope ? `\`${it.scope}\`` : "";
357
+ const typeLabel = it.type === "chore" ? "**chore**" : `**${it.type}**`;
358
+ lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
359
+ lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
360
+ lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
361
+ for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
362
+ for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
363
+ for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
364
+ }
365
+ lines.push("");
366
+ }
367
+ return lines.join("\n");
368
+ }
369
+ /**
370
+ * 生成两种样式的 CHANGELOG 文件
371
+ * @param {{ lang: Lang, format: 'group' | 'timeline' | 'both', groupOutput: string, timelineOutput: string }} options 生成选项
372
+ * @param {'zh-cn'|'en-us'} options.lang 输出语言
373
+ * @param {'group'|'timeline'|'both'} options.format 输出样式
374
+ * @param {string} options.groupOutput 分组样式输出文件路径
375
+ * @param {string} options.timelineOutput 时间轴样式输出文件路径
376
+ * @returns {Promise<void>} 异步任务
377
+ */
378
+ async function generateChangelogFiles(options) {
379
+ const repoRoot = await execCommand("git", ["rev-parse", "--show-toplevel"]);
380
+ const homepage = await readHomepage();
381
+ const tags = await getTags();
382
+ let title = "未发布";
383
+ let date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
384
+ let range = "";
385
+ if (tags.length >= 1) {
386
+ const latest = tags[0];
387
+ const prev = tags[1];
388
+ title = latest;
389
+ date = await getDateForRef(latest);
390
+ range = prev ? `${prev}..${latest}` : `${await getRootCommit()}..${latest}`;
391
+ } else range = "HEAD";
392
+ let items = await getCommitsInRange(range);
393
+ items = await enrichCommit(items);
394
+ const fallback = [
395
+ `## ${title} - ${date}`,
396
+ "",
397
+ "- 无符合 Conventional Commits 标准的提交",
398
+ ""
399
+ ].join("\n");
400
+ if (options.format === "group" || options.format === "both") {
401
+ const content = items.length ? formatSection(title, date, items, homepage, options.lang) : fallback;
402
+ await prependFile(path.join(repoRoot, options.groupOutput), content);
403
+ }
404
+ if (options.format === "timeline" || options.format === "both") {
405
+ const content = items.length ? formatTimeline(items, homepage) : fallback;
406
+ await prependFile(path.join(repoRoot, options.timelineOutput), content);
407
+ }
408
+ }
409
+ function getDay(d) {
410
+ return d.match(/^(\d{4}-\d{2}-\d{2})/)?.[1] || d;
411
+ }
412
+ function getTime(d) {
413
+ return d.match(/^\d{4}-\d{2}-\d{2}\s(\d{2}:\d{2})/)?.[1] || d;
414
+ }
415
+ function groupByDay(items) {
416
+ const map = /* @__PURE__ */ new Map();
417
+ for (const it of items) {
418
+ const day = getDay(it.date);
419
+ const arr = map.get(day) || [];
420
+ arr.push(it);
421
+ map.set(day, arr);
422
+ }
423
+ return map;
424
+ }
425
+
426
+ //#endregion
427
+ //#region src/commands/cleanup.ts
428
+ async function cleanup(paths) {
429
+ await rimraf(paths, { glob: true });
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/config/index.ts
434
+ const defaultOptions = {
435
+ cwd: process.cwd(),
436
+ cleanupDirs: [
437
+ "**/dist",
438
+ "**/node_modules",
439
+ "!node_modules/**"
440
+ ],
441
+ lang: "zh-cn",
442
+ ncuCommandArgs: ["--deep", "-u"],
443
+ gitCommitVerifyIgnores: [
444
+ /^((Merge pull request)|(Merge (.*?) into (.*)|(Merge branch (.*)))(?:\r?\n)*$)/m,
445
+ /^(Merge tag (.*))(?:\r?\n)*$/m,
446
+ /^(R|r)evert (.*)/,
447
+ /^(amend|fixup|squash)!/,
448
+ /^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
449
+ /^Merge remote-tracking branch(\s*)(.*)/,
450
+ /^Automatic merge(.*)/,
451
+ /^Auto-merged (.*?) into (.*)/
452
+ ],
453
+ release: {
454
+ execute: "qui cl",
455
+ push: true
456
+ },
457
+ changelog: {
458
+ groupOutput: "CHANGELOG.md",
459
+ timelineOutput: "CHANGELOG_TIMELINE.md",
460
+ formats: "both"
461
+ },
462
+ gitCommit: { add: true }
463
+ };
464
+ async function loadCliOptions(overrides, cwd = process.cwd()) {
465
+ const { config } = await loadConfig({
466
+ name: "quiteer",
467
+ defaults: defaultOptions,
468
+ overrides,
469
+ cwd,
470
+ packageJson: true
222
471
  });
223
- const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
224
- name: value,
225
- message: `${value.padEnd(30)} (${msg})`
226
- }));
227
- const result = await prompt([
228
- {
229
- name: "types",
230
- type: "select",
231
- message: gitCommitMessages.types,
232
- choices: typesChoices
233
- },
234
- {
235
- name: "scopes",
236
- type: "select",
237
- message: gitCommitMessages.scopes,
238
- choices: scopesChoices
239
- },
240
- {
241
- name: "description",
242
- type: "text",
243
- message: gitCommitMessages.description
472
+ return config;
473
+ }
474
+
475
+ //#endregion
476
+ //#region src/commands/generate-cfg.ts
477
+ /**
478
+ * 函数:在项目根目录生成 quiteer 配置文件
479
+ * 作用:若已有配置则询问是否覆盖;按默认配置生成 `quiteer.config.ts`
480
+ */
481
+ async function generateConfig() {
482
+ const cwd = process.cwd();
483
+ const configPath = path.join(cwd, "quiteer.config.ts");
484
+ const regexList = defaultOptions.gitCommitVerifyIgnores.map((r) => r.toString());
485
+ const indentJson = (val) => {
486
+ return JSON.stringify(val, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
487
+ };
488
+ await writeFile(configPath, [
489
+ "// 自动生成的 quiteer 配置文件",
490
+ "// 说明:可在此文件中覆盖默认配置项,所有未定义项将采用默认值",
491
+ "",
492
+ "export default {",
493
+ " // 清理命令默认清理的目录(支持 glob)",
494
+ ` cleanupDirs: ${indentJson(defaultOptions.cleanupDirs)},`,
495
+ "",
496
+ " // CLI 提示语言:zh-cn | en-us",
497
+ ` lang: ${JSON.stringify(defaultOptions.lang)},`,
498
+ "",
499
+ " // npm-check-updates 命令参数",
500
+ ` ncuCommandArgs: ${indentJson(defaultOptions.ncuCommandArgs)},`,
501
+ "",
502
+ " // 提交信息校验的忽略规则(正则)",
503
+ " gitCommitVerifyIgnores: [",
504
+ ...regexList.map((r, idx, arr) => ` ${r}${idx < arr.length - 1 ? "," : ""}`),
505
+ " ],",
506
+ "",
507
+ " // 发布配置:执行的发布命令与是否推送",
508
+ ` release: ${indentJson(defaultOptions.release)},`,
509
+ "",
510
+ " // changelog 配置(仅保留输出文件与样式)",
511
+ ` changelog: ${indentJson(defaultOptions.changelog)},`,
512
+ "",
513
+ " // git-commit 配置:是否默认添加变更文件到暂存区",
514
+ ` gitCommit: ${indentJson(defaultOptions.gitCommit)},`,
515
+ "}",
516
+ ""
517
+ ].join("\n"), "utf8");
518
+ }
519
+
520
+ //#endregion
521
+ //#region src/commands/git-commit.ts
522
+ /**
523
+ * 交互式添加变更文件到暂存区
524
+ * - 先询问是否添加全部;若否,列出变更文件供多选后执行 git add
525
+ * - 在非交互或输入中断等情况下安全退出,不抛出未处理异常
526
+ * @returns {Promise<void>} 异步任务
527
+ */
528
+ async function gitCommitAdd() {
529
+ const { prompt: ask } = enquirer;
530
+ try {
531
+ if (!!!(await ask([{
532
+ name: "confirm",
533
+ type: "confirm",
534
+ message: "是否添加所有变更文件到暂存区?"
535
+ }]))?.confirm) {
536
+ const files = (await execCommand("git", ["diff", "--name-only"])).split("\n").filter(Boolean);
537
+ if (files.length === 0) return;
538
+ const selected = (await ask([{
539
+ name: "selected",
540
+ type: "multiselect",
541
+ message: "选择需要添加到暂存区的文件(空格选择,回车确认)",
542
+ choices: files.map((f) => ({
543
+ name: f,
544
+ value: f
545
+ }))
546
+ }]))?.selected ?? [];
547
+ if (!selected.length) return;
548
+ await execCommand("git", ["add", ...selected], { stdio: "inherit" });
549
+ return;
244
550
  }
245
- ]);
246
- const breaking = result.description.startsWith("!") ? "!" : "";
247
- const description = result.description.replace(/^!/, "").trim();
248
- await execCommand("git", [
249
- "commit",
250
- "-m",
251
- `${result.types}(${result.scopes})${breaking}: ${description}`
252
- ], { stdio: "inherit" });
551
+ await execCommand("git", ["add", "."], { stdio: "inherit" });
552
+ } catch {}
553
+ }
554
+ /**
555
+ * 交互式生成符合 Conventional Commits 的提交信息并执行提交
556
+ * - 支持取消或非交互环境下安全退出
557
+ * @param {Lang} lang 交互提示语言
558
+ * @returns {Promise<void>} 异步任务
559
+ */
560
+ async function gitCommit(lang = "en-us") {
561
+ try {
562
+ const { prompt: ask } = enquirer;
563
+ const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
564
+ const typesChoices = gitCommitTypes.map(([value, msg]) => {
565
+ return {
566
+ name: value,
567
+ message: `${`${value}:`.padEnd(12)}${msg}`
568
+ };
569
+ });
570
+ const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
571
+ name: value,
572
+ message: `${value.padEnd(30)} (${msg})`
573
+ }));
574
+ const result = await ask([
575
+ {
576
+ name: "types",
577
+ type: "select",
578
+ message: gitCommitMessages.types,
579
+ choices: typesChoices
580
+ },
581
+ {
582
+ name: "scopes",
583
+ type: "select",
584
+ message: gitCommitMessages.scopes,
585
+ choices: scopesChoices
586
+ },
587
+ {
588
+ name: "description",
589
+ type: "text",
590
+ message: gitCommitMessages.description
591
+ }
592
+ ]);
593
+ if (!result) return;
594
+ const breaking = result.description.startsWith("!") ? "!" : "";
595
+ const description = result.description.replace(/^!/, "").trim();
596
+ await execCommand("git", [
597
+ "commit",
598
+ "-m",
599
+ `${result.types}(${result.scopes})${breaking}: ${description}`
600
+ ], { stdio: "inherit" });
601
+ } catch {}
253
602
  }
254
603
  /** Git commit message verify */
255
604
  async function gitCommitVerify(lang = "zh-cn", ignores = []) {
@@ -264,14 +613,77 @@ async function gitCommitVerify(lang = "zh-cn", ignores = []) {
264
613
 
265
614
  //#endregion
266
615
  //#region src/commands/release.ts
267
- async function release(execute = "", push = true) {
616
+ /**
617
+ * 版本管理:交互选择需要提升版本的包、创建自定义前缀标签、生成变更日志
618
+ * - 多层级仓库:通过交互选择需要更新的包,未选中的包跳过
619
+ * - 标签:支持自定义前缀,命名为 <prefix>-v${version},存在则跳过创建
620
+ * - 变更日志:默认生成 CHANGELOG.md 与 CHANGELOG_TIMELINE.md(中文,both 格式)
621
+ * - 不包含推送操作,如需发布请手动执行 git push / pnpm publish
622
+ * @param {string} [tagPrefix] 标签前缀(可选,留空则交互输入)
623
+ * @returns {Promise<void>} 异步任务
624
+ */
625
+ async function release(tagPrefix) {
626
+ const { prompt: ask } = enquirer;
627
+ const repoRoot = await execCommand("git", ["rev-parse", "--show-toplevel"]);
628
+ const files = (await execCommand("git", [
629
+ "-C",
630
+ repoRoot,
631
+ "ls-files",
632
+ "**/package.json"
633
+ ])).split("\n").filter(Boolean).filter((p) => !p.includes("node_modules"));
634
+ const choices = [];
635
+ for (const rel of files) try {
636
+ const abs = path.join(repoRoot, rel);
637
+ const json = JSON.parse(await readFile(abs, "utf8"));
638
+ if (json?.name) choices.push({
639
+ name: `${json.name} (${rel})`,
640
+ value: rel
641
+ });
642
+ } catch {}
643
+ const selected = ((await ask([{
644
+ name: "selected",
645
+ type: "multiselect",
646
+ message: "选择需要提升版本的包(空格选择,回车确认)",
647
+ choices
648
+ }]))?.selected ?? []).map((s) => {
649
+ return choices.find((c) => c.value === s || c.name === s)?.value || s;
650
+ }).filter(Boolean);
651
+ if (!selected.length) return;
268
652
  await versionBump({
269
- files: ["**/package.json", "!**/node_modules"],
270
- execute,
271
- all: true,
272
- tag: true,
273
- commit: "chore(projects): release v%s",
274
- push
653
+ files: selected.map((rel) => path.join(repoRoot, rel)),
654
+ all: false,
655
+ tag: false,
656
+ commit: "chore(release): v%s"
657
+ });
658
+ const firstPkgPath = path.join(repoRoot, selected[0]);
659
+ const version$1 = JSON.parse(await readFile(firstPkgPath, "utf8")).version;
660
+ let prefix = tagPrefix;
661
+ if (prefix === void 0) try {
662
+ prefix = (await ask([{
663
+ name: "prefix",
664
+ type: "text",
665
+ message: "请输入标签前缀(可留空)"
666
+ }]))?.prefix?.trim() || "";
667
+ } catch {
668
+ prefix = "";
669
+ }
670
+ const tagName = `${prefix ? `${prefix}-` : ""}v${version$1}`;
671
+ if (!(await execCommand("git", [
672
+ "tag",
673
+ "--list",
674
+ tagName
675
+ ])).trim()) await execCommand("git", [
676
+ "tag",
677
+ "--annotate",
678
+ "--message",
679
+ `chore(projects): release ${tagName}`,
680
+ tagName
681
+ ]);
682
+ await generateChangelogFiles({
683
+ lang: "zh-cn",
684
+ format: "both",
685
+ groupOutput: "CHANGELOG.md",
686
+ timelineOutput: "CHANGELOG_TIMELINE.md"
275
687
  });
276
688
  }
277
689
 
@@ -291,35 +703,43 @@ async function setupCli() {
291
703
  * @returns Promise<void>
292
704
  */
293
705
  const cliOptions = await loadCliOptions();
294
- const cli = cac(blue("quiteer-scripts"));
295
- cli.version(lightGreen(version)).help();
296
- cli.command("generate-config", "在项目根目录下生成配置文件").alias("g").action(async () => {
706
+ const cli = cac(blue("quiteer"));
707
+ cli.command("generate-config", `${bgGreen(white("便捷命令"))} ${lightCyan("qui g")} 在项目根目录下生成配置文件`).alias("g").action(async () => {
297
708
  await generateConfig();
298
709
  });
299
- cli.command("remove [path]", "删除单个或者多个文件,多个值用逗号分隔,递归删除").alias("rm").action(async (paths) => {
710
+ cli.command("remove [path]", `${bgGreen(white("便捷命令"))} ${lightBlue("qui rm")} 删除单个或者多个文件 , 多个值用逗号分隔,递归删除`).alias("rm").action(async (paths) => {
300
711
  if (paths && paths.includes(",")) await cleanup(paths.split(","));
301
712
  else if (paths) await cleanup([paths]);
302
713
  else console.info("quiteer-script :>> ", gray("无事发生"));
303
714
  });
304
- cli.command("cleanup [path]", "清除目录 node_modules、dist 等").alias("c").action(async (paths) => {
715
+ cli.command("cleanup [path]", `${bgGreen(white("便捷命令"))} ${lightBlue("qui c")} 清除目录 node_modules、dist 等`).alias("c").action(async (paths) => {
305
716
  if (paths && paths.includes(",")) await cleanup(paths.split(","));
306
717
  else if (paths) await cleanup([paths]);
307
718
  else await cleanup(cliOptions.cleanupDirs);
308
719
  });
309
- cli.command("update-pkg", "更新 package.json 依赖版本").alias("u").action(async () => {
720
+ cli.command("update-pkg", `${bgGreen(white("便捷命令"))} ${lightBlue("qui u")} 更新 package.json 依赖版本`).alias("u").action(async () => {
310
721
  await updatePkg(cliOptions.ncuCommandArgs);
311
722
  });
312
- cli.command("git-commit", "git 提交前后的操作和规范等").alias("git-c").option("--add", "添加所有变更文件到暂存区", { default: true }).option("-l ,--lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
723
+ cli.command("git-commit", `${bgGreen(white("便捷命令"))} ${lightBlue("qui gc")} git 提交前后的操作和规范等`).alias("gc").option("--add", "添加所有变更文件到暂存区", { default: cliOptions.gitCommit.add }).option("-l ,--lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
313
724
  if (args?.add) await gitCommitAdd();
314
725
  await gitCommit(args?.lang);
315
726
  });
316
- cli.command("git-commit-verify", "校验提交信息是否符合 Conventional Commits 标准").alias("git-v").option("-l --lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
727
+ cli.command("git-commit-verify", `${bgGreen(white("便捷命令"))} ${lightBlue("qui gv")} 校验提交信息是否符合 Conventional Commits 标准`).alias("gv").option("-l --lang", "校验提交信息的语言", { default: cliOptions.lang }).action(async (args) => {
317
728
  await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
318
729
  });
319
- cli.command("release", "发布:更新版本、生成变更日志、提交代码").alias("r").option("--execute", "执行发布的命令").option("--push", "是否推送代码", { default: true }).action(async (args) => {
320
- await release(args?.execute, args?.push);
730
+ cli.command("release", `${bgGreen(white("便捷命令"))} ${lightBlue("qui r")} 版本管理:选择包并提升版本,创建自定义前缀标签并生成 changelog`).alias("r").option("--tag-prefix <prefix>", "标签前缀(可选,留空则交互输入)").action(async (args) => {
731
+ await release(args?.tagPrefix);
732
+ });
733
+ cli.command("changelog", `${bgGreen(white("便捷命令"))} ${lightBlue("qui cl")} 生成变更日志 CHANGELOG.md、CHANGELOG_TIMELINE`).alias("cl").option("-f, --format <format>", "输出样式:group|timeline|both", { default: cliOptions.changelog.formats }).option("--group-output <path>", "分组样式输出文件", { default: cliOptions.changelog.groupOutput }).option("--timeline-output <path>", "时间轴样式输出文件", { default: cliOptions.changelog.timelineOutput }).option("-l, --lang <lang>", "输出语言", { default: cliOptions.lang }).action(async (args) => {
734
+ const cfg = cliOptions.changelog;
735
+ await generateChangelogFiles({
736
+ lang: args.lang || cliOptions.lang,
737
+ format: args.format || cfg.formats,
738
+ groupOutput: args.groupOutput || cfg.groupOutput,
739
+ timelineOutput: args.timelineOutput || cfg.timelineOutput
740
+ });
321
741
  });
322
- cli.parse();
742
+ cli.version(lightGreen(version)).help().parse();
323
743
  }
324
744
  setupCli();
325
745
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quiteer/scripts",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "homepage": "https://github.com/TaiAiAc/web",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -49,6 +49,6 @@
49
49
  "scripts": {
50
50
  "dev": "tsdown -w",
51
51
  "build": "tsdown",
52
- "release": "pnpm publish"
52
+ "release": "qui r --tag-prefix scripts"
53
53
  }
54
54
  }