@hasna/connectors 1.3.22 → 1.3.23

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 (63) hide show
  1. package/bin/index.js +109 -59
  2. package/bin/mcp.js +236 -182
  3. package/bin/serve.js +77 -33
  4. package/connectors/connect-dropbox/src/api/bulk.ts +285 -0
  5. package/connectors/connect-dropbox/src/api/index.ts +4 -0
  6. package/connectors/connect-dropbox/src/cli/index.ts +151 -0
  7. package/connectors/connect-github/src/api/bulk.ts +328 -0
  8. package/connectors/connect-github/src/api/index.ts +4 -0
  9. package/connectors/connect-github/src/cli/index.ts +145 -0
  10. package/connectors/connect-gmail/src/api/history.ts +82 -0
  11. package/connectors/connect-gmail/src/api/index.ts +12 -0
  12. package/connectors/connect-gmail/src/api/settings.ts +280 -0
  13. package/connectors/connect-gmail/src/api/watch.ts +40 -0
  14. package/connectors/connect-gmail/src/cli/index.ts +237 -0
  15. package/connectors/connect-googleads/src/api/bulk.ts +354 -0
  16. package/connectors/connect-googleads/src/api/index.ts +4 -0
  17. package/connectors/connect-googleads/src/cli/index.ts +192 -0
  18. package/connectors/connect-googlecalendar/src/api/acl.ts +66 -0
  19. package/connectors/connect-googlecalendar/src/api/bulk.ts +452 -0
  20. package/connectors/connect-googlecalendar/src/api/freebusy.ts +35 -0
  21. package/connectors/connect-googlecalendar/src/api/index.ts +9 -0
  22. package/connectors/connect-googlecalendar/src/cli/index.ts +522 -0
  23. package/connectors/connect-googlecalendar/src/types/index.ts +90 -0
  24. package/connectors/connect-googlecontacts/src/api/bulk.ts +365 -0
  25. package/connectors/connect-googlecontacts/src/api/groups.ts +164 -0
  26. package/connectors/connect-googlecontacts/src/api/index.ts +8 -0
  27. package/connectors/connect-googlecontacts/src/cli/index.ts +340 -0
  28. package/connectors/connect-googledocs/src/api/bulk.ts +301 -0
  29. package/connectors/connect-googledocs/src/api/index.ts +4 -0
  30. package/connectors/connect-googledocs/src/cli/index.ts +187 -0
  31. package/connectors/connect-googledrive/src/api/bulk.ts +505 -0
  32. package/connectors/connect-googledrive/src/api/changes.ts +139 -0
  33. package/connectors/connect-googledrive/src/api/client.ts +34 -0
  34. package/connectors/connect-googledrive/src/api/index.ts +12 -0
  35. package/connectors/connect-googledrive/src/api/revisions.ts +132 -0
  36. package/connectors/connect-googledrive/src/cli/index.ts +624 -0
  37. package/connectors/connect-googledrive/src/index.ts +2 -0
  38. package/connectors/connect-googlephotos/src/api/bulk.ts +455 -0
  39. package/connectors/connect-googlephotos/src/api/index.ts +8 -0
  40. package/connectors/connect-googlephotos/src/cli/index.ts +185 -0
  41. package/connectors/connect-googletasks/src/api/bulk.ts +359 -0
  42. package/connectors/connect-googletasks/src/api/index.ts +1 -0
  43. package/connectors/connect-googletasks/src/cli/index.ts +178 -0
  44. package/connectors/connect-linear/src/api/bulk.ts +219 -0
  45. package/connectors/connect-linear/src/api/index.ts +4 -0
  46. package/connectors/connect-linear/src/cli/index.ts +116 -0
  47. package/connectors/connect-shopify/src/api/bulk.ts +288 -0
  48. package/connectors/connect-shopify/src/api/index.ts +4 -0
  49. package/connectors/connect-shopify/src/cli/index.ts +108 -0
  50. package/connectors/connect-slack/src/api/bulk.ts +237 -0
  51. package/connectors/connect-slack/src/api/index.ts +4 -0
  52. package/connectors/connect-slack/src/cli/index.ts +157 -0
  53. package/connectors/connect-stripe/src/api/bulk.ts +488 -0
  54. package/connectors/connect-stripe/src/api/index.ts +4 -0
  55. package/connectors/connect-stripe/src/cli/index.ts +206 -0
  56. package/dist/index.js +75 -33
  57. package/dist/lib/runner.d.ts +15 -5
  58. package/package.json +2 -2
  59. package/connectors/connect-cloudflare/bun.lock +0 -32
  60. package/connectors/connect-gmail/bin/index.js +0 -7027
  61. package/connectors/connect-gmail/dist/cli/index.js +0 -7035
  62. package/connectors/connect-gmail/dist/index.js +0 -2174
  63. package/dist/server/server.test.d.ts +0 -1
