diff --git a/CLAUDE.md b/CLAUDE.md index 5440eda0b..33c175661 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,13 @@ ```bash # 1. 获取索引(最佳实践 + 知识地图) -bun scripts/agent-readme.ts +pnpm agent # 2. 查看当前任务 -bun scripts/agent-readme.ts --roadmap current +pnpm agent --roadmap current # 3. 查阅白皮书必读章节 -bun scripts/agent-readme.ts --chapter 00-必读 +pnpm agent --chapter 00-必读 ``` 完整工作流和命令速查见 [AGENTS.md](./AGENTS.md) diff --git a/package.json b/package.json index 31c681734..52b1d6a61 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "i18n:check": "turbo run i18n:run --", "i18n:run": "bun scripts/i18n-check.ts", "i18n:validate": "bun scripts/i18n-validate.ts", - "agent": "bun scripts/agent-readme.ts", + "agent": "bun scripts/agent/cli.ts", "check": "turbo run typecheck:run test:run i18n:run" }, "dependencies": { diff --git a/scripts/agent-readme.ts b/scripts/agent-readme.ts deleted file mode 100644 index 582b166ea..000000000 --- a/scripts/agent-readme.ts +++ /dev/null @@ -1,570 +0,0 @@ -#!/usr/bin/env bun -/** - * AI Agent 索引脚本 - * - * 为 AI 提供项目索引:最佳实践(纠正直觉)+ 知识地图(按需查阅)+ Roadmap(任务追踪) - * - * Usage: - * bun scripts/agent-readme.ts # 输出索引(最佳实践 + 知识地图) - * bun scripts/agent-readme.ts --roadmap # 查看 GitHub Project 任务列表 - * bun scripts/agent-readme.ts --chapter # 输出指定章节内容 - * - * Examples: - * bun scripts/agent-readme.ts --chapter 03-架构篇/03-导航系统 - * bun scripts/agent-readme.ts --roadmap - */ - -import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' -import { join, resolve, relative } from 'node:path' -import { execSync } from 'node:child_process' - -const ROOT = resolve(import.meta.dirname, '..') -const WHITE_BOOK_DIR = join(ROOT, 'docs/white-book') - -const PROJECT_NUMBER = 5 -const PROJECT_OWNER = 'BioforestChain' - -// 版本别名,CURRENT 指向当前活跃版本 -const RELEASE_ALIASES: Record = { - CURRENT: 'V1', - NEXT: 'V2', -} - -const colors = { - reset: '\x1b[0m', - bold: '\x1b[1m', - dim: '\x1b[2m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -} - -const log = { - title: (msg: string) => console.log(`\n${colors.bold}${colors.cyan}${'='.repeat(60)}${colors.reset}`), - section: (msg: string) => console.log(`\n${colors.bold}${colors.green}## ${msg}${colors.reset}\n`), - subsection: (msg: string) => console.log(`\n${colors.yellow}### ${msg}${colors.reset}\n`), - info: (msg: string) => console.log(`${colors.dim}${msg}${colors.reset}`), -} - -interface ChapterInfo { - name: string - path: string - indexPath?: string - subChapters: ChapterInfo[] -} - -function getChapters(dir: string): ChapterInfo[] { - if (!existsSync(dir)) return [] - - const items = readdirSync(dir) - .filter(item => !item.startsWith('.')) - .sort() - - const chapters: ChapterInfo[] = [] - - for (const item of items) { - const fullPath = join(dir, item) - const stat = statSync(fullPath) - - if (stat.isDirectory()) { - const indexPath = join(fullPath, 'index.md') - const subChapters = getChapters(fullPath) - - chapters.push({ - name: item, - path: fullPath, - indexPath: existsSync(indexPath) ? indexPath : undefined, - subChapters, - }) - } else if (item.endsWith('.md') && item !== 'index.md') { - chapters.push({ - name: item.replace('.md', ''), - path: fullPath, - subChapters: [], - }) - } - } - - return chapters -} - -function printToc(chapters: ChapterInfo[], indent = 0): void { - for (const chapter of chapters) { - const prefix = ' '.repeat(indent) - const relPath = relative(WHITE_BOOK_DIR, chapter.path) - console.log(`${prefix}- ${chapter.name} (${relPath})`) - if (chapter.subChapters.length > 0) { - printToc(chapter.subChapters, indent + 1) - } - } -} - -function readMarkdownFile(filePath: string): string { - if (!existsSync(filePath)) return '' - return readFileSync(filePath, 'utf-8') -} - -function printChapterContent(chapterPath: string): void { - const fullPath = join(WHITE_BOOK_DIR, chapterPath) - - if (!existsSync(fullPath)) { - console.error(`${colors.red}错误: 章节不存在: ${chapterPath}${colors.reset}`) - process.exit(1) - } - - const stat = statSync(fullPath) - - if (stat.isDirectory()) { - const indexPath = join(fullPath, 'index.md') - if (existsSync(indexPath)) { - console.log(readMarkdownFile(indexPath)) - } - - // 输出子章节 - const subFiles = readdirSync(fullPath) - .filter(f => f.endsWith('.md') && f !== 'index.md') - .sort() - - for (const subFile of subFiles) { - log.subsection(subFile.replace('.md', '')) - console.log(readMarkdownFile(join(fullPath, subFile))) - } - } else if (fullPath.endsWith('.md')) { - console.log(readMarkdownFile(fullPath)) - } -} - -interface RoadmapItem { - title: string - status: string - release: string | null - issueNumber: number | null - assignees: string[] -} - -function fetchRoadmap(): RoadmapItem[] { - try { - const output = execSync( - `gh project item-list ${PROJECT_NUMBER} --owner ${PROJECT_OWNER} --format json`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } - ) - const data = JSON.parse(output) - return data.items.map((item: { - title: string - status: string - release?: string - content?: { number?: number } - assignees?: string[] - }) => ({ - title: item.title, - status: item.status, - release: item.release || null, - issueNumber: item.content?.number || null, - assignees: item.assignees || [], - })) - } catch { - return [] - } -} - -function resolveRelease(input: string): string { - const upper = input.toUpperCase() - return RELEASE_ALIASES[upper] || upper -} - -function printRoadmap(releaseFilter?: string): void { - const allItems = fetchRoadmap() - if (allItems.length === 0) { - console.log('无法获取 Roadmap,请确保已配置 gh auth (需要 project scope)') - return - } - - // 解析别名 - const resolvedFilter = releaseFilter ? resolveRelease(releaseFilter) : undefined - const displayName = releaseFilter?.toUpperCase() - const isAlias = releaseFilter && resolvedFilter !== releaseFilter.toUpperCase() - - const items = resolvedFilter - ? allItems.filter(i => i.release?.toUpperCase() === resolvedFilter) - : allItems - - const title = resolvedFilter - ? `KeyApp Roadmap - ${displayName}${isAlias ? ` (${resolvedFilter})` : ''}` - : 'KeyApp Roadmap' - console.log(`# ${title}\n`) - console.log(`> GitHub Project: https://github.com/orgs/${PROJECT_OWNER}/projects/${PROJECT_NUMBER}\n`) - - if (resolvedFilter && items.length === 0) { - console.log(`没有找到 ${displayName} 版本的任务。\n`) - console.log('可用版本: V1, V2, V3, DRAFT, CURRENT, NEXT') - return - } - - // 按版本分组(如果没有过滤) - if (!releaseFilter) { - const releases = ['V1', 'V2', 'V3', 'DRAFT', null] - for (const release of releases) { - const releaseItems = allItems.filter(i => - release === null ? !i.release : i.release === release - ) - if (releaseItems.length === 0) continue - - console.log(`## ${release || '未分配'}\n`) - printStatusGroups(releaseItems) - } - } else { - printStatusGroups(items) - } -} - -function printStatusGroups(items: RoadmapItem[], showDetails = false): void { - const grouped = { - 'In Progress': items.filter(i => i.status === 'In Progress'), - Todo: items.filter(i => i.status === 'Todo'), - Done: items.filter(i => i.status === 'Done'), - } - - for (const [status, tasks] of Object.entries(grouped)) { - if (tasks.length === 0) continue - for (const task of tasks) { - const icon = status === 'Done' ? '✅' : status === 'In Progress' ? '🔄' : '⬚' - const issueRef = task.issueNumber ? `#${task.issueNumber}` : '' - const assignee = task.assignees.length > 0 ? ` @${task.assignees.join(', @')}` : '' - - if (showDetails || task.issueNumber) { - console.log(`- ${icon} ${issueRef} ${task.title}${assignee}`) - } else { - console.log(`- ${icon} ${task.title}`) - } - } - } - console.log() -} - -function printBestPractices(): void { - console.log(` -# 最佳实践 - -❌ Radix Dialog / position:fixed → ✅ Stackflow BottomSheet/Modal -❌ React Router → ✅ Stackflow push/pop/replace -❌ 复制 mpay 代码 → ✅ 理解后用 React/TS 重写 -❌ 随意创建 store → ✅ 遵循 stores/ 现有模式 -❌ 明文选择器 → ✅ data-testid -❌ 安装新 UI 库 → ✅ shadcn/ui(已集成) -❌ 新建 CSS → ✅ Tailwind CSS - -详见: pnpm agent --chapter 00-必读 -`) -} - -function printKnowledgeMap(): void { - console.log(` -# 知识地图 - -代码: - src/stackflow/ → 导航配置、Activity (添加页面/Sheet/Modal) - src/services/ → 服务层、Adapter 模式 - src/stores/ → TanStack Store 状态管理 - src/components/ → UI 组件 - src/i18n/ → 国际化 - -白皮书 (pnpm agent --chapter <路径>): - 03-架构篇/03-导航系统/ → 页面跳转、Tab - 04-服务篇/01-服务架构/ → 服务分层、Adapter - 05-组件篇/01-基础组件/ → Sheet、Dialog - 04-服务篇/08-钱包数据存储/ → 钱包数据 - 07-国际化篇/ → 多语言 - 08-测试篇/03-Playwright配置/e2e-best-practices.md → E2E 测试 - 附录/C-mpay迁移指南/ → mpay 参考 - -外部文档: - Stackflow: https://stackflow.so/llms-full.txt - shadcn/ui: https://ui.shadcn.com/docs - TanStack: https://tanstack.com/query/latest - -mpay 原始代码: /Users/kzf/Dev/bioforestChain/legacy-apps/apps/mpay/ - pages/home/home.component.ts → 首页 - pages/mnemonic/pages/home-transfer/ → 转账 - pages/authorize/pages/signature/ → DWEB 授权 - services/expansion-tools/wallet-data-stroage.ts → 钱包存储 -`) -} - -function claimIssue(issueNumber: string): void { - try { - // 设置 assignee 为当前用户 - execSync( - `gh issue edit ${issueNumber} --add-assignee @me`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } - ) - console.log(`✅ 已领取 Issue #${issueNumber}`) - console.log(`\n下一步:`) - console.log(` 1. git worktree add .git-worktree/issue-${issueNumber} -b feat/issue-${issueNumber}`) - console.log(` 2. cd .git-worktree/issue-${issueNumber} && pnpm install`) - console.log(` 3. 开始开发...`) - console.log(` 4. PR 描述中使用 "Closes #${issueNumber}" 自动关联`) - } catch (e) { - console.error(`❌ 领取失败: ${e}`) - } -} - -function completeIssue(issueNumber: string): void { - try { - execSync( - `gh issue close ${issueNumber} --reason completed`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } - ) - console.log(`✅ 已完成 Issue #${issueNumber}`) - } catch (e) { - console.error(`❌ 完成失败: ${e}`) - } -} - -interface CreateIssueOptions { - title: string - body?: string - roadmap?: string // V1, V2, DRAFT - category?: string // feature, bug, refactor, docs -} - -function createIssue(options: CreateIssueOptions): void { - const { title, body = '', roadmap = 'DRAFT', category = 'feature' } = options - - // 确定 label - const labelMap: Record = { - feature: 'enhancement', - bug: 'bug', - refactor: 'refactor', - docs: 'documentation', - test: 'test', - } - const label = labelMap[category.toLowerCase()] || 'enhancement' - - // 构建 body - const fullBody = `${body} - -**Roadmap**: ${roadmap.toUpperCase()} -**Category**: ${category.charAt(0).toUpperCase() + category.slice(1)}` - - try { - const output = execSync( - `gh issue create --title "${title.replace(/"/g, '\\"')}" --body "${fullBody.replace(/"/g, '\\"')}" --label "${label}" --project "KeyApp Roadmap"`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } - ) - const issueUrl = output.trim() - const issueNumber = issueUrl.split('/').pop() - - console.log(`✅ 已创建 Issue ${issueUrl}`) - - // 自动设置 Release 字段 - try { - const releaseOptionMap: Record = { - V1: '9359aa58', - V2: '6a3e495d', - V3: '7e413227', - DRAFT: 'd75e4b65', - } - const optionId = releaseOptionMap[roadmap.toUpperCase()] - if (optionId) { - // 获取 project item ID - const itemsJson = execSync( - `gh project item-list ${PROJECT_NUMBER} --owner ${PROJECT_OWNER} --format json`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } - ) - const items = JSON.parse(itemsJson) - const item = items.items.find((i: { content?: { number?: number } }) => - i.content?.number === parseInt(issueNumber || '0') - ) - if (item) { - execSync( - `gh project item-edit --project-id PVT_kwDOBJVmF84BLGhY --id "${item.id}" --field-id PVTSSF_lADOBJVmF84BLGhYzg6xpp8 --single-select-option-id ${optionId}`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } - ) - console.log(`✅ 已设置 Release 为 ${roadmap.toUpperCase()}`) - } - } - } catch { - console.log(`⚠️ Release 字段设置失败,请手动设置`) - } - - console.log(`\n下一步:`) - console.log(` pnpm agent --claim ${issueNumber} # 领取任务`) - } catch (e) { - console.error(`❌ 创建失败: ${e}`) - } -} - -function printStats(): void { - const items = fetchRoadmap() - if (items.length === 0) { - console.log('无法获取统计数据') - return - } - - const releases = ['V1', 'V2', 'V3', 'DRAFT'] - - console.log('# 进度统计\n') - - for (const release of releases) { - const releaseItems = items.filter(i => i.release === release) - if (releaseItems.length === 0) continue - - const todo = releaseItems.filter(i => i.status === 'Todo').length - const inProgress = releaseItems.filter(i => i.status === 'In Progress').length - const done = releaseItems.filter(i => i.status === 'Done').length - const total = releaseItems.length - const progress = Math.round((done / total) * 100) - const bar = '█'.repeat(Math.floor(progress / 10)) + '░'.repeat(10 - Math.floor(progress / 10)) - - console.log(`${release}: ${bar} ${progress}% (${done}/${total} done, ${inProgress} wip, ${todo} todo)`) - } - console.log() -} - -function printWorkflow(): void { - console.log(` -# 工作流 - -pnpm agent --claim <#> 领取任务 (自动分配+worktree指引) -pnpm agent --create "x" 创建任务 (--category bug --roadmap v1) -pnpm agent --chapter 查阅白皮书 -pnpm agent --stats 进度统计 - -PR 规则: 描述中使用 Closes #issue编号 自动关联 -`) -} - -function printCurrentTasks(): void { - const items = fetchRoadmap() - const currentItems = items.filter(i => { - const release = i.release?.toUpperCase() - return release === resolveRelease('CURRENT') - }) - - if (currentItems.length === 0) return - - const inProgress = currentItems.filter(i => i.status === 'In Progress') - const todo = currentItems.filter(i => i.status === 'Todo') - const done = currentItems.filter(i => i.status === 'Done') - - console.log(` -# 当前任务 (${resolveRelease('CURRENT')}) ${done.length}/${currentItems.length} done -`) - - if (inProgress.length > 0) { - console.log('进行中:') - for (const t of inProgress) { - console.log(` #${t.issueNumber} ${t.title}${t.assignees.length ? ` @${t.assignees.join(',')}` : ''}`) - } - } - - if (todo.length > 0) { - console.log('待领取:') - for (const t of todo) { - console.log(` #${t.issueNumber} ${t.title}`) - } - } - console.log() -} - -function printIndex(): void { - console.log(`# KeyApp AI 开发索引 -`) - - printBestPractices() - printKnowledgeMap() - printCurrentTasks() - printWorkflow() -} - -function main(): void { - const args = process.argv.slice(2) - - if (args.includes('--help') || args.includes('-h')) { - console.log(` -AI Agent 索引脚本 - -Usage: - bun scripts/agent-readme.ts # 输出索引(最佳实践 + 知识地图) - bun scripts/agent-readme.ts --roadmap # 查看所有版本任务列表 - bun scripts/agent-readme.ts --roadmap current # 查看当前版本任务 - bun scripts/agent-readme.ts --claim # 领取任务(分配给自己) - bun scripts/agent-readme.ts --done # 完成任务(关闭 Issue) - bun scripts/agent-readme.ts --create # 创建新任务(自动设置版本) - [--body <描述>] [--roadmap v1|v2|draft] [--category feature|bug|refactor] - bun scripts/agent-readme.ts --stats # 查看进度统计 - bun scripts/agent-readme.ts --toc # 白皮书目录结构 - bun scripts/agent-readme.ts --chapter <name> # 查阅白皮书章节 - -Aliases: CURRENT -> V1, NEXT -> V2 - -Examples: - bun scripts/agent-readme.ts --roadmap current - bun scripts/agent-readme.ts --claim 28 - bun scripts/agent-readme.ts --create "修复某个问题" --category bug --roadmap v1 - bun scripts/agent-readme.ts --create "探索:新功能想法" --roadmap draft -`) - process.exit(0) - } - - const roadmapIndex = args.indexOf('--roadmap') - if (roadmapIndex !== -1) { - const releaseFilter = args[roadmapIndex + 1] - printRoadmap(releaseFilter) - process.exit(0) - } - - const claimIndex = args.indexOf('--claim') - if (claimIndex !== -1 && args[claimIndex + 1]) { - claimIssue(args[claimIndex + 1]) - process.exit(0) - } - - const doneIndex = args.indexOf('--done') - if (doneIndex !== -1 && args[doneIndex + 1]) { - completeIssue(args[doneIndex + 1]) - process.exit(0) - } - - if (args.includes('--stats')) { - printStats() - process.exit(0) - } - - const createIndex = args.indexOf('--create') - if (createIndex !== -1 && args[createIndex + 1]) { - const title = args[createIndex + 1] - const bodyIndex = args.indexOf('--body') - const roadmapIndex2 = args.indexOf('--roadmap', createIndex) - const categoryIndex = args.indexOf('--category') - - createIssue({ - title, - body: bodyIndex !== -1 ? args[bodyIndex + 1] : undefined, - roadmap: roadmapIndex2 !== -1 ? args[roadmapIndex2 + 1] : 'DRAFT', - category: categoryIndex !== -1 ? args[categoryIndex + 1] : 'feature', - }) - process.exit(0) - } - - if (args.includes('--toc')) { - console.log('# 白皮书目录结构\n') - const chapters = getChapters(WHITE_BOOK_DIR) - printToc(chapters) - process.exit(0) - } - - const chapterIndex = args.indexOf('--chapter') - if (chapterIndex !== -1 && args[chapterIndex + 1]) { - const chapterName = args[chapterIndex + 1] - log.section(`章节: ${chapterName}`) - printChapterContent(chapterName) - process.exit(0) - } - - printIndex() -} - -main() diff --git a/scripts/agent/cli.ts b/scripts/agent/cli.ts new file mode 100644 index 000000000..1622ed657 --- /dev/null +++ b/scripts/agent/cli.ts @@ -0,0 +1,217 @@ +#!/usr/bin/env bun +/** + * AI Agent CLI - 主入口 + * + * Usage: + * pnpm agent # 输出索引(最佳实践 + 知识地图) + * pnpm agent --roadmap [version] # 查看任务列表 + * pnpm agent --claim <issue#> # 领取任务 + * pnpm agent --done <issue#> # 完成任务 + * pnpm agent --create <title> # 创建任务 + * pnpm agent --epic create <title> # 创建 Epic + * pnpm agent --epic list # 查看 Epic 列表 + * pnpm agent --epic view <#> # 查看 Epic 详情 + * pnpm agent --epic sync <#> # 同步 Epic 子任务状态 + * pnpm agent --epic add <epic#> <issue#> # 添加子任务到 Epic + * pnpm agent --stats # 进度统计 + * pnpm agent --toc # 白皮书目录 + * pnpm agent --chapter <name> # 查阅白皮书章节 + */ + +import { log } from './utils' +import { printIndex } from './readme' +import { + printRoadmap, + claimIssue, + completeIssue, + createIssue, + printStats, +} from './roadmap' +import { + createEpic, + listEpics, + viewEpic, + syncEpicStatus, + addSubIssueToEpic, +} from './epic' +import { printWhiteBookToc, printChapterContent } from './whitebook' + +function printHelp(): void { + console.log(` +AI Agent CLI + +Usage: + pnpm agent # 输出索引(最佳实践 + 知识地图) + pnpm agent --roadmap [current|v1|v2|draft] # 查看任务列表 + pnpm agent --claim <issue#> # 领取任务(分配给自己) + pnpm agent --done <issue#> # 完成任务(关闭 Issue) + pnpm agent --create <title> # 创建新任务 + [--body <描述>] [--roadmap v1|v2|draft] [--category feature|bug|refactor] + pnpm agent --stats # 查看进度统计 + pnpm agent --toc # 白皮书目录结构 + pnpm agent --chapter <name> # 查阅白皮书章节 + +Epic 管理: + pnpm agent --epic create <title> # 创建 Epic + [--desc <描述>] [--roadmap v1] [--issues 1,2,3] + pnpm agent --epic list # 查看所有 Epic + pnpm agent --epic view <#> # 查看 Epic 详情 + pnpm agent --epic sync <#> # 同步子任务状态 + pnpm agent --epic add <epic#> <issue#> # 添加子任务到 Epic + +Aliases: CURRENT -> V1, NEXT -> V2 + +Examples: + pnpm agent --roadmap current + pnpm agent --claim 28 + pnpm agent --create "修复某个问题" --category bug --roadmap v1 + pnpm agent --epic create "大功能" --roadmap v1 --issues 44,45,46 +`) +} + +function main(): void { + const args = process.argv.slice(2) + + // Help + if (args.includes('--help') || args.includes('-h')) { + printHelp() + process.exit(0) + } + + // Roadmap + const roadmapIndex = args.indexOf('--roadmap') + if (roadmapIndex !== -1) { + const releaseFilter = args[roadmapIndex + 1] + printRoadmap(releaseFilter) + process.exit(0) + } + + // Claim + const claimIndex = args.indexOf('--claim') + if (claimIndex !== -1 && args[claimIndex + 1]) { + claimIssue(args[claimIndex + 1]) + process.exit(0) + } + + // Done + const doneIndex = args.indexOf('--done') + if (doneIndex !== -1 && args[doneIndex + 1]) { + completeIssue(args[doneIndex + 1]) + process.exit(0) + } + + // Stats + if (args.includes('--stats')) { + printStats() + process.exit(0) + } + + // Create Issue + const createIndex = args.indexOf('--create') + if (createIndex !== -1 && args[createIndex + 1]) { + const title = args[createIndex + 1] + const bodyIndex = args.indexOf('--body') + const roadmapIndex2 = args.indexOf('--roadmap', createIndex) + const categoryIndex = args.indexOf('--category') + + createIssue({ + title, + body: bodyIndex !== -1 ? args[bodyIndex + 1] : undefined, + roadmap: roadmapIndex2 !== -1 ? args[roadmapIndex2 + 1] : 'DRAFT', + category: categoryIndex !== -1 ? args[categoryIndex + 1] : 'feature', + }) + process.exit(0) + } + + // Epic commands + const epicIndex = args.indexOf('--epic') + if (epicIndex !== -1) { + const subCommand = args[epicIndex + 1] + + switch (subCommand) { + case 'create': { + const title = args[epicIndex + 2] + if (!title) { + log.error('请提供 Epic 标题') + process.exit(1) + } + const descIndex = args.indexOf('--desc') + const roadmapIndex3 = args.indexOf('--roadmap', epicIndex) + const issuesIndex = args.indexOf('--issues') + + const subIssues = issuesIndex !== -1 + ? args[issuesIndex + 1].split(',').map(n => parseInt(n.trim())) + : [] + + createEpic({ + title, + description: descIndex !== -1 ? args[descIndex + 1] : undefined, + roadmap: roadmapIndex3 !== -1 ? args[roadmapIndex3 + 1] : 'V1', + subIssues, + }) + break + } + + case 'list': + listEpics() + break + + case 'view': { + const epicNum = parseInt(args[epicIndex + 2]) + if (!epicNum) { + log.error('请提供 Epic 编号') + process.exit(1) + } + viewEpic(epicNum) + break + } + + case 'sync': { + const epicNum = parseInt(args[epicIndex + 2]) + if (!epicNum) { + log.error('请提供 Epic 编号') + process.exit(1) + } + syncEpicStatus(epicNum) + break + } + + case 'add': { + const epicNum = parseInt(args[epicIndex + 2]) + const issueNum = parseInt(args[epicIndex + 3]) + if (!epicNum || !issueNum) { + log.error('请提供 Epic 编号和 Issue 编号') + process.exit(1) + } + addSubIssueToEpic(epicNum, issueNum) + break + } + + default: + log.error(`未知的 Epic 子命令: ${subCommand}`) + console.log('可用命令: create, list, view, sync, add') + process.exit(1) + } + process.exit(0) + } + + // TOC + if (args.includes('--toc')) { + printWhiteBookToc() + process.exit(0) + } + + // Chapter + const chapterIndex = args.indexOf('--chapter') + if (chapterIndex !== -1 && args[chapterIndex + 1]) { + const chapterName = args[chapterIndex + 1] + log.section(`章节: ${chapterName}`) + printChapterContent(chapterName) + process.exit(0) + } + + // Default: print index + printIndex() +} + +main() diff --git a/scripts/agent/epic.ts b/scripts/agent/epic.ts new file mode 100644 index 000000000..09b099df1 --- /dev/null +++ b/scripts/agent/epic.ts @@ -0,0 +1,227 @@ +/** + * Epic 管理 - 大任务拆分和追踪 + */ + +import { execSync } from 'node:child_process' +import { ROOT, log } from './utils' +import { createIssue, addIssueToProject, setIssueRelease, fetchRoadmap } from './roadmap' + +export interface EpicOptions { + title: string + description?: string + roadmap?: string + subIssues?: number[] // 已存在的子 issue 编号 +} + +/** + * 创建 Epic Issue + * Epic 是一个特殊的 Issue,用 Tasklist 追踪子任务 + */ +export function createEpic(options: EpicOptions): string | null { + const { title, description = '', roadmap = 'V1', subIssues = [] } = options + + // 构建 Tasklist + let tasklist = '' + if (subIssues.length > 0) { + tasklist = '\n\n## 子任务\n\n' + subIssues.map(n => `- [ ] #${n}`).join('\n') + } + + const body = `${description}${tasklist} + +--- +*这是一个 Epic Issue,用于追踪大功能的多个子任务。*` + + const issueNumber = createIssue({ + title: `Epic: ${title}`, + body, + roadmap, + category: 'feature', + }) + + return issueNumber +} + +/** + * 添加子任务到 Epic + */ +export function addSubIssueToEpic(epicNumber: number, subIssueNumber: number): boolean { + try { + // 获取当前 Epic body + const output = execSync( + `gh issue view ${epicNumber} --json body`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const { body } = JSON.parse(output) + + // 检查是否已存在 + if (body.includes(`#${subIssueNumber}`)) { + log.warn(`#${subIssueNumber} 已在 Epic #${epicNumber} 中`) + return false + } + + // 添加到 Tasklist + let newBody: string + if (body.includes('## 子任务')) { + // 在子任务列表末尾添加 + newBody = body.replace( + /(## 子任务\n\n(?:- \[[ x]\] #\d+\n?)*)/, + `$1- [ ] #${subIssueNumber}\n` + ) + } else { + // 创建新的子任务部分 + newBody = body + `\n\n## 子任务\n\n- [ ] #${subIssueNumber}` + } + + // 更新 Epic + execSync( + `gh issue edit ${epicNumber} --body "${newBody.replace(/"/g, '\\"')}"`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + + log.success(`已添加 #${subIssueNumber} 到 Epic #${epicNumber}`) + return true + } catch (e) { + log.error(`添加失败: ${e}`) + return false + } +} + +/** + * 列出所有 Epic + */ +export function listEpics(): void { + try { + const output = execSync( + `gh issue list --label enhancement --search "Epic:" --json number,title,state,body --limit 20`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const issues = JSON.parse(output) + + if (issues.length === 0) { + console.log('没有找到 Epic Issues') + return + } + + console.log('# Epic Issues\n') + + for (const issue of issues) { + const status = issue.state === 'OPEN' ? '🔄' : '✅' + + // 解析子任务进度 + const taskMatches = issue.body.match(/- \[([ x])\] #(\d+)/g) || [] + const total = taskMatches.length + const done = taskMatches.filter((m: string) => m.includes('[x]')).length + + const progress = total > 0 ? `(${done}/${total})` : '' + + console.log(`${status} #${issue.number} ${issue.title} ${progress}`) + } + } catch (e) { + log.error(`获取 Epic 列表失败: ${e}`) + } +} + +/** + * 查看 Epic 详情 + */ +export function viewEpic(epicNumber: number): void { + try { + const output = execSync( + `gh issue view ${epicNumber} --json number,title,state,body,assignees`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const issue = JSON.parse(output) + + console.log(`# Epic #${issue.number}: ${issue.title}\n`) + console.log(`状态: ${issue.state === 'OPEN' ? '进行中' : '已完成'}`) + if (issue.assignees?.length > 0) { + console.log(`负责人: ${issue.assignees.map((a: { login: string }) => `@${a.login}`).join(', ')}`) + } + + // 解析子任务 + const taskMatches = issue.body.match(/- \[([ x])\] #(\d+)/g) || [] + if (taskMatches.length > 0) { + console.log(`\n## 子任务 (${taskMatches.filter((m: string) => m.includes('[x]')).length}/${taskMatches.length})\n`) + + for (const match of taskMatches) { + const isDone = match.includes('[x]') + const issueNum = match.match(/#(\d+)/)?.[1] + const icon = isDone ? '✅' : '⬚' + + // 获取子 issue 标题 + try { + const subOutput = execSync( + `gh issue view ${issueNum} --json title,state`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const subIssue = JSON.parse(subOutput) + console.log(` ${icon} #${issueNum} ${subIssue.title}`) + } catch { + console.log(` ${icon} #${issueNum} (无法获取标题)`) + } + } + } + } catch (e) { + log.error(`获取 Epic 详情失败: ${e}`) + } +} + +/** + * 同步 Epic 子任务状态 + * 检查子 issues 的状态,更新 Tasklist checkbox + */ +export function syncEpicStatus(epicNumber: number): void { + try { + const output = execSync( + `gh issue view ${epicNumber} --json body`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const { body } = JSON.parse(output) + + // 提取所有子 issue 编号 + const taskMatches = [...body.matchAll(/- \[([ x])\] #(\d+)/g)] + if (taskMatches.length === 0) { + console.log('没有找到子任务') + return + } + + let newBody = body + let updated = 0 + + for (const match of taskMatches) { + const currentState = match[1] + const issueNum = match[2] + + try { + const subOutput = execSync( + `gh issue view ${issueNum} --json state`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const { state } = JSON.parse(subOutput) + const shouldBeDone = state === 'CLOSED' + const isDone = currentState === 'x' + + if (shouldBeDone !== isDone) { + const oldPattern = `- [${currentState}] #${issueNum}` + const newPattern = `- [${shouldBeDone ? 'x' : ' '}] #${issueNum}` + newBody = newBody.replace(oldPattern, newPattern) + updated++ + } + } catch { + // 忽略无法获取的 issue + } + } + + if (updated > 0) { + execSync( + `gh issue edit ${epicNumber} --body "${newBody.replace(/"/g, '\\"')}"`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + log.success(`已同步 ${updated} 个子任务状态`) + } else { + console.log('所有子任务状态已是最新') + } + } catch (e) { + log.error(`同步失败: ${e}`) + } +} diff --git a/scripts/agent/readme.ts b/scripts/agent/readme.ts new file mode 100644 index 000000000..0f6ea22c6 --- /dev/null +++ b/scripts/agent/readme.ts @@ -0,0 +1,110 @@ +/** + * AI Agent 索引输出 + */ + +import { fetchRoadmap, printStats } from './roadmap' +import { resolveRelease } from './utils' + +export function printBestPractices(): void { + console.log(` +# 最佳实践 + +❌ Radix Dialog / position:fixed → ✅ Stackflow BottomSheet/Modal +❌ React Router → ✅ Stackflow push/pop/replace +❌ 复制 mpay 代码 → ✅ 理解后用 React/TS 重写 +❌ 随意创建 store → ✅ 遵循 stores/ 现有模式 +❌ 明文选择器 → ✅ data-testid +❌ 安装新 UI 库 → ✅ shadcn/ui(已集成) +❌ 新建 CSS → ✅ Tailwind CSS + +详见: pnpm agent --chapter 00-必读 +`) +} + +export function printKnowledgeMap(): void { + console.log(` +# 知识地图 + +代码: + src/stackflow/ → 导航配置、Activity (添加页面/Sheet/Modal) + src/services/ → 服务层、Adapter 模式 + src/stores/ → TanStack Store 状态管理 + src/queries/ → TanStack Query 数据获取 + src/components/ → UI 组件 + src/i18n/ → 国际化 + +白皮书 (pnpm agent --chapter <路径>): + 03-架构篇/03-导航系统/ → 页面跳转、Tab + 04-服务篇/01-服务架构/ → 服务分层、Adapter + 05-组件篇/01-基础组件/ → Sheet、Dialog + 04-服务篇/08-钱包数据存储/ → 钱包数据 + 07-国际化篇/ → 多语言 + 08-测试篇/03-Playwright配置/e2e-best-practices.md → E2E 测试 + 附录/C-mpay迁移指南/ → mpay 参考 + +外部文档: + Stackflow: https://stackflow.so/llms-full.txt + shadcn/ui: https://ui.shadcn.com/docs + TanStack: https://tanstack.com/query/latest + +mpay 原始代码: /Users/kzf/Dev/bioforestChain/legacy-apps/apps/mpay/ +`) +} + +export function printCurrentTasks(): void { + const items = fetchRoadmap() + const currentItems = items.filter(i => { + const release = i.release?.toUpperCase() + return release === resolveRelease('CURRENT') + }) + + if (currentItems.length === 0) return + + const inProgress = currentItems.filter(i => i.status === 'In Progress') + const todo = currentItems.filter(i => i.status === 'Todo') + const done = currentItems.filter(i => i.status === 'Done') + + console.log(` +# 当前任务 (${resolveRelease('CURRENT')}) ${done.length}/${currentItems.length} done +`) + + if (inProgress.length > 0) { + console.log('进行中:') + for (const t of inProgress) { + console.log(` #${t.issueNumber} ${t.title}${t.assignees.length ? ` @${t.assignees.join(',')}` : ''}`) + } + } + + if (todo.length > 0) { + console.log('待领取:') + for (const t of todo) { + console.log(` #${t.issueNumber} ${t.title}`) + } + } + console.log() +} + +export function printWorkflow(): void { + console.log(` +# 工作流 + +pnpm agent --claim <#> 领取任务 (自动分配+worktree指引) +pnpm agent --create "x" 创建任务 (--category bug --roadmap v1) +pnpm agent --epic create "x" 创建 Epic (--roadmap v1) +pnpm agent --epic list 查看所有 Epic +pnpm agent --chapter <x> 查阅白皮书 +pnpm agent --stats 进度统计 + +PR 规则: 描述中使用 Closes #issue编号 自动关联 +`) +} + +export function printIndex(): void { + console.log(`# KeyApp AI 开发索引 +`) + + printBestPractices() + printKnowledgeMap() + printCurrentTasks() + printWorkflow() +} diff --git a/scripts/agent/roadmap.ts b/scripts/agent/roadmap.ts new file mode 100644 index 000000000..4bf74807e --- /dev/null +++ b/scripts/agent/roadmap.ts @@ -0,0 +1,255 @@ +/** + * GitHub Project / Issue 管理 + */ + +import { execSync } from 'node:child_process' +import { + ROOT, + PROJECT_NUMBER, + PROJECT_OWNER, + PROJECT_ID, + RELEASE_FIELD_ID, + RELEASE_OPTIONS, + resolveRelease, + log, +} from './utils' + +export interface RoadmapItem { + id: string + title: string + status: string + release: string | null + issueNumber: number | null + issueUrl: string | null + assignees: string[] +} + +export function fetchRoadmap(): RoadmapItem[] { + try { + const output = execSync( + `gh project item-list ${PROJECT_NUMBER} --owner ${PROJECT_OWNER} --format json`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } + ) + const data = JSON.parse(output) + return data.items.map((item: { + id: string + title: string + status: string + release?: string + content?: { number?: number; url?: string } + assignees?: string[] + }) => ({ + id: item.id, + title: item.title, + status: item.status, + release: item.release || null, + issueNumber: item.content?.number || null, + issueUrl: item.content?.url || null, + assignees: item.assignees || [], + })) + } catch { + return [] + } +} + +export function printRoadmap(releaseFilter?: string): void { + const allItems = fetchRoadmap() + if (allItems.length === 0) { + console.log('无法获取 Roadmap,请确保已配置 gh auth (需要 project scope)') + return + } + + const resolvedFilter = releaseFilter ? resolveRelease(releaseFilter) : undefined + const displayName = releaseFilter?.toUpperCase() + const isAlias = releaseFilter && resolvedFilter !== releaseFilter.toUpperCase() + + const items = resolvedFilter + ? allItems.filter(i => i.release?.toUpperCase() === resolvedFilter) + : allItems + + const title = resolvedFilter + ? `KeyApp Roadmap - ${displayName}${isAlias ? ` (${resolvedFilter})` : ''}` + : 'KeyApp Roadmap' + console.log(`# ${title}\n`) + console.log(`> GitHub Project: https://github.com/orgs/${PROJECT_OWNER}/projects/${PROJECT_NUMBER}\n`) + + if (resolvedFilter && items.length === 0) { + console.log(`没有找到 ${displayName} 版本的任务。\n`) + console.log('可用版本: V1, V2, V3, DRAFT, CURRENT, NEXT') + return + } + + if (!releaseFilter) { + const releases = ['V1', 'V2', 'V3', 'DRAFT', null] + for (const release of releases) { + const releaseItems = allItems.filter(i => + release === null ? !i.release : i.release === release + ) + if (releaseItems.length === 0) continue + + console.log(`## ${release || '未分配'}\n`) + printStatusGroups(releaseItems) + } + } else { + printStatusGroups(items) + } +} + +function printStatusGroups(items: RoadmapItem[]): void { + const grouped = { + 'In Progress': items.filter(i => i.status === 'In Progress'), + Todo: items.filter(i => i.status === 'Todo'), + Done: items.filter(i => i.status === 'Done'), + } + + for (const [status, tasks] of Object.entries(grouped)) { + if (tasks.length === 0) continue + for (const task of tasks) { + const icon = status === 'Done' ? '✅' : status === 'In Progress' ? '🔄' : '⬚' + const issueRef = task.issueNumber ? `#${task.issueNumber}` : '' + const assignee = task.assignees.length > 0 ? ` @${task.assignees.join(', @')}` : '' + console.log(`- ${icon} ${issueRef} ${task.title}${assignee}`) + } + } + console.log() +} + +export function claimIssue(issueNumber: string): void { + try { + execSync( + `gh issue edit ${issueNumber} --add-assignee @me`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + log.success(`已领取 Issue #${issueNumber}`) + console.log(`\n下一步:`) + console.log(` 1. git worktree add .git-worktree/issue-${issueNumber} -b feat/issue-${issueNumber}`) + console.log(` 2. cd .git-worktree/issue-${issueNumber} && pnpm install`) + console.log(` 3. 开始开发...`) + console.log(` 4. PR 描述中使用 "Closes #${issueNumber}" 自动关联`) + } catch (e) { + log.error(`领取失败: ${e}`) + } +} + +export function completeIssue(issueNumber: string): void { + try { + execSync( + `gh issue close ${issueNumber} --reason completed`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + log.success(`已完成 Issue #${issueNumber}`) + } catch (e) { + log.error(`完成失败: ${e}`) + } +} + +export interface CreateIssueOptions { + title: string + body?: string + roadmap?: string + category?: string +} + +export function createIssue(options: CreateIssueOptions): string | null { + const { title, body = '', roadmap = 'DRAFT', category = 'feature' } = options + + const labelMap: Record<string, string> = { + feature: 'enhancement', + bug: 'bug', + refactor: 'refactor', + docs: 'documentation', + test: 'test', + } + const label = labelMap[category.toLowerCase()] || 'enhancement' + + const fullBody = `${body} + +**Roadmap**: ${roadmap.toUpperCase()} +**Category**: ${category.charAt(0).toUpperCase() + category.slice(1)}` + + try { + const output = execSync( + `gh issue create --title "${title.replace(/"/g, '\\"')}" --body "${fullBody.replace(/"/g, '\\"')}" --label "${label}" --project "KeyApp Roadmap"`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: ROOT } + ) + const issueUrl = output.trim() + const issueNumber = issueUrl.split('/').pop() || null + + log.success(`已创建 Issue ${issueUrl}`) + + // 设置 Release 字段 + if (issueNumber) { + setIssueRelease(parseInt(issueNumber), roadmap.toUpperCase()) + } + + console.log(`\n下一步:`) + console.log(` pnpm agent --claim ${issueNumber} # 领取任务`) + + return issueNumber + } catch (e) { + log.error(`创建失败: ${e}`) + return null + } +} + +export function setIssueRelease(issueNumber: number, release: string): boolean { + try { + const optionId = RELEASE_OPTIONS[release.toUpperCase()] + if (!optionId) return false + + const items = fetchRoadmap() + const item = items.find(i => i.issueNumber === issueNumber) + + if (item) { + execSync( + `gh project item-edit --project-id ${PROJECT_ID} --id "${item.id}" --field-id ${RELEASE_FIELD_ID} --single-select-option-id ${optionId}`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } + ) + log.success(`已设置 Release 为 ${release.toUpperCase()}`) + return true + } + return false + } catch { + log.warn(`Release 字段设置失败,请手动设置`) + return false + } +} + +export function addIssueToProject(issueNumber: number): boolean { + try { + execSync( + `gh project item-add ${PROJECT_NUMBER} --owner ${PROJECT_OWNER} --url "https://github.com/BioforestChain/KeyApp/issues/${issueNumber}"`, + { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } + ) + return true + } catch { + return false + } +} + +export function printStats(): void { + const items = fetchRoadmap() + if (items.length === 0) { + console.log('无法获取统计数据') + return + } + + const releases = ['V1', 'V2', 'V3', 'DRAFT'] + + console.log('# 进度统计\n') + + for (const release of releases) { + const releaseItems = items.filter(i => i.release === release) + if (releaseItems.length === 0) continue + + const todo = releaseItems.filter(i => i.status === 'Todo').length + const inProgress = releaseItems.filter(i => i.status === 'In Progress').length + const done = releaseItems.filter(i => i.status === 'Done').length + const total = releaseItems.length + const progress = Math.round((done / total) * 100) + const bar = '█'.repeat(Math.floor(progress / 10)) + '░'.repeat(10 - Math.floor(progress / 10)) + + console.log(`${release}: ${bar} ${progress}% (${done}/${total} done, ${inProgress} wip, ${todo} todo)`) + } + console.log() +} diff --git a/scripts/agent/utils.ts b/scripts/agent/utils.ts new file mode 100644 index 000000000..2dbf14895 --- /dev/null +++ b/scripts/agent/utils.ts @@ -0,0 +1,53 @@ +/** + * Agent 工具共享模块 + */ + +import { resolve } from 'node:path' + +export const ROOT = resolve(import.meta.dirname, '../..') +export const WHITE_BOOK_DIR = resolve(ROOT, 'docs/white-book') + +export const PROJECT_NUMBER = 5 +export const PROJECT_OWNER = 'BioforestChain' +export const PROJECT_ID = 'PVT_kwDOBJVmF84BLGhY' + +// Release 字段 ID 和选项 +export const RELEASE_FIELD_ID = 'PVTSSF_lADOBJVmF84BLGhYzg6xpp8' +export const RELEASE_OPTIONS: Record<string, string> = { + V1: '9359aa58', + V2: '6a3e495d', + V3: '7e413227', + DRAFT: 'd75e4b65', +} + +// 版本别名 +export const RELEASE_ALIASES: Record<string, string> = { + CURRENT: 'V1', + NEXT: 'V2', +} + +export const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', +} + +export const log = { + title: (msg: string) => console.log(`\n${colors.bold}${colors.cyan}${'='.repeat(60)}${colors.reset}`), + section: (msg: string) => console.log(`\n${colors.bold}${colors.green}## ${msg}${colors.reset}\n`), + subsection: (msg: string) => console.log(`\n${colors.yellow}### ${msg}${colors.reset}\n`), + info: (msg: string) => console.log(`${colors.dim}${msg}${colors.reset}`), + success: (msg: string) => console.log(`${colors.green}✅ ${msg}${colors.reset}`), + error: (msg: string) => console.log(`${colors.red}❌ ${msg}${colors.reset}`), + warn: (msg: string) => console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`), +} + +export function resolveRelease(input: string): string { + const upper = input.toUpperCase() + return RELEASE_ALIASES[upper] || upper +} diff --git a/scripts/agent/whitebook.ts b/scripts/agent/whitebook.ts new file mode 100644 index 000000000..3a5121ec8 --- /dev/null +++ b/scripts/agent/whitebook.ts @@ -0,0 +1,100 @@ +/** + * 白皮书相关功能 + */ + +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import { join, relative } from 'node:path' +import { WHITE_BOOK_DIR, colors, log } from './utils' + +export interface ChapterInfo { + name: string + path: string + indexPath?: string + subChapters: ChapterInfo[] +} + +export function getChapters(dir: string): ChapterInfo[] { + if (!existsSync(dir)) return [] + + const items = readdirSync(dir) + .filter(item => !item.startsWith('.')) + .sort() + + const chapters: ChapterInfo[] = [] + + for (const item of items) { + const fullPath = join(dir, item) + const stat = statSync(fullPath) + + if (stat.isDirectory()) { + const indexPath = join(fullPath, 'index.md') + const subChapters = getChapters(fullPath) + + chapters.push({ + name: item, + path: fullPath, + indexPath: existsSync(indexPath) ? indexPath : undefined, + subChapters, + }) + } else if (item.endsWith('.md') && item !== 'index.md') { + chapters.push({ + name: item.replace('.md', ''), + path: fullPath, + subChapters: [], + }) + } + } + + return chapters +} + +export function printToc(chapters: ChapterInfo[], indent = 0): void { + for (const chapter of chapters) { + const prefix = ' '.repeat(indent) + const relPath = relative(WHITE_BOOK_DIR, chapter.path) + console.log(`${prefix}- ${chapter.name} (${relPath})`) + if (chapter.subChapters.length > 0) { + printToc(chapter.subChapters, indent + 1) + } + } +} + +function readMarkdownFile(filePath: string): string { + if (!existsSync(filePath)) return '' + return readFileSync(filePath, 'utf-8') +} + +export function printChapterContent(chapterPath: string): void { + const fullPath = join(WHITE_BOOK_DIR, chapterPath) + + if (!existsSync(fullPath)) { + console.error(`${colors.red}错误: 章节不存在: ${chapterPath}${colors.reset}`) + process.exit(1) + } + + const stat = statSync(fullPath) + + if (stat.isDirectory()) { + const indexPath = join(fullPath, 'index.md') + if (existsSync(indexPath)) { + console.log(readMarkdownFile(indexPath)) + } + + const subFiles = readdirSync(fullPath) + .filter(f => f.endsWith('.md') && f !== 'index.md') + .sort() + + for (const subFile of subFiles) { + log.subsection(subFile.replace('.md', '')) + console.log(readMarkdownFile(join(fullPath, subFile))) + } + } else if (fullPath.endsWith('.md')) { + console.log(readMarkdownFile(fullPath)) + } +} + +export function printWhiteBookToc(): void { + console.log('# 白皮书目录结构\n') + const chapters = getChapters(WHITE_BOOK_DIR) + printToc(chapters) +}