@quiteer/scripts 0.0.5 → 0.0.7

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.
Files changed (2) hide show
  1. package/dist/index.mjs +319 -140
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import cac from "cac";
3
3
  import { bgGreen, bgRed, blue, gray, green, lightBlue, lightCyan, lightGreen, red, white, yellow } from "kolorist";
4
- import { access, readFile, writeFile } from "node:fs/promises";
4
+ import { access, readFile, readdir, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { execa } from "execa";
@@ -12,7 +12,7 @@ import enquirer from "enquirer";
12
12
  import { versionBump } from "bumpp";
13
13
 
14
14
  //#region package.json
15
- var version = "0.0.4";
15
+ var version = "0.0.6";
16
16
 
17
17
  //#endregion
18
18
  //#region src/locales/changelog.ts
@@ -213,18 +213,40 @@ async function getCommitsInRange(range) {
213
213
  ])).split("\n").filter(Boolean).map(parseCommit).filter((x) => !!x);
214
214
  }
215
215
  /**
216
- * 读取 package homepage 以生成提交链接
217
- * @returns 仓库主页 URL
216
+ * 读取 package homepage(若为 GitHub Pages URL,转换为仓库地址)
217
+ * @returns 可能的仓库主页 URL
218
218
  */
219
219
  async function readHomepage() {
220
220
  try {
221
221
  const content = await readFile(path.join(process.cwd(), "scripts", "package.json"), "utf8");
222
222
  const json = JSON.parse(content);
223
- return typeof json.homepage === "string" ? json.homepage.replace(/\/$/, "") : void 0;
223
+ const home = typeof json.homepage === "string" ? json.homepage.replace(/\/$/, "") : void 0;
224
+ if (!home) return void 0;
225
+ const m = home.match(/^https:\/\/([\w-]+)\.github\.io\/([\w.-]+)\//);
226
+ if (m) return `https://github.com/${m[1]}/${m[2]}`;
227
+ return home;
224
228
  } catch {
225
229
  return;
226
230
  }
227
231
  }
232
+ /**
233
+ * 获取 Web 版仓库地址(优先使用 git remote,回退到 homepage 推断)
234
+ * @returns 仓库 Web URL(如 https://github.com/<owner>/<repo>)
235
+ */
236
+ async function getRepoWebUrl() {
237
+ try {
238
+ const remote = await execCommand("git", [
239
+ "remote",
240
+ "get-url",
241
+ "origin"
242
+ ]);
243
+ if (remote) {
244
+ if (remote.startsWith("git@github.com:")) return `https://github.com/${remote.replace("git@github.com:", "").replace(/\.git$/, "")}`;
245
+ if (remote.startsWith("https://github.com/")) return `https://github.com/${remote.replace("https://github.com/", "").replace(/\.git$/, "")}`;
246
+ }
247
+ } catch {}
248
+ return await readHomepage();
249
+ }
228
250
  async function prependFile(filePath, content) {
229
251
  try {
230
252
  await access(filePath);
@@ -313,9 +335,18 @@ function formatSection(title, date, items, repoUrl, lang) {
313
335
  lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
314
336
  lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
315
337
  lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
316
- for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
317
- for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
318
- for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
338
+ for (const f of it.filesAdded ?? []) {
339
+ const url = repoUrl ? `${repoUrl}/blob/${it.hash}/${f}` : void 0;
340
+ lines.push(url ? ` - [\`${f}\`](${url})` : ` - \`${f}\``);
341
+ }
342
+ for (const f of it.filesModified ?? []) {
343
+ const url = repoUrl ? `${repoUrl}/blob/${it.hash}/${f}` : void 0;
344
+ lines.push(url ? ` - ✏️ [\`${f}\`](${url})` : ` - ✏️ \`${f}\``);
345
+ }
346
+ for (const f of it.filesDeleted ?? []) {
347
+ const url = repoUrl ? `${repoUrl}/commit/${it.hash}` : void 0;
348
+ lines.push(url ? ` - 🗑️ [~~\`${f}\`~~](${url})` : ` - 🗑️ ~~\`${f}\`~~`);
349
+ }
319
350
  }
320
351
  lines.push("");
321
352
  }
@@ -342,9 +373,18 @@ function formatTimeline(items, repoUrl) {
342
373
  lines.push(`- ${typeIcon} ${typeLabel} ${scopeFmt ? `${scopeFmt}: ` : ""}${it.description}`);
343
374
  lines.push(` > **🕒 ${time}** · \`➕${it.added}\` / \`➖${it.deleted}\``);
344
375
  lines.push(` > \`👤 ${it.author}\` ${email}${link}`);
345
- for (const f of it.filesAdded ?? []) lines.push(` - ➕ \`${f}\``);
346
- for (const f of it.filesModified ?? []) lines.push(` - ✏️ \`${f}\``);
347
- for (const f of it.filesDeleted ?? []) lines.push(` - 🗑️ ~~~~\`${f}\`~~~~`);
376
+ for (const f of it.filesAdded ?? []) {
377
+ const url = repoUrl ? `${repoUrl}/blob/${it.hash}/${f}` : void 0;
378
+ lines.push(url ? ` - [\`${f}\`](${url})` : ` - \`${f}\``);
379
+ }
380
+ for (const f of it.filesModified ?? []) {
381
+ const url = repoUrl ? `${repoUrl}/blob/${it.hash}/${f}` : void 0;
382
+ lines.push(url ? ` - ✏️ [\`${f}\`](${url})` : ` - ✏️ \`${f}\``);
383
+ }
384
+ for (const f of it.filesDeleted ?? []) {
385
+ const url = repoUrl ? `${repoUrl}/commit/${it.hash}` : void 0;
386
+ lines.push(url ? ` - 🗑️ [~~\`${f}\`~~](${url})` : ` - 🗑️ ~~\`${f}\`~~`);
387
+ }
348
388
  }
349
389
  lines.push("");
350
390
  }
@@ -361,7 +401,7 @@ function formatTimeline(items, repoUrl) {
361
401
  */
362
402
  async function generateChangelogFiles(options) {
363
403
  const repoRoot = await execCommand("git", ["rev-parse", "--show-toplevel"]);
364
- const homepage = await readHomepage();
404
+ const homepage = await getRepoWebUrl();
365
405
  const root = await getRootCommit();
366
406
  const title = "全部历史";
367
407
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -435,7 +475,18 @@ const defaultOptions = {
435
475
  timelineOutput: "CHANGELOG_TIMELINE.md",
436
476
  formats: "both"
437
477
  },
438
- gitCommit: { add: true }
478
+ gitCommit: { add: true },
479
+ dirTree: {
480
+ md: false,
481
+ output: "DIRECTORY_TREE.md",
482
+ ignore: [
483
+ "node_modules",
484
+ ".git",
485
+ "dist",
486
+ "out",
487
+ "logs"
488
+ ]
489
+ }
439
490
  };
440
491
  async function loadCliOptions(overrides, cwd = process.cwd()) {
441
492
  const { config } = await loadConfig({
@@ -493,6 +544,137 @@ async function generateConfig() {
493
544
  ].join("\n"), "utf8");
494
545
  }
495
546
 
547
+ //#endregion
548
+ //#region src/commands/git-branches.ts
549
+ /**
550
+ * 列出远程仓库所有分支(含最新提交时间),并按时间倒序打印
551
+ * - 会先执行 `git fetch --prune --tags` 同步远程引用
552
+ * - 通过 `git for-each-ref` 获取 `refs/remotes/<remote>` 下的分支与提交时间
553
+ * @param remote 远程仓库名,默认 `origin`
554
+ * @returns Promise<void>
555
+ */
556
+ async function gitRemoteBranches(urlOrRemote) {
557
+ const temp = "gb-temp";
558
+ const target = (urlOrRemote || "").trim();
559
+ let refBase = "";
560
+ try {
561
+ if (target) {
562
+ await execCommand("git", [
563
+ "fetch",
564
+ target,
565
+ `+refs/heads/*:refs/remotes/${temp}/*`,
566
+ "--prune",
567
+ "--no-tags"
568
+ ]);
569
+ refBase = `refs/remotes/${temp}`;
570
+ } else {
571
+ const r = "origin";
572
+ await execCommand("git", [
573
+ "fetch",
574
+ r,
575
+ "--prune",
576
+ "--tags"
577
+ ]);
578
+ refBase = `refs/remotes/${r}`;
579
+ }
580
+ } catch {
581
+ console.log("拉取远程分支失败,请检查 URL 或网络连接");
582
+ return;
583
+ }
584
+ const lines = (await execCommand("git", [
585
+ "for-each-ref",
586
+ "--format=%(refname:short) %(committerdate:relative) %(objectname:short) %(contents:subject)",
587
+ "--sort=-committerdate",
588
+ refBase
589
+ ])).split("\n").filter(Boolean);
590
+ if (lines.length === 0) {
591
+ console.log("未找到远程分支,请确认已配置远程并有追踪分支");
592
+ return;
593
+ }
594
+ /**
595
+ * 英文相对时间转中文
596
+ * @param rel 英文相对时间字符串(如:2 hours ago)
597
+ * @returns 中文相对时间(如:2小时前)
598
+ */
599
+ function zhRelative(rel) {
600
+ const s = rel.toLowerCase().trim();
601
+ let m;
602
+ if (m = s.match(/^(\d+)\s+seconds?\s+ago$/)) return `${m[1]}秒前`;
603
+ if (m = s.match(/^(\d+)\s+minutes?\s+ago$/)) return `${m[1]}分钟前`;
604
+ if (m = s.match(/^(\d+)\s+hours?\s+ago$/)) return `${m[1]}小时前`;
605
+ if (m = s.match(/^(\d+)\s+days?\s+ago$/)) return `${m[1]}天前`;
606
+ if (m = s.match(/^(\d+)\s+weeks?\s+ago$/)) return `${m[1]}周前`;
607
+ if (m = s.match(/^(\d+)\s+months?\s+ago$/)) return `${m[1]}个月前`;
608
+ if (m = s.match(/^(\d+)\s+years?\s+ago$/)) return `${m[1]}年前`;
609
+ if (s === "yesterday") return "昨天";
610
+ if (s === "today") return "今天";
611
+ if (m = s.match(/^about\s+(\d+)\s+hours?\s+ago$/)) return `${m[1]}小时前`;
612
+ if (m = s.match(/^about\s+(\d+)\s+minutes?\s+ago$/)) return `${m[1]}分钟前`;
613
+ if (m = s.match(/^about\s+(\d+)\s+days?\s+ago$/)) return `${m[1]}天前`;
614
+ if (m = s.match(/^about\s+(\d+)\s+months?\s+ago$/)) return `${m[1]}个月前`;
615
+ if (m = s.match(/^about\s+(\d+)\s+years?\s+ago$/)) return `${m[1]}年前`;
616
+ return rel;
617
+ }
618
+ /**
619
+ * 计算字符串在等宽终端中的显示宽度(简单处理全角字符为宽度 2)
620
+ * @param s 输入字符串(不含 ANSI 颜色码)
621
+ * @returns 可视宽度
622
+ */
623
+ function displayWidth(s) {
624
+ let w = 0;
625
+ for (const ch of s) {
626
+ const code = ch.codePointAt(0) || 0;
627
+ w += code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 ? 2 : 1;
628
+ }
629
+ return w;
630
+ }
631
+ /**
632
+ * 基于显示宽度的左对齐填充
633
+ * @param s 源字符串
634
+ * @param width 目标显示宽度
635
+ * @returns 左对齐并填充空格的字符串
636
+ */
637
+ function padEndDisplay(s, width) {
638
+ const w = displayWidth(s);
639
+ if (w >= width) return s;
640
+ return s + " ".repeat(width - w);
641
+ }
642
+ const rows = lines.map((line) => {
643
+ const [nameRaw, relRaw, shaRaw, subjectRaw] = line.split(" ");
644
+ return {
645
+ name: nameRaw.replace(/^.+\//, ""),
646
+ relZh: zhRelative(relRaw || ""),
647
+ sha: (shaRaw || "").slice(0, 7),
648
+ subject: subjectRaw || ""
649
+ };
650
+ }).filter((item) => item.name && item.name !== "HEAD");
651
+ const nameWidth = Math.max(...rows.map((r) => displayWidth(r.name)));
652
+ const relValues = rows.map((r) => `(${r.relZh})`);
653
+ const relWidth = Math.max(...relValues.map((v) => displayWidth(v)), 0);
654
+ const label = " 最新 ";
655
+ const labelWidth = displayWidth(label);
656
+ const tag = bgGreen(white(label));
657
+ const pad = " ".repeat(labelWidth);
658
+ rows.forEach((row, i) => {
659
+ const relText = padEndDisplay(`(${row.relZh})`, relWidth);
660
+ const line = `${padEndDisplay(row.name, nameWidth)} ${relText} ${row.sha} ${row.subject}`;
661
+ if (i === 0) console.log(`${tag} ${lightGreen(line)}`);
662
+ else console.log(`${pad} ${white(line)}`);
663
+ });
664
+ if (target) {
665
+ const refnames = await execCommand("git", [
666
+ "for-each-ref",
667
+ "--format=%(refname)",
668
+ refBase
669
+ ]);
670
+ for (const ref of refnames.split("\n").filter(Boolean)) await execCommand("git", [
671
+ "update-ref",
672
+ "-d",
673
+ ref
674
+ ]);
675
+ }
676
+ }
677
+
496
678
  //#endregion
497
679
  //#region src/commands/git-commit.ts
498
680
  /**
@@ -655,148 +837,134 @@ async function release(tagPrefix) {
655
837
  `chore(projects): release ${tagName}`,
656
838
  tagName
657
839
  ]);
840
+ const cli = await loadCliOptions();
841
+ const lang = cli.lang;
658
842
  await generateChangelogFiles({
659
- lang: "zh-cn",
660
- format: "both",
661
- groupOutput: "CHANGELOG.md",
662
- timelineOutput: "CHANGELOG_TIMELINE.md"
843
+ lang,
844
+ format: cli.changelog.formats,
845
+ groupOutput: cli.changelog.groupOutput,
846
+ timelineOutput: cli.changelog.timelineOutput
663
847
  });
664
848
  }
665
849
 
666
850
  //#endregion
667
- //#region src/commands/update-pkg.ts
668
- async function updatePkg(args = ["--deep", "-u"]) {
669
- execCommand("npx", ["npm-check-updates", ...args], { stdio: "inherit" });
670
- }
671
-
672
- //#endregion
673
- //#region src/commands/git-branches.ts
851
+ //#region src/commands/self-update.ts
674
852
  /**
675
- * 列出远程仓库所有分支(含最新提交时间),并按时间倒序打印
676
- * - 会先执行 `git fetch --prune --tags` 同步远程引用
677
- * - 通过 `git for-each-ref` 获取 `refs/remotes/<remote>` 下的分支与提交时间
678
- * @param remote 远程仓库名,默认 `origin`
679
- * @returns Promise<void>
853
+ * 检查 @quiteer/scripts 是否有新版本并提示更新
854
+ * - 启动任意命令时调用,仅提示不执行安装
680
855
  */
681
- async function gitRemoteBranches(urlOrRemote) {
682
- const temp = "gb-temp";
683
- const target = (urlOrRemote || "").trim();
684
- let refBase = "";
856
+ async function checkUpdateAndNotify() {
685
857
  try {
686
- if (target) {
687
- await execCommand("git", [
688
- "fetch",
689
- target,
690
- `+refs/heads/*:refs/remotes/${temp}/*`,
691
- "--prune",
692
- "--no-tags"
693
- ]);
694
- refBase = `refs/remotes/${temp}`;
695
- } else {
696
- const r = "origin";
697
- await execCommand("git", [
698
- "fetch",
699
- r,
700
- "--prune",
701
- "--tags"
702
- ]);
703
- refBase = `refs/remotes/${r}`;
704
- }
705
- } catch {
706
- console.log("拉取远程分支失败,请检查 URL 或网络连接");
858
+ const latest = await execCommand("pnpm", [
859
+ "view",
860
+ "@quiteer/scripts",
861
+ "version"
862
+ ]);
863
+ if (latest && latest !== version) console.info("quiteer-script :>> ", lightCyan(`检测到新版本 ${lightGreen(latest)},当前版本 ${lightBlue(version)},建议执行 ${bgGreen(white("qui su"))} 进行更新`));
864
+ } catch {}
865
+ }
866
+ /**
867
+ * 自更新到最新版本
868
+ * - 对比当前版本与远端版本,不一致则使用 pnpm 全局更新
869
+ */
870
+ async function selfUpdate() {
871
+ const latest = await execCommand("pnpm", [
872
+ "view",
873
+ "@quiteer/scripts",
874
+ "version"
875
+ ]);
876
+ if (!latest) {
877
+ console.info("quiteer-script :>> ", lightBlue("无法获取远端版本,请检查网络后重试"));
707
878
  return;
708
879
  }
709
- const lines = (await execCommand("git", [
710
- "for-each-ref",
711
- "--format=%(refname:short) %(committerdate:relative) %(objectname:short) %(contents:subject)",
712
- "--sort=-committerdate",
713
- refBase
714
- ])).split("\n").filter(Boolean);
715
- if (lines.length === 0) {
716
- console.log("未找到远程分支,请确认已配置远程并有追踪分支");
880
+ if (latest === version) {
881
+ console.info("quiteer-script :>> ", lightGreen(`已是最新版本 ${latest}`));
717
882
  return;
718
883
  }
719
- /**
720
- * 英文相对时间转中文
721
- * @param rel 英文相对时间字符串(如:2 hours ago)
722
- * @returns 中文相对时间(如:2小时前)
723
- */
724
- function zhRelative(rel) {
725
- const s = rel.toLowerCase().trim();
726
- let m;
727
- if (m = s.match(/^(\d+)\s+seconds?\s+ago$/)) return `${m[1]}秒前`;
728
- if (m = s.match(/^(\d+)\s+minutes?\s+ago$/)) return `${m[1]}分钟前`;
729
- if (m = s.match(/^(\d+)\s+hours?\s+ago$/)) return `${m[1]}小时前`;
730
- if (m = s.match(/^(\d+)\s+days?\s+ago$/)) return `${m[1]}天前`;
731
- if (m = s.match(/^(\d+)\s+weeks?\s+ago$/)) return `${m[1]}周前`;
732
- if (m = s.match(/^(\d+)\s+months?\s+ago$/)) return `${m[1]}个月前`;
733
- if (m = s.match(/^(\d+)\s+years?\s+ago$/)) return `${m[1]}年前`;
734
- if (s === "yesterday") return "昨天";
735
- if (s === "today") return "今天";
736
- if (m = s.match(/^about\s+(\d+)\s+hours?\s+ago$/)) return `${m[1]}小时前`;
737
- if (m = s.match(/^about\s+(\d+)\s+minutes?\s+ago$/)) return `${m[1]}分钟前`;
738
- if (m = s.match(/^about\s+(\d+)\s+days?\s+ago$/)) return `${m[1]}天前`;
739
- if (m = s.match(/^about\s+(\d+)\s+months?\s+ago$/)) return `${m[1]}个月前`;
740
- if (m = s.match(/^about\s+(\d+)\s+years?\s+ago$/)) return `${m[1]}年前`;
741
- return rel;
884
+ console.info("quiteer-script :>> ", lightCyan(`开始更新到最新版本 ${latest}(当前 ${version})`));
885
+ try {
886
+ await execa("pnpm", [
887
+ "add",
888
+ "-g",
889
+ `@quiteer/scripts@${latest}`
890
+ ], { stdio: "inherit" });
891
+ console.info("quiteer-script :>> ", lightGreen("更新完成,请重新运行命令"));
892
+ } catch (e) {
893
+ console.info("quiteer-script :>> ", lightBlue(`更新失败:${e?.message || "未知错误"}`));
742
894
  }
743
- /**
744
- * 计算字符串在等宽终端中的显示宽度(简单处理全角字符为宽度 2)
745
- * @param s 输入字符串(不含 ANSI 颜色码)
746
- * @returns 可视宽度
747
- */
748
- function displayWidth(s) {
749
- let w = 0;
750
- for (const ch of s) {
751
- const code = ch.codePointAt(0) || 0;
752
- w += code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 ? 2 : 1;
895
+ }
896
+
897
+ //#endregion
898
+ //#region src/commands/update-pkg.ts
899
+ async function updatePkg(args = ["--deep", "-u"]) {
900
+ execCommand("npx", ["npm-check-updates", ...args], { stdio: "inherit" });
901
+ }
902
+
903
+ //#endregion
904
+ //#region src/commands/dir-tree.ts
905
+ /**
906
+ * 生成目录树结构字符串(ASCII Tree)
907
+ * - 默认忽略常见目录:node_modules、.git、dist、out、logs
908
+ * @param root 起始目录,默认为当前工作目录
909
+ * @returns 目录树字符串
910
+ */
911
+ async function buildDirTree(root = process.cwd(), ignoreList) {
912
+ const ignore = new Set(ignoreList ?? [
913
+ "node_modules",
914
+ ".git",
915
+ "dist",
916
+ "out",
917
+ "logs"
918
+ ]);
919
+ async function walk(dir, prefix = "") {
920
+ const items = (await readdir(dir, { withFileTypes: true })).filter((e) => !ignore.has(e.name)).map((e) => ({
921
+ name: e.name,
922
+ isDir: e.isDirectory()
923
+ })).sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1);
924
+ const lines = [];
925
+ const lastIdx = items.length - 1;
926
+ for (let i = 0; i < items.length; i++) {
927
+ const it = items[i];
928
+ const isLast = i === lastIdx;
929
+ const connector = isLast ? "└── " : "├── ";
930
+ const nextPrefix = prefix + (isLast ? " " : "│ ");
931
+ lines.push(`${prefix}${connector}${it.name}`);
932
+ if (it.isDir) {
933
+ const sub = await walk(path.join(dir, it.name), nextPrefix);
934
+ lines.push(...sub);
935
+ }
753
936
  }
754
- return w;
755
- }
756
- /**
757
- * 基于显示宽度的左对齐填充
758
- * @param s 源字符串
759
- * @param width 目标显示宽度
760
- * @returns 左对齐并填充空格的字符串
761
- */
762
- function padEndDisplay(s, width) {
763
- const w = displayWidth(s);
764
- if (w >= width) return s;
765
- return s + " ".repeat(width - w);
937
+ return lines;
766
938
  }
767
- const rows = lines.map((line) => {
768
- const [nameRaw, relRaw, shaRaw, subjectRaw] = line.split(" ");
769
- return {
770
- name: nameRaw.replace(/^.+\//, ""),
771
- relZh: zhRelative(relRaw || ""),
772
- sha: (shaRaw || "").slice(0, 7),
773
- subject: subjectRaw || ""
774
- };
775
- }).filter((item) => item.name && item.name !== "HEAD");
776
- const nameWidth = Math.max(...rows.map((r) => displayWidth(r.name)));
777
- const relValues = rows.map((r) => `(${r.relZh})`);
778
- const relWidth = Math.max(...relValues.map((v) => displayWidth(v)), 0);
779
- const label = " 最新 ";
780
- const labelWidth = displayWidth(label);
781
- const tag = bgGreen(white(label));
782
- const pad = " ".repeat(labelWidth);
783
- rows.forEach((row, i) => {
784
- const relText = padEndDisplay(`(${row.relZh})`, relWidth);
785
- const line = `${padEndDisplay(row.name, nameWidth)} ${relText} ${row.sha} ${row.subject}`;
786
- if (i === 0) console.log(`${tag} ${lightGreen(line)}`);
787
- else console.log(`${pad} ${white(line)}`);
788
- });
789
- if (target) {
790
- const refnames = await execCommand("git", [
791
- "for-each-ref",
792
- "--format=%(refname)",
793
- refBase
794
- ]);
795
- for (const ref of refnames.split("\n").filter(Boolean)) await execCommand("git", [
796
- "update-ref",
797
- "-d",
798
- ref
799
- ]);
939
+ const title = path.basename(root);
940
+ const content = await walk(root);
941
+ return [`|-- ${title}`, ...content].join("\n");
942
+ }
943
+ /**
944
+ * 根据目录树内容生成 Markdown 并写入文件
945
+ * - 使用代码块包裹,便于阅读
946
+ * @param tree 目录树字符串
947
+ * @param outfile 输出文件路径,默认 `DIRECTORY_TREE.md`
948
+ */
949
+ async function writeDirTreeMd(tree, outfile = "DIRECTORY_TREE.md") {
950
+ const md = `## 目录结构
951
+
952
+ \`\`\`text\n${tree}\n\`\`\`\n`;
953
+ await writeFile(path.join(process.cwd(), outfile), md, "utf8");
954
+ }
955
+ /**
956
+ * 生成并输出目录结构,支持可选生成 Markdown 文件
957
+ * - 默认只在控制台输出
958
+ * @param dir 目标目录,默认当前工作目录
959
+ * @param md 是否生成 Markdown 文件,默认 false
960
+ */
961
+ async function generateDirTree(dir, opts) {
962
+ const tree = await buildDirTree(dir ? path.resolve(process.cwd(), dir) : process.cwd(), opts?.ignore);
963
+ console.info("quiteer-script :>>", gray(`\n${tree}\n`));
964
+ if (opts?.md) {
965
+ const out = opts?.output || "DIRECTORY_TREE.md";
966
+ await writeDirTreeMd(tree, out);
967
+ console.info(lightCyan("quiteer-script :>>"), lightGreen(`已生成 Markdown: ${lightBlue(out)}`));
800
968
  }
801
969
  }
802
970
 
@@ -811,6 +979,7 @@ async function setupCli() {
811
979
  */
812
980
  const cliOptions = await loadCliOptions();
813
981
  const cli = cac(blue("quiteer"));
982
+ await checkUpdateAndNotify();
814
983
  cli.command("generate-config", `${bgGreen(white("便捷命令"))} ${lightCyan("qui g")} 在项目根目录下生成配置文件`).alias("g").action(async () => {
815
984
  await generateConfig();
816
985
  });
@@ -827,6 +996,16 @@ async function setupCli() {
827
996
  cli.command("update-pkg", `${bgGreen(white("便捷命令"))} ${lightBlue("qui u")} 更新 package.json 依赖版本`).alias("u").action(async () => {
828
997
  await updatePkg(cliOptions.ncuCommandArgs);
829
998
  });
999
+ cli.command("self-update", `${bgGreen(white("便捷命令"))} ${lightCyan("qui su")} 自更新:检查并更新 @quiteer/scripts 到最新版本`).alias("su").action(async () => {
1000
+ await selfUpdate();
1001
+ });
1002
+ cli.command("tree [path]", `${bgGreen(white("便捷命令"))} ${lightBlue("qui t")} 生成当前目录结构(默认控制台输出),可选生成 Markdown`).alias("t").option("--md", "是否生成 Markdown 文件", { default: cliOptions.dirTree.md }).action(async (path$1, args) => {
1003
+ await generateDirTree(path$1, {
1004
+ md: !!args?.md,
1005
+ output: cliOptions.dirTree.output,
1006
+ ignore: cliOptions.dirTree.ignore
1007
+ });
1008
+ });
830
1009
  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) => {
831
1010
  if (args?.add) await gitCommitAdd();
832
1011
  await gitCommit(args?.lang);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quiteer/scripts",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.7",
5
5
  "homepage": "https://taiaiac.github.io/web/ci/scripts.html",
6
6
  "publishConfig": {
7
7
  "access": "public",