@@ -0,0 +1,359 @@
1
+ import {
2
+ getClientId,
3
+ getClientSecret,
4
+ getAccessToken,
5
+ getRefreshToken,
6
+ setTokens,
7
+ isTokenExpired,
8
+ } from '../utils/config';
9
+ import type {
10
+ Task,
11
+ TasksResponse,
12
+ GoogleTasksError,
13
+ AuthenticationError,
14
+ } from '../types';
15
+
16
+ const TASKS_API_BASE = 'https://tasks.googleapis.com/tasks/v1';
17
+ const OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
18
+
19
+ export interface TaskSummary {
20
+ id: string;
21
+ title: string;
22
+ status: string;
23
+ due?: string;
24
+ notes?: string;
25
+ }
26
+
27
+ export interface BulkOperationOptions {
28
+ taskListId: string;
29
+ /** Filter by completion status */
30
+ showCompleted?: boolean;
31
+ /** Filter by search (matches title) */
32
+ query?: string;
33
+ /** Maximum tasks to process (default: 100) */
34
+ maxResults?: number;
35
+ /** Maximum concurrent API calls (default: 10) */
36
+ concurrency?: number;
37
+ /** Dry run - don't actually modify */
38
+ dryRun?: boolean;
39
+ /** Progress callback */
40
+ onProgress?: (current: number, total: number, task: TaskSummary) => void;
41
+ /** Error callback */
42
+ onError?: (error: Error, task: TaskSummary) => void;
43
+ }
44
+
45
+ export interface BulkOperationResult {
46
+ total: number;
47
+ success: number;
48
+ failed: number;
49
+ skipped: number;
50
+ errors: Array<{ taskId: string; error: string }>;
51
+ processedTasks: TaskSummary[];
52
+ }
53
+
54
+ export interface PreviewResult {
55
+ tasks: TaskSummary[];
56
+ total: number;
57
+ taskListId: string;
58
+ }
59
+
60
+ export class BulkApi {
61
+ private token: string | null = null;
62
+
63
+ async #getValidToken(): Promise<string> {
64
+ if (this.token) return this.token;
65
+
66
+ let accessToken = getAccessToken();
67
+ if (!accessToken) {
68
+ throw new Error('Not authenticated. Run "connect-googletasks auth login" first.');
69
+ }
70
+
71
+ if (isTokenExpired()) {
72
+ const refreshToken = getRefreshToken();
73
+ const clientId = getClientId();
74
+ const clientSecret = getClientSecret();
75
+
76
+ if (!refreshToken || !clientId || !clientSecret) {
77
+ throw new Error('Token expired and cannot refresh. Run "connect-googletasks auth login"');
78
+ }
79
+
80
+ const response = await fetch(OAUTH_TOKEN_URL, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
83
+ body: new URLSearchParams({
84
+ refresh_token: refreshToken,
85
+ client_id: clientId,
86
+ client_secret: clientSecret,
87
+ grant_type: 'refresh_token',
88
+ }).toString(),
89
+ });
90
+
91
+ if (!response.ok) {
92
+ throw new Error('Failed to refresh token');
93
+ }
94
+
95
+ const tokens = await response.json();
96
+ setTokens(tokens.access_token, tokens.refresh_token, tokens.expires_in);
97
+ accessToken = tokens.access_token;
98
+ }
99
+
100
+ this.token = accessToken;
101
+ return accessToken;
102
+ }
103
+
104
+ async #request<T>(method: string, path: string, body?: unknown, params?: Record<string, string | number | boolean | undefined>): Promise<T> {
105
+ const token = await this.#getValidToken();
106
+
107
+ let url = `${TASKS_API_BASE}${path}`;
108
+ if (params) {
109
+ const searchParams = new URLSearchParams();
110
+ for (const [key, value] of Object.entries(params)) {
111
+ if (value !== undefined) searchParams.append(key, String(value));
112
+ }
113
+ const qs = searchParams.toString();
114
+ if (qs) url += `?${qs}`;
115
+ }
116
+
117
+ const headers: Record<string, string> = {
118
+ 'Authorization': `Bearer ${token}`,
119
+ 'Content-Type': 'application/json',
120
+ };
121
+
122
+ const response = await fetch(url, {
123
+ method,
124
+ headers,
125
+ body: body ? JSON.stringify(body) : undefined,
126
+ });
127
+
128
+ if (!response.ok) {
129
+ const error = await response.json().catch(() => ({}));
130
+ const message = error.error?.message || response.statusText;
131
+ throw new Error(message);
132
+ }
133
+
134
+ if (response.status === 204) return {} as T;
135
+ return response.json();
136
+ }
137
+
138
+ // ============================================
139
+ // Preview
140
+ // ============================================
141
+
142
+ async preview(taskListId: string, options?: { query?: string; showCompleted?: boolean; maxResults?: number }): Promise<PreviewResult> {
143
+ const tasks = await this.#fetchTasks(taskListId, {
144
+ q: options?.query,
145
+ showCompleted: options?.showCompleted,
146
+ maxResults: options?.maxResults || 50,
147
+ });
148
+ return { tasks, total: tasks.length, taskListId };
149
+ }
150
+
151
+ // ============================================
152
+ // Complete
153
+ // ============================================
154
+
155
+ async complete(options: BulkOperationOptions): Promise<BulkOperationResult> {
156
+ const tasks = await this.#fetchTasks(options.taskListId, {
157
+ q: options.query,
158
+ showCompleted: options.showCompleted ?? false,
159
+ maxResults: options.maxResults || 100,
160
+ });
161
+
162
+ const pending = tasks.filter(t => t.status !== 'completed');
163
+
164
+ return this.#executeBatch(pending, {
165
+ dryRun: options.dryRun || false,
166
+ concurrency: options.concurrency || 10,
167
+ onProgress: options.onProgress,
168
+ onError: options.onError,
169
+ operation: async (task) => {
170
+ await this.#request<Task>(
171
+ 'PATCH',
172
+ `/lists/${options.taskListId}/tasks/${task.id}`,
173
+ { status: 'completed', completed: new Date().toISOString() }
174
+ );
175
+ },
176
+ });
177
+ }
178
+
179
+ // ============================================
180
+ // Uncomplete
181
+ // ============================================
182
+
183
+ async uncomplete(options: BulkOperationOptions): Promise<BulkOperationResult> {
184
+ const tasks = await this.#fetchTasks(options.taskListId, {
185
+ showCompleted: true,
186
+ maxResults: options.maxResults || 100,
187
+ });
188
+
189
+ const completed = tasks.filter(t => t.status === 'completed');
190
+
191
+ return this.#executeBatch(completed, {
192
+ dryRun: options.dryRun || false,
193
+ concurrency: options.concurrency || 10,
194
+ onProgress: options.onProgress,
195
+ onError: options.onError,
196
+ operation: async (task) => {
197
+ await this.#request<Task>(
198
+ 'PATCH',
199
+ `/lists/${options.taskListId}/tasks/${task.id}`,
200
+ { status: 'needsAction' }
201
+ );
202
+ },
203
+ });
204
+ }
205
+
206
+ // ============================================
207
+ // Delete
208
+ // ============================================
209
+
210
+ async delete(options: BulkOperationOptions): Promise<BulkOperationResult> {
211
+ const tasks = await this.#fetchTasks(options.taskListId, {
212
+ q: options.query,
213
+ showCompleted: options.showCompleted,
214
+ maxResults: options.maxResults || 100,
215
+ });
216
+
217
+ return this.#executeBatch(tasks, {
218
+ dryRun: options.dryRun || false,
219
+ concurrency: options.concurrency || 10,
220
+ onProgress: options.onProgress,
221
+ onError: options.onError,
222
+ operation: async (task) => {
223
+ await this.#request<void>('DELETE', `/lists/${options.taskListId}/tasks/${task.id}`);
224
+ },
225
+ });
226
+ }
227
+
228
+ // ============================================
229
+ // Update (batch update notes or due dates)
230
+ // ============================================
231
+
232
+ async update(taskListId: string, options: {
233
+ query?: string;
234
+ showCompleted?: boolean;
235
+ maxResults?: number;
236
+ concurrency?: number;
237
+ dryRun?: boolean;
238
+ onProgress?: (current: number, total: number, task: TaskSummary) => void;
239
+ onError?: (error: Error, task: TaskSummary) => void;
240
+ updates: { due?: string; notes?: string };
241
+ }): Promise<BulkOperationResult> {
242
+ const tasks = await this.#fetchTasks(taskListId, {
243
+ q: options.query,
244
+ showCompleted: options.showCompleted,
245
+ maxResults: options.maxResults || 100,
246
+ });
247
+
248
+ return this.#executeBatch(tasks, {
249
+ dryRun: options.dryRun || false,
250
+ concurrency: options.concurrency || 10,
251
+ onProgress: options.onProgress,
252
+ onError: options.onError,
253
+ operation: async (task) => {
254
+ const patch: Record<string, string> = {};
255
+ if (options.updates.due !== undefined) patch.due = options.updates.due;
256
+ if (options.updates.notes !== undefined) patch.notes = options.updates.notes;
257
+ await this.#request<Task>('PATCH', `/lists/${taskListId}/tasks/${task.id}`, patch);
258
+ },
259
+ });
260
+ }
261
+
262
+ // ============================================
263
+ // Helpers
264
+ // ============================================
265
+
266
+ async #fetchTasks(taskListId: string, params: { q?: string; showCompleted?: boolean; maxResults?: number }): Promise<TaskSummary[]> {
267
+ const tasks: TaskSummary[] = [];
268
+ let pageToken: string | undefined;
269
+ const max = params.maxResults || 100;
270
+
271
+ while (tasks.length < max) {
272
+ const requestParams: Record<string, string | number | boolean | undefined> = {
273
+ maxResults: Math.min(100, max - tasks.length),
274
+ pageToken,
275
+ showCompleted: params.showCompleted ?? false,
276
+ };
277
+ if (params.q) requestParams.q = params.q;
278
+
279
+ const response = await this.#request<TasksResponse>('GET', `/lists/${taskListId}/tasks`, undefined, requestParams);
280
+
281
+ if (!response.items || response.items.length === 0) break;
282
+
283
+ for (const t of response.items) {
284
+ tasks.push({
285
+ id: t.id,
286
+ title: t.title || '(untitled)',
287
+ status: t.status || 'needsAction',
288
+ due: t.due,
289
+ notes: t.notes,
290
+ });
291
+ }
292
+
293
+ pageToken = response.nextPageToken;
294
+ if (!pageToken) break;
295
+ }
296
+
297
+ return tasks;
298
+ }
299
+
300
+ async #executeBatch(
301
+ tasks: TaskSummary[],
302
+ options: {
303
+ dryRun: boolean;
304
+ concurrency: number;
305
+ onProgress?: (current: number, total: number, task: TaskSummary) => void;
306
+ onError?: (error: Error, task: TaskSummary) => void;
307
+ operation: (task: TaskSummary) => Promise<void>;
308
+ }
309
+ ): Promise<BulkOperationResult> {
310
+ const { dryRun, concurrency, onProgress, onError, operation } = options;
311
+
312
+ const result: BulkOperationResult = {
313
+ total: tasks.length,
314
+ success: 0,
315
+ failed: 0,
316
+ skipped: 0,
317
+ errors: [],
318
+ processedTasks: [],
319
+ };
320
+
321
+ if (tasks.length === 0) return result;
322
+
323
+ const chunks = this.#chunkArray(tasks, concurrency);
324
+
325
+ for (const chunk of chunks) {
326
+ await Promise.all(
327
+ chunk.map(async (task) => {
328
+ try {
329
+ if (dryRun) {
330
+ result.success++;
331
+ result.processedTasks.push(task);
332
+ } else {
333
+ await operation(task);
334
+ result.success++;
335
+ result.processedTasks.push(task);
336
+ }
337
+
338
+ if (onProgress) onProgress(result.success + result.failed, result.total, task);
339
+ } catch (err) {
340
+ result.failed++;
341
+ const errorMessage = err instanceof Error ? err.message : String(err);
342
+ result.errors.push({ taskId: task.id, error: errorMessage });
343
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), task);
344
+ }
345
+ })
346
+ );
347
+ }
348
+
349
+ return result;
350
+ }
351
+
352
+ #chunkArray<T>(array: T[], size: number): T[][] {
353
+ const chunks: T[][] = [];
354
+ for (let i = 0; i < array.length; i += size) {
355
+ chunks.push(array.slice(i, i + size));
356
+ }
357
+ return chunks;
358
+ }
359
+ }
@@ -318,4 +318,5 @@ export class GoogleTasksClient {
318
318
  }
