@mindtnv/todoist-cli 0.3.1 → 0.5.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.
Files changed (85) hide show
  1. package/marketplace.json +16 -0
  2. package/package.json +7 -6
  3. package/src/api/activity.ts +8 -0
  4. package/src/api/client.ts +214 -0
  5. package/src/api/comments.ts +18 -0
  6. package/src/api/completed.ts +15 -0
  7. package/src/api/labels.ts +18 -0
  8. package/src/api/projects.ts +22 -0
  9. package/src/api/sections.ts +20 -0
  10. package/src/api/stats.ts +38 -0
  11. package/src/api/tasks.ts +34 -0
  12. package/src/api/types.ts +202 -0
  13. package/src/cli/auth.ts +40 -0
  14. package/src/cli/commands/task/add.ts +328 -0
  15. package/src/cli/commands/task/complete.ts +62 -0
  16. package/src/cli/commands/task/delete.ts +62 -0
  17. package/src/cli/commands/task/helpers.ts +289 -0
  18. package/src/cli/commands/task/index.ts +27 -0
  19. package/src/cli/commands/task/list.ts +151 -0
  20. package/src/cli/commands/task/move.ts +49 -0
  21. package/src/cli/commands/task/reopen.ts +43 -0
  22. package/src/cli/commands/task/show.ts +115 -0
  23. package/src/cli/commands/task/update.ts +122 -0
  24. package/src/cli/comment.ts +83 -0
  25. package/src/cli/completed.ts +87 -0
  26. package/src/cli/completion.ts +360 -0
  27. package/src/cli/filter.ts +115 -0
  28. package/src/cli/index.ts +638 -0
  29. package/src/cli/label.ts +120 -0
  30. package/src/cli/log.ts +57 -0
  31. package/src/cli/matrix.ts +100 -0
  32. package/src/cli/plugin-loader.ts +38 -0
  33. package/src/cli/plugin.ts +289 -0
  34. package/src/cli/project.ts +172 -0
  35. package/src/cli/review.ts +116 -0
  36. package/src/cli/section.ts +98 -0
  37. package/src/cli/stats.ts +62 -0
  38. package/src/cli/template.ts +89 -0
  39. package/src/config/index.ts +229 -0
  40. package/src/plugins/api-proxy.ts +70 -0
  41. package/src/plugins/extension-registry.ts +53 -0
  42. package/src/plugins/hook-registry.ts +36 -0
  43. package/src/plugins/loader.ts +200 -0
  44. package/src/plugins/marketplace-types.ts +55 -0
  45. package/src/plugins/marketplace.ts +576 -0
  46. package/src/plugins/palette-registry.ts +21 -0
  47. package/src/plugins/storage.ts +101 -0
  48. package/src/plugins/types.ts +226 -0
  49. package/src/plugins/view-registry.ts +19 -0
  50. package/src/ui/App.tsx +234 -0
  51. package/src/ui/components/Breadcrumb.tsx +18 -0
  52. package/src/ui/components/CommandPalette.tsx +237 -0
  53. package/src/ui/components/ConfirmDialog.tsx +28 -0
  54. package/src/ui/components/EditTaskModal.tsx +484 -0
  55. package/src/ui/components/HelpOverlay.tsx +195 -0
  56. package/src/ui/components/InputPrompt.tsx +109 -0
  57. package/src/ui/components/LabelPicker.tsx +110 -0
  58. package/src/ui/components/ModalManager.tsx +275 -0
  59. package/src/ui/components/ProjectPicker.tsx +95 -0
  60. package/src/ui/components/Sidebar.tsx +282 -0
  61. package/src/ui/components/SortMenu.tsx +77 -0
  62. package/src/ui/components/StatusBar.tsx +67 -0
  63. package/src/ui/components/TaskList.tsx +258 -0
  64. package/src/ui/components/TaskRow.tsx +105 -0
  65. package/src/ui/hooks/useKeyboardHandler.ts +291 -0
  66. package/src/ui/hooks/useStatusMessage.ts +32 -0
  67. package/src/ui/hooks/useTaskOperations.ts +558 -0
  68. package/src/ui/hooks/useUndoSystem.ts +218 -0
  69. package/src/ui/views/ActivityView.tsx +213 -0
  70. package/src/ui/views/CompletedView.tsx +337 -0
  71. package/src/ui/views/StatsView.tsx +178 -0
  72. package/src/ui/views/TaskDetailView.tsx +438 -0
  73. package/src/ui/views/TasksView.tsx +851 -0
  74. package/src/utils/colors.ts +27 -0
  75. package/src/utils/date-format.ts +54 -0
  76. package/src/utils/errors.ts +159 -0
  77. package/src/utils/exit.ts +11 -0
  78. package/src/utils/format.ts +46 -0
  79. package/src/utils/open-url.ts +9 -0
  80. package/src/utils/output.ts +29 -0
  81. package/src/utils/quick-add.ts +202 -0
  82. package/src/utils/resolve.ts +359 -0
  83. package/src/utils/sorting.ts +27 -0
  84. package/src/utils/validation.ts +88 -0
  85. package/dist/index.js +0 -10989
