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_MODE=cli
# 存储模式 json/sqlite/drizzle-sqlite/drizzle-mysql
STORAGE_MODE=json
# 调试堆栈输出 true/false
DEBUG=false
# SQLite 配置 v2.1使用
SQLITE_FILE=data/todo-sqlite.db
# Drizzle SQLite v2.2使用
DRIZZLE_SQLITE_FILE=data/drizzle-sqlite.db
# MySQL v2.3使用
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DB=todo
2. 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_MODE=cli");
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(终端交互)| api(HTTP 服务,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): Promise<TaskStorage> {
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;
/** 任务 ID(done / 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 {
/** 整体加载:读取完整的 TaskStore(JSON 模式下为全文件读取) */
fullLoad: () => Promise<TaskStore>;
/** 整体保存:写入完整的 TaskStore(JSON 模式下为全文件写入) */
fullSave: (store: TaskStore) => Promise<void>;
/** 查询任务列表,可按状态筛选 */
findAll: (status?: Task["status"]) => Promise<Task[]>;
/** 根据 ID 查询单个任务,不存在返回 null */
findOne: (id: number) => Promise<Task | null>;
/** 创建任务,返回包含自增 ID 的完整任务对象 */
create: (task: Omit<Task, "id">) => Promise<Task>;
/** 更新任务,返回是否成功(false 表示 ID 不存在) */
update: (id: number, patch: Partial<Omit<Task, "id">>) => Promise<boolean>;
/** 删除任务,返回是否成功(false 表示 ID 不存在) */
remove: (id: number) => Promise<boolean>;
/** 清空所有已完成任务,返回删除数量 */
clearCompleted: () => Promise<number>;
}
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: Promise<void>;
constructor() {
this.ready = initFile();
}
/**
* 整体加载:读取 tasks.json 并解析为 TaskStore
* @throws StorageError 文件读取或解析失败
*/
async fullLoad(): Promise<TaskStore> {
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): Promise<void> {
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"]): Promise<Task[]> {
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): Promise<Task | null> {
const store = await this.fullLoad();
return store.list.find((t) => t.id === id) || null;
}
/**
* 创建新任务
* 自动分配自增 ID,写入后返回完整任务对象
*/
async create(task: Omit<Task, "id">): Promise<Task> {
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: Partial<Omit<Task, "id">>): Promise<boolean> {
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): Promise<boolean> {
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(): Promise<number> {
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: Record<string, any>) => Promise<void>;
/** 命令名 → 命令函数的映射表 */
export const cmdMap: Record<string, 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: Record<string, 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. --key=value(等号分隔)
* 2. --key value(空格分隔)
*
* 解析规则:
* - 第一个词作为命令名
* - 以 -- 开头的词作为参数 key
* - 紧跟其后的非 -- 开头词作为 value(空格分隔模式)
* - 或按 = 分割为 key/value(等号分隔模式)
* - 数字类型的 value 自动转为 number
*
* 示例:
* "add --title 学习TS --priority=high"
* → { cmd: "add", params: { title: "学习TS", priority: "high" } }
*/
export function parseCliInput(input: string): { cmd: string; params: Record<string, string | number | boolean> } {
const parts = input.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return { cmd: "", params: {} };
const cmd = parts[0];
const params: Record<string, string | number | boolean> = {};
let i = 1;
while (i < parts.length) {
const part = parts[i];
if (part.startsWith("--")) {
// 等号分隔:--key=value
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 cli
CLI 指令示例
# 添加任务
add --title 学习TypeScript --priority high
# 查看全部任务
list
# 标记完成
done --id 1
# 修改任务
update --id 1 --title 学习TypeScript进阶
# 删除任务
delete --id 1
# 清空已完成任务
clear
# 查看帮助
help
# 退出程序
exit