319
319
  }
320
320
 
321
+ export { BulkApi } from './bulk';
321
322
  export { getClientId, getClientSecret, getAccessToken, getRefreshToken };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Command } from 'commander';
3
3
  import { GoogleTasksClient } from '../api';
4
+ import { BulkApi } from '../api/bulk';
4
5
  import {
5
6
  setProfileOverride,
6
7
  getCurrentProfile,
@@ -558,5 +559,182 @@ tasksCmd
558
559
  }
559
560
  });
560
561
 
562
+ // ============================================
563
+ // Bulk Operations
564
+ // ============================================
565
+ const bulkCmd = program
566
+ .command('bulk')
567
+ .description('Bulk operations on tasks');
568
+
569
+ bulkCmd
570
+ .command('preview <listId>')
571
+ .description('Preview tasks in a list')
572
+ .option('-q, --query <text>', 'Filter by search query')
573
+ .option('--show-completed', 'Include completed tasks')
574
+ .option('-n, --max <number>', 'Maximum tasks to preview', '50')
575
+ .action(async function(this: Command, listId: string, opts) {
576
+ try {
577
+ const api = new BulkApi();
578
+ const result = await api.preview(listId, {
579
+ query: opts.query,
580
+ showCompleted: opts.showCompleted,
581
+ maxResults: parseInt(opts.max),
582
+ });
583
+ success(`Found ${result.total} task(s) in list ${listId}`);
584
+ print(result.tasks, getFormat(this));
585
+ } catch (err) {
586
+ error(String(err));
587
+ process.exit(1);
588
+ }
589
+ });
590
+
591
+ bulkCmd
592
+ .command('complete <listId>')
593
+ .description('Bulk mark tasks as completed')
594
+ .option('-q, --query <text>', 'Filter by search query')
595
+ .option('--show-completed', 'Include completed in search')
596
+ .option('-n, --max <number>', 'Maximum tasks to process', '100')
597
+ .option('-c, --concurrency <number>', 'Max concurrent API calls', '10')
598
+ .option('--dry-run', 'Preview without making changes')
599
+ .action(async function(this: Command, listId: string, opts) {
600
+ try {
601
+ const api = new BulkApi();
602
+ let current = 0;
603
+ const result = await api.complete({
604
+ taskListId: listId,
605
+ query: opts.query,
606
+ showCompleted: opts.showCompleted,
607
+ maxResults: parseInt(opts.max),
608
+ concurrency: parseInt(opts.concurrency),
609
+ dryRun: opts.dryRun,
610
+ onProgress: (cur, total) => {
611
+ current = cur;
612
+ process.stdout.write(`\r Progress: ${cur}/${total}`);
613
+ },
614
+ });
615
+ process.stdout.write('\n');
616
+ if (opts.dryRun) {
617
+ info(`[Dry run] Would mark ${result.success} task(s) as completed`);
618
+ } else {
619
+ success(`${result.success} task(s) marked as completed, ${result.failed} failed`);
620
+ }
621
+ if (result.errors.length > 0) {
622
+ warn(`Errors: ${JSON.stringify(result.errors)}`);
623
+ }
624
+ } catch (err) {
625
+ error(String(err));
626
+ process.exit(1);
627
+ }
628
+ });
629
+
630
+ bulkCmd
631
+ .command('uncomplete <listId>')
632
+ .description('Bulk mark completed tasks back to pending')
633
+ .option('-n, --max <number>', 'Maximum tasks to process', '100')
634
+ .option('-c, --concurrency <number>', 'Max concurrent API calls', '10')
635
+ .option('--dry-run', 'Preview without making changes')
636
+ .action(async function(this: Command, listId: string, opts) {
637
+ try {
638
+ const api = new BulkApi();
639
+ const result = await api.uncomplete({
640
+ taskListId: listId,
641
+ maxResults: parseInt(opts.max),
642
+ concurrency: parseInt(opts.concurrency),
643
+ dryRun: opts.dryRun,
644
+ });
645
+ if (opts.dryRun) {
646
+ info(`[Dry run] Would uncomplete ${result.success} task(s)`);
647
+ } else {
648
+ success(`${result.success} task(s) uncompleted, ${result.failed} failed`);
649
+ }
650
+ if (result.errors.length > 0) {
651
+ warn(`Errors: ${JSON.stringify(result.errors)}`);
652
+ }
653
+ } catch (err) {
654
+ error(String(err));
655
+ process.exit(1);
656
+ }
657
+ });
658
+
659
+ bulkCmd
660
+ .command('delete <listId>')
661
+ .description('Bulk delete tasks')
662
+ .option('-q, --query <text>', 'Filter by search query')
663
+ .option('--show-completed', 'Include completed tasks')
664
+ .option('-n, --max <number>', 'Maximum tasks to process', '100')
665
+ .option('-c, --concurrency <number>', 'Max concurrent API calls', '10')
666
+ .option('--dry-run', 'Preview without making changes')
667
+ .option('--confirm', 'Confirm deletion (required)')
668
+ .action(async function(this: Command, listId: string, opts) {
669
+ if (!opts.confirm) {
670
+ error('Use --confirm to confirm deletion');
671
+ process.exit(1);
672
+ }
673
+ try {
674
+ const api = new BulkApi();
675
+ const result = await api.delete({
676
+ taskListId: listId,
677
+ query: opts.query,
678
+ showCompleted: opts.showCompleted,
679
+ maxResults: parseInt(opts.max),
680
+ concurrency: parseInt(opts.concurrency),
681
+ dryRun: opts.dryRun,
682
+ });
683
+ if (opts.dryRun) {
684
+ info(`[Dry run] Would delete ${result.success} task(s)`);
685
+ } else {
686
+ success(`${result.success} task(s) deleted, ${result.failed} failed`);
687
+ }
688
+ if (result.errors.length > 0) {
689
+ warn(`Errors: ${JSON.stringify(result.errors)}`);
690
+ }
691
+ } catch (err) {
692
+ error(String(err));
693
+ process.exit(1);
694
+ }
695
+ });
696
+
697
+ bulkCmd
698
+ .command('update <listId>')
699
+ .description('Bulk update due dates or notes on tasks')
700
+ .option('-q, --query <text>', 'Filter by search query')
701
+ .option('--show-completed', 'Include completed tasks')
702
+ .option('-n, --max <number>', 'Maximum tasks to process', '100')
703
+ .option('-c, --concurrency <number>', 'Max concurrent API calls', '10')
704
+ .option('--dry-run', 'Preview without making changes')
705
+ .option('--due <date>', 'Set due date (YYYY-MM-DD)')
706
+ .option('--notes <text>', 'Set notes')
707
+ .action(async function(this: Command, listId: string, opts) {
708
+ if (!opts.due && !opts.notes) {
709
+ error('Specify at least --due or --notes');
710
+ process.exit(1);
711
+ }
712
+ try {
713
+ const api = new BulkApi();
714
+ const result = await api.update(listId, {
715
+ query: opts.query,
716
+ showCompleted: opts.showCompleted,
717
+ maxResults: parseInt(opts.max),
718
+ concurrency: parseInt(opts.concurrency),
719
+ dryRun: opts.dryRun,
720
+ updates: {
721
+ due: opts.due ? new Date(opts.due).toISOString() : undefined,
722
+ notes: opts.notes,
723
+ },
724
+ });
725
+ if (opts.dryRun) {
726
+ info(`[Dry run] Would update ${result.success} task(s)`);
727
+ } else {
728
+ success(`${result.success} task(s) updated, ${result.failed} failed`);
729
+ }
730
+ if (result.errors.length > 0) {
731
+ warn(`Errors: ${JSON.stringify(result.errors)}`);
732
+ }
733
+ } catch (err) {
734
+ error(String(err));
735
+ process.exit(1);
736
+ }
737
+ });
738
+
561
739
  // Parse and execute
562
740
  program.parse();