@tsutsutaku/tick 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -0
- package/dist/App-7CLL32TV.js +294 -0
- package/dist/chunk-EMCHWDJ2.js +120 -0
- package/dist/index.js +260 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# tick ✓
|
|
2
|
+
|
|
3
|
+
> A CLI TODO manager designed for AI Agents — deterministic output, JSON mode, and a rich interactive TUI.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
tick add "Deploy to production" -p high -t infra
|
|
7
|
+
tick list --json
|
|
8
|
+
tick done 3
|
|
9
|
+
tick -i
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why tick?
|
|
15
|
+
|
|
16
|
+
Most TODO CLIs are built for humans. **tick** is built for both.
|
|
17
|
+
|
|
18
|
+
- **AI Agent friendly** — every command outputs structured JSON via `--json`, exits with clear codes, and never emits spinners or animations in non-interactive mode
|
|
19
|
+
- **Human friendly** — `tick -i` launches a full-featured terminal UI with keyboard navigation, color-coded priorities, and inline editing
|
|
20
|
+
- **Zero dependencies at runtime** — data lives in `~/.tick/data.json`, no database or server required
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Clone and install globally
|
|
28
|
+
git clone https://github.com/tsutsutaku/tick.git
|
|
29
|
+
cd tick
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
32
|
+
npm link
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Verify
|
|
37
|
+
tick --version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Add a task
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
tick add "Write unit tests"
|
|
48
|
+
tick add "Deploy to staging" -p high -d 2026-04-10 -t infra -t devops
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Option | Short | Description |
|
|
52
|
+
|--------|-------|-------------|
|
|
53
|
+
| `--priority <level>` | `-p` | `high` / `medium` (default) / `low` |
|
|
54
|
+
| `--due <date>` | `-d` | Due date in `YYYY-MM-DD` format |
|
|
55
|
+
| `--tag <tag...>` | `-t` | One or more tags |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### List tasks
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
tick list # all tasks
|
|
63
|
+
tick list --status todo # incomplete only
|
|
64
|
+
tick list --status done # completed only
|
|
65
|
+
tick list --priority high # filter by priority
|
|
66
|
+
tick list --tag infra # filter by tag
|
|
67
|
+
tick list --json # machine-readable output
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Complete a task
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
tick done 3
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Edit a task
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
tick edit 3 --title "New title"
|
|
84
|
+
tick edit 3 -p low --status todo
|
|
85
|
+
tick edit 3 -t backend -t api # replace tags
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Delete a task
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
tick delete 3
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Stats
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
tick stats
|
|
102
|
+
tick stats --json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Interactive TUI
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
tick -i
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Key | Action |
|
|
114
|
+
|-----|--------|
|
|
115
|
+
| `j` / `↓` | Move down |
|
|
116
|
+
| `k` / `↑` | Move up |
|
|
117
|
+
| `Enter` | Toggle done |
|
|
118
|
+
| `a` | Add new task |
|
|
119
|
+
| `e` | Edit selected task |
|
|
120
|
+
| `d` | Delete selected task |
|
|
121
|
+
| `f` | Cycle status filter (all → todo → done) |
|
|
122
|
+
| `F` | Cycle priority filter |
|
|
123
|
+
| `q` | Quit |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## JSON mode (AI Agent)
|
|
128
|
+
|
|
129
|
+
Every command accepts `--json` as a global flag. Output follows the envelope:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "success": true, "data": { ... } }
|
|
133
|
+
{ "success": false, "error": "Todo with id 99 not found" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Errors are written to **stderr**, data to **stdout**. Exit codes: `0` = success, `1` = error.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Use in scripts / agents
|
|
140
|
+
tick list --json | jq '.data[] | select(.status == "todo")'
|
|
141
|
+
tick add "Summarize PR" -p high --json
|
|
142
|
+
tick done 5 --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Global options
|
|
148
|
+
|
|
149
|
+
| Option | Description |
|
|
150
|
+
|--------|-------------|
|
|
151
|
+
| `--json` | Output as JSON |
|
|
152
|
+
| `--data-path <path>` | Custom data file path |
|
|
153
|
+
| `-i, --interactive` | Launch TUI mode |
|
|
154
|
+
|
|
155
|
+
**Environment variable:** `TICK_DATA_PATH` overrides the default data path (`~/.tick/data.json`).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Data schema
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"nextId": 4,
|
|
164
|
+
"todos": [
|
|
165
|
+
{
|
|
166
|
+
"id": 1,
|
|
167
|
+
"title": "Deploy to staging",
|
|
168
|
+
"status": "todo",
|
|
169
|
+
"priority": "high",
|
|
170
|
+
"tags": ["infra", "devops"],
|
|
171
|
+
"createdAt": "2026-04-02T10:00:00.000Z",
|
|
172
|
+
"updatedAt": "2026-04-02T10:00:00.000Z",
|
|
173
|
+
"dueDate": "2026-04-10"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Tech stack
|
|
182
|
+
|
|
183
|
+
- [React Ink](https://github.com/vadimdemedes/ink) — React renderer for the terminal
|
|
184
|
+
- [Commander.js](https://github.com/tj/commander.js) — CLI argument parsing
|
|
185
|
+
- [TypeScript](https://www.typescriptlang.org/) + [tsup](https://tsup.egoist.dev/)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm install
|
|
193
|
+
|
|
194
|
+
# Run without building
|
|
195
|
+
npx tsx src/index.tsx add "test task"
|
|
196
|
+
|
|
197
|
+
# Build
|
|
198
|
+
npm run build
|
|
199
|
+
|
|
200
|
+
# Link globally
|
|
201
|
+
npm link
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addTodo,
|
|
4
|
+
deleteTodo,
|
|
5
|
+
editTodo,
|
|
6
|
+
listTodos,
|
|
7
|
+
markDone
|
|
8
|
+
} from "./chunk-EMCHWDJ2.js";
|
|
9
|
+
|
|
10
|
+
// src/ui/App.tsx
|
|
11
|
+
import { useState as useState3, useEffect, useCallback } from "react";
|
|
12
|
+
import { Box as Box7, Text as Text7, useInput as useInput3, useApp } from "ink";
|
|
13
|
+
|
|
14
|
+
// src/ui/TodoList.tsx
|
|
15
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
16
|
+
|
|
17
|
+
// src/ui/TodoItem.tsx
|
|
18
|
+
import { Box, Text } from "ink";
|
|
19
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
20
|
+
var PRIORITY_COLOR = {
|
|
21
|
+
high: "red",
|
|
22
|
+
medium: "yellow",
|
|
23
|
+
low: "green"
|
|
24
|
+
};
|
|
25
|
+
function TodoItem({ todo, isSelected }) {
|
|
26
|
+
const now = /* @__PURE__ */ new Date();
|
|
27
|
+
const overdue = todo.status === "todo" && todo.dueDate !== null && new Date(todo.dueDate) < now;
|
|
28
|
+
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { inverse: isSelected, color: isSelected ? "cyan" : void 0, children: [
|
|
29
|
+
" ",
|
|
30
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: todo.id.toString().padStart(3) }),
|
|
31
|
+
" ",
|
|
32
|
+
/* @__PURE__ */ jsx(Text, { color: todo.status === "done" ? "gray" : void 0, children: todo.status === "done" ? "[x]" : "[ ]" }),
|
|
33
|
+
" ",
|
|
34
|
+
/* @__PURE__ */ jsx(
|
|
35
|
+
Text,
|
|
36
|
+
{
|
|
37
|
+
dimColor: todo.status === "done",
|
|
38
|
+
strikethrough: todo.status === "done",
|
|
39
|
+
children: todo.title
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
" ",
|
|
43
|
+
/* @__PURE__ */ jsxs(Text, { color: PRIORITY_COLOR[todo.priority], children: [
|
|
44
|
+
"[",
|
|
45
|
+
todo.priority.toUpperCase(),
|
|
46
|
+
"]"
|
|
47
|
+
] }),
|
|
48
|
+
todo.tags.length > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
49
|
+
" #",
|
|
50
|
+
todo.tags.join(" #")
|
|
51
|
+
] }),
|
|
52
|
+
todo.dueDate && /* @__PURE__ */ jsxs(Text, { color: overdue ? "red" : "gray", children: [
|
|
53
|
+
" ",
|
|
54
|
+
"due:",
|
|
55
|
+
new Date(todo.dueDate).toLocaleDateString("ja-JP"),
|
|
56
|
+
overdue ? "(\u671F\u9650\u8D85\u904E)" : ""
|
|
57
|
+
] })
|
|
58
|
+
] }) });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/ui/TodoList.tsx
|
|
62
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
63
|
+
function TodoList({ todos, selectedIndex }) {
|
|
64
|
+
if (todos.length === 0) {
|
|
65
|
+
return /* @__PURE__ */ jsx2(Box2, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: 'TODO\u304C\u3042\u308A\u307E\u305B\u3093\u3002 "a" \u30AD\u30FC\u3067\u8FFD\u52A0\u3067\u304D\u307E\u3059\u3002' }) });
|
|
66
|
+
}
|
|
67
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: todos.map((todo, i) => /* @__PURE__ */ jsx2(TodoItem, { todo, isSelected: i === selectedIndex }, todo.id)) });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/ui/FilterBar.tsx
|
|
71
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
72
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
73
|
+
function FilterBar({ statusFilter, priorityFilter }) {
|
|
74
|
+
return /* @__PURE__ */ jsxs2(Box3, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
|
|
75
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u30D5\u30A3\u30EB\u30BF\u30FC: " }),
|
|
76
|
+
/* @__PURE__ */ jsxs2(Text3, { color: statusFilter !== "all" ? "cyan" : "gray", children: [
|
|
77
|
+
"\u72B6\u614B:",
|
|
78
|
+
statusFilter
|
|
79
|
+
] }),
|
|
80
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " }),
|
|
81
|
+
/* @__PURE__ */ jsxs2(Text3, { color: priorityFilter !== "all" ? "cyan" : "gray", children: [
|
|
82
|
+
"\u512A\u5148\u5EA6:",
|
|
83
|
+
priorityFilter
|
|
84
|
+
] })
|
|
85
|
+
] });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/ui/StatusBar.tsx
|
|
89
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
90
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
91
|
+
var HINTS = {
|
|
92
|
+
list: "j/k:\u79FB\u52D5 Enter:\u5B8C\u4E86 a:\u8FFD\u52A0 e:\u7DE8\u96C6 d:\u524A\u9664 f:\u30D5\u30A3\u30EB\u30BF\u30FC q:\u7D42\u4E86",
|
|
93
|
+
add: "Enter:\u78BA\u5B9A Esc:\u30AD\u30E3\u30F3\u30BB\u30EB",
|
|
94
|
+
edit: "Enter:\u78BA\u5B9A Esc:\u30AD\u30E3\u30F3\u30BB\u30EB"
|
|
95
|
+
};
|
|
96
|
+
function StatusBar({ mode }) {
|
|
97
|
+
return /* @__PURE__ */ jsx4(Box4, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: HINTS[mode] }) });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/ui/AddForm.tsx
|
|
101
|
+
import { useState } from "react";
|
|
102
|
+
import { Box as Box5, Text as Text5, useInput } from "ink";
|
|
103
|
+
import TextInput from "ink-text-input";
|
|
104
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
105
|
+
var PRIORITIES = ["high", "medium", "low"];
|
|
106
|
+
var PRIORITY_COLOR2 = { high: "red", medium: "yellow", low: "green" };
|
|
107
|
+
function AddForm({ onSubmit, onCancel }) {
|
|
108
|
+
const [title, setTitle] = useState("");
|
|
109
|
+
const [priorityIndex, setPriorityIndex] = useState(1);
|
|
110
|
+
const [step, setStep] = useState("title");
|
|
111
|
+
useInput((input, key) => {
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
onCancel();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (step === "title") {
|
|
117
|
+
if (key.return && title.trim()) {
|
|
118
|
+
setStep("priority");
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (key.leftArrow || input === "h") {
|
|
122
|
+
setPriorityIndex((i) => Math.max(0, i - 1));
|
|
123
|
+
} else if (key.rightArrow || input === "l") {
|
|
124
|
+
setPriorityIndex((i) => Math.min(PRIORITIES.length - 1, i + 1));
|
|
125
|
+
} else if (key.return) {
|
|
126
|
+
onSubmit(title.trim(), PRIORITIES[priorityIndex]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return /* @__PURE__ */ jsxs3(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
131
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "\u2500\u2500 \u65B0\u3057\u3044TODO\u3092\u8FFD\u52A0 \u2500\u2500" }),
|
|
132
|
+
/* @__PURE__ */ jsxs3(Box5, { children: [
|
|
133
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u30BF\u30A4\u30C8\u30EB: " }),
|
|
134
|
+
step === "title" ? /* @__PURE__ */ jsx5(TextInput, { value: title, onChange: setTitle, onSubmit: () => title.trim() && setStep("priority") }) : /* @__PURE__ */ jsx5(Text5, { children: title })
|
|
135
|
+
] }),
|
|
136
|
+
step === "priority" && /* @__PURE__ */ jsxs3(Box5, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u512A\u5148\u5EA6: " }),
|
|
138
|
+
PRIORITIES.map((p, i) => /* @__PURE__ */ jsx5(Text5, { color: i === priorityIndex ? PRIORITY_COLOR2[p] : "gray", bold: i === priorityIndex, children: i === priorityIndex ? `[${p}]` : ` ${p} ` }, p)),
|
|
139
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u2190\u2192\u3067\u9078\u629E\u3001Enter\u3067\u78BA\u5B9A" })
|
|
140
|
+
] })
|
|
141
|
+
] });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/ui/EditForm.tsx
|
|
145
|
+
import { useState as useState2 } from "react";
|
|
146
|
+
import { Box as Box6, Text as Text6, useInput as useInput2 } from "ink";
|
|
147
|
+
import TextInput2 from "ink-text-input";
|
|
148
|
+
import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
149
|
+
var PRIORITIES2 = ["high", "medium", "low"];
|
|
150
|
+
var PRIORITY_COLOR3 = { high: "red", medium: "yellow", low: "green" };
|
|
151
|
+
function EditForm({ todo, onSubmit, onCancel }) {
|
|
152
|
+
const [title, setTitle] = useState2(todo.title);
|
|
153
|
+
const [priorityIndex, setPriorityIndex] = useState2(PRIORITIES2.indexOf(todo.priority));
|
|
154
|
+
const [step, setStep] = useState2("title");
|
|
155
|
+
useInput2((input, key) => {
|
|
156
|
+
if (key.escape) {
|
|
157
|
+
onCancel();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (step === "title") {
|
|
161
|
+
if (key.return && title.trim()) {
|
|
162
|
+
setStep("priority");
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (key.leftArrow || input === "h") {
|
|
166
|
+
setPriorityIndex((i) => Math.max(0, i - 1));
|
|
167
|
+
} else if (key.rightArrow || input === "l") {
|
|
168
|
+
setPriorityIndex((i) => Math.min(PRIORITIES2.length - 1, i + 1));
|
|
169
|
+
} else if (key.return) {
|
|
170
|
+
onSubmit(title.trim(), PRIORITIES2[priorityIndex]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
175
|
+
/* @__PURE__ */ jsxs4(Text6, { bold: true, color: "yellow", children: [
|
|
176
|
+
"\u2500\u2500 TODO\u3092\u7DE8\u96C6 (id: ",
|
|
177
|
+
todo.id,
|
|
178
|
+
") \u2500\u2500"
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxs4(Box6, { children: [
|
|
181
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u30BF\u30A4\u30C8\u30EB: " }),
|
|
182
|
+
step === "title" ? /* @__PURE__ */ jsx6(TextInput2, { value: title, onChange: setTitle, onSubmit: () => title.trim() && setStep("priority") }) : /* @__PURE__ */ jsx6(Text6, { children: title })
|
|
183
|
+
] }),
|
|
184
|
+
step === "priority" && /* @__PURE__ */ jsxs4(Box6, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u512A\u5148\u5EA6: " }),
|
|
186
|
+
PRIORITIES2.map((p, i) => /* @__PURE__ */ jsx6(Text6, { color: i === priorityIndex ? PRIORITY_COLOR3[p] : "gray", bold: i === priorityIndex, children: i === priorityIndex ? `[${p}]` : ` ${p} ` }, p)),
|
|
187
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u2190\u2192\u3067\u9078\u629E\u3001Enter\u3067\u78BA\u5B9A" })
|
|
188
|
+
] })
|
|
189
|
+
] });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/ui/App.tsx
|
|
193
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
194
|
+
function App({ dataPath }) {
|
|
195
|
+
const { exit } = useApp();
|
|
196
|
+
const [todos, setTodos] = useState3([]);
|
|
197
|
+
const [selectedIndex, setSelectedIndex] = useState3(0);
|
|
198
|
+
const [mode, setMode] = useState3("list");
|
|
199
|
+
const [statusFilter, setStatusFilter] = useState3("all");
|
|
200
|
+
const [priorityFilter, setPriorityFilter] = useState3("all");
|
|
201
|
+
const [message, setMessage] = useState3(null);
|
|
202
|
+
const reload = useCallback(() => {
|
|
203
|
+
const filtered = listTodos(dataPath, {
|
|
204
|
+
status: statusFilter,
|
|
205
|
+
priority: priorityFilter === "all" ? void 0 : priorityFilter
|
|
206
|
+
});
|
|
207
|
+
setTodos(filtered);
|
|
208
|
+
}, [dataPath, statusFilter, priorityFilter]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
reload();
|
|
211
|
+
}, [reload]);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
setSelectedIndex((i) => Math.min(i, Math.max(0, todos.length - 1)));
|
|
214
|
+
}, [todos.length]);
|
|
215
|
+
const showMessage = (msg) => {
|
|
216
|
+
setMessage(msg);
|
|
217
|
+
setTimeout(() => setMessage(null), 2e3);
|
|
218
|
+
};
|
|
219
|
+
useInput3((input, key) => {
|
|
220
|
+
if (mode !== "list") return;
|
|
221
|
+
if (input === "q") {
|
|
222
|
+
exit();
|
|
223
|
+
} else if (input === "j" || key.downArrow) {
|
|
224
|
+
setSelectedIndex((i) => Math.min(i + 1, todos.length - 1));
|
|
225
|
+
} else if (input === "k" || key.upArrow) {
|
|
226
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
227
|
+
} else if (key.return) {
|
|
228
|
+
const todo = todos[selectedIndex];
|
|
229
|
+
if (todo && todo.status === "todo") {
|
|
230
|
+
markDone(dataPath, todo.id);
|
|
231
|
+
reload();
|
|
232
|
+
showMessage(`\u5B8C\u4E86: ${todo.title}`);
|
|
233
|
+
}
|
|
234
|
+
} else if (input === "a") {
|
|
235
|
+
setMode("add");
|
|
236
|
+
} else if (input === "e") {
|
|
237
|
+
if (todos[selectedIndex]) setMode("edit");
|
|
238
|
+
} else if (input === "d") {
|
|
239
|
+
const todo = todos[selectedIndex];
|
|
240
|
+
if (todo) {
|
|
241
|
+
deleteTodo(dataPath, todo.id);
|
|
242
|
+
reload();
|
|
243
|
+
showMessage(`\u524A\u9664: ${todo.title}`);
|
|
244
|
+
}
|
|
245
|
+
} else if (input === "f") {
|
|
246
|
+
const cycle = ["all", "todo", "done"];
|
|
247
|
+
setStatusFilter((s) => cycle[(cycle.indexOf(s) + 1) % cycle.length]);
|
|
248
|
+
} else if (input === "F") {
|
|
249
|
+
const cycle = ["all", "high", "medium", "low"];
|
|
250
|
+
setPriorityFilter((p) => cycle[(cycle.indexOf(p) + 1) % cycle.length]);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
const handleAddSubmit = (title, priority) => {
|
|
254
|
+
addTodo(dataPath, { title, priority });
|
|
255
|
+
reload();
|
|
256
|
+
setMode("list");
|
|
257
|
+
showMessage(`\u8FFD\u52A0: ${title}`);
|
|
258
|
+
};
|
|
259
|
+
const handleEditSubmit = (title, priority) => {
|
|
260
|
+
const todo = todos[selectedIndex];
|
|
261
|
+
if (todo) {
|
|
262
|
+
editTodo(dataPath, todo.id, { title, priority });
|
|
263
|
+
reload();
|
|
264
|
+
showMessage(`\u66F4\u65B0: ${title}`);
|
|
265
|
+
}
|
|
266
|
+
setMode("list");
|
|
267
|
+
};
|
|
268
|
+
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", children: [
|
|
269
|
+
/* @__PURE__ */ jsxs5(Box7, { paddingX: 1, children: [
|
|
270
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "\u2500\u2500 TODO Manager \u2500\u2500" }),
|
|
271
|
+
/* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
|
|
272
|
+
" (",
|
|
273
|
+
todos.length,
|
|
274
|
+
"\u4EF6)"
|
|
275
|
+
] })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ jsx7(FilterBar, { statusFilter, priorityFilter }),
|
|
278
|
+
/* @__PURE__ */ jsx7(TodoList, { todos, selectedIndex }),
|
|
279
|
+
message && /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: message }) }),
|
|
280
|
+
mode === "add" && /* @__PURE__ */ jsx7(AddForm, { onSubmit: handleAddSubmit, onCancel: () => setMode("list") }),
|
|
281
|
+
mode === "edit" && todos[selectedIndex] && /* @__PURE__ */ jsx7(
|
|
282
|
+
EditForm,
|
|
283
|
+
{
|
|
284
|
+
todo: todos[selectedIndex],
|
|
285
|
+
onSubmit: handleEditSubmit,
|
|
286
|
+
onCancel: () => setMode("list")
|
|
287
|
+
}
|
|
288
|
+
),
|
|
289
|
+
/* @__PURE__ */ jsx7(StatusBar, { mode })
|
|
290
|
+
] });
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
App as default
|
|
294
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/store.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
function defaultDataPath() {
|
|
9
|
+
return join(homedir(), ".tick", "data.json");
|
|
10
|
+
}
|
|
11
|
+
function ensureDir(filePath) {
|
|
12
|
+
const dir = dirname(filePath);
|
|
13
|
+
if (!existsSync(dir)) {
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function loadData(dataPath) {
|
|
18
|
+
ensureDir(dataPath);
|
|
19
|
+
if (!existsSync(dataPath)) {
|
|
20
|
+
const initial = { nextId: 1, todos: [] };
|
|
21
|
+
writeFileSync(dataPath, JSON.stringify(initial, null, 2), "utf-8");
|
|
22
|
+
return initial;
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(readFileSync(dataPath, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
function saveData(dataPath, data) {
|
|
27
|
+
ensureDir(dataPath);
|
|
28
|
+
writeFileSync(dataPath, JSON.stringify(data, null, 2), "utf-8");
|
|
29
|
+
}
|
|
30
|
+
function addTodo(dataPath, input) {
|
|
31
|
+
const data = loadData(dataPath);
|
|
32
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33
|
+
const todo = {
|
|
34
|
+
id: data.nextId,
|
|
35
|
+
title: input.title,
|
|
36
|
+
status: "todo",
|
|
37
|
+
priority: input.priority ?? "medium",
|
|
38
|
+
tags: input.tag ?? [],
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
dueDate: input.due ?? null
|
|
42
|
+
};
|
|
43
|
+
data.todos.push(todo);
|
|
44
|
+
data.nextId++;
|
|
45
|
+
saveData(dataPath, data);
|
|
46
|
+
return todo;
|
|
47
|
+
}
|
|
48
|
+
function listTodos(dataPath, filters = {}) {
|
|
49
|
+
const data = loadData(dataPath);
|
|
50
|
+
let todos = data.todos;
|
|
51
|
+
const statusFilter = filters.status ?? "all";
|
|
52
|
+
if (statusFilter !== "all") {
|
|
53
|
+
todos = todos.filter((t) => t.status === statusFilter);
|
|
54
|
+
}
|
|
55
|
+
if (filters.priority) {
|
|
56
|
+
todos = todos.filter((t) => t.priority === filters.priority);
|
|
57
|
+
}
|
|
58
|
+
if (filters.tag) {
|
|
59
|
+
todos = todos.filter((t) => t.tags.includes(filters.tag));
|
|
60
|
+
}
|
|
61
|
+
return todos;
|
|
62
|
+
}
|
|
63
|
+
function markDone(dataPath, id) {
|
|
64
|
+
const data = loadData(dataPath);
|
|
65
|
+
const todo = data.todos.find((t) => t.id === id);
|
|
66
|
+
if (!todo) throw new Error(`Todo with id ${id} not found`);
|
|
67
|
+
todo.status = "done";
|
|
68
|
+
todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
69
|
+
saveData(dataPath, data);
|
|
70
|
+
return todo;
|
|
71
|
+
}
|
|
72
|
+
function editTodo(dataPath, id, changes) {
|
|
73
|
+
const data = loadData(dataPath);
|
|
74
|
+
const todo = data.todos.find((t) => t.id === id);
|
|
75
|
+
if (!todo) throw new Error(`Todo with id ${id} not found`);
|
|
76
|
+
if (changes.title !== void 0) todo.title = changes.title;
|
|
77
|
+
if (changes.priority !== void 0) todo.priority = changes.priority;
|
|
78
|
+
if (changes.due !== void 0) todo.dueDate = changes.due;
|
|
79
|
+
if (changes.tag !== void 0) todo.tags = changes.tag;
|
|
80
|
+
if (changes.status !== void 0) todo.status = changes.status;
|
|
81
|
+
todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
82
|
+
saveData(dataPath, data);
|
|
83
|
+
return todo;
|
|
84
|
+
}
|
|
85
|
+
function deleteTodo(dataPath, id) {
|
|
86
|
+
const data = loadData(dataPath);
|
|
87
|
+
const index = data.todos.findIndex((t) => t.id === id);
|
|
88
|
+
if (index === -1) throw new Error(`Todo with id ${id} not found`);
|
|
89
|
+
const [todo] = data.todos.splice(index, 1);
|
|
90
|
+
saveData(dataPath, data);
|
|
91
|
+
return todo;
|
|
92
|
+
}
|
|
93
|
+
function getStats(dataPath) {
|
|
94
|
+
const data = loadData(dataPath);
|
|
95
|
+
const now = /* @__PURE__ */ new Date();
|
|
96
|
+
const overdue = data.todos.filter(
|
|
97
|
+
(t) => t.status === "todo" && t.dueDate !== null && new Date(t.dueDate) < now
|
|
98
|
+
).length;
|
|
99
|
+
return {
|
|
100
|
+
total: data.todos.length,
|
|
101
|
+
todo: data.todos.filter((t) => t.status === "todo").length,
|
|
102
|
+
done: data.todos.filter((t) => t.status === "done").length,
|
|
103
|
+
byPriority: {
|
|
104
|
+
high: data.todos.filter((t) => t.priority === "high").length,
|
|
105
|
+
medium: data.todos.filter((t) => t.priority === "medium").length,
|
|
106
|
+
low: data.todos.filter((t) => t.priority === "low").length
|
|
107
|
+
},
|
|
108
|
+
overdue
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
defaultDataPath,
|
|
114
|
+
addTodo,
|
|
115
|
+
listTodos,
|
|
116
|
+
markDone,
|
|
117
|
+
editTodo,
|
|
118
|
+
deleteTodo,
|
|
119
|
+
getStats
|
|
120
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addTodo,
|
|
4
|
+
defaultDataPath,
|
|
5
|
+
deleteTodo,
|
|
6
|
+
editTodo,
|
|
7
|
+
getStats,
|
|
8
|
+
listTodos,
|
|
9
|
+
markDone
|
|
10
|
+
} from "./chunk-EMCHWDJ2.js";
|
|
11
|
+
|
|
12
|
+
// src/cli.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/renderers/text.ts
|
|
16
|
+
var PRIORITY_COLORS = {
|
|
17
|
+
high: "\x1B[31m",
|
|
18
|
+
// red
|
|
19
|
+
medium: "\x1B[33m",
|
|
20
|
+
// yellow
|
|
21
|
+
low: "\x1B[32m"
|
|
22
|
+
// green
|
|
23
|
+
};
|
|
24
|
+
var RESET = "\x1B[0m";
|
|
25
|
+
var DIM = "\x1B[2m";
|
|
26
|
+
var BOLD = "\x1B[1m";
|
|
27
|
+
function priorityBadge(p) {
|
|
28
|
+
return `${PRIORITY_COLORS[p]}[${p.toUpperCase()}]${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
function statusIcon(s) {
|
|
31
|
+
return s === "done" ? `${DIM}[x]${RESET}` : "[ ]";
|
|
32
|
+
}
|
|
33
|
+
function formatDue(dueDate) {
|
|
34
|
+
if (!dueDate) return "";
|
|
35
|
+
const d = new Date(dueDate);
|
|
36
|
+
const now = /* @__PURE__ */ new Date();
|
|
37
|
+
const overdue = d < now;
|
|
38
|
+
const str = d.toLocaleDateString("ja-JP");
|
|
39
|
+
return overdue ? ` \x1B[31mdue:${str}(\u671F\u9650\u8D85\u904E)${RESET}` : ` ${DIM}due:${str}${RESET}`;
|
|
40
|
+
}
|
|
41
|
+
function renderTodo(todo) {
|
|
42
|
+
const tags = todo.tags.length > 0 ? ` ${DIM}#${todo.tags.join(" #")}${RESET}` : "";
|
|
43
|
+
const due = formatDue(todo.dueDate);
|
|
44
|
+
const titleStyle = todo.status === "done" ? DIM : "";
|
|
45
|
+
return `${DIM}${todo.id.toString().padStart(3)}${RESET} ${statusIcon(todo.status)} ${titleStyle}${todo.title}${RESET} ${priorityBadge(todo.priority)}${tags}${due}`;
|
|
46
|
+
}
|
|
47
|
+
function renderTodoList(todos) {
|
|
48
|
+
if (todos.length === 0) {
|
|
49
|
+
process.stdout.write("TODO\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\n");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const todo of todos) {
|
|
53
|
+
process.stdout.write(renderTodo(todo) + "\n");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function renderAddResult(todo) {
|
|
57
|
+
process.stdout.write(`${BOLD}\u8FFD\u52A0\u3057\u307E\u3057\u305F${RESET} (id: ${todo.id})
|
|
58
|
+
`);
|
|
59
|
+
process.stdout.write(renderTodo(todo) + "\n");
|
|
60
|
+
}
|
|
61
|
+
function renderDoneResult(todo) {
|
|
62
|
+
process.stdout.write(`${BOLD}\u5B8C\u4E86\u306B\u3057\u307E\u3057\u305F${RESET} (id: ${todo.id})
|
|
63
|
+
`);
|
|
64
|
+
process.stdout.write(renderTodo(todo) + "\n");
|
|
65
|
+
}
|
|
66
|
+
function renderEditResult(todo) {
|
|
67
|
+
process.stdout.write(`${BOLD}\u66F4\u65B0\u3057\u307E\u3057\u305F${RESET} (id: ${todo.id})
|
|
68
|
+
`);
|
|
69
|
+
process.stdout.write(renderTodo(todo) + "\n");
|
|
70
|
+
}
|
|
71
|
+
function renderDeleteResult(todo) {
|
|
72
|
+
process.stdout.write(`${BOLD}\u524A\u9664\u3057\u307E\u3057\u305F${RESET} (id: ${todo.id}): ${todo.title}
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
function renderStats(stats) {
|
|
76
|
+
process.stdout.write(`${BOLD}=== TODO\u7D71\u8A08 ===${RESET}
|
|
77
|
+
`);
|
|
78
|
+
process.stdout.write(`\u5408\u8A08: ${stats.total} \u672A\u5B8C\u4E86: ${stats.todo} \u5B8C\u4E86: ${stats.done}
|
|
79
|
+
`);
|
|
80
|
+
process.stdout.write(`\u512A\u5148\u5EA6: ${PRIORITY_COLORS.high}\u9AD8:${stats.byPriority.high}${RESET} ${PRIORITY_COLORS.medium}\u4E2D:${stats.byPriority.medium}${RESET} ${PRIORITY_COLORS.low}\u4F4E:${stats.byPriority.low}${RESET}
|
|
81
|
+
`);
|
|
82
|
+
if (stats.overdue > 0) {
|
|
83
|
+
process.stdout.write(`\x1B[31m\u671F\u9650\u8D85\u904E: ${stats.overdue}\u4EF6${RESET}
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function renderError(message) {
|
|
88
|
+
process.stderr.write(`\x1B[31m\u30A8\u30E9\u30FC: ${message}${RESET}
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/renderers/json.ts
|
|
93
|
+
function outputSuccess(data) {
|
|
94
|
+
process.stdout.write(JSON.stringify({ success: true, data }, null, 2) + "\n");
|
|
95
|
+
}
|
|
96
|
+
function outputError(message) {
|
|
97
|
+
process.stderr.write(JSON.stringify({ success: false, error: message }, null, 2) + "\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/commands/add.ts
|
|
101
|
+
function runAdd(title, opts) {
|
|
102
|
+
try {
|
|
103
|
+
const todo = addTodo(opts.dataPath, {
|
|
104
|
+
title,
|
|
105
|
+
priority: opts.priority,
|
|
106
|
+
due: opts.due,
|
|
107
|
+
tag: opts.tag
|
|
108
|
+
});
|
|
109
|
+
if (opts.json) {
|
|
110
|
+
outputSuccess(todo);
|
|
111
|
+
} else {
|
|
112
|
+
renderAddResult(todo);
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
116
|
+
if (opts.json) outputError(msg);
|
|
117
|
+
else renderError(msg);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/commands/list.ts
|
|
123
|
+
function runList(opts) {
|
|
124
|
+
try {
|
|
125
|
+
const todos = listTodos(opts.dataPath, {
|
|
126
|
+
status: opts.status,
|
|
127
|
+
priority: opts.priority,
|
|
128
|
+
tag: opts.tag
|
|
129
|
+
});
|
|
130
|
+
if (opts.json) {
|
|
131
|
+
outputSuccess(todos);
|
|
132
|
+
} else {
|
|
133
|
+
renderTodoList(todos);
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
137
|
+
if (opts.json) outputError(msg);
|
|
138
|
+
else renderError(msg);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/commands/done.ts
|
|
144
|
+
function runDone(id, opts) {
|
|
145
|
+
try {
|
|
146
|
+
const todo = markDone(opts.dataPath, parseInt(id, 10));
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
outputSuccess(todo);
|
|
149
|
+
} else {
|
|
150
|
+
renderDoneResult(todo);
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
154
|
+
if (opts.json) outputError(msg);
|
|
155
|
+
else renderError(msg);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/commands/edit.ts
|
|
161
|
+
function runEdit(id, opts) {
|
|
162
|
+
try {
|
|
163
|
+
const todo = editTodo(opts.dataPath, parseInt(id, 10), {
|
|
164
|
+
title: opts.title,
|
|
165
|
+
priority: opts.priority,
|
|
166
|
+
due: opts.due,
|
|
167
|
+
tag: opts.tag,
|
|
168
|
+
status: opts.status
|
|
169
|
+
});
|
|
170
|
+
if (opts.json) {
|
|
171
|
+
outputSuccess(todo);
|
|
172
|
+
} else {
|
|
173
|
+
renderEditResult(todo);
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
177
|
+
if (opts.json) outputError(msg);
|
|
178
|
+
else renderError(msg);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/commands/delete.ts
|
|
184
|
+
function runDelete(id, opts) {
|
|
185
|
+
try {
|
|
186
|
+
const todo = deleteTodo(opts.dataPath, parseInt(id, 10));
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
outputSuccess(todo);
|
|
189
|
+
} else {
|
|
190
|
+
renderDeleteResult(todo);
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
194
|
+
if (opts.json) outputError(msg);
|
|
195
|
+
else renderError(msg);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/commands/stats.ts
|
|
201
|
+
function runStats(opts) {
|
|
202
|
+
try {
|
|
203
|
+
const stats = getStats(opts.dataPath);
|
|
204
|
+
if (opts.json) {
|
|
205
|
+
outputSuccess(stats);
|
|
206
|
+
} else {
|
|
207
|
+
renderStats(stats);
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
211
|
+
if (opts.json) outputError(msg);
|
|
212
|
+
else renderError(msg);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/cli.ts
|
|
218
|
+
function buildCli() {
|
|
219
|
+
const program = new Command();
|
|
220
|
+
program.name("tick").version("1.0.0").description("AI Agent\u5BFE\u5FDC TODO CLI").option("--data-path <path>", "\u30C7\u30FC\u30BF\u30D5\u30A1\u30A4\u30EB\u306E\u30D1\u30B9", process.env["TICK_DATA_PATH"] ?? defaultDataPath()).option("--json", "JSON\u5F62\u5F0F\u3067\u51FA\u529B").option("-i, --interactive", "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6TUI\u30E2\u30FC\u30C9\u3067\u8D77\u52D5");
|
|
221
|
+
program.command("add <title>").description("TODO\u3092\u8FFD\u52A0\u3059\u308B").option("-p, --priority <level>", "\u512A\u5148\u5EA6 (high|medium|low)", "medium").option("-d, --due <date>", "\u671F\u9650\u65E5 (YYYY-MM-DD)").option("-t, --tag <tag...>", "\u30BF\u30B0").action((title, localOpts, cmd) => {
|
|
222
|
+
const global = cmd.optsWithGlobals();
|
|
223
|
+
runAdd(title, { ...localOpts, dataPath: global.dataPath, json: global.json });
|
|
224
|
+
});
|
|
225
|
+
program.command("list").description("TODO\u3092\u4E00\u89A7\u8868\u793A\u3059\u308B").option("-s, --status <status>", "\u30D5\u30A3\u30EB\u30BF\u30FC (todo|done|all)", "all").option("-p, --priority <level>", "\u512A\u5148\u5EA6\u30D5\u30A3\u30EB\u30BF\u30FC (high|medium|low)").option("-t, --tag <tag>", "\u30BF\u30B0\u30D5\u30A3\u30EB\u30BF\u30FC").action((localOpts, cmd) => {
|
|
226
|
+
const global = cmd.optsWithGlobals();
|
|
227
|
+
runList({ ...localOpts, dataPath: global.dataPath, json: global.json });
|
|
228
|
+
});
|
|
229
|
+
program.command("done <id>").description("TODO\u3092\u5B8C\u4E86\u306B\u3059\u308B").action((id, _localOpts, cmd) => {
|
|
230
|
+
const global = cmd.optsWithGlobals();
|
|
231
|
+
runDone(id, { dataPath: global.dataPath, json: global.json });
|
|
232
|
+
});
|
|
233
|
+
program.command("edit <id>").description("TODO\u3092\u7DE8\u96C6\u3059\u308B").option("--title <title>", "\u65B0\u3057\u3044\u30BF\u30A4\u30C8\u30EB").option("-p, --priority <level>", "\u512A\u5148\u5EA6 (high|medium|low)").option("-d, --due <date>", "\u671F\u9650\u65E5 (YYYY-MM-DD)").option("-t, --tag <tag...>", "\u30BF\u30B0").option("-s, --status <status>", "\u30B9\u30C6\u30FC\u30BF\u30B9 (todo|done)").action((id, localOpts, cmd) => {
|
|
234
|
+
const global = cmd.optsWithGlobals();
|
|
235
|
+
runEdit(id, { ...localOpts, dataPath: global.dataPath, json: global.json });
|
|
236
|
+
});
|
|
237
|
+
program.command("delete <id>").description("TODO\u3092\u524A\u9664\u3059\u308B").action((id, _localOpts, cmd) => {
|
|
238
|
+
const global = cmd.optsWithGlobals();
|
|
239
|
+
runDelete(id, { dataPath: global.dataPath, json: global.json });
|
|
240
|
+
});
|
|
241
|
+
program.command("stats").description("\u7D71\u8A08\u3092\u8868\u793A\u3059\u308B").action((_localOpts, cmd) => {
|
|
242
|
+
const global = cmd.optsWithGlobals();
|
|
243
|
+
runStats({ dataPath: global.dataPath, json: global.json });
|
|
244
|
+
});
|
|
245
|
+
return program;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/index.tsx
|
|
249
|
+
import { jsx } from "react/jsx-runtime";
|
|
250
|
+
var isInteractive = process.argv.includes("-i") || process.argv.includes("--interactive");
|
|
251
|
+
if (isInteractive) {
|
|
252
|
+
const dpIndex = process.argv.findIndex((a) => a === "--data-path");
|
|
253
|
+
const dataPath = dpIndex !== -1 && process.argv[dpIndex + 1] ? process.argv[dpIndex + 1] : process.env["TICK_DATA_PATH"] ?? defaultDataPath();
|
|
254
|
+
const { render } = await import("ink");
|
|
255
|
+
const { default: App } = await import("./App-7CLL32TV.js");
|
|
256
|
+
render(/* @__PURE__ */ jsx(App, { dataPath }));
|
|
257
|
+
} else {
|
|
258
|
+
const program = buildCli();
|
|
259
|
+
program.parse();
|
|
260
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsutsutaku/tick",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tick": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsx src/index.tsx",
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"prepare": "tsup",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^14.0.0",
|
|
19
|
+
"ink": "^6.0.0",
|
|
20
|
+
"ink-text-input": "^6.0.0",
|
|
21
|
+
"react": "^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^19.0.0",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"tsx": "^4.0.0",
|
|
27
|
+
"typescript": "^5.7.0"
|
|
28
|
+
}
|
|
29
|
+
}
|