@mindtnv/todoist-cli 0.4.0 → 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 (84) hide show
  1. package/package.json +6 -6
  2. package/src/api/activity.ts +8 -0
  3. package/src/api/client.ts +214 -0
  4. package/src/api/comments.ts +18 -0
  5. package/src/api/completed.ts +15 -0
  6. package/src/api/labels.ts +18 -0
  7. package/src/api/projects.ts +22 -0
  8. package/src/api/sections.ts +20 -0
  9. package/src/api/stats.ts +38 -0
  10. package/src/api/tasks.ts +34 -0
  11. package/src/api/types.ts +202 -0
  12. package/src/cli/auth.ts +40 -0
  13. package/src/cli/commands/task/add.ts +328 -0
  14. package/src/cli/commands/task/complete.ts +62 -0
  15. package/src/cli/commands/task/delete.ts +62 -0
  16. package/src/cli/commands/task/helpers.ts +289 -0
  17. package/src/cli/commands/task/index.ts +27 -0
  18. package/src/cli/commands/task/list.ts +151 -0
  19. package/src/cli/commands/task/move.ts +49 -0
  20. package/src/cli/commands/task/reopen.ts +43 -0
  21. package/src/cli/commands/task/show.ts +115 -0
  22. package/src/cli/commands/task/update.ts +122 -0
  23. package/src/cli/comment.ts +83 -0
  24. package/src/cli/completed.ts +87 -0
  25. package/src/cli/completion.ts +360 -0
  26. package/src/cli/filter.ts +115 -0
  27. package/src/cli/index.ts +638 -0
  28. package/src/cli/label.ts +120 -0
  29. package/src/cli/log.ts +57 -0
  30. package/src/cli/matrix.ts +100 -0
  31. package/src/cli/plugin-loader.ts +38 -0
  32. package/src/cli/plugin.ts +289 -0
  33. package/src/cli/project.ts +172 -0
  34. package/src/cli/review.ts +116 -0
  35. package/src/cli/section.ts +98 -0
  36. package/src/cli/stats.ts +62 -0
  37. package/src/cli/template.ts +89 -0
  38. package/src/config/index.ts +229 -0
  39. package/src/plugins/api-proxy.ts +70 -0
  40. package/src/plugins/extension-registry.ts +53 -0
  41. package/src/plugins/hook-registry.ts +36 -0
  42. package/src/plugins/loader.ts +200 -0
  43. package/src/plugins/marketplace-types.ts +55 -0
  44. package/src/plugins/marketplace.ts +576 -0
  45. package/src/plugins/palette-registry.ts +21 -0
  46. package/src/plugins/storage.ts +101 -0
  47. package/src/plugins/types.ts +226 -0
  48. package/src/plugins/view-registry.ts +19 -0
  49. package/src/ui/App.tsx +234 -0
  50. package/src/ui/components/Breadcrumb.tsx +18 -0
  51. package/src/ui/components/CommandPalette.tsx +237 -0
  52. package/src/ui/components/ConfirmDialog.tsx +28 -0
  53. package/src/ui/components/EditTaskModal.tsx +484 -0
  54. package/src/ui/components/HelpOverlay.tsx +195 -0
  55. package/src/ui/components/InputPrompt.tsx +109 -0
  56. package/src/ui/components/LabelPicker.tsx +110 -0
  57. package/src/ui/components/ModalManager.tsx +275 -0
  58. package/src/ui/components/ProjectPicker.tsx +95 -0
  59. package/src/ui/components/Sidebar.tsx +282 -0
  60. package/src/ui/components/SortMenu.tsx +77 -0
  61. package/src/ui/components/StatusBar.tsx +67 -0
  62. package/src/ui/components/TaskList.tsx +258 -0
  63. package/src/ui/components/TaskRow.tsx +105 -0
  64. package/src/ui/hooks/useKeyboardHandler.ts +291 -0
  65. package/src/ui/hooks/useStatusMessage.ts +32 -0
  66. package/src/ui/hooks/useTaskOperations.ts +558 -0
  67. package/src/ui/hooks/useUndoSystem.ts +218 -0
  68. package/src/ui/views/ActivityView.tsx +213 -0
  69. package/src/ui/views/CompletedView.tsx +337 -0
  70. package/src/ui/views/StatsView.tsx +178 -0
  71. package/src/ui/views/TaskDetailView.tsx +438 -0
  72. package/src/ui/views/TasksView.tsx +851 -0
  73. package/src/utils/colors.ts +27 -0
  74. package/src/utils/date-format.ts +54 -0
  75. package/src/utils/errors.ts +159 -0
  76. package/src/utils/exit.ts +11 -0
  77. package/src/utils/format.ts +46 -0
  78. package/src/utils/open-url.ts +9 -0
  79. package/src/utils/output.ts +29 -0
  80. package/src/utils/quick-add.ts +202 -0
  81. package/src/utils/resolve.ts +359 -0
  82. package/src/utils/sorting.ts +27 -0
  83. package/src/utils/validation.ts +88 -0
  84. package/dist/index.js +0 -11355
