Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

141 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* help — 脚本目录的单一真相源呈现层。
*
* bun run help 按分组彩色打印脚本目录
* bun run help --check 校验 package.json 的 scripts 与 scripts.json 双向对齐
* (有脚本没说明 / 有说明没脚本 / group 未定义 → 退出 1)
* bun run help --readme 用 scripts.json 重新生成 README 的「## Scripts」表
* (在一对 HTML 注释锚点之间幂等替换)
*
* 描述来自 scripts/scripts.json(唯一真相源);实际 shell 命令只存在于
* package.json(不在此重复,避免双写漂移)。新增脚本的契约:package.json 加键
* + scripts.json 加一行说明,二者必须对齐(--check 守门),README 表 --readme 再生。
*
* 纯 node:fs,无第三方依赖;非 TTY 或设置 NO_COLOR 时自动降级为无色。
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const PKG = join(ROOT, 'package.json');
const MANIFEST = join(ROOT, 'scripts', 'scripts.json');
const README = join(ROOT, 'README.md');
const BEGIN = '<!-- BEGIN GENERATED SCRIPTS (bun run help --readme) -->';
const END = '<!-- END GENERATED SCRIPTS -->';
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
const paint = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
const bold = (s) => paint('1', s);
const cyan = (s) => paint('36', s);
const dim = (s) => paint('2', s);
const red = (s) => paint('31', s);
const green = (s) => paint('32', s);
const pkg = JSON.parse(readFileSync(PKG, 'utf8'));
const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'));
const scripts = pkg.scripts ?? {};
const groups = manifest.groups ?? [];
const entries = manifest.scripts ?? {};
/** 返回对齐问题列表(空 = 对齐)。 */
function alignmentProblems() {
const inPkg = Object.keys(scripts);
const inManifest = Object.keys(entries);
const manifestSet = new Set(inManifest);
const pkgSet = new Set(inPkg);
const groupIds = new Set(groups.map((g) => g.id));
const missingDesc = inPkg.filter((k) => !manifestSet.has(k));
const orphanDesc = inManifest.filter((k) => !pkgSet.has(k));
const badGroup = inManifest.filter((k) => !groupIds.has(entries[k].group));
const problems = [];
if (missingDesc.length)
problems.push(`package.json 脚本缺 scripts.json 说明: ${missingDesc.join(', ')}`);
if (orphanDesc.length)
problems.push(`scripts.json 有说明但 package.json 无脚本: ${orphanDesc.join(', ')}`);
if (badGroup.length)
problems.push(`scripts.json 脚本 group 未在 groups 定义: ${badGroup.join(', ')}`);
return problems;
}
/** 按 groups 顺序分组;组内按 scripts.json 键序。 */
function groupedRows() {
const rows = [];
for (const g of groups) {
const keys = Object.keys(entries).filter((k) => entries[k].group === g.id);
if (keys.length) rows.push({ group: g, keys });
}
return rows;
}
function printList() {
const width = Object.keys(entries).reduce((m, k) => Math.max(m, k.length), 0);
console.log('\n' + bold('NomiFun 脚本目录') + dim(' bun run <script>') + '\n');
for (const { group, keys } of groupedRows()) {
console.log(bold(group.title));
for (const k of keys) {
console.log(' ' + cyan(k.padEnd(width)) + ' ' + dim('— ' + entries[k].desc));
}
console.log('');
}
console.log(
dim(`${Object.keys(entries).length} 个脚本。bun run help --check 校验登记完整性。`) + '\n'
);
}
function readmeTable() {
const lines = ['| 脚本 | 说明 |', '| --- | --- |'];
for (const { group, keys } of groupedRows()) {
lines.push(`| **${group.title}** | |`);
for (const k of keys) lines.push(`| \`bun run ${k}\` | ${entries[k].desc} |`);
}
return lines.join('\n');
}
function writeReadme() {
const md = readFileSync(README, 'utf8');
const b = md.indexOf(BEGIN);
const e = md.indexOf(END);
if (b === -1 || e === -1) {
console.error(red(`README 缺少锚点。请在 README.md 中加入一对锚点:\n ${BEGIN}\n ${END}`));
process.exit(1);
}
if (e < b) {
console.error(red('README 锚点顺序颠倒(END 在 BEGIN 之前)。'));
process.exit(1);
}
const next = md.slice(0, b + BEGIN.length) + '\n\n' + readmeTable() + '\n\n' + md.slice(e);
if (next === md) {
console.log(green('README「## Scripts」表已是最新(无变化)。'));
return;
}
writeFileSync(README, next);
console.log(green('README「## Scripts」表已更新。'));
}
const arg = process.argv[2];
if (arg === '--check') {
const problems = alignmentProblems();
if (problems.length) {
console.error(red('✗ 脚本登记未对齐:'));
for (const p of problems) console.error(' - ' + p);
process.exit(1);
}
console.log(green('✓ package.json 与 scripts.json 对齐。'));
} else if (arg === '--readme') {
const problems = alignmentProblems();
if (problems.length) {
console.error(red('请先修复对齐再生成 README'));
for (const p of problems) console.error(' - ' + p);
process.exit(1);
}
writeReadme();
} else {
printList();
}