diff --git a/CLAUDE.md b/CLAUDE.md index 543b239..f5d0031 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -310,28 +310,10 @@ CI/CD(Forgejo,`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译 ### 项目 TODO 管理 -**唯一待办系统**:任务执行过程中如发现需要做的事项,**必须**用 `/todo add <描述>` 记入项目 todo 系统,**禁止**使用 TaskCreate/TodoWrite 等其它 todo 工具,保持单一真相源。 +**本项目已停用 todo。** 即便全局 `~/.claude/CLAUDE.md` 要求用 `/todo` 记录待办,**本项目一律不记 todo、不调用 `/todo`、不创建 `todo/` 数据**(此项目规则覆盖全局规则)。需要追踪的事项直接在对话里说明即可。 -- **真相源**:`todo/todo.json`(结构化 JSON,含 id/标题/重要度/平台标签/创建时间/完成版本) -- **HTML**:`todo/todo.html` 为生成物,每次写操作自动重渲染 -- **重要度**:`high`(阻断/紧急)/ `mid`(重要)/ `low`(一般/优化)三级 -- **改动等级**:`tier 1`(接口/schema/跨 2+ 模块)/ `tier 2`(5+ 文件)/ `tier 3`(小改/bugfix);**一级改动须先进 plan 模式经用户批准,完成后同步更新设计文档**,详见 `/todo` -- **状态**:`open`(待开始)→ `doing`(开发中)→ `done`(待验收)→ `accepted`(已验收) -- **完成即标记**:开发完成后 `/todo status done`,验收通过后用户运行 `/todo done ` 记入版本 - -常用命令: -``` -/todo — 查看 summary(按状态分组) -/todo add 修复... — 新增待办(Claude 自动推断级别/标签) -/todo status doing — 标记开发中(Claude 开始处理时调用) -/todo status done — 标记待验收(Claude 完成开发时调用) -/todo done — 用户验收通过,自动记入版本号 -/todo reject <原因> — 用户拒绝验收,退回 open 并升为最高优先级 -/todo reopen — 重新开启(清空验收/拒绝记录) -/todo rm — 删除 -``` - -底层脚本:`node todo/todo.mjs <子命令> [参数]` +- **改动等级仍把控**:接口/schema/跨 2+ 模块的「大改」先进 plan 模式经用户批准,完成后同步更新设计文档;小 bugfix 直接做。 +- 仍**禁止**使用 TaskCreate/TodoWrite 等内置 todo 工具。 --- diff --git a/todo/todo.html b/todo/todo.html deleted file mode 100644 index 1987ef3..0000000 --- a/todo/todo.html +++ /dev/null @@ -1,482 +0,0 @@ - - - - - -酒库管理系统 — 项目 TODO - - - - -
-
-

酒库管理系统 — 项目 TODO

-
生成于 2026-06-16 · 真相源 todo/todo.json
-
-
0全部
-
0待开始
-
0开发中
-
0待验收
-
0已验收
-
-
-
- -
-
- 重要度 - - - - -
-
-
- 状态 - - - - - -
-
-
- 改动等级 - - - - -
- -
- -
- - -
-
- 📋 待开始 0 - ▴ 收起 -
-
-
    -

    暂无条目

    -
-
-
-
-
- 🔨 开发中 0 - ▴ 收起 -
-
-
    -

    暂无条目

    -
-
-
-
-
- 🔍 待验收 0 - ▴ 收起 -
-
-
    -

    暂无条目

    -
-
-
-
-
- ✅ 已验收 0 - ▾ 展开 -
- -
- -
- - - - - - - \ No newline at end of file diff --git a/todo/todo.json b/todo/todo.json deleted file mode 100644 index 0b66fd2..0000000 --- a/todo/todo.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "meta": { - "title": "酒库管理系统 — 项目 TODO", - "updated_at": "2026-06-16T14:38:14.559Z" - }, - "seq": 0, - "items": [] -} \ No newline at end of file diff --git a/todo/todo.mjs b/todo/todo.mjs deleted file mode 100644 index 39befd4..0000000 --- a/todo/todo.mjs +++ /dev/null @@ -1,1066 +0,0 @@ -#!/usr/bin/env node -/** - * todo/todo.mjs — 项目 TODO 管理 CLI(零依赖,Node.js >= 18) - * - * 状态流转:open → doing → done → accepted - * done ←─ reject(退回,升为最高优先级) - * open 待开始 Claude 可设 - * doing 开发中 Claude 可设 - * done 待验收 Claude 可设(开发完成,等用户确认) - * accepted 已验收 仅限用户设置(手动编辑 JSON 或 `done ` 命令) - * - * 子命令: - * add --title "..." [--level high|mid|low] [--tier 1|2|3] [--tags a,b] [--desc "..."] - * status — 更新开发状态(Claude 可调用) - * done [--version vX.Y.Z] — 标记已验收交付(仅用户调用) - * reject --reason "原因" — 拒绝验收,退回 open 并升为 high(仅用户调用) - * reopen — 重新开启(清空拒绝记录) - * rm - * list - * render - * sub add --title "..." [--tier 1|2|3] [--deps "21A,21B"] - * sub status — 更新子任务状态(如 21A) - */ - -import { readFileSync, writeFileSync, existsSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import { execSync } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; - -const __dir = dirname(fileURLToPath(import.meta.url)); -const DB = resolve(__dir, 'todo.json'); -const HTML = resolve(__dir, 'todo.html'); - -// ─── 常量 ──────────────────────────────────────────────────────────────────── - -const LEVEL_LABEL = { high: '高', mid: '中', low: '低' }; -const LEVEL_ORDER = { high: 0, mid: 1, low: 2 }; - -const STATUS_LABEL = { open: '待开始', doing: '开发中', done: '待验收', accepted: '已验收' }; -const STATUS_ORDER = { open: 0, doing: 1, done: 2, accepted: 3 }; - -const TIER_LABEL = { 1: '一级', 2: '二级', 3: '三级' }; - -const SUB_STATUS_ICON = { open: '○', doing: '◐', done: '✓' }; -const SUB_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -// ─── 数据读写 ──────────────────────────────────────────────────────────────── - -function load() { - if (!existsSync(DB)) { - return { meta: { title: '酒库管理系统 — 项目 TODO', updated_at: now() }, seq: 0, items: [] }; - } - const db = JSON.parse(readFileSync(DB, 'utf8')); - for (const item of db.items) { - if (!item.status) { - item.status = item.done ? 'accepted' : 'open'; - } - } - return db; -} - -function save(db) { - db.meta.updated_at = now(); - writeFileSync(DB, JSON.stringify(db, null, 2) + '\n', 'utf8'); -} - -function now() { - return new Date().toISOString(); -} - -function fmtDate(iso) { - if (!iso) return ''; - const d = new Date(iso); - return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; -} - -function gitTag() { - try { - return execSync('git describe --tags --abbrev=0', { encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }).trim(); - } catch { - return null; - } -} - -// ─── 子任务工具 ────────────────────────────────────────────────────────────── - -function nextSubtaskLetter(item) { - const existing = (item.subtasks || []).map(s => s.sid); - for (const l of SUB_LETTERS) { - const sid = `${item.id}${l}`; - if (!existing.includes(sid)) return l; - } - return null; -} - -function parseSid(sid) { - const m = String(sid).match(/^(\d+)([A-Za-z])$/); - if (!m) return null; - return { parentId: parseInt(m[1], 10), letter: m[2].toUpperCase() }; -} - -function findSubtask(db, sid) { - const parsed = parseSid(sid); - if (!parsed) { console.error(`错误:无效子任务 ID "${sid}",格式应为 21A`); process.exit(1); } - const parent = db.items.find(i => i.id === parsed.parentId); - if (!parent) { console.error(`错误:找不到父任务 id=${parsed.parentId}`); process.exit(1); } - const normalSid = sid.toUpperCase(); - const sub = (parent.subtasks||[]).find(s => s.sid === normalSid); - if (!sub) { console.error(`错误:找不到子任务 ${normalSid}`); process.exit(1); } - return { parent, sub }; -} - -function subtasksDoneCount(item) { - if (!item.subtasks || !item.subtasks.length) return { done: 0, total: 0 }; - const done = item.subtasks.filter(s => s.status === 'done' || s.status === 'accepted').length; - return { done, total: item.subtasks.length }; -} - -function allSubtasksDone(item) { - const { done, total } = subtasksDoneCount(item); - return total > 0 && done === total; -} - -// ─── 终端 summary ──────────────────────────────────────────────────────────── - -function levelText(level) { - return { high: '高优 · 紧急', mid: '重要', low: '一般 / 优化' }[level] || level; -} - -function printSummary(db) { - const byStatus = { open: [], doing: [], done: [], accepted: [] }; - for (const i of db.items) { - (byStatus[i.status] || byStatus.open).push(i); - } - for (const st of ['open','doing','done','accepted']) { - byStatus[st].sort((a,b) => (LEVEL_ORDER[a.level]??9) - (LEVEL_ORDER[b.level]??9) || a.id - b.id); - } - - const total = db.items.length; - const pending = byStatus.open.length + byStatus.doing.length + byStatus.done.length; - const accepted = byStatus.accepted.length; - - console.log(`\nTODO 共 ${total} · 进行中 ${pending} · 已验收 ${accepted}`); - - for (const [st, label] of [['open','待开始'],['doing','开发中'],['done','待验收']]) { - if (byStatus[st].length === 0) continue; - console.log(`${label}(${byStatus[st].length}):`); - for (const i of byStatus[st]) { - const tags = i.tags && i.tags.length ? ' ' + i.tags.join('/') : ''; - const lvl = LEVEL_LABEL[i.level] || i.level; - const tier = i.tier ? '·' + TIER_LABEL[i.tier] : ''; - const { done, total: stTotal } = subtasksDoneCount(i); - const subInfo = stTotal > 0 ? ` [${done}/${stTotal}子任务]` : ''; - console.log(` [${lvl}${tier}] #${i.id} ${i.title}${tags}${subInfo}`); - if (stTotal > 0) { - for (const s of i.subtasks) { - const icon = SUB_STATUS_ICON[s.status] || '○'; - const deps = s.deps && s.deps.length ? ` (需: ${s.deps.join(',')})` : ''; - const stier = s.tier ? ` [${TIER_LABEL[s.tier]}]` : ''; - console.log(` ${icon} ${s.sid}${stier} ${s.title}${deps}`); - } - } - } - } - - if (byStatus.accepted.length) { - console.log(`已验收(最近 5 条):`); - for (const i of byStatus.accepted.slice(0, 5)) { - const ver = i.version ? `(${i.version})` : ''; - console.log(` ✅ #${i.id} ${i.title}${ver}`); - } - } - console.log(''); -} - -// ─── HTML 渲染 ─────────────────────────────────────────────────────────────── - -function esc(s) { - return String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); -} - -function renderDesc(s) { - if (!s) return ''; - return esc(s).replace(/`([^`]+)`/g, '$1'); -} - -function levelClass(level) { - return { high: 't-block', mid: 't-high', low: 't-low' }[level] || 't-low'; -} - -function statusClass(status) { - return { open: 's-open', doing: 's-doing', done: 's-done', accepted: 's-accepted' }[status] || 's-open'; -} - -function renderSubtasks(item) { - if (!item.subtasks || item.subtasks.length === 0) return ''; - const { done, total } = subtasksDoneCount(item); - const pct = total > 0 ? Math.round(done / total * 100) : 0; - - const subItems = item.subtasks.map(s => { - const stCls = statusClass(s.status); - const stLbl = STATUS_LABEL[s.status] || s.status; - const icon = SUB_STATUS_ICON[s.status] || '○'; - const tierBadge = s.tier ? `${TIER_LABEL[s.tier]}` : ''; - const depBadges = (s.deps||[]).map(dep => { - const depSub = item.subtasks.find(x => x.sid === dep); - const depDone = depSub && (depSub.status === 'done' || depSub.status === 'accepted'); - return `${esc(dep)}`; - }).join(''); - const depsHtml = s.deps && s.deps.length - ? `依赖:${depBadges}` - : ''; - - return `
  • - ${icon} - ${esc(s.sid)} - ${esc(s.title)} -
    ${tierBadge}${stLbl}${depsHtml}
    -
  • `; - }).join('\n'); - - return `
    -
    - 子任务 - ${done} / ${total} 完成 -
    -
    -
      ${subItems}
    -
    `; -} - -function renderItems(items) { - if (!items.length) return '

    暂无条目

    '; - return items.map(i => { - const tags = (i.tags||[]).map(t => `${esc(t)}`).join(' '); - const desc = i.desc ? `
    ${renderDesc(i.desc)}
    ` : ''; - const stCls = statusClass(i.status); - const stLbl = STATUS_LABEL[i.status] || i.status; - const rejectNote = i.reject_reason - ? `
    🚫 拒绝原因:${esc(i.reject_reason)} · ${fmtDate(i.rejected_at)}
    ` - : ''; - const rejectBtn = i.status === 'done' - ? `` - : ''; - const { done: subDone, total: subTotal } = subtasksDoneCount(i); - const subProgressBadge = subTotal > 0 - ? `${subDone}/${subTotal} 子任务` - : ''; - const meta = `
    - 🕐 ${fmtDate(i.created_at)} - ${i.status === 'accepted' ? `✅ 验收 ${fmtDate(i.completed_at)}${i.version ? ' · '+esc(i.version)+'' : ''}` : ''} -
    `; - const lvlCls = levelClass(i.level); - const subtasksBlock = renderSubtasks(i); - return ` -
  • -
    - ${esc(i.title)} -
    - ${stLbl} - ${levelText(i.level)} - ${i.tier ? `${TIER_LABEL[i.tier]}` : ''} - ${subProgressBadge} - ${rejectBtn} -
    -
    - ${rejectNote} - ${desc} - ${subtasksBlock} - -
  • `; - }).join('\n'); -} - -function buildHtml(db) { - const allTags = [...new Set(db.items.flatMap(i => i.tags||[]))].sort(); - - const byStatus = { open: [], doing: [], done: [], accepted: [] }; - for (const i of db.items) { - (byStatus[i.status] || byStatus.open).push(i); - } - const sortFn = (a,b) => (LEVEL_ORDER[a.level]??9) - (LEVEL_ORDER[b.level]??9) || a.id - b.id; - for (const st of Object.keys(byStatus)) byStatus[st].sort(sortFn); - byStatus.accepted.sort((a,b) => (b.completed_at||'') > (a.completed_at||'') ? 1 : -1); - - const total = db.items.length; - const pending = byStatus.open.length + byStatus.doing.length + byStatus.done.length; - - const tagChips = allTags.map(t => - `` - ).join(''); - - return ` - - - - -${esc(db.meta.title)} - - - - -
    -
    -

    ${esc(db.meta.title)}

    -
    生成于 ${fmtDate(db.meta.updated_at)} · 真相源 todo/todo.json
    -
    -
    ${total}全部
    -
    ${byStatus.open.length}待开始
    -
    ${byStatus.doing.length}开发中
    -
    ${byStatus.done.length}待验收
    -
    ${byStatus.accepted.length}已验收
    -
    -
    -
    - -
    -
    - 重要度 - - - - -
    -
    -
    - 状态 - - - - - -
    -
    -
    - 改动等级 - - - - -
    - ${allTags.length ? `
    -
    - 平台 / 标签 - ${tagChips} -
    ` : ''} -
    - -
    - - ${[ - { st: 'open', label: '📋 待开始', cls: 'st-open', collapsed: false }, - { st: 'doing', label: '🔨 开发中', cls: 'st-doing', collapsed: false }, - { st: 'done', label: '🔍 待验收', cls: 'st-done', collapsed: false }, - { st: 'accepted', label: '✅ 已验收', cls: 'st-accepted', collapsed: true }, - ].map(({ st, label, cls, collapsed }) => ` -
    -
    - ${label} ${byStatus[st].length} - ${collapsed ? '▾ 展开' : '▴ 收起'} -
    -
    -
      - ${renderItems(byStatus[st])} -
    -
    -
    `).join('')} - -
    - - - - - - -`; -} - -// ─── CLI 解析 ──────────────────────────────────────────────────────────────── - -function parseArgs(argv) { - const args = {}; - let pos = []; - for (let i = 0; i < argv.length; i++) { - if (argv[i].startsWith('--')) { - const key = argv[i].slice(2); - const val = argv[i+1] && !argv[i+1].startsWith('--') ? argv[++i] : true; - args[key] = val; - } else { - pos.push(argv[i]); - } - } - args._ = pos; - return args; -} - -function findItem(db, id) { - const n = parseInt(id, 10); - const item = db.items.find(i => i.id === n); - if (!item) { console.error(`错误:找不到 id=${n} 的条目`); process.exit(1); } - return item; -} - -// ─── 子命令 ───────────────────────────────────────────────────────────────── - -function cmdAdd(db, args) { - const title = args.title || (args._.join(' ') || '').trim(); - if (!title) { console.error('错误:--title 不能为空'); process.exit(1); } - - const level = ['high','mid','low'].includes(args.level) ? args.level : 'mid'; - const tags = args.tags ? args.tags.split(',').map(t => t.trim()).filter(Boolean) : []; - const desc = args.desc || null; - const tierRaw = parseInt(args.tier, 10); - const tier = [1,2,3].includes(tierRaw) ? tierRaw : null; - - db.seq++; - const item = { - id: db.seq, - title, - desc, - level, - tier, - tags, - status: 'open', - created_at: now(), - done: false, - completed_at: null, - version: null, - }; - db.items.push(item); - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - const tierStr = tier ? '·' + TIER_LABEL[tier] : ''; - console.log(`✅ 已添加 #${item.id} [${levelText(level)}${tierStr}] ${title}${tags.length ? ' ' + tags.join('/') : ''}`); - printSummary(db); -} - -/** status */ -function cmdStatus(db, args) { - const id = args._[0]; - const newStatus = args._[1]; - - if (newStatus === 'accepted') { - console.error('❌ 状态 "accepted"(已验收)只能由用户手动修改 todo.json,或使用 `done ` 命令'); - process.exit(1); - } - if (!['open','doing','done'].includes(newStatus)) { - console.error(`错误:无效状态 "${newStatus}",可选:open | doing | done`); - process.exit(1); - } - - const item = findItem(db, id); - - // 有未完成子任务时,禁止手动将父任务标记为 done - if (newStatus === 'done' && !allSubtasksDone(item) && item.subtasks && item.subtasks.length > 0) { - const { done: doneCount, total } = subtasksDoneCount(item); - console.error(`❌ 父任务还有未完成的子任务(${doneCount}/${total} 完成),请先通过 sub status 完成所有子任务`); - process.exit(1); - } - - item.status = newStatus; - item.done = false; - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - const emoji = { open: '📋', doing: '🔨', done: '🔍' }[newStatus]; - console.log(`${emoji} #${item.id}「${item.title}」→ ${STATUS_LABEL[newStatus]}`); - printSummary(db); -} - -/** done [--version vX.Y.Z] */ -function cmdDone(db, args) { - const id = args._[0]; - const item = findItem(db, id); - - // 有未完成子任务时提示警告(但允许用户强制验收) - if (item.subtasks && item.subtasks.length > 0 && !allSubtasksDone(item)) { - const { done: doneCount, total } = subtasksDoneCount(item); - console.warn(`⚠️ 注意:还有未完成的子任务(${doneCount}/${total} 完成),已强制标记为验收`); - } - - let version = args.version || null; - if (!version) { - version = gitTag(); - if (!version) console.warn('⚠️ 无法从 git 获取版本号,version 将为空'); - } - - item.status = 'accepted'; - item.done = true; - item.completed_at = now(); - item.version = version; - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - console.log(`✅ #${item.id}「${item.title}」已验收${version ? ',记入版本 ' + version : ''}`); - printSummary(db); -} - -/** reject --reason "原因" */ -function cmdReject(db, args) { - const id = args._[0]; - const reason = (args.reason || '').trim(); - - if (!reason) { - console.error('错误:--reason 不能为空,请说明拒绝原因'); - process.exit(1); - } - - const item = findItem(db, id); - if (item.status !== 'done') { - console.error(`错误:只有「待验收(done)」状态的条目才能拒绝,当前状态:${item.status}`); - process.exit(1); - } - - const prevLevel = item.level; - item.status = 'open'; - item.done = false; - item.level = 'high'; - item.reject_reason = reason; - item.rejected_at = now(); - item.version = null; - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - console.log(`🚫 #${item.id}「${item.title}」已拒绝${prevLevel !== 'high' ? ',优先级升为最高' : ''}`); - console.log(` 原因:${reason}`); - printSummary(db); -} - -function cmdReopen(db, args) { - const id = args._[0]; - const item = findItem(db, id); - - item.status = 'open'; - item.done = false; - item.completed_at = null; - item.version = null; - item.reject_reason = null; - item.rejected_at = null; - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - console.log(`🔄 #${item.id}「${item.title}」已重新开启`); - printSummary(db); -} - -function cmdRm(db, args) { - const id = parseInt(args._[0], 10); - const idx = db.items.findIndex(i => i.id === id); - if (idx < 0) { console.error(`错误:找不到 id=${id}`); process.exit(1); } - - const [removed] = db.items.splice(idx, 1); - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - console.log(`🗑️ #${removed.id}「${removed.title}」已删除`); - printSummary(db); -} - -function cmdList(db) { - writeFileSync(HTML, buildHtml(db), 'utf8'); - printSummary(db); - console.log(`📄 HTML:todo/todo.html`); -} - -function cmdRender(db) { - writeFileSync(HTML, buildHtml(db), 'utf8'); - console.log('🖨️ todo/todo.html 已重新渲染'); -} - -// ─── 子任务命令 ─────────────────────────────────────────────────────────────── - -/** sub add --title "..." [--tier 1|2|3] [--deps "21A,21B"] */ -function cmdSubAdd(db, args) { - const parentIdRaw = args._[0]; - const title = (args.title || '').trim(); - if (!parentIdRaw) { console.error('错误:请指定父任务 id,例如:sub add 21 --title "..."'); process.exit(1); } - if (!title) { console.error('错误:--title 不能为空'); process.exit(1); } - - const parent = findItem(db, parentIdRaw); - if (!parent.subtasks) parent.subtasks = []; - - const letter = nextSubtaskLetter(parent); - if (!letter) { console.error('错误:子任务数量已达上限(26)'); process.exit(1); } - - const sid = `${parent.id}${letter}`; - const tierRaw = parseInt(args.tier, 10); - const tier = [1,2,3].includes(tierRaw) ? tierRaw : null; - const deps = args.deps - ? args.deps.split(',').map(d => d.trim().toUpperCase()).filter(Boolean) - : []; - - // 校验依赖的 sid 是否存在 - for (const dep of deps) { - const exists = parent.subtasks.find(s => s.sid === dep); - if (!exists) { console.error(`错误:依赖的子任务 ${dep} 不存在`); process.exit(1); } - } - - const sub = { sid, title, tier, deps, status: 'open', created_at: now() }; - parent.subtasks.push(sub); - - // 有子任务的父任务自动进入 doing - if (parent.status === 'open') { - parent.status = 'doing'; - } - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - const tierStr = tier ? ` [${TIER_LABEL[tier]}]` : ''; - const depsStr = deps.length ? ` 依赖: ${deps.join(', ')}` : ''; - console.log(`✅ 已添加子任务 ${sid}${tierStr}「${title}」→ #${parent.id}「${parent.title}」${depsStr}`); - printSummary(db); -} - -/** sub status */ -function cmdSubStatus(db, args) { - const sid = (args._[0] || '').toUpperCase(); - const newStatus = args._[1]; - - if (!['open','doing','done'].includes(newStatus)) { - console.error(`错误:无效状态 "${newStatus}",可选:open | doing | done`); - process.exit(1); - } - - const { parent, sub } = findSubtask(db, sid); - sub.status = newStatus; - - // 所有子任务完成时自动将父任务推进为 done(待验收) - if (newStatus === 'done' && allSubtasksDone(parent)) { - parent.status = 'done'; - parent.done = false; - console.log(`🎉 所有子任务均已完成,父任务 #${parent.id}「${parent.title}」自动转为「待验收」`); - } - - save(db); - writeFileSync(HTML, buildHtml(db), 'utf8'); - - const emoji = { open: '📋', doing: '🔨', done: '🔍' }[newStatus]; - console.log(`${emoji} ${sub.sid}「${sub.title}」→ ${STATUS_LABEL[newStatus]}`); - printSummary(db); -} - -/** sub ... */ -function cmdSub(db, args) { - const subCmd = args._[0]; - const subArgs = { ...args, _: args._.slice(1) }; - - switch (subCmd) { - case 'add': cmdSubAdd(db, subArgs); break; - case 'status': cmdSubStatus(db, subArgs); break; - default: - console.error(`未知子命令:sub ${subCmd}\n用法:\n node todo.mjs sub add --title "..." [--tier N] [--deps "21A,21B"]\n node todo.mjs sub status `); - process.exit(1); - } -} - -// ─── 入口 ──────────────────────────────────────────────────────────────────── - -const [,, subcmd, ...rest] = process.argv; -const db = load(); -const args = parseArgs(rest); - -switch (subcmd) { - case 'add': cmdAdd(db, args); break; - case 'status': cmdStatus(db, args); break; - case 'done': cmdDone(db, args); break; - case 'reject': cmdReject(db, args); break; - case 'reopen': cmdReopen(db, args); break; - case 'rm': cmdRm(db, args); break; - case 'sub': cmdSub(db, args); break; - case 'list': - case undefined: - case '': cmdList(db); break; - case 'render': cmdRender(db); break; - default: - console.error(`未知子命令:${subcmd}\n用法:node todo.mjs add|status|done|reject|reopen|rm|sub|list|render`); - process.exit(1); -}