@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.
- package/bin/index.js +109 -59
- package/bin/mcp.js +236 -182
- package/bin/serve.js +77 -33
- package/connectors/connect-dropbox/src/api/bulk.ts +285 -0
- package/connectors/connect-dropbox/src/api/index.ts +4 -0
- package/connectors/connect-dropbox/src/cli/index.ts +151 -0
- package/connectors/connect-github/src/api/bulk.ts +328 -0
- package/connectors/connect-github/src/api/index.ts +4 -0
- package/connectors/connect-github/src/cli/index.ts +145 -0
- package/connectors/connect-gmail/src/api/history.ts +82 -0
- package/connectors/connect-gmail/src/api/index.ts +12 -0
- package/connectors/connect-gmail/src/api/settings.ts +280 -0
- package/connectors/connect-gmail/src/api/watch.ts +40 -0
- package/connectors/connect-gmail/src/cli/index.ts +237 -0
- package/connectors/connect-googleads/src/api/bulk.ts +354 -0
- package/connectors/connect-googleads/src/api/index.ts +4 -0
- package/connectors/connect-googleads/src/cli/index.ts +192 -0
- package/connectors/connect-googlecalendar/src/api/acl.ts +66 -0
- package/connectors/connect-googlecalendar/src/api/bulk.ts +452 -0
- package/connectors/connect-googlecalendar/src/api/freebusy.ts +35 -0
- package/connectors/connect-googlecalendar/src/api/index.ts +9 -0
- package/connectors/connect-googlecalendar/src/cli/index.ts +522 -0
- package/connectors/connect-googlecalendar/src/types/index.ts +90 -0
- package/connectors/connect-googlecontacts/src/api/bulk.ts +365 -0
- package/connectors/connect-googlecontacts/src/api/groups.ts +164 -0
- package/connectors/connect-googlecontacts/src/api/index.ts +8 -0
- package/connectors/connect-googlecontacts/src/cli/index.ts +340 -0
- package/connectors/connect-googledocs/src/api/bulk.ts +301 -0
- package/connectors/connect-googledocs/src/api/index.ts +4 -0
- package/connectors/connect-googledocs/src/cli/index.ts +187 -0
- package/connectors/connect-googledrive/src/api/bulk.ts +505 -0
- package/connectors/connect-googledrive/src/api/changes.ts +139 -0
- package/connectors/connect-googledrive/src/api/client.ts +34 -0
- package/connectors/connect-googledrive/src/api/index.ts +12 -0
- package/connectors/connect-googledrive/src/api/revisions.ts +132 -0
- package/connectors/connect-googledrive/src/cli/index.ts +624 -0
- package/connectors/connect-googledrive/src/index.ts +2 -0
- package/connectors/connect-googlephotos/src/api/bulk.ts +455 -0
- package/connectors/connect-googlephotos/src/api/index.ts +8 -0
- package/connectors/connect-googlephotos/src/cli/index.ts +185 -0
- package/connectors/connect-googletasks/src/api/bulk.ts +359 -0
- package/connectors/connect-googletasks/src/api/index.ts +1 -0
- package/connectors/connect-googletasks/src/cli/index.ts +178 -0
- package/connectors/connect-linear/src/api/bulk.ts +219 -0
- package/connectors/connect-linear/src/api/index.ts +4 -0
- package/connectors/connect-linear/src/cli/index.ts +116 -0
- package/connectors/connect-shopify/src/api/bulk.ts +288 -0
- package/connectors/connect-shopify/src/api/index.ts +4 -0
- package/connectors/connect-shopify/src/cli/index.ts +108 -0
- package/connectors/connect-slack/src/api/bulk.ts +237 -0
- package/connectors/connect-slack/src/api/index.ts +4 -0
- package/connectors/connect-slack/src/cli/index.ts +157 -0
- package/connectors/connect-stripe/src/api/bulk.ts +488 -0
- package/connectors/connect-stripe/src/api/index.ts +4 -0
- package/connectors/connect-stripe/src/cli/index.ts +206 -0
- package/dist/index.js +75 -33
- package/dist/lib/runner.d.ts +15 -5
- package/package.json +2 -2
- package/connectors/connect-cloudflare/bun.lock +0 -32
- package/connectors/connect-gmail/bin/index.js +0 -7027
- package/connectors/connect-gmail/dist/cli/index.js +0 -7035
- package/connectors/connect-gmail/dist/index.js +0 -2174
- 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
|
+
}
|
|
@@ -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();
|