#!/usr/bin/env node /** * Gitea CLI — 项目管理工具 * * 用法: node gitea.js --role <角色> [subcommand] [options] * 所有配置(连接信息、Token、角色映射、标签定义)从 gitea-config.json 读取。 */ const fs = require('fs'); const path = require('path'); const { GiteaAPI } = require('./gitea-api'); // ═══════════════════════════════════════════════════ // 加载配置 // ═══════════════════════════════════════════════════ const CONFIG_PATH = path.join(__dirname, 'gitea-config.json'); let config; try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch (e) { console.error(`❌ 无法读取配置文件: ${CONFIG_PATH}`); console.error(` ${e.message}`); process.exit(1); } // ═══════════════════════════════════════════════════ // 命令行参数解析 // ═══════════════════════════════════════════════════ function parseArgs(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--') { positional.push(...argv.slice(i + 1)); break; } if (arg.startsWith('--')) { const key = arg.slice(2); const next = argv[i + 1]; if (next && !next.startsWith('--')) { options[key] = next; i++; } else { options[key] = true; } } else { positional.push(arg); } } return { command: positional[0] || '', sub: positional[1] || '', rest: positional.slice(2), options }; } // ═══════════════════════════════════════════════════ // 输出格式化 // ═══════════════════════════════════════════════════ function out(data) { if (globalArgs.options.json) { console.log(JSON.stringify(data, null, 2)); } } function outText(text) { if (!globalArgs.options.json) console.log(text); } function ok(msg) { outText(`✅ ${msg}`); } function fail(msg) { outText(`❌ ${msg}`); } function info(msg) { outText(`ℹ️ ${msg}`); } function truncate(s, max = 60) { if (!s) return ''; const oneLine = s.replace(/\n/g, ' ').trim(); return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine; } function labelNames(issue) { return (issue.labels || []).map(l => l.name); } /** 简易表格输出 */ function table(headers, rows) { if (!rows.length) { outText(' (空)'); return; } const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length))) ; const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' '); outText(' ' + fmt(headers)); outText(' ' + fmt(headers.map(() => '─'.repeat(2)))); for (const r of rows) outText(' ' + fmt(r)); } // ═══════════════════════════════════════════════════ // 角色解析 // ═══════════════════════════════════════════════════ /** * 获取 Token(优先级:环境变量 > config.roles[role].token) */ function resolveToken(options) { if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN; const role = options.role; if (role) { const validRoles = Object.keys(config.roles); if (!validRoles.includes(role)) { console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`); process.exit(1); } const token = config.roles[role]?.token; if (!token) { console.error(`❌ 角色 "${role}" 未配置 token。请在 gitea-config.json 中设置。`); process.exit(1); } return token; } console.error('❌ 缺少认证信息。请使用以下方式之一:'); console.error(' --role <角色名> 从 gitea-config.json 读取 Token'); console.error(' GITEA_TOKEN=xxx 通过环境变量传入'); process.exit(1); } /** * 解析 assignee:支持角色名(自动映射)或直接用户名 */ function resolveAssignee(value) { if (!value) return null; return config.roles[value]?.assignee || value; } /** * 自动从 --role 解析 assignee(用于 issue create) */ function autoAssignee(role) { if (!role) return []; const assignee = config.roles[role]?.assignee; return assignee ? [assignee] : []; } // ═══════════════════════════════════════════════════ // 命令实现 // ═══════════════════════════════════════════════════ const cmd = {}; // ──────── issue ──────── cmd.issue = { async list(api, opts) { const params = {}; if (opts.options.state) params.state = opts.options.state; if (opts.options.milestone) { const ms = isNaN(opts.options.milestone) ? await api.findMilestoneByTitle(opts.options.milestone) : { id: Number(opts.options.milestone) }; if (ms) params.milestones = ms.id; else { fail(`未找到里程碑: ${opts.options.milestone}`); return; } } if (opts.options.labels) params.labels = opts.options.labels; if (opts.options.assigned) { // 支持角色名或直接用户名 params.assigned_by = resolveAssignee(opts.options.assigned); } const issues = await api.listIssues(params); out(issues); if (!opts.options.json) { outText(`\n📋 Issues (${issues.length || 0})\n`); if (!issues || !issues.length) { outText(' (无)'); return; } table( ['#', '标题', '状态', '里程碑', '标签'], issues.map(i => [ i.number, truncate(i.title, 40), i.state, i.milestone?.title || '—', labelNames(i).join(', ') || '—', ]) ); } }, async get(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue get '); return; } const issue = await api.getIssue(num); out(issue); if (!opts.options.json) { outText(`\n📌 Issue #${issue.number}: ${issue.title}`); outText(` 状态: ${issue.state}`); outText(` 里程碑: ${issue.milestone?.title || '—'}`); outText(` 指派: ${(issue.assignees || []).map(a => a.login).join(', ') || '—'}`); outText(` 标签: ${labelNames(issue).join(', ') || '—'}`); outText(` 创建: ${issue.created_at?.slice(0, 10)}`); outText(` 更新: ${issue.updated_at?.slice(0, 10)}`); if (issue.body) { outText(`\n--- Body ---\n${issue.body.slice(0, 500)}${issue.body.length > 500 ? '…' : ''}`); } } }, async create(api, opts) { const title = opts.options.title; const body = opts.options.body || ''; if (!title) { fail('用法: issue create --title "标题" [--body "内容"] [--milestone ] [--labels ]'); return; } const data = { title, body }; if (opts.options.milestone) { const ms = isNaN(opts.options.milestone) ? await api.findMilestoneByTitle(opts.options.milestone) : { id: Number(opts.options.milestone) }; if (ms) data.milestone = ms.id; } if (opts.options.labels) { data.labels = opts.options.labels.split(',').map(Number); } // assignee: 显式指定 > 自动从 --role 映射 if (opts.options.assignees) { data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); } else { data.assignees = autoAssignee(opts.options.role); } const issue = await api.createIssue(data); out(issue); if (!opts.options.json) { ok(`Issue #${issue.number} 已创建: ${issue.title}`); outText(` ${issue.html_url}`); } }, async update(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue update [--title "…"] [--body "…"] [--state open|closed]'); return; } const data = {}; if (opts.options.title !== undefined) data.title = opts.options.title; if (opts.options.body !== undefined) data.body = opts.options.body; if (opts.options.state) data.state = opts.options.state; if (opts.options.milestone) { const ms = isNaN(opts.options.milestone) ? await api.findMilestoneByTitle(opts.options.milestone) : { id: Number(opts.options.milestone) }; if (ms) data.milestone = ms.id; } if (opts.options.assignees) { data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a)); } const issue = await api.updateIssue(num, data); out(issue); if (!opts.options.json) ok(`Issue #${num} 已更新`); }, async close(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue close '); return; } const comment = opts.options.body || opts.options.comment; await api.closeIssue(num, comment); out({ closed: true, number: Number(num) }); if (!opts.options.json) ok(`Issue #${num} 已关闭`); }, async reopen(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue reopen '); return; } await api.reopenIssue(num); out({ reopened: true, number: Number(num) }); if (!opts.options.json) ok(`Issue #${num} 已重新打开`); }, async comment(api, opts) { const num = opts.rest[0]; const body = opts.options.body; if (!num || !body) { fail('用法: issue comment --body "内容"'); return; } const c = await api.createComment(num, body); out(c); if (!opts.options.json) ok(`评论已添加到 Issue #${num}`); }, async comments(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue comments '); return; } const comments = await api.listComments(num); out(comments); if (!opts.options.json) { outText(`\n💬 Issue #${num} 的评论 (${comments.length})\n`); if (!comments.length) { outText(' (无评论)'); return; } for (const c of comments) { outText(` @${c.user?.login || '?'} (${c.created_at?.slice(0, 10)}):`); outText(` ${truncate(c.body, 80)}`); } } }, async labels(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue labels [--add ] [--set ]'); return; } if (opts.options.add) { const ids = opts.options.add.split(',').map(Number); await api.addIssueLabels(num, ids); if (!opts.options.json) ok(`标签已添加到 Issue #${num}: ${ids.join(', ')}`); } else if (opts.options.set) { const ids = opts.options.set.split(',').map(Number); await api.replaceIssueLabels(num, ids); if (!opts.options.json) ok(`Issue #${num} 的标签已替换为: ${ids.join(', ')}`); } else { const labels = await api.getIssueLabels(num); out(labels); if (!opts.options.json) { outText(`\n🏷️ Issue #${num} 的标签 (${labels.length})`); for (const l of labels) { outText(` ${l.name} (${l.id}) #${l.color}`); } } } }, async deps(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: issue deps '); return; } const [blockedBy, blocking] = await Promise.all([ api.listDependencies(num).catch(() => []), api.listBlocks(num).catch(() => []), ]); out({ issue: Number(num), blockedBy, blocking }); if (!opts.options.json) { outText(`\n🔗 Issue #${num} 的依赖关系`); outText(` 被阻塞于 (${blockedBy.length}): ${blockedBy.map(i => '#' + i.number).join(', ') || '无'}`); outText(` 阻塞了 (${blocking.length}): ${blocking.map(i => '#' + i.number).join(', ') || '无'}`); } }, async block(api, opts) { const [num, blocked] = opts.rest; if (!num || !blocked) { fail('用法: issue block '); return; } await api.addBlock(Number(num), Number(blocked)); out({ blocker: Number(num), blocked: Number(blocked) }); if (!opts.options.json) ok(`Issue #${blocked} 现在依赖于 Issue #${num}`); }, async unblock(api, opts) { const [num, blocked] = opts.rest; if (!num || !blocked) { fail('用法: issue unblock '); return; } await api.removeBlock(Number(num), Number(blocked)); out({ blocker: Number(num), unblocked: Number(blocked) }); if (!opts.options.json) ok(`Issue #${blocked} 不再依赖于 Issue #${num}`); }, }; // ──────── milestone ──────── cmd.milestone = { async list(api, opts) { const milestones = await api.listMilestones({ state: opts.options.state || 'all' }); out(milestones); if (!opts.options.json) { outText(`\n🏁 里程碑 (${milestones.length})\n`); if (!milestones.length) { outText(' (无)'); return; } table( ['ID', '标题', '状态', 'Open', 'Closed', '截止日期'], milestones.map(m => [ m.id, m.title, m.state, m.open_issues, m.closed_issues, m.due_on?.slice(0, 10) || '—', ]) ); } }, async create(api, opts) { const title = opts.options.title; if (!title) { fail('用法: milestone create --title "标题" [--description "描述"] [--due YYYY-MM-DD]'); return; } const data = { title, state: 'open' }; if (opts.options.description) data.description = opts.options.description; if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z'; const ms = await api.createMilestone(data); out(ms); if (!opts.options.json) ok(`里程碑已创建: ${ms.title} (ID: ${ms.id})`); }, async update(api, opts) { const id = opts.rest[0]; if (!id) { fail('用法: milestone update [--title "…"] [--description "…"] [--state open|closed]'); return; } const data = {}; if (opts.options.title) data.title = opts.options.title; if (opts.options.description !== undefined) data.description = opts.options.description; if (opts.options.state) data.state = opts.options.state; if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z'; const ms = await api.updateMilestone(id, data); out(ms); if (!opts.options.json) ok(`里程碑 ${id} 已更新`); }, }; // ──────── label ──────── cmd.label = { async list(api, opts) { const labels = await api.listLabels(); out(labels); if (!opts.options.json) { outText(`\n🏷️ 标签 (${labels.length})\n`); if (!labels.length) { outText(' (无)'); return; } table( ['ID', '名称', '颜色'], labels.map(l => [l.id, l.name, '#' + l.color]) ); } }, async create(api, opts) { const name = opts.options.name; const color = (opts.options.color || '#ededed').replace('#', ''); if (!name) { fail('用法: label create --name "名称" [--color "#RRGGBB"]'); return; } const label = await api.createLabel({ name, color }); out(label); if (!opts.options.json) ok(`标签已创建: ${name} (ID: ${label.id})`); }, async init(api, opts) { outText('\n🏷️ 初始化标准标签体系…\n'); // 从 config.labels 合并所有标签定义 const allLabels = Object.values(config.labels).flat(); const existing = await api.listLabels(); const existingNames = new Set(existing.map(l => l.name)); let created = 0, skipped = 0; for (const def of allLabels) { if (existingNames.has(def.name)) { info(`跳过 (已存在): ${def.name}`); skipped++; } else { await api.createLabel({ name: def.name, color: def.color.replace('#', ''), description: def.description }); ok(`已创建: ${def.name}`); created++; } } outText(`\n完成: 新建 ${created} 个, 跳过 ${skipped} 个`); }, }; // ──────── pipeline ──────── cmd.pipeline = { async init(api, opts) { const title = opts.options.title || 'Pipeline Tracker'; const milestone = opts.options.milestone; const body = `# 流水线进度追踪 ## 当前阶段 草图阶段 ## 当前迭代 (非迭代阶段,留空) ## 阶段历史 | 阶段 | 里程碑 | 进入日期 | 完成日期 | |------|--------|----------|----------| | 草图阶段 | 草图阶段 | ${new Date().toISOString().slice(0, 10)} | — |`; const data = { title, body }; if (milestone) { const ms = isNaN(milestone) ? await api.findMilestoneByTitle(milestone) : { id: Number(milestone) }; if (ms) data.milestone = ms.id; } const issue = await api.createIssue(data); out(issue); if (!opts.options.json) { ok(`Pipeline Tracker 已创建: Issue #${issue.number}`); outText(` ${issue.html_url}`); } }, async status(api, opts) { let tracker = await api.findIssueByTitle('Pipeline Tracker'); if (!tracker) { fail('未找到 Pipeline Tracker Issue。请先运行: pipeline init'); return; } tracker = await api.getIssue(tracker.number); out({ number: tracker.number, title: tracker.title, body: tracker.body }); if (!opts.options.json) { outText(`\n📊 Pipeline Tracker #${tracker.number}\n`); outText(tracker.body); } }, async update(api, opts) { const num = opts.rest[0]; const field = opts.options.field; const value = opts.options.value; if (!num || !field || !value) { fail('用法: pipeline update --field <阶段|当前迭代> --value <值>'); return; } const issue = await api.getIssue(num); let body = issue.body || ''; if (field === '阶段') { body = body.replace(/## 当前阶段\n.*/, `## 当前阶段\n${value}`); } else if (field === '当前迭代') { body = body.replace(/## 当前迭代\n.*/, `## 当前迭代\n${value}`); } else { fail(`无效字段: ${field},可选: 阶段, 当前迭代`); return; } await api.updateIssue(num, { body }); out({ number: Number(num), field, value, updated: true }); if (!opts.options.json) ok(`Pipeline Tracker 字段 "${field}" 已更新为: ${value}`); }, }; // ──────── pr ──────── cmd.pr = { async list(api, opts) { const params = {}; if (opts.options.state) params.state = opts.options.state; if (opts.options.base) params.base = opts.options.base; if (opts.options.head) params.head = opts.options.head; const prs = await api.listPullRequests(params); out(prs); if (!opts.options.json) { outText(`\n🔀 Pull Requests (${prs.length || 0})\n`); if (!prs || !prs.length) { outText(' (无)'); return; } table( ['#', '标题', '状态', 'Base', 'Head', '合并'], prs.map(p => [ p.number, truncate(p.title, 35), p.state, p.base?.ref || '—', p.head?.ref || '—', p.merged ? '✅' : '—', ]) ); } }, async get(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: pr get '); return; } const pr = await api.getPullRequest(num); out(pr); if (!opts.options.json) { outText(`\n🔀 PR #${pr.number}: ${pr.title}`); outText(` 状态: ${pr.state}`); outText(` ${pr.base?.ref} ← ${pr.head?.ref}`); outText(` 合并: ${pr.merged ? '✅ 已合并' : '❌ 未合并'}`); if (pr.body) { outText(`\n--- Body ---\n${pr.body.slice(0, 500)}${pr.body.length > 500 ? '…' : ''}`); } } }, async create(api, opts) { const base = opts.options.base || 'main'; const head = opts.options.head; const title = opts.options.title; const body = opts.options.body || ''; if (!head || !title) { fail('用法: pr create --head <分支> --title "标题" [--base main] [--body "描述"]'); return; } const pr = await api.createPullRequest({ base, head, title, body }); out(pr); if (!opts.options.json) { ok(`PR #${pr.number} 已创建: ${pr.title}`); outText(` ${pr.html_url}`); } }, async merge(api, opts) { const num = opts.rest[0]; if (!num) { fail('用法: pr merge [--style merge|squash|rebase]'); return; } const style = opts.options.style || 'merge'; const pr = await api.getPullRequest(num); if (pr.merged) { fail(`PR #${num} 已合并,无需重复操作`); return; } if (pr.mergeable === false) { fail(`PR #${num} 存在冲突,无法自动合并。请先解决冲突。`); return; } const result = await api.mergePullRequest(num, { style }); out(result); if (!opts.options.json) ok(`PR #${num} 已合并 (${style})`); }, }; // ──────── test ──────── cmd.test = { async run(api, opts) { outText('\n🧪 Gitea API 接口验证\n'); const results = []; let passed = 0, failed = 0; async function test(name, fn) { try { const result = await fn(); ok(name); results.push({ name, status: 'pass', detail: result }); passed++; } catch (e) { fail(`${name} — ${e.message}`); results.push({ name, status: 'fail', error: e.message }); failed++; } } // P0: 阻塞性验证 outText('── P0: 阻塞性验证 ──\n'); await test('GET /milestones', () => api.listMilestones()); await test('GET /issues', () => api.listIssues({ limit: 5 })); await test('GET /labels', () => api.listLabels()); // 查找第一个存在的 Issue 用于后续测试 const issues = await api.listIssues({ limit: 5, state: 'all' }); const testIssue = issues.find(i => i.state === 'open') || issues[0]; if (testIssue) { await test(`GET /issues/${testIssue.number}`, () => api.getIssue(testIssue.number)); await test(`GET /issues/${testIssue.number}/comments`, () => api.listComments(testIssue.number)); await test(`GET /issues/${testIssue.number}/dependencies`, () => api.listDependencies(testIssue.number)); await test(`GET /issues/${testIssue.number}/blocks`, () => api.listBlocks(testIssue.number)); } await test('POST 创建含中文 Issue', async () => { const i = await api.createIssue({ title: '测试 Issue — UTF-8 验证 🎮', body: '这是中文内容测试。\n\n- 列表项\n- 特殊字符:✅❌🔴⬜\n\n创建时间: ' + new Date().toISOString(), }); await api.closeIssue(i.number); return `#${i.number}: ${i.title}`; }); if (testIssue) { await test('POST 创建含中文评论', async () => { const c = await api.createComment(testIssue.number, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19)); return `comment_id: ${c.id}`; }); await test('PATCH 更新 Issue', async () => { const orig = await api.getIssue(testIssue.number); const newTitle = orig.title + ' [测试更新]'; await api.updateIssue(testIssue.number, { title: newTitle }); await api.updateIssue(testIssue.number, { title: orig.title }); return `Issue #${testIssue.number} 标题更新并还原`; }); } await test('PATCH 关闭/重开 Issue', async () => { const tmp = await api.createIssue({ title: '临时测试 — 开关', body: '将被关闭再打开' }); await api.closeIssue(tmp.number); await api.reopenIssue(tmp.number); await api.closeIssue(tmp.number); return `#${tmp.number} 关闭→重开→关闭`; }); await test('POST 创建 Label', async () => { const l = await api.createLabel({ name: '_test-label', color: 'ededed' }); try { await api.deleteLabel(l.id); } catch { /* ignore */ } return `label: ${l.name} (ID: ${l.id}) 已创建并清理`; }); if (testIssue) { await test('POST Issue 添加标签', async () => { const labels = await api.listLabels(); if (!labels.length) return '无可用标签,跳过'; await api.addIssueLabels(testIssue.number, [labels[0].id]); return `添加标签 ${labels[0].name} 到 Issue #${testIssue.number}`; }); } // P1: 核心功能 outText('\n── P1: 核心功能验证 ──\n'); if (testIssue && issues.length >= 2) { const other = issues.find(i => i.number !== testIssue.number); if (other) { await test('POST/DELETE Issue 依赖', async () => { await api.addBlock(testIssue.number, other.number); await api.removeBlock(testIssue.number, other.number); return `依赖关系 ${testIssue.number}→${other.number} 添加→移除`; }); } } await test('POST/PATCH Milestone', async () => { const ms = await api.createMilestone({ title: '_test-milestone', state: 'open' }); await api.updateMilestone(ms.id, { description: '测试用' }); await api.updateMilestone(ms.id, { state: 'closed' }); return `milestone ${ms.id} 创建→更新→关闭`; }); // 汇总 outText(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`); outText(`总计: ${passed + failed} 项, ✅ ${passed} 通过, ❌ ${failed} 失败`); out(results); if (failed > 0) process.exit(1); }, }; // ═══════════════════════════════════════════════════ // 帮助文本 // ═══════════════════════════════════════════════════ function showHelp() { const roles = Object.keys(config.roles).join('|'); console.log(` Gitea CLI — 项目管理工具 用法: node gitea.js --role <${roles}> [subcommand] [options] 全局选项: --help 显示帮助信息 --json 以 JSON 格式输出 --role <角色> 使用对应角色的身份(自动解析 Token 和 Assignee) 命令列表: issue list 列出 Issues [--state open|closed|all] [--milestone ] [--labels ] [--assigned <角色名|用户名>] issue get <#> 查看 Issue 详情 issue create 创建 Issue(assignee 自动从 --role 映射) --title "标题" [--body "内容"] [--milestone ] [--labels ] [--assignees <角色名,...>] issue update <#> 更新 Issue [--title "…"] [--body "…"] [--state open|closed] issue close <#> 关闭 Issue [--body "完成评论"] issue reopen <#> 重新打开 Issue issue comment <#> 添加评论 --body "内容" issue comments <#> 查看评论列表 issue labels <#> 查看/管理标签 [--add ] [--set ] issue deps <#> 查看依赖关系 issue block <#> <#> 设置依赖关系 issue unblock <#> <#> 移除依赖关系 milestone list 列出里程碑 [--state open|closed|all] milestone create 创建里程碑 --title "标题" [--description "…"] [--due YYYY-MM-DD] milestone update 更新里程碑 [--title "…"] [--state open|closed] label list 列出标签 label create 创建标签 --name "名称" [--color "#RRGGBB"] label init 初始化标准标签体系(从 gitea-config.json 读取定义) pipeline init 创建 Pipeline Tracker Meta Issue pipeline status 查看当前流水线进度 pipeline update <#> 更新 Pipeline 字段 --field <阶段|当前迭代> --value <值> pr list 列出 Pull Requests [--state open|closed|all] [--base <分支>] [--head <分支>] pr get <#> 查看 PR 详情 pr create 创建 PR --head <分支> --title "标题" [--base main] [--body "描述"] pr merge <#> 合并 PR [--style merge|squash|rebase] test 验证所有 API 接口 角色自动映射: --role <角色名> 自动解析为 Token + Assignee --assigned <角色名> 自动解析为 Gitea 用户名 --assignees <角色名,...> 同上 配置文件: gitea-config.json(与 gitea.js 同目录) `); } // ═══════════════════════════════════════════════════ // 路由与入口 // ═══════════════════════════════════════════════════ let globalArgs; async function main() { const args = parseArgs(process.argv.slice(2)); globalArgs = args; if (args.options.help || (!args.command && !args.sub)) { showHelp(); return; } // Token 解析 const token = resolveToken(args.options); // 构建 API 实例(从 config 读取连接信息) const api = new GiteaAPI(token, { host: config.host, port: config.port, repoOwner: config.repoOwner, repoName: config.repoName, }); // 路由 const commandGroup = cmd[args.command]; if (!commandGroup) { console.error(`❌ 未知命令: ${args.command}`); console.error(' 运行 --help 查看可用命令'); process.exit(1); } if (args.command === 'test') { await cmd.test.run(api, args); return; } const handler = commandGroup[args.sub]; if (!handler) { console.error(`❌ 未知子命令: ${args.command} ${args.sub}`); console.error(` 运行 --help 查看 ${args.command} 的可用子命令`); process.exit(1); } try { await handler(api, args); } catch (e) { if (args.options.json) { console.error(JSON.stringify({ error: e.message, statusCode: e.statusCode })); } else { console.error(`\n❌ 错误: ${e.message}`); } process.exit(1); } } main().catch(e => { console.error('致命错误:', e); process.exit(1); });