@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 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
+ }