@@ -0,0 +1,237 @@
1
+ import React, { useState, useMemo, useEffect, useRef } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ export interface Command {
5
+ name: string;
6
+ description: string;
7
+ action: () => void;
8
+ category?: string;
9
+ shortcut?: string;
10
+ }
11
+
12
+ interface CommandPaletteProps {
13
+ commands: Command[];
14
+ onCancel: () => void;
15
+ }
16
+
17
+ const CATEGORY_ORDER = ["task", "navigation", "view", "project", "template", "bulk", "general"];
18
+ const CATEGORY_LABELS: Record<string, string> = {
19
+ task: "Task Actions",
20
+ navigation: "Navigation",
21
+ view: "Views",
22
+ project: "Projects",
23
+ template: "Templates",
24
+ bulk: "Bulk Actions",
25
+ general: "General",
26
+ };
27
+
28
+ function CommandRow({ cmd, isActive }: { cmd: Command; isActive: boolean }) {
29
+ return (
30
+ <Box justifyContent="space-between">
31
+ <Text
32
+ backgroundColor={isActive ? "cyan" : undefined}
33
+ color={isActive ? "black" : "white"}
34
+ bold={isActive}
35
+ >
36
+ {isActive ? "\u25B6 " : " "}
37
+ <Text bold={isActive}>{cmd.name}</Text>
38
+ <Text color={isActive ? "black" : "gray"}>{` ${cmd.description}`}</Text>
39
+ </Text>
40
+ {cmd.shortcut ? (
41
+ <Text color={isActive ? "black" : "gray"} backgroundColor={isActive ? "cyan" : undefined} dimColor={!isActive}>
42
+ {` ${cmd.shortcut}`}
43
+ </Text>
44
+ ) : null}
45
+ </Box>
46
+ );
47
+ }
48
+
49
+ export function CommandPalette({ commands, onCancel }: CommandPaletteProps) {
50
+ const [query, setQuery] = useState("");
51
+ const [cursor, setCursor] = useState(0);
52
+ const [selectedIndex, setSelectedIndex] = useState(0);
53
+ const scrollOffsetRef = useRef(0);
54
+
55
+ // Filter commands by query
56
+ const filtered = useMemo(() => {
57
+ if (!query) return commands;
58
+ const q = query.toLowerCase();
59
+ return commands.filter(
60
+ (cmd) =>
61
+ cmd.name.toLowerCase().includes(q) ||
62
+ cmd.description.toLowerCase().includes(q) ||
63
+ (cmd.category ?? "").toLowerCase().includes(q),
64
+ );
65
+ }, [commands, query]);
66
+
67
+ // Reorder by category for grouped display; flat order when searching
68
+ const ordered = useMemo(() => {
69
+ if (query) return filtered;
70
+ const groups = new Map<string, Command[]>();
71
+ for (const cmd of filtered) {
72
+ const cat = cmd.category ?? "general";
73
+ const existing = groups.get(cat) ?? [];
74
+ existing.push(cmd);
75
+ groups.set(cat, existing);
76
+ }
77
+ const result: Command[] = [];
78
+ for (const cat of CATEGORY_ORDER) {
79
+ const items = groups.get(cat);
80
+ if (items) result.push(...items);
81
+ }
82
+ for (const [cat, items] of groups) {
83
+ if (CATEGORY_ORDER.includes(cat)) continue;
84
+ result.push(...items);
85
+ }
86
+ return result;
87
+ }, [filtered, query]);
88
+
89
+ // Clamp selectedIndex when list shrinks
90
+ useEffect(() => {
91
+ if (ordered.length > 0 && selectedIndex >= ordered.length) {
92
+ setSelectedIndex(ordered.length - 1);
93
+ }
94
+ }, [ordered.length, selectedIndex]);
95
+
96
+ // Reset scroll when query changes
97
+ useEffect(() => {
98
+ scrollOffsetRef.current = 0;
99
+ }, [query]);
100
+
101
+ const maxVisible = 18;
102
+
103
+ // Adjust scroll to keep selected item visible
104
+ if (selectedIndex < scrollOffsetRef.current) {
105
+ scrollOffsetRef.current = selectedIndex;
106
+ } else if (selectedIndex >= scrollOffsetRef.current + maxVisible) {
107
+ scrollOffsetRef.current = selectedIndex - maxVisible + 1;
108
+ }
109
+ scrollOffsetRef.current = Math.max(0, Math.min(
110
+ scrollOffsetRef.current,
111
+ Math.max(0, ordered.length - maxVisible),
112
+ ));
113
+ const scrollOffset = scrollOffsetRef.current;
114
+
115
+ useInput((input, key) => {
116
+ if (key.escape) {
117
+ onCancel();
118
+ return;
119
+ }
120
+ if (key.return) {
121
+ const cmd = ordered[selectedIndex];
122
+ if (cmd) cmd.action();
123
+ return;
124
+ }
125
+ if (key.upArrow) {
126
+ setSelectedIndex((i) => Math.max(0, i - 1));
127
+ return;
128
+ }
129
+ if (key.downArrow) {
130
+ setSelectedIndex((i) => Math.min(ordered.length - 1, i + 1));
131
+ return;
132
+ }
133
+ if (key.backspace || key.delete) {
134
+ if (cursor > 0) {
135
+ setQuery((v) => v.slice(0, cursor - 1) + v.slice(cursor));
136
+ setCursor((c) => c - 1);
137
+ setSelectedIndex(0);
138
+ }
139
+ return;
140
+ }
141
+ if (key.leftArrow) {
142
+ setCursor((c) => Math.max(0, c - 1));
143
+ return;
144
+ }
145
+ if (key.rightArrow) {
146
+ setCursor((c) => Math.min(query.length, c + 1));
147
+ return;
148
+ }
149
+ if (input && !key.ctrl && !key.meta) {
150
+ setQuery((v) => v.slice(0, cursor) + input + v.slice(cursor));
151
+ setCursor((c) => c + input.length);
152
+ setSelectedIndex(0);
153
+ }
154
+ });
155
+
156
+ const before = query.slice(0, cursor);
157
+ const cursorChar = query[cursor] ?? " ";
158
+ const after = query.slice(cursor + 1);
159
+
160
+ // Visible slice
161
+ const visibleCommands = ordered.slice(scrollOffset, scrollOffset + maxVisible);
162
+
163
+ // Render items with category headers when not searching
164
+ const renderItems = () => {
165
+ const elements: React.ReactNode[] = [];
166
+ let lastCategory = "";
167
+
168
+ for (let vi = 0; vi < visibleCommands.length; vi++) {
169
+ const cmd = visibleCommands[vi]!;
170
+ const globalIdx = scrollOffset + vi;
171
+ const cat = cmd.category ?? "general";
172
+
173
+ if (!query && cat !== lastCategory) {
174
+ if (lastCategory) elements.push(<Box key={`s-${cat}`} height={1} />);
175
+ elements.push(
176
+ <Text key={`h-${cat}`} color="yellow" bold>
177
+ {"\u2500\u2500 "}{CATEGORY_LABELS[cat] ?? cat}{" \u2500\u2500"}
178
+ </Text>,
179
+ );
180
+ lastCategory = cat;
181
+ }
182
+
183
+ elements.push(
184
+ <CommandRow key={cmd.name} cmd={cmd} isActive={globalIdx === selectedIndex} />,
185
+ );
186
+ }
187
+ return elements;
188
+ };
189
+
190
+ const termWidth = process.stdout.columns ?? 80;
191
+ const paletteWidth = Math.min(60, Math.max(40, Math.floor(termWidth * 0.5)));
192
+
193
+ const hasMore = ordered.length > scrollOffset + maxVisible;
194
+ const hasAbove = scrollOffset > 0;
195
+
196
+ return (
197
+ <Box
198
+ flexDirection="column"
199
+ borderStyle="round"
200
+ borderColor="cyan"
201
+ paddingX={1}
202
+ width={paletteWidth}
203
+ >
204
+ <Box>
205
+ <Text>
206
+ <Text color="cyan" bold>{"\u276F "}</Text>
207
+ <Text>{before}</Text>
208
+ <Text backgroundColor="white" color="black">{cursorChar}</Text>
209
+ <Text>{after}</Text>
210
+ </Text>
211
+ </Box>
212
+ {ordered.length > 0 ? (
213
+ <Box flexDirection="column" marginTop={1}>
214
+ {hasAbove && (
215
+ <Text color="gray" dimColor>{` \u2191 ${scrollOffset} more above`}</Text>
216
+ )}
217
+ {renderItems()}
218
+ {hasMore && (
219
+ <Text color="gray" dimColor>{` \u2193 ${ordered.length - scrollOffset - maxVisible} more below`}</Text>
220
+ )}
221
+ </Box>
222
+ ) : (
223
+ <Box marginTop={1}>
224
+ <Text color="gray">No matching commands</Text>
225
+ </Box>
226
+ )}
227
+ <Box marginTop={1} justifyContent="space-between">
228
+ <Text color="gray" dimColor>
229
+ {ordered.length} command{ordered.length !== 1 ? "s" : ""}
230
+ </Text>
231
+ <Text color="gray" dimColor>
232
+ {"\u2191\u2193"} navigate {"\u23CE"} execute Esc cancel
233
+ </Text>
234
+ </Box>
235
+ </Box>
236
+ );
237
+ }
@@ -0,0 +1,28 @@
1
+ import { Box, Text, useInput } from "ink";
2
+
3
+ interface ConfirmDialogProps {
4
+ message: string;
5
+ onConfirm: () => void;
6
+ onCancel: () => void;
7
+ }
8
+
9
+ export function ConfirmDialog({ message, onConfirm, onCancel }: ConfirmDialogProps) {
10
+ useInput((input, key) => {
11
+ if (input === "y" || input === "Y") {
12
+ onConfirm();
13
+ } else if (input === "n" || input === "N" || key.escape || key.return) {
14
+ onCancel();
15
+ }
16
+ });
17
+
18
+ return (
19
+ <Box borderStyle="single" borderColor="red" paddingX={1}>
20
+ <Text>
21
+ <Text color="red" bold>⚠ {message}</Text>
22
+ <Text dimColor> [y/</Text>
23
+ <Text bold>N</Text>
24
+ <Text dimColor>]</Text>
25
+ </Text>
26
+ </Box>
27
+ );
28
+ }