@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.
- package/package.json +6 -6
- package/src/api/activity.ts +8 -0
- package/src/api/client.ts +214 -0
- package/src/api/comments.ts +18 -0
- package/src/api/completed.ts +15 -0
- package/src/api/labels.ts +18 -0
- package/src/api/projects.ts +22 -0
- package/src/api/sections.ts +20 -0
- package/src/api/stats.ts +38 -0
- package/src/api/tasks.ts +34 -0
- package/src/api/types.ts +202 -0
- package/src/cli/auth.ts +40 -0
- package/src/cli/commands/task/add.ts +328 -0
- package/src/cli/commands/task/complete.ts +62 -0
- package/src/cli/commands/task/delete.ts +62 -0
- package/src/cli/commands/task/helpers.ts +289 -0
- package/src/cli/commands/task/index.ts +27 -0
- package/src/cli/commands/task/list.ts +151 -0
- package/src/cli/commands/task/move.ts +49 -0
- package/src/cli/commands/task/reopen.ts +43 -0
- package/src/cli/commands/task/show.ts +115 -0
- package/src/cli/commands/task/update.ts +122 -0
- package/src/cli/comment.ts +83 -0
- package/src/cli/completed.ts +87 -0
- package/src/cli/completion.ts +360 -0
- package/src/cli/filter.ts +115 -0
- package/src/cli/index.ts +638 -0
- package/src/cli/label.ts +120 -0
- package/src/cli/log.ts +57 -0
- package/src/cli/matrix.ts +100 -0
- package/src/cli/plugin-loader.ts +38 -0
- package/src/cli/plugin.ts +289 -0
- package/src/cli/project.ts +172 -0
- package/src/cli/review.ts +116 -0
- package/src/cli/section.ts +98 -0
- package/src/cli/stats.ts +62 -0
- package/src/cli/template.ts +89 -0
- package/src/config/index.ts +229 -0
- package/src/plugins/api-proxy.ts +70 -0
- package/src/plugins/extension-registry.ts +53 -0
- package/src/plugins/hook-registry.ts +36 -0
- package/src/plugins/loader.ts +200 -0
- package/src/plugins/marketplace-types.ts +55 -0
- package/src/plugins/marketplace.ts +576 -0
- package/src/plugins/palette-registry.ts +21 -0
- package/src/plugins/storage.ts +101 -0
- package/src/plugins/types.ts +226 -0
- package/src/plugins/view-registry.ts +19 -0
- package/src/ui/App.tsx +234 -0
- package/src/ui/components/Breadcrumb.tsx +18 -0
- package/src/ui/components/CommandPalette.tsx +237 -0
- package/src/ui/components/ConfirmDialog.tsx +28 -0
- package/src/ui/components/EditTaskModal.tsx +484 -0
- package/src/ui/components/HelpOverlay.tsx +195 -0
- package/src/ui/components/InputPrompt.tsx +109 -0
- package/src/ui/components/LabelPicker.tsx +110 -0
- package/src/ui/components/ModalManager.tsx +275 -0
- package/src/ui/components/ProjectPicker.tsx +95 -0
- package/src/ui/components/Sidebar.tsx +282 -0
- package/src/ui/components/SortMenu.tsx +77 -0
- package/src/ui/components/StatusBar.tsx +67 -0
- package/src/ui/components/TaskList.tsx +258 -0
- package/src/ui/components/TaskRow.tsx +105 -0
- package/src/ui/hooks/useKeyboardHandler.ts +291 -0
- package/src/ui/hooks/useStatusMessage.ts +32 -0
- package/src/ui/hooks/useTaskOperations.ts +558 -0
- package/src/ui/hooks/useUndoSystem.ts +218 -0
- package/src/ui/views/ActivityView.tsx +213 -0
- package/src/ui/views/CompletedView.tsx +337 -0
- package/src/ui/views/StatsView.tsx +178 -0
- package/src/ui/views/TaskDetailView.tsx +438 -0
- package/src/ui/views/TasksView.tsx +851 -0
- package/src/utils/colors.ts +27 -0
- package/src/utils/date-format.ts +54 -0
- package/src/utils/errors.ts +159 -0
- package/src/utils/exit.ts +11 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/open-url.ts +9 -0
- package/src/utils/output.ts +29 -0
- package/src/utils/quick-add.ts +202 -0
- package/src/utils/resolve.ts +359 -0
- package/src/utils/sorting.ts +27 -0
- package/src/utils/validation.ts +88 -0
- 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
|
+
}
|