@@ -0,0 +1,122 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getTask, updateTask } from "../../../api/tasks.ts";
4
+ import type { UpdateTaskParams } from "../../../api/types.ts";
5
+ import { handleError } from "../../../utils/errors.ts";
6
+ import { validateContent, validatePriority, validateDateString } from "../../../utils/validation.ts";
7
+ import { cliExit } from "../../../utils/exit.ts";
8
+ import { resolveTaskArg, resolveProjectArg, resolveSectionArg } from "../../../utils/resolve.ts";
9
+ import { getCliHookRegistry } from "../../plugin-loader.ts";
10
+
11
+ export function registerUpdateCommand(task: Command): void {
12
+ task
13
+ .command("update")
14
+ .description("Update a task")
15
+ .argument("<id>", "Task ID")
16
+ .option("--text <text>", "New content")
17
+ .option("--priority <priority>", "New priority (1-4)")
18
+ .option("--due <string>", "New due date string (use 'none' or 'clear' to remove)")
19
+ .option("--deadline <date>", "Deadline date YYYY-MM-DD (use 'none' or 'clear' to remove)")
20
+ .option("--description <text>", "New description")
21
+ .option("--label <spec>", "Add/remove labels. Examples: --label errands (add), --label add:errands, --label remove:waiting. Repeat for multiple.", (val: string, acc: string[]) => { acc.push(val); return acc; }, [] as string[])
22
+ .option("--project <name-or-id>", "Move to project (name or ID)")
23
+ .option("--section <name-or-id>", "Move to section (name or ID)")
24
+ .action(async (rawId: string, opts: { text?: string; priority?: string; due?: string; deadline?: string; description?: string; label: string[]; project?: string; section?: string }) => {
25
+ try {
26
+ const id = await resolveTaskArg(rawId);
27
+
28
+ // Validate content if provided
29
+ if (opts.text) {
30
+ const contentError = validateContent(opts.text);
31
+ if (contentError) {
32
+ console.error(chalk.red(contentError));
33
+ cliExit(1);
34
+ }
35
+ }
36
+
37
+ // Validate priority if provided
38
+ if (opts.priority) {
39
+ const p = parseInt(opts.priority, 10);
40
+ const priError = validatePriority(p);
41
+ if (priError) {
42
+ console.error(chalk.red(priError));
43
+ cliExit(1);
44
+ }
45
+ }
46
+
47
+ // Validate deadline format if provided
48
+ if (opts.deadline && opts.deadline !== "none" && opts.deadline !== "clear") {
49
+ const dateError = validateDateString(opts.deadline);
50
+ if (dateError) {
51
+ console.error(chalk.red(dateError));
52
+ cliExit(1);
53
+ }
54
+ }
55
+
56
+ const params: Record<string, unknown> = {};
57
+ if (opts.text) params.content = opts.text;
58
+ if (opts.priority) params.priority = parseInt(opts.priority, 10);
59
+ if (opts.description !== undefined) params.description = opts.description;
60
+
61
+ if (opts.due) {
62
+ if (opts.due === "none" || opts.due === "clear") {
63
+ params.due_string = null as unknown as string;
64
+ } else {
65
+ params.due_string = opts.due;
66
+ }
67
+ }
68
+
69
+ if (opts.deadline) {
70
+ if (opts.deadline === "none" || opts.deadline === "clear") {
71
+ params.deadline_date = null;
72
+ } else {
73
+ params.deadline_date = opts.deadline;
74
+ }
75
+ }
76
+
77
+ if (opts.project) {
78
+ params.project_id = await resolveProjectArg(opts.project);
79
+ }
80
+
81
+ if (opts.section) {
82
+ params.section_id = await resolveSectionArg(opts.section, params.project_id as string | undefined);
83
+ }
84
+
85
+ // Handle label add/remove
86
+ if (opts.label.length > 0) {
87
+ const task = await getTask(id);
88
+ let labels = [...task.labels];
89
+
90
+ for (const spec of opts.label) {
91
+ if (spec.startsWith("add:")) {
92
+ const name = spec.slice(4).replace(/^@/, "");
93
+ if (!labels.includes(name)) labels.push(name);
94
+ } else if (spec.startsWith("remove:")) {
95
+ const name = spec.slice(7).replace(/^@/, "");
96
+ labels = labels.filter((l) => l !== name);
97
+ } else {
98
+ // Treat as add
99
+ const name = spec.replace(/^@/, "");
100
+ if (!labels.includes(name)) labels.push(name);
101
+ }
102
+ }
103
+
104
+ params.labels = labels;
105
+ }
106
+
107
+ if (Object.keys(params).length === 0) {
108
+ console.error(chalk.red("No update options provided. Use --text, --priority, --due, --deadline, --description, --label, --project, or --section."));
109
+ cliExit(1);
110
+ }
111
+
112
+ const hooks = getCliHookRegistry();
113
+ const task = await getTask(id);
114
+ try { await hooks?.emit("task.updating", { task, changes: params as UpdateTaskParams }); } catch { /* hook errors non-fatal */ }
115
+ const result = await updateTask(id, params);
116
+ try { await hooks?.emit("task.updated", { task: result, changes: params as UpdateTaskParams }); } catch { /* hook errors non-fatal */ }
117
+ console.log(chalk.green(`Task ${result.id} updated: ${result.content}`));
118
+ } catch (err) {
119
+ handleError(err);
120
+ }
121
+ });
122
+ }
@@ -0,0 +1,83 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getComments, createComment, updateComment, deleteComment } from "../api/comments.ts";
4
+ import { handleError } from "../utils/errors.ts";
5
+ import { ID_WIDTH } from "../utils/format.ts";
6
+ import { resolveTaskArg } from "../utils/resolve.ts";
7
+
8
+ const POSTED_WIDTH = 22;
9
+
10
+ export function registerCommentCommand(program: Command): void {
11
+ const comment = program
12
+ .command("comment")
13
+ .description("Manage comments");
14
+
15
+ comment
16
+ .command("add")
17
+ .description("Add a comment to a task")
18
+ .argument("<task-id>", "Task ID")
19
+ .argument("<text>", "Comment text")
20
+ .action(async (rawTaskId: string, text: string) => {
21
+ try {
22
+ const taskId = await resolveTaskArg(rawTaskId);
23
+ const result = await createComment({ task_id: taskId, content: text });
24
+ console.log(chalk.green(`Comment added (${result.id})`));
25
+ } catch (err) {
26
+ handleError(err);
27
+ }
28
+ });
29
+
30
+ comment
31
+ .command("list")
32
+ .description("List comments for a task")
33
+ .argument("<task-id>", "Task ID")
34
+ .action(async (rawTaskId: string) => {
35
+ try {
36
+ const taskId = await resolveTaskArg(rawTaskId);
37
+ const comments = await getComments(taskId);
38
+ if (comments.length === 0) {
39
+ console.log(chalk.dim("No comments found."));
40
+ return;
41
+ }
42
+
43
+ const header = `${"ID".padEnd(ID_WIDTH)} ${"Posted".padEnd(POSTED_WIDTH)} Content`;
44
+ console.log(chalk.bold(header));
45
+ console.log(chalk.dim("-".repeat(ID_WIDTH + 1 + POSTED_WIDTH + 1 + 30)));
46
+
47
+ for (const c of comments) {
48
+ const id = c.id.padEnd(ID_WIDTH);
49
+ const posted = c.posted_at.padEnd(POSTED_WIDTH);
50
+ console.log(`${id} ${posted} ${c.content}`);
51
+ }
52
+ } catch (err) {
53
+ handleError(err);
54
+ }
55
+ });
56
+
57
+ comment
58
+ .command("update")
59
+ .description("Update a comment")
60
+ .argument("<id>", "Comment ID")
61
+ .requiredOption("--text <text>", "New comment text")
62
+ .action(async (id: string, opts: { text: string }) => {
63
+ try {
64
+ const result = await updateComment(id, { content: opts.text });
65
+ console.log(chalk.green(`Comment ${result.id} updated.`));
66
+ } catch (err) {
67
+ handleError(err);
68
+ }
69
+ });
70
+
71
+ comment
72
+ .command("delete")
73
+ .description("Delete a comment")
74
+ .argument("<id>", "Comment ID")
75
+ .action(async (id: string) => {
76
+ try {
77
+ await deleteComment(id);
78
+ console.log(chalk.green(`Comment ${id} deleted.`));
79
+ } catch (err) {
80
+ handleError(err);
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,87 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import type { CompletedTask } from "../api/types.ts";
4
+ import { getCompletedTasks } from "../api/completed.ts";
5
+ import { getProjects } from "../api/projects.ts";
6
+ import { padEnd } from "../utils/format.ts";
7
+ import { handleError } from "../utils/errors.ts";
8
+
9
+ function sinceToDate(since: string): string {
10
+ const now = new Date();
11
+ switch (since) {
12
+ case "today": {
13
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14
+ return d.toISOString();
15
+ }
16
+ case "7 days": {
17
+ const d = new Date(now);
18
+ d.setDate(d.getDate() - 7);
19
+ return d.toISOString();
20
+ }
21
+ case "30 days": {
22
+ const d = new Date(now);
23
+ d.setDate(d.getDate() - 30);
24
+ return d.toISOString();
25
+ }
26
+ default:
27
+ return since;
28
+ }
29
+ }
30
+
31
+ function formatDate(iso: string): string {
32
+ const d = new Date(iso);
33
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
34
+ }
35
+
36
+ export function registerCompletedCommand(program: Command): void {
37
+ program
38
+ .command("completed")
39
+ .description("Show completed tasks")
40
+ .option("--since <string>", 'Time range: "today", "7 days", "30 days"', "today")
41
+ .option("--group-by <field>", "Group by: project")
42
+ .action(async (opts: { since: string; groupBy?: string }) => {
43
+ try {
44
+ const sinceDate = sinceToDate(opts.since);
45
+ const tasks = await getCompletedTasks(sinceDate);
46
+
47
+ if (tasks.length === 0) {
48
+ console.log(chalk.dim("No completed tasks found."));
49
+ return;
50
+ }
51
+
52
+ if (opts.groupBy === "project") {
53
+ const projects = await getProjects();
54
+ const projectMap = new Map(projects.map((p) => [p.id, p.name]));
55
+ const groups = new Map<string, CompletedTask[]>();
56
+
57
+ for (const t of tasks) {
58
+ const name = projectMap.get(t.project_id) ?? "Unknown";
59
+ if (!groups.has(name)) groups.set(name, []);
60
+ groups.get(name)!.push(t);
61
+ }
62
+
63
+ for (const [projectName, items] of groups) {
64
+ console.log("");
65
+ console.log(chalk.bold.underline(projectName) + chalk.dim(` (${items.length})`));
66
+ for (const t of items) {
67
+ const date = chalk.dim(formatDate(t.completed_at));
68
+ console.log(` ${chalk.green("✓")} ${padEnd(t.content, 45)} ${date}`);
69
+ }
70
+ }
71
+ } else {
72
+ console.log(chalk.bold(`Completed tasks (since ${opts.since}):`));
73
+ console.log(chalk.dim("-".repeat(70)));
74
+
75
+ for (const t of tasks) {
76
+ const date = chalk.dim(formatDate(t.completed_at));
77
+ console.log(` ${chalk.green("✓")} ${padEnd(t.content, 45)} ${date}`);
78
+ }
79
+ }
80
+
81
+ console.log("");
82
+ console.log(chalk.dim(`Total: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`));
83
+ } catch (err) {
84
+ handleError(err);
85
+ }
86
+ });
87
+ }
@@ -0,0 +1,360 @@
1
+ import type { Command } from "commander";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import chalk from "chalk";
5
+ import { CONFIG_DIR } from "../config/index.ts";
6
+ import { cliExit } from "../utils/exit.ts";
7
+
8
+ const COMPLETION_CACHE_PATH = join(CONFIG_DIR, ".completion-cache.json");
9
+
10
+ export interface CompletionCache {
11
+ projects: string[];
12
+ labels: string[];
13
+ updated_at?: string;
14
+ }
15
+
16
+ export function getCompletionCache(): CompletionCache {
17
+ if (!existsSync(COMPLETION_CACHE_PATH)) {
18
+ return { projects: [], labels: [] };
19
+ }
20
+ try {
21
+ const raw = readFileSync(COMPLETION_CACHE_PATH, "utf-8");
22
+ return JSON.parse(raw) as CompletionCache;
23
+ } catch {
24
+ return { projects: [], labels: [] };
25
+ }
26
+ }
27
+
28
+ export function saveCompletionCache(cache: CompletionCache): void {
29
+ writeFileSync(COMPLETION_CACHE_PATH, JSON.stringify(cache, null, 2), "utf-8");
30
+ }
31
+
32
+ const BASH_COMPLETION = `#!/usr/bin/env bash
33
+ # todoist CLI bash completion
34
+ _todoist_completions() {
35
+ local cur prev commands
36
+ COMPREPLY=()
37
+ cur="\${COMP_WORDS[COMP_CWORD]}"
38
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
39
+
40
+ commands="task project label comment template section auth today inbox ui completion completed review matrix log stats next upcoming overdue search deadlines"
41
+ task_sub="add list complete delete update show reopen move"
42
+ project_sub="list create delete update show"
43
+ label_sub="list create delete update"
44
+ section_sub="list create delete update"
45
+ comment_sub="add list delete update"
46
+ template_sub="save apply list"
47
+
48
+ # Dynamic completion from cache
49
+ local cache_file="\${HOME}/.config/todoist-cli/.completion-cache.json"
50
+ local cached_projects=""
51
+ local cached_labels=""
52
+ if [ -f "\${cache_file}" ]; then
53
+ cached_projects=$(cat "\${cache_file}" | grep -o '"projects":\\s*\\[.*\\]' | sed 's/"projects":\\s*\\[//;s/\\]//;s/"//g;s/,/ /g' 2>/dev/null || echo "")
54
+ cached_labels=$(cat "\${cache_file}" | grep -o '"labels":\\s*\\[.*\\]' | sed 's/"labels":\\s*\\[//;s/\\]//;s/"//g;s/,/ /g' 2>/dev/null || echo "")
55
+ fi
56
+
57
+ # Complete project names for -P/--project flags
58
+ case "\${prev}" in
59
+ -P|--project)
60
+ COMPREPLY=( $(compgen -W "\${cached_projects}" -- "\${cur}") )
61
+ return 0
62
+ ;;
63
+ -l|--label)
64
+ COMPREPLY=( $(compgen -W "\${cached_labels}" -- "\${cur}") )
65
+ return 0
66
+ ;;
67
+ esac
68
+
69
+ case "\${COMP_WORDS[1]}" in
70
+ task)
71
+ COMPREPLY=( $(compgen -W "\${task_sub}" -- "\${cur}") )
72
+ return 0
73
+ ;;
74
+ project)
75
+ COMPREPLY=( $(compgen -W "\${project_sub}" -- "\${cur}") )
76
+ return 0
77
+ ;;
78
+ label)
79
+ COMPREPLY=( $(compgen -W "\${label_sub}" -- "\${cur}") )
80
+ return 0
81
+ ;;
82
+ section)
83
+ COMPREPLY=( $(compgen -W "\${section_sub}" -- "\${cur}") )
84
+ return 0
85
+ ;;
86
+ comment)
87
+ COMPREPLY=( $(compgen -W "\${comment_sub}" -- "\${cur}") )
88
+ return 0
89
+ ;;
90
+ template)
91
+ COMPREPLY=( $(compgen -W "\${template_sub}" -- "\${cur}") )
92
+ return 0
93
+ ;;
94
+ completion)
95
+ COMPREPLY=( $(compgen -W "bash zsh fish update" -- "\${cur}") )
96
+ return 0
97
+ ;;
98
+ esac
99
+
100
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
101
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
102
+ fi
103
+ return 0
104
+ }
105
+ complete -F _todoist_completions todoist`;
106
+
107
+ const ZSH_COMPLETION = `#compdef todoist
108
+ # todoist CLI zsh completion
109
+
110
+ _todoist_projects() {
111
+ local cache_file="\${HOME}/.config/todoist-cli/.completion-cache.json"
112
+ if [[ -f "\${cache_file}" ]]; then
113
+ local projects
114
+ projects=(\${(f)"$(cat "\${cache_file}" | python3 -c "import sys,json; [print(p) for p in json.load(sys.stdin).get('projects',[])]" 2>/dev/null)"})
115
+ compadd -a projects
116
+ fi
117
+ }
118
+
119
+ _todoist_labels() {
120
+ local cache_file="\${HOME}/.config/todoist-cli/.completion-cache.json"
121
+ if [[ -f "\${cache_file}" ]]; then
122
+ local labels
123
+ labels=(\${(f)"$(cat "\${cache_file}" | python3 -c "import sys,json; [print(l) for l in json.load(sys.stdin).get('labels',[])]" 2>/dev/null)"})
124
+ compadd -a labels
125
+ fi
126
+ }
127
+
128
+ _todoist() {
129
+ local -a commands task_sub project_sub label_sub section_sub comment_sub template_sub
130
+
131
+ commands=(
132
+ 'task:Manage tasks'
133
+ 'project:Manage projects'
134
+ 'label:Manage labels'
135
+ 'comment:Manage comments'
136
+ 'template:Manage templates'
137
+ 'section:Manage sections'
138
+ 'auth:Authenticate with Todoist'
139
+ 'today:Show today and overdue tasks'
140
+ 'inbox:Show inbox tasks'
141
+ 'ui:Launch interactive TUI'
142
+ 'completion:Generate shell completion script'
143
+ 'completed:Show completed tasks'
144
+ 'review:Interactive GTD weekly review'
145
+ 'matrix:Eisenhower matrix view'
146
+ 'log:Show activity log'
147
+ 'stats:Show productivity stats'
148
+ 'next:Show highest-priority actionable task'
149
+ 'upcoming:Show tasks for next 7 days'
150
+ 'overdue:Show overdue tasks'
151
+ 'search:Search tasks by text'
152
+ 'deadlines:Show tasks with upcoming deadlines'
153
+ )
154
+
155
+ task_sub=(
156
+ 'add:Add a new task'
157
+ 'list:List tasks'
158
+ 'complete:Complete one or more tasks'
159
+ 'delete:Delete one or more tasks'
160
+ 'update:Update a task'
161
+ 'show:Show full task details'
162
+ 'reopen:Reopen completed tasks'
163
+ 'move:Move task to another project'
164
+ )
165
+
166
+ project_sub=(
167
+ 'list:List all projects'
168
+ 'create:Create a new project'
169
+ 'delete:Delete a project'
170
+ 'update:Update a project'
171
+ 'show:Show project details'
172
+ )
173
+
174
+ label_sub=(
175
+ 'list:List all labels'
176
+ 'create:Create a new label'
177
+ 'delete:Delete a label'
178
+ 'update:Update a label'
179
+ )
180
+
181
+ section_sub=(
182
+ 'list:List sections'
183
+ 'create:Create a new section'
184
+ 'delete:Delete a section'
185
+ 'update:Update a section'
186
+ )
187
+
188
+ comment_sub=(
189
+ 'add:Add a comment to a task'
190
+ 'list:List comments for a task'
191
+ 'delete:Delete a comment'
192
+ 'update:Update a comment'
193
+ )
194
+
195
+ template_sub=(
196
+ 'save:Save a task as a template'
197
+ 'apply:Apply a template'
198
+ 'list:List templates'
199
+ )
200
+
201
+ # Handle -P/--project and -l/--label flag completions
202
+ case "\${words[CURRENT-1]}" in
203
+ -P|--project) _todoist_projects; return ;;
204
+ -l|--label) _todoist_labels; return ;;
205
+ esac
206
+
207
+ if (( CURRENT == 2 )); then
208
+ _describe -t commands 'todoist commands' commands
209
+ elif (( CURRENT == 3 )); then
210
+ case "\${words[2]}" in
211
+ task) _describe -t task_sub 'task subcommands' task_sub ;;
212
+ project) _describe -t project_sub 'project subcommands' project_sub ;;
213
+ label) _describe -t label_sub 'label subcommands' label_sub ;;
214
+ section) _describe -t section_sub 'section subcommands' section_sub ;;
215
+ comment) _describe -t comment_sub 'comment subcommands' comment_sub ;;
216
+ template) _describe -t template_sub 'template subcommands' template_sub ;;
217
+ completion) _values 'shell or action' bash zsh fish update ;;
218
+ esac
219
+ fi
220
+ }
221
+
222
+ _todoist "$@"`;
223
+
224
+ const FISH_COMPLETION = `# todoist CLI fish completion
225
+
226
+ # Disable file completions
227
+ complete -c todoist -f
228
+
229
+ # Dynamic completion helpers
230
+ function __todoist_cached_projects
231
+ set -l cache_file "$HOME/.config/todoist-cli/.completion-cache.json"
232
+ if test -f "$cache_file"
233
+ cat "$cache_file" | python3 -c "import sys,json; [print(p) for p in json.load(sys.stdin).get('projects',[])]" 2>/dev/null
234
+ end
235
+ end
236
+
237
+ function __todoist_cached_labels
238
+ set -l cache_file "$HOME/.config/todoist-cli/.completion-cache.json"
239
+ if test -f "$cache_file"
240
+ cat "$cache_file" | python3 -c "import sys,json; [print(l) for l in json.load(sys.stdin).get('labels',[])]" 2>/dev/null
241
+ end
242
+ end
243
+
244
+ # Dynamic completions for -P/--project and -l/--label flags
245
+ complete -c todoist -l project -s P -x -a "(__todoist_cached_projects)" -d "Project name"
246
+ complete -c todoist -l label -s l -x -a "(__todoist_cached_labels)" -d "Label name"
247
+
248
+ # Main commands
249
+ complete -c todoist -n "__fish_use_subcommand" -a "task" -d "Manage tasks"
250
+ complete -c todoist -n "__fish_use_subcommand" -a "project" -d "Manage projects"
251
+ complete -c todoist -n "__fish_use_subcommand" -a "label" -d "Manage labels"
252
+ complete -c todoist -n "__fish_use_subcommand" -a "comment" -d "Manage comments"
253
+ complete -c todoist -n "__fish_use_subcommand" -a "template" -d "Manage templates"
254
+ complete -c todoist -n "__fish_use_subcommand" -a "section" -d "Manage sections"
255
+ complete -c todoist -n "__fish_use_subcommand" -a "auth" -d "Authenticate with Todoist"
256
+ complete -c todoist -n "__fish_use_subcommand" -a "today" -d "Show today and overdue tasks"
257
+ complete -c todoist -n "__fish_use_subcommand" -a "inbox" -d "Show inbox tasks"
258
+ complete -c todoist -n "__fish_use_subcommand" -a "ui" -d "Launch interactive TUI"
259
+ complete -c todoist -n "__fish_use_subcommand" -a "completion" -d "Generate shell completion script"
260
+ complete -c todoist -n "__fish_use_subcommand" -a "completed" -d "Show completed tasks"
261
+ complete -c todoist -n "__fish_use_subcommand" -a "review" -d "Interactive GTD weekly review"
262
+ complete -c todoist -n "__fish_use_subcommand" -a "matrix" -d "Eisenhower matrix view"
263
+ complete -c todoist -n "__fish_use_subcommand" -a "log" -d "Show activity log"
264
+ complete -c todoist -n "__fish_use_subcommand" -a "stats" -d "Show productivity stats"
265
+ complete -c todoist -n "__fish_use_subcommand" -a "next" -d "Show highest-priority actionable task"
266
+ complete -c todoist -n "__fish_use_subcommand" -a "upcoming" -d "Show tasks for next 7 days"
267
+ complete -c todoist -n "__fish_use_subcommand" -a "overdue" -d "Show overdue tasks"
268
+ complete -c todoist -n "__fish_use_subcommand" -a "search" -d "Search tasks by text"
269
+ complete -c todoist -n "__fish_use_subcommand" -a "deadlines" -d "Show tasks with upcoming deadlines"
270
+
271
+ # task subcommands
272
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "add" -d "Add a new task"
273
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "list" -d "List tasks"
274
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "complete" -d "Complete one or more tasks"
275
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "delete" -d "Delete one or more tasks"
276
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "update" -d "Update a task"
277
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "show" -d "Show full task details"
278
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "reopen" -d "Reopen completed tasks"
279
+ complete -c todoist -n "__fish_seen_subcommand_from task" -a "move" -d "Move task to another project"
280
+
281
+ # project subcommands
282
+ complete -c todoist -n "__fish_seen_subcommand_from project" -a "list" -d "List all projects"
283
+ complete -c todoist -n "__fish_seen_subcommand_from project" -a "create" -d "Create a new project"
284
+ complete -c todoist -n "__fish_seen_subcommand_from project" -a "delete" -d "Delete a project"
285
+ complete -c todoist -n "__fish_seen_subcommand_from project" -a "update" -d "Update a project"
286
+ complete -c todoist -n "__fish_seen_subcommand_from project" -a "show" -d "Show project details"
287
+
288
+ # label subcommands
289
+ complete -c todoist -n "__fish_seen_subcommand_from label" -a "list" -d "List all labels"
290
+ complete -c todoist -n "__fish_seen_subcommand_from label" -a "create" -d "Create a new label"
291
+ complete -c todoist -n "__fish_seen_subcommand_from label" -a "delete" -d "Delete a label"
292
+ complete -c todoist -n "__fish_seen_subcommand_from label" -a "update" -d "Update a label"
293
+
294
+ # section subcommands
295
+ complete -c todoist -n "__fish_seen_subcommand_from section" -a "list" -d "List sections"
296
+ complete -c todoist -n "__fish_seen_subcommand_from section" -a "create" -d "Create a new section"
297
+ complete -c todoist -n "__fish_seen_subcommand_from section" -a "delete" -d "Delete a section"
298
+ complete -c todoist -n "__fish_seen_subcommand_from section" -a "update" -d "Update a section"
299
+
300
+ # comment subcommands
301
+ complete -c todoist -n "__fish_seen_subcommand_from comment" -a "add" -d "Add a comment to a task"
302
+ complete -c todoist -n "__fish_seen_subcommand_from comment" -a "list" -d "List comments for a task"
303
+ complete -c todoist -n "__fish_seen_subcommand_from comment" -a "delete" -d "Delete a comment"
304
+ complete -c todoist -n "__fish_seen_subcommand_from comment" -a "update" -d "Update a comment"
305
+
306
+ # template subcommands
307
+ complete -c todoist -n "__fish_seen_subcommand_from template" -a "save" -d "Save a task as a template"
308
+ complete -c todoist -n "__fish_seen_subcommand_from template" -a "apply" -d "Apply a template"
309
+ complete -c todoist -n "__fish_seen_subcommand_from template" -a "list" -d "List templates"
310
+
311
+ # completion subcommands
312
+ complete -c todoist -n "__fish_seen_subcommand_from completion" -a "bash zsh fish update" -d "Shell type or action"`;
313
+
314
+ async function updateCompletionCache(): Promise<void> {
315
+ const { getProjects } = await import("../api/projects.ts");
316
+ const { getLabels } = await import("../api/labels.ts");
317
+
318
+ try {
319
+ const [projects, labels] = await Promise.all([getProjects(), getLabels()]);
320
+ const cache: CompletionCache = {
321
+ projects: projects.map((p) => p.name),
322
+ labels: labels.map((l) => l.name),
323
+ updated_at: new Date().toISOString(),
324
+ };
325
+ saveCompletionCache(cache);
326
+ console.log(
327
+ chalk.green(`Completion cache updated: ${cache.projects.length} projects, ${cache.labels.length} labels`)
328
+ );
329
+ console.log(chalk.dim(`Saved to ${COMPLETION_CACHE_PATH}`));
330
+ } catch (err) {
331
+ console.error(chalk.red(`Failed to update completion cache: ${err instanceof Error ? err.message : err}`));
332
+ cliExit(1);
333
+ }
334
+ }
335
+
336
+ export function registerCompletionCommand(program: Command): void {
337
+ program
338
+ .command("completion")
339
+ .description("Generate shell completion script or update completion cache")
340
+ .argument("<shell-or-action>", "Shell type (bash, zsh, fish) or 'update' to refresh completion cache")
341
+ .action(async (arg: string) => {
342
+ switch (arg) {
343
+ case "bash":
344
+ console.log(BASH_COMPLETION);
345
+ break;
346
+ case "zsh":
347
+ console.log(ZSH_COMPLETION);
348
+ break;
349
+ case "fish":
350
+ console.log(FISH_COMPLETION);
351
+ break;
352
+ case "update":
353
+ await updateCompletionCache();
354
+ break;
355
+ default:
356
+ console.error(`Unknown argument: ${arg}. Supported: bash, zsh, fish, update`);
357
+ cliExit(2);
358
+ }
359
+ });
360
+ }