diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94038d2..94ccae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,20 @@ name: CI on: push: - branches: [main, master] + branches: [main] pull_request: - branches: [main, master] + branches: [main] jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20' + node-version: ${{ matrix.node-version }} cache: 'npm' - - run: npm ci || npm install - - run: npm run build || true - - run: npm test || true + - run: npm ci + - run: npm run build diff --git a/.gitignore b/.gitignore index 6720b04..dd6e803 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/README.md b/README.md index 9681433..4f62985 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,147 @@ # cace-timer +[![npm version](https://img.shields.io/npm/v/@cacinie/cace-timer.svg)](https://www.npmjs.com/package/@cacinie/cace-timer) +[![license](https://img.shields.io/npm/l/@cacinie/cace-timer.svg)](https://github.com/CacinieP/cace-timer/blob/main/LICENSE) + A minimal time tracking CLI with cute anime girl mascot. -Built with TypeScript. A simple and lightweight time management tool for the terminal. +``` +npm install -g @cacinie/cace-timer +``` + +``` + ▄▄▄▄▄▄▄▄▄▄▄▄ + █░░░░░░░░░░░░█ + █░▄▄▄▄▄▄▄▄▄░█ + █░│ ● ● │░█ + █░│ ▽ │░█ + █░│ ─── │░█ + ╰────────────╯ + CACE TIMER +``` + +## Quick Start + +```bash +# Start a task +tk start "写周报" --tag work --estimate 30 + +# Mark progress +tk mark "完成数据分析" + +# Check status +tk status + +# Stop and get efficiency score +tk stop +``` + +## Commands + +### `tk start [options]` + +Start a new task. + +| Option | Description | +|--------|-------------| +| `--tag ` | Add a tag | +| `--estimate ` | Estimated duration (for efficiency score) | + +```bash +tk start "开发登录功能" --tag coding --estimate 60 +``` + +### `tk mark ` + +Record a time checkpoint. + +```bash +tk mark "完成API接口" +tk mark "开始写测试" +``` + +### `tk stop` + +Stop current task, show summary and efficiency score. + +| Score | Emoji | Meaning | +|-------|-------|---------| +| 100% | 🏆 | Actual ≤ Estimate | +| 80-99% | 💪 | Slightly over | +| <50% | 💀 | Far over estimate | + +### `tk status` + +Show current running task. + +### `tk list [options]` + +View history. + +| Option | Description | +|--------|-------------| +| `--today` | Today only | +| `--tag ` | Filter by tag | +| `--limit ` | Limit count (default 10) | + +```bash +tk list --today +tk list --tag coding --limit 20 +``` + +### `tk search ` + +Search across task names, tags, and mark notes. + +```bash +tk search "登录" +``` + +### `tk sync ` + +Set sync file path for cross-device sync (Dropbox / iCloud / OneDrive). + +```bash +tk sync ~/Dropbox/cace-timer.json +``` + +### `tk help` + +Show help. + +## CACE Mascot + +CACE reacts to what you do: + +| State | Eyes | Mouth | When | +|-------|------|-------|------| +| Normal | ● ● | ─── | Default | +| Happy | ★ ★ | ◡◡◡ | Task completed | +| Sleepy | ─ ─ | ─── | No active task | +| Blink | ─ ─ | ─── | Animation frame | + +## Data + +All data is stored in `~/.cace-timer.json`. Single JSON file, easy to backup and sync. + +## Changelog + +### v1.1.0 + +- Restructured repo layout (flat structure, no nested `timekeeper/`) +- Fixed `.gitignore` (dist/ was not properly ignored) +- Fixed CI workflow (was running in wrong directory) +- Added Node 18/20/22 matrix in CI +- Added `engines` field in package.json + +### v1.0.0 + +- Initial release +- start / mark / stop / status / list / search / sync commands +- CACE mascot animation +- Efficiency scoring +- Cloud sync support ## License -MIT \ No newline at end of file +MIT diff --git a/timekeeper/package-lock.json b/package-lock.json similarity index 98% rename from timekeeper/package-lock.json rename to package-lock.json index 90d6633..2f030f8 100644 --- a/timekeeper/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "cace-timer", - "version": "1.0.0", + "name": "@cacinie/cace-timer", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cace-timer", - "version": "1.0.0", + "name": "@cacinie/cace-timer", + "version": "1.1.0", "license": "MIT", "bin": { "cace-timer": "dist/index.js", @@ -16,6 +16,9 @@ "@types/node": "^20.10.0", "ts-node": "^10.9.2", "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index 2c07fb3..aa67e4b 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,39 @@ { - "name": "timekeeper-260302", - "version": "1.0.0", - "description": "", - "main": "index.js", + "name": "@cacinie/cace-timer", + "version": "1.1.0", + "description": "A minimal time tracking CLI with cute anime girl mascot", + "main": "dist/index.js", + "bin": { + "tk": "dist/index.js", + "cace-timer": "dist/index.js" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc", + "dev": "ts-node src/index.ts", + "start": "node dist/index.js", + "prepublishOnly": "npm run build" }, + "files": ["dist", "README.md", "LICENSE"], + "keywords": ["time", "tracker", "cli", "timer", "productivity", "anime", "mascot"], + "author": "CacinieP ", + "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/CacinieP/cace-timer.git" }, - "keywords": [], - "author": "", - "license": "ISC", "bugs": { "url": "https://github.com/CacinieP/cace-timer/issues" }, - "homepage": "https://github.com/CacinieP/cace-timer#readme" + "homepage": "https://github.com/CacinieP/cace-timer#readme", + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "ts-node": "^10.9.2", + "typescript": "^5.3.0" + } } diff --git a/timekeeper/src/index.ts b/src/index.ts similarity index 100% rename from timekeeper/src/index.ts rename to src/index.ts diff --git a/timekeeper/README.md b/timekeeper/README.md deleted file mode 100644 index 17b21fd..0000000 --- a/timekeeper/README.md +++ /dev/null @@ -1,333 +0,0 @@ -# CACE TIMER - -一个极简的时间记录 CLI 工具,配有可爱的短发小女孩动画吉祥物。 - -## 安装 - -```bash -cd timekeeper -npm link -``` - -安装后全局命令 `tk` 即可使用。 - -## 快速开始 - -```bash -# 开始一个任务 -tk start "写周报" --tag work --estimate 30 - -# 记录进度节点 -tk mark "完成数据分析" - -# 查看当前状态 -tk status - -# 结束任务(显示效率评分) -tk stop -``` - -## 命令一览 - -### tk start - -开始新任务。 - -```bash -tk start <任务名> [选项] - -选项: - --tag <标签> 添加标签 - --estimate <分钟> 预估时长(用于计算效率分) -``` - -示例: -```bash -tk start "开发登录功能" --tag coding --estimate 60 -``` - -### tk mark - -记录时间节点。 - -```bash -tk mark <备注> -``` - -示例: -```bash -tk mark "完成API接口" -tk mark "开始写测试" -``` - -### tk stop - -结束当前任务,显示总结和效率评分。 - -```bash -tk stop -``` - -效率评分规则: -- 实际时长 ≤ 预估时长 → 100%(🏆 优秀) -- 实际时长稍超预估 → 80-99%(💪 良好) -- 实际时长远超预估 → <50%(💀 需改进) - -### tk status - -查看当前进行中的任务状态。 - -```bash -tk status -``` - -### tk list - -查看历史记录。 - -```bash -tk list [选项] - -选项: - --today 仅显示今天 - --tag <标签> 按标签筛选 - --limit <数量> 限制显示数量(默认10) -``` - -示例: -```bash -tk list --today -tk list --tag coding --limit 20 -``` - -### tk search - -搜索历史记录。 - -```bash -tk search <关键词> -``` - -会搜索任务名、标签、标记备注。 - -```bash -tk search "登录" -tk search "coding" -``` - -### tk sync - -设置同步文件路径,实现跨设备同步。 - -```bash -tk sync <文件路径> -``` - -建议将同步文件放在云盘目录: -```bash -tk sync ~/Dropbox/cace-timer.json -tk sync ~/OneDrive/cace-timer.json -``` - -## 数据同步指南 - -### 工作原理 - -CACE TIMER 采用**单文件同步**策略: - -``` -┌─────────────┐ ┌─────────────┐ -│ 设备 A │ │ 设备 B │ -│ ~/(本地) │ │ ~/(本地) │ -│ .cace-timer │ │ .cace-timer │ -│ .json │ │ .json │ -└──────┬──────┘ └──────┬──────┘ - │ │ - ▼ ▼ -┌─────────────────────────────────────┐ -│ 云盘同步目录 │ -│ Dropbox / iCloud / OneDrive │ -│ cace-timer.json │ -└─────────────────────────────────────┘ -``` - -每次执行 `tk` 命令时: -1. 写入本地 `~/.cace-timer.json` -2. 同时写入配置的 `syncPath`(如果设置了) - -### 设置步骤 - -#### Dropbox - -```bash -# macOS / Linux -tk sync ~/Dropbox/cace-timer.json - -# Windows -tk sync "%USERPROFILE%\Dropbox\cace-timer.json" -``` - -#### iCloud - -```bash -# macOS -tk sync ~/Library/Mobile\ Documents/com~apple~CloudDocs/cace-timer.json -``` - -#### OneDrive - -```bash -# macOS -tk sync ~/OneDrive/cace-timer.json - -# Windows -tk sync "%USERPROFILE%\OneDrive\cace-timer.json" -``` - -#### 自建同步 (Syncthing / Resilio) - -```bash -tk sync ~/Sync/cace-timer.json -``` - -### 多设备使用 - -**首次在设备 B 使用:** - -```bash -# 1. 安装 CACE TIMER -cd timekeeper && npm link - -# 2. 设置同步路径(指向云盘已同步的文件) -tk sync ~/Dropbox/cace-timer.json - -# 3. 查看同步的数据 -tk list -``` - -**注意:** 新设备需要先设置 `tk sync`,然后才能读取云盘数据。 - -### 同步冲突处理 - -由于采用单文件策略,建议: - -| 场景 | 建议 | -|------|------| -| 多设备同时使用 | 避免同时在多个设备操作 | -| 云盘同步延迟 | 操作后等待几秒再切换设备 | -| 冲突文件 | 手动合并 JSON,保留需要的记录 | - -### 手动备份 - -```bash -# 导出备份 -cp ~/.cace-timer.json ~/backup/cace-timer-$(date +%Y%m%d).json - -# 恢复备份 -cp ~/backup/cace-timer-20240302.json ~/.cace-timer.json -``` - -### 数据格式 - -同步文件为标准 JSON 格式: - -```json -{ - "syncPath": "/Users/you/Dropbox/cace-timer.json", - "current": null, - "history": [ - { - "id": "lq3x9k2", - "task": "开发功能", - "start": "2024-03-02T10:00:00.000Z", - "end": "2024-03-02T11:30:00.000Z", - "tags": ["coding"], - "marks": [ - { "time": "2024-03-02T10:30:00.000Z", "note": "完成API" } - ], - "estimatedMinutes": 60 - } - ] -} -``` - -### 取消同步 - -编辑 `~/.cace-timer.json`,删除 `syncPath` 字段即可。 - ---- - -### tk help - -显示帮助信息。 - -```bash -tk help -``` - -## CACE 吉祥物 - -CACE 是一个可爱的短发小女孩头像: - -``` - ▄▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░░█ ← 刘海 - █░▄▄▄▄▄▄▄▄▄░█ - █░│ ● ● │░█ ← 大眼睛 - █░│ ▽ │░█ ← 小鼻子 - █░│ ─── │░█ ← 嘴巴 - ╰───────────╯ -``` - -**动画表情:** - -| 状态 | 眼睛 | 嘴巴 | 触发场景 | -|------|------|------|----------| -| 正常 | ● ● | ─── | 默认状态 | -| 开心 | ★ ★ | ◡◡◡ | 完成任务 | -| 眨眼 | ─ ─ | ─── | 动画帧 | -| 超开心 | ◉ ◉ | ▽△▽ | 动画帧 | -| 困倦 | ─ ─ | ─── | 无任务时 | - -动画循环:正常 → 眨眼 → 开心 → 超开心(循环2次) - -## 数据存储 - -数据保存在 `~/.cace-timer.json`: - -```json -{ - "syncPath": "~/Dropbox/cace-timer.json", - "current": null, - "history": [ - { - "id": "xxx", - "task": "写周报", - "start": "2024-03-02T10:00:00Z", - "end": "2024-03-02T10:30:00Z", - "tags": ["work"], - "marks": [ - { "time": "2024-03-02T10:15:00Z", "note": "完成数据分析" } - ], - "estimatedMinutes": 30 - } - ] -} -``` - -## 使用场景 - -- **编程开发**:记录编码时间,评估效率 -- **学习复习**:跟踪学习时长和进度 -- **项目管理**:标记关键节点,回顾时间分配 -- **自由职业**:为客户项目计时 - -## 卸载 - -```bash -npm unlink -g cace-timer -``` - -## License - -MIT diff --git a/timekeeper/dist/index.d.ts b/timekeeper/dist/index.d.ts deleted file mode 100644 index b798801..0000000 --- a/timekeeper/dist/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -export {}; diff --git a/timekeeper/dist/index.js b/timekeeper/dist/index.js deleted file mode 100644 index 921f753..0000000 --- a/timekeeper/dist/index.js +++ /dev/null @@ -1,521 +0,0 @@ -#!/usr/bin/env node -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); -const os = __importStar(require("os")); -// ============ CACE TIMER Animation ============ -// 可爱短发小女孩头像 -const CACE_FRAMES = [ - // Frame 1: 正常 - ` - ▄▄▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░░░█ - █░▄▄▄▄▄▄▄▄▄░█ - █░│ ● ● │░█ - █░│ ▽ │░█ - █░│ ─── │░█ - ╰────────────╯ -`, - // Frame 2: 眨眼 - ` - ▄▄▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░░░█ - █░▄▄▄▄▄▄▄▄▄░█ - █░│ ─ ─ │░█ - █░│ ▽ │░█ - █░│ ─── │░█ - ╰────────────╯ -`, - // Frame 3: 开心 - ` - ▄▄▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░░░█ - █░▄▄▄▄▄▄▄▄▄░█ - █░│ ★ ★ │░█ - █░│ ▽ │░█ - █░│ ◡◡◡ │░█ - ╰────────────╯ -`, - // Frame 4: 超开心 - ` - ▄▄▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░░░█ - █░▄▄▄▄▄▄▄▄▄░█ - █░│ ◉ ◉ │░█ - █░│ ▽ │░█ - █░│ ▽△▽ │░█ - ╰────────────╯ -`, -]; -const CACE_SMALL = ` - ▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░█ - █░▄▄▄▄▄▄▄░█ - █│ ● ● │ - █│ ▽ │ - █│ ─── │ - ╰─────────╯`; -const CACE_HAPPY = ` - ▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░█ - █░▄▄▄▄▄▄▄░█ - █│ ★ ★ │ - █│ ▽ │ - █│ ◡◡◡ │ - ╰─────────╯`; -const CACE_SLEEPY = ` - ▄▄▄▄▄▄▄▄▄▄ - █░░░░░░░░░░█ - █░▄▄▄▄▄▄▄░█ - █│ ─ ─ │ - █│ ▽ │ - █│ ─── │ - ╰─────────╯`; -function getGreeting() { - const hour = new Date().getHours(); - if (hour < 6) - return '夜深了'; - if (hour < 12) - return '早上好'; - if (hour < 18) - return '下午好'; - return '晚上好'; -} -async function showCaceAnimation(message = '') { - const cyan = '\x1b[36m'; - const bold = '\x1b[1m'; - const reset = '\x1b[0m'; - // Animation loop - for (let i = 0; i < 2; i++) { - for (const frame of CACE_FRAMES) { - console.clear(); - console.log(cyan + frame + reset); - console.log(); - console.log(cyan + bold + ' ════════════════════════════════' + reset); - console.log(cyan + bold + ' C A C E T I M E R' + reset); - console.log(cyan + bold + ' ════════════════════════════════' + reset); - if (message) { - console.log(); - console.log(' ' + message); - } - await sleep(180); - } - } -} -function showCaceSmall(status = '', mood = 'normal') { - const cyan = '\x1b[36m'; - const reset = '\x1b[0m'; - let face = CACE_SMALL; - if (mood === 'happy') - face = CACE_HAPPY; - else if (mood === 'sleepy') - face = CACE_SLEEPY; - console.log(cyan + face + reset); - if (status) { - console.log(cyan + ' ' + status + reset); - } -} -// ============ Utilities ============ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} -function generateId() { - return Date.now().toString(36) + Math.random().toString(36).substr(2); -} -function formatDuration(ms) { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - if (hours > 0) { - return `${hours}h ${minutes % 60}m ${seconds % 60}s`; - } - else if (minutes > 0) { - return `${minutes}m ${seconds % 60}s`; - } - else { - return `${seconds}s`; - } -} -function formatTime(isoString) { - const date = new Date(isoString); - return date.toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); -} -function formatDate(isoString) { - const date = new Date(isoString); - return date.toLocaleDateString('zh-CN'); -} -// ============ Data Management ============ -const DATA_FILE = path.join(os.homedir(), '.cace-timer.json'); -function loadData() { - try { - if (fs.existsSync(DATA_FILE)) { - const content = fs.readFileSync(DATA_FILE, 'utf-8'); - return JSON.parse(content); - } - } - catch (e) { - // Ignore errors - } - return { current: null, history: [] }; -} -function saveData(data) { - fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); - // Sync to external path if set - if (data.syncPath && fs.existsSync(path.dirname(data.syncPath))) { - fs.writeFileSync(data.syncPath, JSON.stringify(data, null, 2)); - } -} -// ============ Commands ============ -async function cmdStart(task, options) { - const data = loadData(); - if (data.current) { - console.log('\x1b[33m⚠ 已有进行中的任务,请先使用 tk stop 结束\x1b[0m'); - return; - } - const session = { - id: generateId(), - start: new Date().toISOString(), - task: task || '未命名任务', - tags: options.tag ? [options.tag] : [], - marks: [], - estimatedMinutes: options.estimate - }; - data.current = session; - saveData(data); - await showCaceAnimation(`${getGreeting()}!开始记录: ${session.task}`); - console.log(); - console.log(` 📌 任务: ${session.task}`); - if (session.tags.length > 0) { - console.log(` 🏷 标签: ${session.tags.join(', ')}`); - } - if (session.estimatedMinutes) { - console.log(` ⏱ 预计: ${session.estimatedMinutes} 分钟`); - } - console.log(` 🕐 开始: ${formatTime(session.start)}`); - console.log(); -} -async function cmdMark(note) { - const data = loadData(); - if (!data.current) { - console.log('\x1b[33m⚠ 没有进行中的任务,请先用 tk start 开始\x1b[0m'); - return; - } - const mark = { - time: new Date().toISOString(), - note: note || '标记点' - }; - data.current.marks.push(mark); - saveData(data); - const elapsed = Date.now() - new Date(data.current.start).getTime(); - console.log(); - showCaceSmall('✓ 已标记', 'happy'); - console.log(); - console.log(` 📍 ${formatTime(mark.time)} - ${mark.note}`); - console.log(` ⏱ 已用时: ${formatDuration(elapsed)}`); - console.log(); -} -async function cmdStop() { - const data = loadData(); - if (!data.current) { - console.log('\x1b[33m⚠ 没有进行中的任务\x1b[0m'); - return; - } - const session = data.current; - session.end = new Date().toISOString(); - const duration = new Date(session.end).getTime() - new Date(session.start).getTime(); - const durationMinutes = duration / 60000; - // Calculate efficiency score - let efficiency = 100; - if (session.estimatedMinutes && session.estimatedMinutes > 0) { - efficiency = Math.min(100, Math.round((session.estimatedMinutes / durationMinutes) * 100)); - } - data.history.unshift(session); - data.current = null; - saveData(data); - console.log(); - showCaceSmall('任务完成!', 'happy'); - console.log(); - console.log(' ┌─────────────────────────────────┐'); - console.log(' │ 📊 任务总结 │'); - console.log(' └─────────────────────────────────┘'); - console.log(); - console.log(` 📌 任务: ${session.task}`); - console.log(` 🕐 时长: ${formatDuration(duration)}`); - if (session.tags.length > 0) { - console.log(` 🏷 标签: ${session.tags.join(', ')}`); - } - if (session.marks.length > 0) { - console.log(` 📍 标记: ${session.marks.length} 个`); - } - // Efficiency display - let effEmoji = '⭐'; - let effColor = '\x1b[32m'; - if (efficiency < 50) { - effEmoji = '💀'; - effColor = '\x1b[31m'; - } - else if (efficiency < 80) { - effEmoji = '💪'; - effColor = '\x1b[33m'; - } - else if (efficiency >= 100) { - effEmoji = '🏆'; - } - console.log(` ${effEmoji} 效率分: ${effColor}${efficiency}%\x1b[0m`); - console.log(); -} -function cmdStatus() { - const data = loadData(); - console.log(); - showCaceSmall('', data.current ? 'normal' : 'sleepy'); - if (!data.current) { - console.log(); - console.log(' 😴 当前没有进行中的任务'); - console.log(' 使用 tk start "任务名" 开始新任务'); - console.log(); - return; - } - const elapsed = Date.now() - new Date(data.current.start).getTime(); - console.log(); - console.log(' 🔥 进行中'); - console.log(); - console.log(` 📌 任务: ${data.current.task}`); - console.log(` 🕐 开始: ${formatTime(data.current.start)}`); - console.log(` ⏱ 已用: ${formatDuration(elapsed)}`); - if (data.current.tags.length > 0) { - console.log(` 🏷 标签: ${data.current.tags.join(', ')}`); - } - if (data.current.marks.length > 0) { - console.log(` 📍 标记: ${data.current.marks.length} 个`); - data.current.marks.forEach((m, i) => { - console.log(` ${i + 1}. ${formatTime(m.time)} - ${m.note}`); - }); - } - console.log(); -} -function cmdList(options) { - const data = loadData(); - let sessions = data.history; - // Filter by today - if (options.today) { - const today = formatDate(new Date().toISOString()); - sessions = sessions.filter(s => formatDate(s.start) === today); - } - // Filter by tag - if (options.tag) { - sessions = sessions.filter(s => s.tags.includes(options.tag)); - } - // Limit - const limit = options.limit || 10; - sessions = sessions.slice(0, limit); - console.log(); - showCaceSmall('历史记录'); - console.log(); - if (sessions.length === 0) { - console.log(' 📭 暂无记录'); - console.log(); - return; - } - sessions.forEach((session, i) => { - const duration = session.end - ? formatDuration(new Date(session.end).getTime() - new Date(session.start).getTime()) - : '进行中'; - console.log(` ${i + 1}. ${session.task}`); - console.log(` 📅 ${formatTime(session.start)} | ⏱ ${duration}`); - if (session.tags.length > 0) { - console.log(` 🏷 ${session.tags.map(t => '#' + t).join(' ')}`); - } - console.log(); - }); -} -function cmdSearch(keyword) { - const data = loadData(); - const results = data.history.filter(s => s.task.toLowerCase().includes(keyword.toLowerCase()) || - s.tags.some(t => t.toLowerCase().includes(keyword.toLowerCase())) || - s.marks.some(m => m.note.toLowerCase().includes(keyword.toLowerCase()))); - console.log(); - showCaceSmall(`搜索: "${keyword}"`); - console.log(); - if (results.length === 0) { - console.log(' 🔍 未找到匹配记录'); - console.log(); - return; - } - console.log(` 找到 ${results.length} 条记录:\n`); - results.forEach((session, i) => { - const duration = session.end - ? formatDuration(new Date(session.end).getTime() - new Date(session.start).getTime()) - : '进行中'; - console.log(` ${i + 1}. ${session.task}`); - console.log(` 📅 ${formatTime(session.start)} | ⏱ ${duration}`); - console.log(); - }); -} -function cmdSync(filePath) { - const data = loadData(); - const absPath = path.resolve(filePath); - data.syncPath = absPath; - saveData(data); - console.log(); - showCaceSmall('同步已配置'); - console.log(); - console.log(` 📂 同步路径: ${absPath}`); - console.log(' 💡 提示: 可将文件放在 Dropbox/iCloud/OneDrive 等同步目录'); - console.log(); -} -function cmdHelp() { - console.log(); - showCaceSmall(''); - console.log(); - console.log(' ╔═══════════════════════════════════════════╗'); - console.log(' ║ CACE TIMER - 命令帮助 ║'); - console.log(' ╚═══════════════════════════════════════════╝'); - console.log(); - console.log(' tk start <任务名> [选项]'); - console.log(' 开始新任务'); - console.log(' --tag <标签> 添加标签'); - console.log(' --estimate <分钟> 预估时长'); - console.log(); - console.log(' tk mark <备注>'); - console.log(' 记录时间点'); - console.log(); - console.log(' tk stop'); - console.log(' 结束当前任务(显示效率评分)'); - console.log(); - console.log(' tk status'); - console.log(' 查看当前状态'); - console.log(); - console.log(' tk list [选项]'); - console.log(' 查看历史记录'); - console.log(' --today 仅今天'); - console.log(' --tag <标签> 按标签筛选'); - console.log(' --limit <数量> 限制数量'); - console.log(); - console.log(' tk search <关键词>'); - console.log(' 搜索记录'); - console.log(); - console.log(' tk sync <文件路径>'); - console.log(' 设置同步文件路径'); - console.log(); - console.log(' tk help'); - console.log(' 显示帮助'); - console.log(); -} -function parseArgs(args) { - const result = { - command: '', - positional: [], - options: {} - }; - let i = 0; - while (i < args.length) { - const arg = args[i]; - if (arg.startsWith('--')) { - const key = arg.slice(2); - const nextArg = args[i + 1]; - if (nextArg && !nextArg.startsWith('-')) { - result.options[key] = nextArg; - i += 2; - } - else { - result.options[key] = true; - i++; - } - } - else if (!result.command) { - result.command = arg; - i++; - } - else { - result.positional.push(arg); - i++; - } - } - return result; -} -// ============ Main ============ -async function main() { - const args = process.argv.slice(2); - const parsed = parseArgs(args); - const { command, positional, options } = parsed; - switch (command) { - case 'start': - await cmdStart(positional[0] || '', { - tag: options.tag, - estimate: options.estimate ? parseInt(options.estimate) : undefined - }); - break; - case 'mark': - await cmdMark(positional[0] || '标记点'); - break; - case 'stop': - await cmdStop(); - break; - case 'status': - cmdStatus(); - break; - case 'list': - case 'ls': - cmdList({ - today: !!options.today, - tag: options.tag, - limit: options.limit ? parseInt(options.limit) : 10 - }); - break; - case 'search': - cmdSearch(positional[0] || ''); - break; - case 'sync': - cmdSync(positional[0] || ''); - break; - case 'help': - case '--help': - case '-h': - case '': - cmdHelp(); - break; - default: - console.log(`\x1b[31m未知命令: ${command}\x1b[0m`); - console.log('使用 tk help 查看帮助'); - } -} -main().catch(console.error); diff --git a/timekeeper/package.json b/timekeeper/package.json deleted file mode 100644 index 3a9712b..0000000 --- a/timekeeper/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@cacinie/cace-timer", - "version": "1.0.0", - "description": "A minimal time tracking CLI with cute anime girl mascot", - "main": "dist/index.js", - "bin": { - "tk": "dist/index.js", - "cace-timer": "dist/index.js" - }, - "scripts": { - "build": "tsc", - "dev": "ts-node src/index.ts", - "start": "node dist/index.js", - "prepublishOnly": "npm run build" - }, - "files": ["dist", "README.md", "LICENSE"], - "keywords": ["time", "tracker", "cli", "timer", "productivity", "anime", "mascot"], - "author": "CacinieP ", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/CacinieP/cace-timer.git" - }, - "bugs": { - "url": "https://github.com/CacinieP/cace-timer/issues" - }, - "homepage": "https://github.com/CacinieP/cace-timer#readme", - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "ts-node": "^10.9.2", - "typescript": "^5.3.0" - } -} \ No newline at end of file diff --git a/timekeeper/tsconfig.json b/tsconfig.json similarity index 100% rename from timekeeper/tsconfig.json rename to tsconfig.json