
TodoCLI v2.0.1 完整可运行源码说明v2.0.1 基于 v2.0 修复并增强包含 JSON 存储 CLI 交互支持空格/等号两种参数格式完善注释与异常处理。目录结构├── .env.example # 环境变量模板 ├── package.json # 项目依赖 ├── tsconfig.json # TypeScript 配置 ├── data/ # 运行时自动生成 tasks.json ├── docs/ │ └── v2.0.md # 使用文档 └── src/ ├── main.ts # 程序入口 ├── config/ │ ├── env.ts # 环境配置加载 │ └── bootstrap.ts # 存储实例工厂 ├── core/ │ ├── types.ts # 核心类型定义 │ ├── errors.ts # 自定义异常 │ └── storage/ │ ├── base.ts # 存储抽象接口 │ ├── json.store.ts # JSON 文件存储实现 │ └── factory.ts # 存储工厂预留 ├── commands/ │ ├── index.ts # 命令注册表 │ ├── add.ts # 添加任务 │ ├── list.ts # 查看列表 │ ├── done.ts # 标记完成 │ ├── update.ts # 修改任务 │ ├── delete.ts # 删除任务 │ └── clear.ts # 清空已完成 ├── cli/ │ ├── parser.ts # 输入解析器 │ └── run.ts # 交互式终端 └── utils/ ├── logger.ts # 彩色日志 └── response.ts # API 返回封装预留1. .env.example# 运行模式 cli / api (v2.4才支持api) RUN_MODEcli # 存储模式 json/sqlite/drizzle-sqlite/drizzle-mysql STORAGE_MODEjson # 调试堆栈输出 true/false DEBUGfalse # SQLite 配置 v2.1使用 SQLITE_FILEdata/todo-sqlite.db # Drizzle SQLite v2.2使用 DRIZZLE_SQLITE_FILEdata/drizzle-sqlite.db # MySQL v2.3使用 MYSQL_HOST127.0.0.1 MYSQL_PORT3306 MYSQL_USERroot MYSQL_PASSWORD MYSQL_DBtodo2. package.json{ name: todo-cli, version: 2.0.1, type: module, scripts: { cli: bun src/main.ts }, dependencies: { dotenv: ^16.4.5 }, devDependencies: { types/bun: latest } }3. tsconfig.json{ compilerOptions: { target: ESNext, module: ESNext, moduleResolution: bundler, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, declaration: true, declarationMap: true, sourceMap: true, outDir: ./dist, rootDir: ./src }, include: [src/**/*] }4. src/main.ts/** * TodoCLI v2.0 程序入口 * * 启动流程 * 1. 从 .env 加载全局配置 * 2. 根据 storageMode 创建对应的存储实例 * 3. 根据 runMode 启动对应的服务v2.0 仅支持 cli */ import { loadConfig } from ./config/env; import { createStorage } from ./config/bootstrap; import { runCli } from ./cli/run; async function main() { const cfg loadConfig(); const storage await createStorage(cfg.storageMode); if (cfg.runMode cli) { await runCli(storage); } else { console.error(v2.0 仅支持 RUN_MODEcli); process.exit(1); } } main().catch((err) { console.error(程序启动失败, err); process.exit(1); });5. src/config/env.ts/** * 环境配置模块 * * 职责 * - 读取 .env 文件并注入 process.env * - 提供类型安全的配置对象 * - 所有配置项均有默认值.env 仅需覆盖要修改的项 * * 配置优先级临时环境变量 .env 代码默认值 */ import dotenv from dotenv; import path from path; dotenv.config({ path: path.resolve(.env) }); /** 全局配置结构定义覆盖所有版本的配置项 */ export type Config { /** 运行模式cli终端交互| apiHTTP 服务v2.4 启用 */ runMode: cli | api; /** 存储模式json | sqlite | drizzle-sqlite | drizzle-mysql */ storageMode: json | sqlite | drizzle-sqlite | drizzle-mysql; /** 是否输出完整错误堆栈 */ debug: boolean; /** SQLite 数据库文件路径v2.1 使用 */ sqliteFile: string; /** Drizzle SQLite 数据库文件路径v2.2 使用 */ drizzleSqliteFile: string; /** MySQL 连接配置v2.3 使用 */ mysql: { host: string; port: number; user: string; password: string; db: string; }; }; /** * 加载并返回完整的配置对象 * 从 process.env 读取缺失项使用默认值 */ export function loadConfig(): Config { return { runMode: (process.env.RUN_MODE as Config[runMode]) || cli, storageMode: (process.env.STORAGE_MODE as Config[storageMode]) || json, debug: process.env.DEBUG true, sqliteFile: process.env.SQLITE_FILE || data/todo-sqlite.db, drizzleSqliteFile: process.env.DRIZZLE_SQLITE_FILE || data/drizzle-sqlite.db, mysql: { host: process.env.MYSQL_HOST || 127.0.0.1, port: Number(process.env.MYSQL_PORT || 3306), user: process.env.MYSQL_USER || root, password: process.env.MYSQL_PASSWORD || , db: process.env.MYSQL_DB || todo, }, }; }6. src/config/bootstrap.ts/** * 存储实例工厂 * * 根据配置的存储模式创建对应的 TaskStorage 实现 * v2.0 仅支持 json其他模式预留分支供后续版本扩展 * * 设计原则单进程互斥一次只启用一种存储 */ import type { TaskStorage } from ../core/storage/base; import { JsonStorage } from ../core/storage/json.store; import { StorageError } from ../core/errors; /** * 创建存储实例 * param mode 存储模式对应 STORAGE_MODE 配置项 * returns TaskStorage 实例 * throws StorageError 不支持的存储模式 */ export async function createStorage(mode: string): PromiseTaskStorage { switch (mode) { case json: return new JsonStorage(); // v2.1: case sqlite: return new SqliteStorage(config); // v2.2: case drizzle-sqlite: return new DrizzleSqliteStorage(config); // v2.3: case drizzle-mysql: return new DrizzleMysqlStorage(config); case sqlite: case drizzle-sqlite: case drizzle-mysql: throw new StorageError(v2.0 不支持存储模式 ${mode}请切换为 json); default: throw new StorageError(非法存储模式: ${mode}); } }7. src/core/types.ts/** * 核心类型定义 * * 定义领域实体和数据传输结构所有层共享这些类型 */ /** 待办任务领域实体 */ export type Task { /** 自增主键 */ id: number; /** 任务标题 */ title: string; /** 优先级low / medium / high */ priority: low | medium | high; /** 状态active待办/ done已完成 */ status: active | done; /** 创建时间ISO 8601 格式 */ createAt: string; }; /** * JSON 存储的原子容器 * 整个文件作为一个整体读写保证数据一致性 */ export type TaskStore { /** 任务列表 */ list: Task[]; /** 下一个可用的自增 ID */ nextId: number; }; /** CLI 命令统一参数结构所有可选字段由各命令自行校验 */ export type CliParams { /** 任务标题add / update 使用 */ title?: string; /** 任务 IDdone / update / delete 使用 */ id?: number; /** 优先级add / update 使用 */ priority?: Task[priority]; };8. src/core/errors.ts/** * 自定义异常体系 * * 按错误来源分三层CLI 层根据异常类型输出不同级别的日志 * - ParamError / BizError → warn用户可修正 * - StorageError → error系统级故障 */ /** 参数校验错误缺少必填参数、参数值不合法等 */ export class ParamError extends Error { constructor(msg: string) { super(msg); this.name ParamError; } } /** 业务逻辑错误操作对象不存在、状态冲突等 */ export class BizError extends Error { constructor(msg: string) { super(msg); this.name BizError; } } /** 存储层错误文件读写失败、数据库连接异常等 */ export class StorageError extends Error { constructor(msg: string) { super(msg); this.name StorageError; } }9. src/core/storage/base.ts/** * 存储抽象接口 * * 所有存储实现JsonStorage / SqliteStorage / DrizzleStorage 等 * 必须实现此接口确保上层代码与存储解耦 * * 方法分两组 * - fullLoad / fullSave整体读写用于 JSON 原子操作 * - findAll ~ clearCompleted细粒度 CRUD各实现可优化为数据库原生操作 */ import type { Task, TaskStore } from ../types; export interface TaskStorage { /** 整体加载读取完整的 TaskStoreJSON 模式下为全文件读取 */ fullLoad: () PromiseTaskStore; /** 整体保存写入完整的 TaskStoreJSON 模式下为全文件写入 */ fullSave: (store: TaskStore) Promisevoid; /** 查询任务列表可按状态筛选 */ findAll: (status?: Task[status]) PromiseTask[]; /** 根据 ID 查询单个任务不存在返回 null */ findOne: (id: number) PromiseTask | null; /** 创建任务返回包含自增 ID 的完整任务对象 */ create: (task: OmitTask, id) PromiseTask; /** 更新任务返回是否成功false 表示 ID 不存在 */ update: (id: number, patch: PartialOmitTask, id) Promiseboolean; /** 删除任务返回是否成功false 表示 ID 不存在 */ remove: (id: number) Promiseboolean; /** 清空所有已完成任务返回删除数量 */ clearCompleted: () Promisenumber; }10. src/core/storage/json.store.ts/** * JSON 文件存储实现 * * 将任务数据持久化为 data/tasks.json 文件 * 采用整体读写策略每次操作都读取完整文件 → 修改 → 写回 * 适合小数据量场景无需数据库依赖 * * 关键设计 * - initFile() 确保文件存在且内容合法不存在或损坏时自动重建 * - ready Promise 保证初始化完成后才允许读写避免竞态条件 */ import fs from fs/promises; import path from path; import { TaskStorage } from ./base; import { Task, TaskStore } from ../types; import { StorageError } from ../errors; /** 数据文件固定路径 */ const FILE_PATH path.resolve(data/tasks.json); /** * 初始化存储文件 * 尝试读取并解析 JSON失败文件不存在/为空/格式损坏则重建空存储 */ async function initFile() { try { const raw await fs.readFile(FILE_PATH, utf8); JSON.parse(raw); // 验证 JSON 格式合法性 } catch { // 文件不存在、为空、或 JSON 格式损坏重建 await fs.mkdir(path.dirname(FILE_PATH), { recursive: true }); const empty: TaskStore { list: [], nextId: 1 }; await fs.writeFile(FILE_PATH, JSON.stringify(empty, null, 2), utf8); } } export class JsonStorage implements TaskStorage { /** 初始化 Promise所有读写操作必须等待此 Promise 完成 */ private ready: Promisevoid; constructor() { this.ready initFile(); } /** * 整体加载读取 tasks.json 并解析为 TaskStore * throws StorageError 文件读取或解析失败 */ async fullLoad(): PromiseTaskStore { await this.ready; try { const raw await fs.readFile(FILE_PATH, utf8); return JSON.parse(raw) as TaskStore; } catch (e) { throw new StorageError(读取tasks.json失败: (e as Error).message); } } /** * 整体保存将 TaskStore 序列化后写入 tasks.json * throws StorageError 文件写入失败 */ async fullSave(store: TaskStore): Promisevoid { await this.ready; try { await fs.writeFile(FILE_PATH, JSON.stringify(store, null, 2), utf8); } catch (e) { throw new StorageError(写入tasks.json失败: (e as Error).message); } } /** * 查询任务列表 * param status 可选按状态筛选active/done不传则返回全部 */ async findAll(status?: Task[status]): PromiseTask[] { const store await this.fullLoad(); if (!status) return store.list; return store.list.filter((t) t.status status); } /** * 根据 ID 查询单个任务 * returns 匹配的任务对象不存在返回 null */ async findOne(id: number): PromiseTask | null { const store await this.fullLoad(); return store.list.find((t) t.id id) || null; } /** * 创建新任务 * 自动分配自增 ID写入后返回完整任务对象 */ async create(task: OmitTask, id): PromiseTask { const store await this.fullLoad(); const newTask: Task { ...task, id: store.nextId }; store.list.push(newTask); store.nextId 1; await this.fullSave(store); return newTask; } /** * 更新任务的指定字段 * param id 目标任务 ID * param patch 要更新的字段不含 id * returns 是否更新成功false 表示 ID 不存在 */ async update(id: number, patch: PartialOmitTask, id): Promiseboolean { const store await this.fullLoad(); const target store.list.find((t) t.id id); if (!target) return false; Object.assign(target, patch); await this.fullSave(store); return true; } /** * 删除指定任务 * returns 是否删除成功false 表示 ID 不存在 */ async remove(id: number): Promiseboolean { const store await this.fullLoad(); const oldLen store.list.length; store.list store.list.filter((t) t.id ! id); if (store.list.length oldLen) return false; await this.fullSave(store); return true; } /** * 清空所有已完成status done的任务 * returns 实际删除的任务数量 */ async clearCompleted(): Promisenumber { const store await this.fullLoad(); const before store.list.length; store.list store.list.filter((t) t.status ! done); const clearCount before - store.list.length; await this.fullSave(store); return clearCount; } }11. src/core/storage/factory.ts/** * 存储工厂预留 * * v2.0 的存储实例化逻辑在 bootstrap.ts 中实现 * 此文件预留为 v2.1~v2.3 扩展时的工厂模式入口 */12. src/commands/index.ts/** * 命令注册表 * * 将所有 CLI 命令函数注册到统一的 cmdMap 中 * cli/run.ts 根据用户输入的命令名从 cmdMap 查找并执行对应函数 * * 扩展新命令只需 * 1. 在 src/commands/ 下新建命令文件 * 2. 在此文件中 import 并注册到 cmdMap */ import type { TaskStorage } from ../core/storage/base; import { addCmd } from ./add; import { listCmd } from ./list; import { doneCmd } from ./done; import { updateCmd } from ./update; import { deleteCmd } from ./delete; import { clearCmd } from ./clear; /** 命令函数签名接收存储实例和解析后的参数 */ export type CommandFn (storage: TaskStorage, params: Recordstring, any) Promisevoid; /** 命令名 → 命令函数的映射表 */ export const cmdMap: Recordstring, CommandFn { add: addCmd, list: listCmd, done: doneCmd, update: updateCmd, delete: deleteCmd, clear: clearCmd, };13. src/commands/add.ts/** * 添加任务命令 * * 用法add --title 任务标题 --priority low/medium/high * - --title 必填任务标题 * - --priority 可选默认 medium */ import type { TaskStorage } from ../core/storage/base; import { ParamError } from ../core/errors; import { logger } from ../utils/logger; import type { CliParams } from ../core/types; export async function addCmd(storage: TaskStorage, params: CliParams) { if (!params.title) throw new ParamError(必须传入 --title 任务标题); const priority params.priority || medium; if (![low, medium, high].includes(priority)) { throw new ParamError(优先级仅支持 low / medium / high); } const task await storage.create({ title: params.title, priority, status: active, createAt: new Date().toISOString(), }); logger.success(创建任务成功ID: ${task.id}); }14. src/commands/list.ts/** * 查看任务列表命令 * * 用法list * 显示所有任务无任务时提示暂无待办任务 */ import type { TaskStorage } from ../core/storage/base; import { logger } from ../utils/logger; export async function listCmd(storage: TaskStorage) { const list await storage.findAll(); if (list.length 0) { logger.info(暂无待办任务); return; } logger.info( 全部任务列表 ); for (const t of list) { logger.info([${t.id}] ${t.title} | 优先级:${t.priority} | 状态:${t.status}); } }15. src/commands/done.ts/** * 标记任务完成命令 * * 用法done --id 任务ID * 将指定任务的 status 更新为 done */ import type { TaskStorage } from ../core/storage/base; import { ParamError, BizError } from ../core/errors; import { logger } from ../utils/logger; import type { CliParams } from ../core/types; export async function doneCmd(storage: TaskStorage, params: CliParams) { const id params.id; if (!id) throw new ParamError(必须传入 --id); const exist await storage.findOne(id); if (!exist) throw new BizError(ID ${id} 任务不存在); await storage.update(id, { status: done }); logger.success(任务 ${id} 已标记完成); }16. src/commands/update.ts/** * 修改任务命令 * * 用法update --id 任务ID --title 新标题 --priority 新优先级 * - --id 必填 * - --title / --priority 至少传入一项 */ import type { TaskStorage } from ../core/storage/base; import { ParamError, BizError } from ../core/errors; import { logger } from ../utils/logger; import type { CliParams } from ../core/types; export async function updateCmd(storage: TaskStorage, params: CliParams) { const id params.id; if (!id) throw new ParamError(必须传入 --id); const exist await storage.findOne(id); if (!exist) throw new BizError(ID ${id} 任务不存在); const patch: Recordstring, any {}; if (params.title) patch.title params.title; if (params.priority) { if (![low, medium, high].includes(params.priority)) { throw new ParamError(优先级仅支持 low / medium / high); } patch.priority params.priority; } if (Object.keys(patch).length 0) { throw new ParamError(至少传入 title / priority 其中一项); } await storage.update(id, patch); logger.success(任务 ${id} 修改成功); }17. src/commands/delete.ts/** * 删除任务命令 * * 用法delete --id 任务ID * 永久删除指定任务不可恢复 */ import type { TaskStorage } from ../core/storage/base; import { ParamError, BizError } from ../core/errors; import { logger } from ../utils/logger; import type { CliParams } from ../core/types; export async function deleteCmd(storage: TaskStorage, params: CliParams) { const id params.id; if (!id) throw new ParamError(必须传入 --id); const ok await storage.remove(id); if (!ok) throw new BizError(ID ${id} 任务不存在); logger.success(任务 ${id} 已删除); }18. src/commands/clear.ts/** * 清空已完成任务命令 * * 用法clear * 删除所有 status 为 done 的任务返回删除数量 */ import type { TaskStorage } from ../core/storage/base; import { logger } from ../utils/logger; export async function clearCmd(storage: TaskStorage) { const count await storage.clearCompleted(); logger.success(清空已完成任务 ${count} 条); }19. src/cli/parser.ts/** * CLI 输入解析器 * * 将用户输入的字符串解析为命令名 参数对象 * 支持两种参数格式 * 1. --keyvalue等号分隔 * 2. --key value空格分隔 * * 解析规则 * - 第一个词作为命令名 * - 以 -- 开头的词作为参数 key * - 紧跟其后的非 -- 开头词作为 value空格分隔模式 * - 或按 分割为 key/value等号分隔模式 * - 数字类型的 value 自动转为 number * * 示例 * add --title 学习TS --priorityhigh * → { cmd: add, params: { title: 学习TS, priority: high } } */ export function parseCliInput(input: string): { cmd: string; params: Recordstring, string | number | boolean } { const parts input.trim().split(/\s/).filter(Boolean); if (parts.length 0) return { cmd: , params: {} }; const cmd parts[0]; const params: Recordstring, string | number | boolean {}; let i 1; while (i parts.length) { const part parts[i]; if (part.startsWith(--)) { // 等号分隔--keyvalue if (part.includes()) { const [k, v] part.slice(2).split(); let val: string | number v; if (!isNaN(Number(v))) val Number(v); params[k] val; i; } // 空格分隔--key value else { const key part.slice(2); const next parts[i 1]; if (next !next.startsWith(--)) { let val: string | number next; if (!isNaN(Number(next))) val Number(next); params[key] val; i 2; } else { // 没有 value当作布尔标志 params[key] true; i; } } } else { i; } } return { cmd, params }; }20. src/cli/run.ts/** * CLI 交互式终端 * * 使用 Node.js readline 创建交互式命令行界面 * 循环流程显示提示符 → 读取输入 → 解析 → 执行命令 → 处理异常 → 重新显示提示符 * * 内置命令help显示帮助、exit退出程序 * 业务命令从 cmdMap 中查找并执行 * * 错误处理策略 * - ParamError / BizError → 黄色警告用户可修正 * - StorageError → 红色错误系统级问题 * - 其他异常 → 红色错误 可选堆栈由 DEBUG 配置控制 */ import readline from readline; import type { TaskStorage } from ../core/storage/base; import { parseCliInput } from ./parser; import { cmdMap } from ../commands; import { ParamError, BizError, StorageError } from ../core/errors; import { logger } from ../utils/logger; import { loadConfig } from ../config/env; /** * 启动交互式 CLI * param storage 存储实例由 bootstrap 创建并注入 */ export async function runCli(storage: TaskStorage) { const cfg loadConfig(); const rl readline.createInterface({ input: process.stdin, output: process.stdout, prompt: todo , }); logger.info(TodoCLI v2.0 启动完成输入 help 查看指令exit 退出程序); rl.prompt(); rl.on(line, async (line) { const { cmd, params } parseCliInput(line); try { // 空输入或 exit → 关闭终端 if (cmd || cmd exit) { rl.close(); return; } // 内置 help 命令 if (cmd help) { logger.info(可用指令); logger.info(add --title xxx --priority low/medium/high); logger.info(list); logger.info(done --id 数字); logger.info(update --id 数字 --title xxx); logger.info(delete --id 数字); logger.info(clear); logger.info(exit 退出); } // 业务命令从注册表查找 else if (cmdMap[cmd]) { await cmdMap[cmd](storage, params); } // 未知命令 else { logger.warn(未知命令 ${cmd}输入 help 查看支持指令); } } catch (err) { // 按异常类型分级输出 if (err instanceof ParamError || err instanceof BizError) { logger.warn(err.message); } else if (err instanceof StorageError) { logger.error(err.message); } else { logger.error(未知异常 (err as Error).message); if (cfg.debug) console.error((err as Error).stack); } } rl.prompt(); }); rl.on(close, () { logger.info(程序退出); process.exit(0); }); }21. src/utils/logger.ts/** * 彩色分级日志工具 * * 使用 ANSI 转义码为终端输出着色区分不同级别的日志 * - info青色普通信息 * - success绿色操作成功 * - warn黄色警告参数错误、业务异常 * - error红色错误存储故障、未知异常 */ const color { info: \x1b[36m, success: \x1b[32m, warn: \x1b[33m, error: \x1b[31m, reset: \x1b[0m, }; export const logger { info: (msg: string) console.log(${color.info}${msg}${color.reset}), success: (msg: string) console.log(${color.success}${msg}${color.reset}), warn: (msg: string) console.log(${color.warn}${msg}${color.reset}), error: (msg: string) console.log(${color.error}${msg}${color.reset}), };22. src/utils/response.ts/** * API 标准返回封装预留 * * v2.4 启用 Hono HTTP API 时统一所有接口的返回格式 * { code: number, message: string, data: any } * * v2.0 仅 CLI 模式此文件暂不实现 */快速启动# 1. 安装依赖 bun install # 2. 复制环境配置 cp .env.example .env # 3. 启动程序 bun cliCLI 指令示例# 添加任务 add --title 学习TypeScript --priority high # 查看全部任务 list # 标记完成 done --id 1 # 修改任务 update --id 1 --title 学习TypeScript进阶 # 删除任务 delete --id 1 # 清空已完成任务 clear # 查看帮助 help # 退出程序 exit