@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,120 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getLabels, createLabel, updateLabel, deleteLabel } from "../api/labels.ts";
4
+ import { handleError } from "../utils/errors.ts";
5
+ import { cliExit } from "../utils/exit.ts";
6
+ import { ID_WIDTH } from "../utils/format.ts";
7
+ import { saveLastList, resolveLabelArg } from "../utils/resolve.ts";
8
+
9
+ const NAME_WIDTH = 25;
10
+ const COLOR_WIDTH = 12;
11
+
12
+ export function registerLabelCommand(program: Command): void {
13
+ const label = program
14
+ .command("label")
15
+ .description("Manage labels");
16
+
17
+ label
18
+ .command("list")
19
+ .description("List all labels")
20
+ .option("--json <fields>", "Output JSON with specified fields (comma-separated)")
21
+ .option("-q, --quiet", "Print only label IDs")
22
+ .action(async (opts: { json?: string; quiet?: boolean }) => {
23
+ try {
24
+ const labels = await getLabels();
25
+
26
+ if (opts.quiet) {
27
+ for (const l of labels) console.log(l.id);
28
+ return;
29
+ }
30
+
31
+ if (opts.json !== undefined) {
32
+ const fields = opts.json.split(",").map((f) => f.trim());
33
+ const data = labels.map((l) => {
34
+ const obj: Record<string, unknown> = {};
35
+ for (const f of fields) {
36
+ if (f in l) obj[f] = (l as unknown as Record<string, unknown>)[f];
37
+ }
38
+ return obj;
39
+ });
40
+ console.log(JSON.stringify(data, null, 2));
41
+ return;
42
+ }
43
+
44
+ if (labels.length === 0) {
45
+ console.log(chalk.dim("No labels found."));
46
+ return;
47
+ }
48
+
49
+ const header = `${"#".padStart(3)} ${"ID".padEnd(ID_WIDTH)} ${"Name".padEnd(NAME_WIDTH)} ${"Color".padEnd(COLOR_WIDTH)} Favorite`;
50
+ console.log(chalk.bold(header));
51
+ console.log(chalk.dim("-".repeat(3 + 1 + ID_WIDTH + 1 + NAME_WIDTH + 1 + COLOR_WIDTH + 1 + 8)));
52
+
53
+ for (let i = 0; i < labels.length; i++) {
54
+ const l = labels[i]!;
55
+ const num = chalk.dim(String(i + 1).padStart(3));
56
+ const id = l.id.padEnd(ID_WIDTH);
57
+ const name = l.name.padEnd(NAME_WIDTH);
58
+ const color = l.color.padEnd(COLOR_WIDTH);
59
+ const fav = l.is_favorite ? chalk.yellow("*") : " ";
60
+ console.log(`${num} ${id} ${name} ${color} ${fav}`);
61
+ }
62
+
63
+ saveLastList("label", labels.map(l => ({ id: l.id, label: l.name })));
64
+ } catch (err) {
65
+ handleError(err);
66
+ }
67
+ });
68
+
69
+ label
70
+ .command("create")
71
+ .description("Create a new label")
72
+ .argument("<name>", "Label name")
73
+ .action(async (name: string) => {
74
+ try {
75
+ const result = await createLabel({ name });
76
+ console.log(chalk.green(`Label created: ${result.name} (${result.id})`));
77
+ } catch (err) {
78
+ handleError(err);
79
+ }
80
+ });
81
+
82
+ label
83
+ .command("update")
84
+ .description("Update a label")
85
+ .argument("<id>", "Label ID")
86
+ .option("--name <name>", "New label name")
87
+ .option("--color <color>", "New color")
88
+ .action(async (rawId: string, opts: { name?: string; color?: string }) => {
89
+ try {
90
+ const id = await resolveLabelArg(rawId);
91
+ const params: Record<string, unknown> = {};
92
+ if (opts.name) params.name = opts.name;
93
+ if (opts.color) params.color = opts.color;
94
+
95
+ if (Object.keys(params).length === 0) {
96
+ console.error(chalk.red("No update options provided. Use --name or --color."));
97
+ cliExit(1);
98
+ }
99
+
100
+ const result = await updateLabel(id, params);
101
+ console.log(chalk.green(`Label ${result.id} updated: ${result.name}`));
102
+ } catch (err) {
103
+ handleError(err);
104
+ }
105
+ });
106
+
107
+ label
108
+ .command("delete")
109
+ .description("Delete a label")
110
+ .argument("<id>", "Label ID")
111
+ .action(async (rawId: string) => {
112
+ try {
113
+ const id = await resolveLabelArg(rawId);
114
+ await deleteLabel(id);
115
+ console.log(chalk.green(`Label ${id} deleted.`));
116
+ } catch (err) {
117
+ handleError(err);
118
+ }
119
+ });
120
+ }
package/src/cli/log.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { getActivity } from "../api/activity.ts";
4
+ import { padEnd } from "../utils/format.ts";
5
+ import { handleError } from "../utils/errors.ts";
6
+
7
+ function eventColor(eventType: string): (text: string) => string {
8
+ switch (eventType) {
9
+ case "completed": return chalk.green;
10
+ case "added":
11
+ case "created": return chalk.blue;
12
+ case "updated": return chalk.yellow;
13
+ case "deleted": return chalk.red;
14
+ default: return chalk.white;
15
+ }
16
+ }
17
+
18
+ function formatTimestamp(iso: string): string {
19
+ const d = new Date(iso);
20
+ return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) +
21
+ " " + d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
22
+ }
23
+
24
+ export function registerLogCommand(program: Command): void {
25
+ program
26
+ .command("log")
27
+ .description("Show activity log")
28
+ .option("-n, --limit <number>", "Number of events to show", "30")
29
+ .action(async (opts: { limit: string }) => {
30
+ try {
31
+ const events = await getActivity(parseInt(opts.limit, 10));
32
+
33
+ if (events.length === 0) {
34
+ console.log(chalk.dim("No activity found."));
35
+ return;
36
+ }
37
+
38
+ console.log(chalk.bold("Activity log:"));
39
+ console.log(chalk.dim("-".repeat(70)));
40
+
41
+ for (const e of events) {
42
+ const time = chalk.dim(formatTimestamp(e.event_date));
43
+ const colorFn = eventColor(e.event_type);
44
+ const type = padEnd(colorFn(e.event_type), 12);
45
+ const extra = e.extra_data && typeof e.extra_data === "object" && "content" in e.extra_data
46
+ ? chalk.dim(` — ${String(e.extra_data.content)}`)
47
+ : `${e.object_type}`;
48
+ console.log(` ${time} ${type} ${extra}`);
49
+ }
50
+
51
+ console.log("");
52
+ console.log(chalk.dim(`Total: ${events.length} event${events.length === 1 ? "" : "s"}`));
53
+ } catch (err) {
54
+ handleError(err);
55
+ }
56
+ });
57
+ }
@@ -0,0 +1,100 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import type { Task, Priority } from "../api/types.ts";
4
+ import { getTasks } from "../api/tasks.ts";
5
+ import { padEnd, truncate } from "../utils/format.ts";
6
+ import { handleError } from "../utils/errors.ts";
7
+
8
+ const COL_WIDTH = 35;
9
+
10
+ function formatTaskLine(task: Task): string {
11
+ const content = truncate(task.content, COL_WIDTH - 4);
12
+ return ` ${content}`;
13
+ }
14
+
15
+ function renderQuadrant(title: string, tasks: Task[], color: (s: string) => string): string[] {
16
+ const lines: string[] = [];
17
+ lines.push(color(` ${title}`));
18
+ if (tasks.length === 0) {
19
+ lines.push(chalk.dim(" (empty)"));
20
+ } else {
21
+ for (const t of tasks.slice(0, 8)) {
22
+ lines.push(formatTaskLine(t));
23
+ }
24
+ if (tasks.length > 8) {
25
+ lines.push(chalk.dim(` +${tasks.length - 8} more`));
26
+ }
27
+ }
28
+ return lines;
29
+ }
30
+
31
+ function mergeColumns(left: string[], right: string[], colWidth: number): string[] {
32
+ const maxLen = Math.max(left.length, right.length);
33
+ const result: string[] = [];
34
+ for (let i = 0; i < maxLen; i++) {
35
+ const l = padEnd(left[i] ?? "", colWidth);
36
+ const r = padEnd(right[i] ?? "", colWidth);
37
+ result.push(`${l} ${chalk.dim("\u2502")} ${r}`);
38
+ }
39
+ return result;
40
+ }
41
+
42
+ export function registerMatrixCommand(program: Command): void {
43
+ program
44
+ .command("matrix")
45
+ .description("Eisenhower matrix view")
46
+ .option("--today", "Only show tasks due today")
47
+ .action(async (opts: { today?: boolean }) => {
48
+ try {
49
+ let tasks: Task[];
50
+ if (opts.today) {
51
+ tasks = await getTasks({ filter: "today" });
52
+ } else {
53
+ tasks = await getTasks();
54
+ }
55
+
56
+ const buckets: Record<Priority, Task[]> = { 1: [], 2: [], 3: [], 4: [] };
57
+ for (const t of tasks) {
58
+ buckets[t.priority].push(t);
59
+ }
60
+
61
+ const totalWidth = COL_WIDTH * 2 + 3;
62
+ const hLine = chalk.dim("\u2500".repeat(COL_WIDTH));
63
+ const hSep = `${hLine}${chalk.dim("\u253C")}${hLine}`;
64
+ const topBorder = `${chalk.dim("\u250C")}${hLine}${chalk.dim("\u252C")}${hLine}${chalk.dim("\u2510")}`;
65
+ const midBorder = `${chalk.dim("\u251C")}${hSep.replace(/\u253C/, "\u253C")}${chalk.dim("\u2524")}`;
66
+ const botBorder = `${chalk.dim("\u2514")}${hLine}${chalk.dim("\u2534")}${hLine}${chalk.dim("\u2518")}`;
67
+
68
+ const header = chalk.dim(" ".repeat(Math.floor((totalWidth - 20) / 2))) + chalk.bold("Eisenhower Matrix");
69
+ console.log("");
70
+ console.log(header);
71
+ console.log("");
72
+
73
+ // Top quadrants: p4 (DO FIRST / urgent) | p3 (SCHEDULE)
74
+ const q1 = renderQuadrant("DO FIRST (p4)", buckets[4], chalk.red.bold);
75
+ const q2 = renderQuadrant("SCHEDULE (p3)", buckets[3], chalk.yellow.bold);
76
+ const topRows = mergeColumns(q1, q2, COL_WIDTH);
77
+
78
+ // Bottom quadrants: p2 (DELEGATE) | p1 (ELIMINATE / normal)
79
+ const q3 = renderQuadrant("DELEGATE (p2)", buckets[2], chalk.blue.bold);
80
+ const q4 = renderQuadrant("ELIMINATE (p1)", buckets[1], chalk.white.bold);
81
+ const botRows = mergeColumns(q3, q4, COL_WIDTH);
82
+
83
+ console.log(topBorder);
84
+ for (const row of topRows) {
85
+ console.log(`${chalk.dim("\u2502")}${row}${chalk.dim("\u2502")}`);
86
+ }
87
+ console.log(midBorder);
88
+ for (const row of botRows) {
89
+ console.log(`${chalk.dim("\u2502")}${row}${chalk.dim("\u2502")}`);
90
+ }
91
+ console.log(botBorder);
92
+ console.log("");
93
+
94
+ const total = tasks.length;
95
+ console.log(chalk.dim(`Total: ${total} task${total === 1 ? "" : "s"} — p4: ${buckets[4].length}, p3: ${buckets[3].length}, p2: ${buckets[2].length}, p1: ${buckets[1].length}`));
96
+ } catch (err) {
97
+ handleError(err);
98
+ }
99
+ });
100
+ }
@@ -0,0 +1,38 @@
1
+ import type { Command } from "commander";
2
+ import { createHookRegistry } from "../plugins/hook-registry.ts";
3
+ import { createViewRegistry } from "../plugins/view-registry.ts";
4
+ import { createExtensionRegistry } from "../plugins/extension-registry.ts";
5
+ import { createPaletteRegistry } from "../plugins/palette-registry.ts";
6
+ import { loadPlugins, type LoadedPlugins } from "../plugins/loader.ts";
7
+ import type { HookRegistry } from "../plugins/types.ts";
8
+
9
+ let loadedPlugins: LoadedPlugins | null = null;
10
+ let hookRegistry: HookRegistry | null = null;
11
+
12
+ /**
13
+ * Load all plugins and register their CLI commands on the Commander program.
14
+ */
15
+ export async function loadCliPlugins(program: Command): Promise<void> {
16
+ const hooks = createHookRegistry();
17
+ const views = createViewRegistry();
18
+ const extensions = createExtensionRegistry();
19
+ const palette = createPaletteRegistry();
20
+
21
+ const loaded = await loadPlugins(hooks, views, extensions, palette);
22
+ loadedPlugins = loaded;
23
+ hookRegistry = hooks;
24
+
25
+ for (const { plugin, ctx } of loaded.plugins) {
26
+ if (plugin.registerCommands) {
27
+ plugin.registerCommands(program, ctx);
28
+ }
29
+ }
30
+ }
31
+
32
+ export function getCliHookRegistry(): HookRegistry | null {
33
+ return hookRegistry;
34
+ }
35
+
36
+ export function getCliLoadedPlugins(): LoadedPlugins | null {
37
+ return loadedPlugins;
38
+ }
@@ -0,0 +1,289 @@
1
+ import type { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import {
4
+ discoverPlugins,
5
+ installPlugin,
6
+ removePlugin,
7
+ updatePlugin,
8
+ updateAllPlugins,
9
+ enablePlugin,
10
+ disablePlugin,
11
+ getRegisteredMarketplaces,
12
+ addMarketplace,
13
+ removeMarketplace,
14
+ refreshMarketplaceCache,
15
+ } from "../plugins/marketplace.ts";
16
+ import { getConfig } from "../config/index.ts";
17
+
18
+ export function registerPluginCommand(program: Command): void {
19
+ const plugin = program
20
+ .command("plugin")
21
+ .description("Manage plugins and marketplaces");
22
+
23
+ // ── todoist plugin list ──
24
+ plugin
25
+ .command("list")
26
+ .description("List installed plugins")
27
+ .action(() => {
28
+ try {
29
+ const config = getConfig();
30
+ const plugins = config.plugins;
31
+
32
+ if (!plugins || Object.keys(plugins).length === 0) {
33
+ console.log(chalk.dim("No plugins installed."));
34
+ console.log(chalk.dim("Discover plugins with: todoist plugin discover"));
35
+ return;
36
+ }
37
+
38
+ console.log(chalk.bold("Installed Plugins"));
39
+ console.log("");
40
+
41
+ for (const [name, entry] of Object.entries(plugins)) {
42
+ const isEnabled = entry.enabled !== false;
43
+ const status = isEnabled ? chalk.green("●") : chalk.yellow("○");
44
+ const statusLabel = isEnabled ? chalk.green("enabled") : chalk.yellow("disabled");
45
+ const version = (entry.version as string) ?? "unknown";
46
+ const source = (entry.source as string) ?? "";
47
+
48
+ console.log(
49
+ ` ${status} ${chalk.bold(name)} ${chalk.cyan("v" + version)} ${statusLabel} ${chalk.dim(source)}`,
50
+ );
51
+ }
52
+ } catch (err) {
53
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
54
+ process.exit(1);
55
+ }
56
+ });
57
+
58
+ // ── todoist plugin discover ──
59
+ plugin
60
+ .command("discover")
61
+ .description("Browse available plugins from all marketplaces")
62
+ .action(async () => {
63
+ try {
64
+ console.log(chalk.dim("Fetching plugins from marketplaces..."));
65
+ const discovered = await discoverPlugins();
66
+
67
+ if (discovered.length === 0) {
68
+ console.log(chalk.dim("No plugins found in any marketplace."));
69
+ return;
70
+ }
71
+
72
+ // Group by marketplace
73
+ const grouped = new Map<string, typeof discovered>();
74
+ for (const plugin of discovered) {
75
+ const group = grouped.get(plugin.marketplace) ?? [];
76
+ group.push(plugin);
77
+ grouped.set(plugin.marketplace, group);
78
+ }
79
+
80
+ for (const [marketplace, plugins] of grouped) {
81
+ console.log("");
82
+ console.log(chalk.bold.underline(marketplace));
83
+ console.log("");
84
+
85
+ for (const p of plugins) {
86
+ let indicator: string;
87
+ if (p.installed && p.enabled) {
88
+ indicator = chalk.green("●");
89
+ } else if (p.installed && !p.enabled) {
90
+ indicator = chalk.yellow("◐");
91
+ } else {
92
+ indicator = chalk.dim("○");
93
+ }
94
+
95
+ const version = p.version ? chalk.cyan("v" + p.version) : "";
96
+ const description = p.description ? chalk.dim(p.description) : "";
97
+
98
+ console.log(` ${indicator} ${chalk.bold(p.name)} ${version} ${description}`);
99
+ }
100
+ }
101
+ } catch (err) {
102
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ // ── todoist plugin install <name> ──
108
+ plugin
109
+ .command("install")
110
+ .description("Install a plugin (optionally name@marketplace)")
111
+ .argument("<name>", "Plugin name (or name@marketplace)")
112
+ .action(async (nameArg: string) => {
113
+ try {
114
+ let pluginName = nameArg;
115
+ let marketplaceName: string | undefined;
116
+
117
+ // Parse name@marketplace syntax
118
+ const atIndex = nameArg.lastIndexOf("@");
119
+ if (atIndex > 0) {
120
+ pluginName = nameArg.substring(0, atIndex);
121
+ marketplaceName = nameArg.substring(atIndex + 1);
122
+ }
123
+
124
+ console.log(chalk.dim(`Installing plugin "${pluginName}"...`));
125
+ const result = await installPlugin(pluginName, marketplaceName);
126
+ console.log(chalk.green(`Installed ${result.name} v${result.version} from ${result.marketplace}`));
127
+ if (result.description) {
128
+ console.log(chalk.dim(` ${result.description}`));
129
+ }
130
+ } catch (err) {
131
+ console.error(chalk.red(`Failed to install: ${err instanceof Error ? err.message : String(err)}`));
132
+ process.exit(1);
133
+ }
134
+ });
135
+
136
+ // ── todoist plugin remove <name> ──
137
+ plugin
138
+ .command("remove")
139
+ .description("Remove an installed plugin")
140
+ .argument("<name>", "Plugin name")
141
+ .action((name: string) => {
142
+ try {
143
+ removePlugin(name);
144
+ console.log(chalk.green(`Removed ${name}`));
145
+ } catch (err) {
146
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
147
+ process.exit(1);
148
+ }
149
+ });
150
+
151
+ // ── todoist plugin update [name] ──
152
+ plugin
153
+ .command("update")
154
+ .description("Update a specific plugin or all plugins")
155
+ .argument("[name]", "Plugin name (omit to update all)")
156
+ .action(async (name?: string) => {
157
+ try {
158
+ if (name) {
159
+ const result = await updatePlugin(name);
160
+ if (result.updated) {
161
+ console.log(chalk.green(`${result.name}: ${result.message}`));
162
+ } else {
163
+ console.log(chalk.dim(`${result.name}: ${result.message}`));
164
+ }
165
+ } else {
166
+ console.log(chalk.dim("Updating all plugins..."));
167
+ const results = await updateAllPlugins();
168
+
169
+ if (results.length === 0) {
170
+ console.log(chalk.dim("No plugins installed."));
171
+ return;
172
+ }
173
+
174
+ for (const result of results) {
175
+ if (result.updated) {
176
+ console.log(chalk.green(` ${result.name}: ${result.message}`));
177
+ } else {
178
+ console.log(chalk.dim(` ${result.name}: ${result.message}`));
179
+ }
180
+ }
181
+ }
182
+ } catch (err) {
183
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
184
+ process.exit(1);
185
+ }
186
+ });
187
+
188
+ // ── todoist plugin enable <name> ──
189
+ plugin
190
+ .command("enable")
191
+ .description("Enable a disabled plugin")
192
+ .argument("<name>", "Plugin name")
193
+ .action((name: string) => {
194
+ try {
195
+ enablePlugin(name);
196
+ console.log(chalk.green(`Enabled ${name}`));
197
+ } catch (err) {
198
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ // ── todoist plugin disable <name> ──
204
+ plugin
205
+ .command("disable")
206
+ .description("Disable a plugin without removing it")
207
+ .argument("<name>", "Plugin name")
208
+ .action((name: string) => {
209
+ try {
210
+ disablePlugin(name);
211
+ console.log(chalk.yellow(`Disabled ${name}`));
212
+ } catch (err) {
213
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
214
+ process.exit(1);
215
+ }
216
+ });
217
+
218
+ // ── todoist plugin marketplace ──
219
+ const marketplace = plugin
220
+ .command("marketplace")
221
+ .description("Manage plugin marketplaces");
222
+
223
+ // ── todoist plugin marketplace list ──
224
+ marketplace
225
+ .command("list")
226
+ .description("List registered marketplaces")
227
+ .action(() => {
228
+ try {
229
+ const marketplaces = getRegisteredMarketplaces();
230
+
231
+ console.log(chalk.bold("Registered Marketplaces"));
232
+ console.log("");
233
+
234
+ for (const m of marketplaces) {
235
+ const autoUpdate = m.autoUpdate ? chalk.green("auto-update") : chalk.dim("manual");
236
+ console.log(` ${chalk.bold(m.name)} ${chalk.dim(m.source)} ${autoUpdate}`);
237
+ }
238
+ } catch (err) {
239
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
240
+ process.exit(1);
241
+ }
242
+ });
243
+
244
+ // ── todoist plugin marketplace add <source> ──
245
+ marketplace
246
+ .command("add")
247
+ .description("Add a marketplace (e.g. github:user/repo)")
248
+ .argument("<source>", "Marketplace source (github:user/repo)")
249
+ .action((source: string) => {
250
+ try {
251
+ const name = addMarketplace(source);
252
+ console.log(chalk.green(`Added marketplace "${name}" from ${source}`));
253
+ } catch (err) {
254
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
255
+ process.exit(1);
256
+ }
257
+ });
258
+
259
+ // ── todoist plugin marketplace remove <name> ──
260
+ marketplace
261
+ .command("remove")
262
+ .description("Remove a registered marketplace")
263
+ .argument("<name>", "Marketplace name")
264
+ .action((name: string) => {
265
+ try {
266
+ removeMarketplace(name);
267
+ console.log(chalk.green(`Removed marketplace "${name}"`));
268
+ } catch (err) {
269
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
270
+ process.exit(1);
271
+ }
272
+ });
273
+
274
+ // ── todoist plugin marketplace refresh [name] ──
275
+ marketplace
276
+ .command("refresh")
277
+ .description("Refresh marketplace cache")
278
+ .argument("[name]", "Marketplace name (omit to refresh all)")
279
+ .action(async (name?: string) => {
280
+ try {
281
+ console.log(chalk.dim(`Refreshing ${name ? `"${name}"` : "all marketplaces"}...`));
282
+ await refreshMarketplaceCache(name);
283
+ console.log(chalk.green(`Marketplace cache refreshed.`));
284
+ } catch (err) {
285
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
286
+ process.exit(1);
287
+ }
288
+ });
289
+ }