m365-agent-cli 1.2.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 (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,1678 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { Command } from 'commander';
3
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
4
+ import {
5
+ addPlannerChecklistItem,
6
+ addPlannerFavoritePlan,
7
+ addPlannerReference,
8
+ addPlannerRosterMember,
9
+ buildPlannerAssignments,
10
+ type CreatePlannerTaskExtras,
11
+ createPlannerBucket,
12
+ createPlannerPlan,
13
+ createPlannerPlanInRoster,
14
+ createPlannerRoster,
15
+ createTask,
16
+ deletePlannerBucket,
17
+ deletePlannerPlan,
18
+ deletePlannerTask,
19
+ getAssignedToTaskBoardFormat,
20
+ getBucketTaskBoardFormat,
21
+ getPlanDetails,
22
+ getPlannerBucket,
23
+ getPlannerDeltaPage,
24
+ getPlannerPlan,
25
+ getPlannerRoster,
26
+ getPlannerTaskDetails,
27
+ getPlannerUser,
28
+ getProgressTaskBoardFormat,
29
+ getTask,
30
+ listFavoritePlans,
31
+ listGroupPlans,
32
+ listPlanBuckets,
33
+ listPlannerPlansForUser,
34
+ listPlannerRosterMembers,
35
+ listPlannerTasksForUser,
36
+ listPlanTasks,
37
+ listRosterPlans,
38
+ listUserPlans,
39
+ listUserTasks,
40
+ mergePlannerAssignments,
41
+ normalizeAppliedCategories,
42
+ type PlannerCategorySlot,
43
+ type PlannerPlanDetails,
44
+ type PlannerTask,
45
+ parsePlannerLabelKey,
46
+ removePlannerChecklistItem,
47
+ removePlannerFavoritePlan,
48
+ removePlannerReference,
49
+ removePlannerRosterMember,
50
+ type UpdatePlannerPlanDetailsParams,
51
+ type UpdatePlannerTaskDetailsParams,
52
+ updateAssignedToTaskBoardFormat,
53
+ updateBucketTaskBoardFormat,
54
+ updatePlannerBucket,
55
+ updatePlannerChecklistItem,
56
+ updatePlannerPlan,
57
+ updatePlannerPlanDetails,
58
+ updatePlannerTaskDetails,
59
+ updateProgressTaskBoardFormat,
60
+ updateTask
61
+ } from '../lib/planner-client.js';
62
+ import { checkReadOnly } from '../lib/utils.js';
63
+
64
+ const LABEL_SLOTS: PlannerCategorySlot[] = [
65
+ 'category1',
66
+ 'category2',
67
+ 'category3',
68
+ 'category4',
69
+ 'category5',
70
+ 'category6'
71
+ ];
72
+
73
+ function formatTaskLabels(task: PlannerTask, descriptions?: PlannerPlanDetails['categoryDescriptions']): string {
74
+ if (!task.appliedCategories) return '';
75
+ const parts: string[] = [];
76
+ for (const slot of LABEL_SLOTS) {
77
+ if (task.appliedCategories[slot]) {
78
+ const name = descriptions?.[slot]?.trim();
79
+ parts.push(name || slot);
80
+ }
81
+ }
82
+ return parts.join(', ');
83
+ }
84
+
85
+ export const plannerCommand = new Command('planner').description('Manage Microsoft Planner tasks and plans');
86
+
87
+ plannerCommand
88
+ .command('list-my-tasks')
89
+ .description('List tasks assigned to you')
90
+ .option('--json', 'Output JSON')
91
+ .option('--token <token>', 'Use a specific token')
92
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
93
+ .action(async (opts: { json?: boolean; token?: string; identity?: string }) => {
94
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
95
+ if (!auth.success) {
96
+ console.error(`Auth error: ${auth.error}`);
97
+ process.exit(1);
98
+ }
99
+ const result = await listUserTasks(auth.token!);
100
+ if (!result.ok || !result.data) {
101
+ console.error(`Error listing tasks: ${result.error?.message}`);
102
+ process.exit(1);
103
+ }
104
+ if (opts.json) {
105
+ console.log(JSON.stringify(result.data, null, 2));
106
+ } else {
107
+ const planDetailsCache = new Map<string, PlannerPlanDetails['categoryDescriptions']>();
108
+ for (const t of result.data) {
109
+ if (!planDetailsCache.has(t.planId)) {
110
+ const d = await getPlanDetails(auth.token!, t.planId);
111
+ planDetailsCache.set(t.planId, d.ok ? d.data?.categoryDescriptions : undefined);
112
+ }
113
+ const desc = planDetailsCache.get(t.planId);
114
+ const labels = formatTaskLabels(t, desc);
115
+ console.log(`- [${t.percentComplete === 100 ? 'x' : ' '}] ${t.title} (ID: ${t.id})`);
116
+ console.log(` Plan ID: ${t.planId} | Bucket ID: ${t.bucketId}${labels ? ` | Labels: ${labels}` : ''}`);
117
+ }
118
+ }
119
+ });
120
+
121
+ plannerCommand
122
+ .command('list-plans')
123
+ .description('List your plans or plans for a group')
124
+ .option('-g, --group <groupId>', 'Group ID to list plans for')
125
+ .option('--json', 'Output JSON')
126
+ .option('--token <token>', 'Use a specific token')
127
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
128
+ .action(async (opts: { group?: string; json?: boolean; token?: string; identity?: string }) => {
129
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
130
+ if (!auth.success) {
131
+ console.error(`Auth error: ${auth.error}`);
132
+ process.exit(1);
133
+ }
134
+ const result = opts.group ? await listGroupPlans(auth.token!, opts.group) : await listUserPlans(auth.token!);
135
+ if (!result.ok || !result.data) {
136
+ console.error(`Error listing plans: ${result.error?.message}`);
137
+ process.exit(1);
138
+ }
139
+ if (opts.json) {
140
+ console.log(JSON.stringify(result.data, null, 2));
141
+ } else {
142
+ for (const p of result.data) {
143
+ console.log(`- ${p.title} (ID: ${p.id})`);
144
+ }
145
+ }
146
+ });
147
+
148
+ plannerCommand
149
+ .command('list-user-tasks')
150
+ .description('List Planner tasks for a user (Graph GET /users/{id}/planner/tasks; may 403 if not permitted)')
151
+ .requiredOption('-u, --user <userId>', 'Azure AD object id of the user')
152
+ .option('--json', 'Output JSON')
153
+ .option('--token <token>', 'Use a specific token')
154
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
155
+ .action(async (opts: { user: string; json?: boolean; token?: string; identity?: string }) => {
156
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
157
+ if (!auth.success) {
158
+ console.error(`Auth error: ${auth.error}`);
159
+ process.exit(1);
160
+ }
161
+ const result = await listPlannerTasksForUser(auth.token!, opts.user);
162
+ if (!result.ok || !result.data) {
163
+ console.error(`Error listing tasks: ${result.error?.message}`);
164
+ process.exit(1);
165
+ }
166
+ if (opts.json) {
167
+ console.log(JSON.stringify(result.data, null, 2));
168
+ } else {
169
+ const planDetailsCache = new Map<string, PlannerPlanDetails['categoryDescriptions']>();
170
+ for (const t of result.data) {
171
+ if (!planDetailsCache.has(t.planId)) {
172
+ const d = await getPlanDetails(auth.token!, t.planId);
173
+ planDetailsCache.set(t.planId, d.ok ? d.data?.categoryDescriptions : undefined);
174
+ }
175
+ const desc = planDetailsCache.get(t.planId);
176
+ const labels = formatTaskLabels(t, desc);
177
+ console.log(`- [${t.percentComplete === 100 ? 'x' : ' '}] ${t.title} (ID: ${t.id})`);
178
+ console.log(` Plan ID: ${t.planId} | Bucket ID: ${t.bucketId}${labels ? ` | Labels: ${labels}` : ''}`);
179
+ }
180
+ }
181
+ });
182
+
183
+ plannerCommand
184
+ .command('list-user-plans')
185
+ .description('List Planner plans for a user (Graph GET /users/{id}/planner/plans; may 403 if not permitted)')
186
+ .requiredOption('-u, --user <userId>', 'Azure AD object id of the user')
187
+ .option('--json', 'Output JSON')
188
+ .option('--token <token>', 'Use a specific token')
189
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
190
+ .action(async (opts: { user: string; json?: boolean; token?: string; identity?: string }) => {
191
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
192
+ if (!auth.success) {
193
+ console.error(`Auth error: ${auth.error}`);
194
+ process.exit(1);
195
+ }
196
+ const result = await listPlannerPlansForUser(auth.token!, opts.user);
197
+ if (!result.ok || !result.data) {
198
+ console.error(`Error listing plans: ${result.error?.message}`);
199
+ process.exit(1);
200
+ }
201
+ if (opts.json) {
202
+ console.log(JSON.stringify(result.data, null, 2));
203
+ } else {
204
+ for (const p of result.data) {
205
+ console.log(`- ${p.title} (ID: ${p.id})`);
206
+ }
207
+ }
208
+ });
209
+
210
+ plannerCommand
211
+ .command('list-buckets')
212
+ .description('List buckets in a plan')
213
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
214
+ .option('--json', 'Output JSON')
215
+ .option('--token <token>', 'Use a specific token')
216
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
217
+ .action(async (opts: { plan: string; json?: boolean; token?: string; identity?: string }) => {
218
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
219
+ if (!auth.success) {
220
+ console.error(`Auth error: ${auth.error}`);
221
+ process.exit(1);
222
+ }
223
+ const result = await listPlanBuckets(auth.token!, opts.plan);
224
+ if (!result.ok || !result.data) {
225
+ console.error(`Error listing buckets: ${result.error?.message}`);
226
+ process.exit(1);
227
+ }
228
+ if (opts.json) {
229
+ console.log(JSON.stringify(result.data, null, 2));
230
+ } else {
231
+ for (const b of result.data) {
232
+ console.log(`- ${b.name} (ID: ${b.id})`);
233
+ }
234
+ }
235
+ });
236
+
237
+ plannerCommand
238
+ .command('list-tasks')
239
+ .description('List tasks in a plan')
240
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
241
+ .option('--json', 'Output JSON')
242
+ .option('--token <token>', 'Use a specific token')
243
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
244
+ .action(async (opts: { plan: string; json?: boolean; token?: string; identity?: string }) => {
245
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
246
+ if (!auth.success) {
247
+ console.error(`Auth error: ${auth.error}`);
248
+ process.exit(1);
249
+ }
250
+ const result = await listPlanTasks(auth.token!, opts.plan);
251
+ if (!result.ok || !result.data) {
252
+ console.error(`Error listing tasks: ${result.error?.message}`);
253
+ process.exit(1);
254
+ }
255
+ const detailsR = await getPlanDetails(auth.token!, opts.plan);
256
+ const descriptions = detailsR.ok ? detailsR.data?.categoryDescriptions : undefined;
257
+ if (opts.json) {
258
+ console.log(JSON.stringify(result.data, null, 2));
259
+ } else {
260
+ for (const t of result.data) {
261
+ const labels = formatTaskLabels(t, descriptions);
262
+ console.log(
263
+ `- [${t.percentComplete === 100 ? 'x' : ' '}] ${t.title} (ID: ${t.id})${labels ? ` | ${labels}` : ''}`
264
+ );
265
+ }
266
+ }
267
+ });
268
+
269
+ plannerCommand
270
+ .command('create-task')
271
+ .description('Create a new task in a plan')
272
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
273
+ .requiredOption('-t, --title <title>', 'Task title')
274
+ .option('-b, --bucket <bucketId>', 'Bucket ID')
275
+ .option('--due <ISO-8601>', 'Due date/time (PATCH after create)')
276
+ .option('--start <ISO-8601>', 'Start date/time (PATCH after create)')
277
+ .option(
278
+ '--label <slot>',
279
+ 'Label slot: 1-6 or category1..category6 (repeatable; names are defined in plan details)',
280
+ (v: string, prev: string[]) => [...prev, v],
281
+ [] as string[]
282
+ )
283
+ .option(
284
+ '--assign <userId>',
285
+ 'Assign user(s) on create (repeatable)',
286
+ (v: string, prev: string[]) => [...prev, v],
287
+ [] as string[]
288
+ )
289
+ .option('--conversation-thread <id>', 'Teams conversation thread id (PATCH after create)')
290
+ .option('--order-hint <hint>', 'Task order hint (PATCH after create)')
291
+ .option('--assignee-priority <hint>', 'Assignee priority order hint (PATCH after create)')
292
+ .option('--priority <0-10>', 'Task priority: 0 highest .. 10 lowest (PATCH after create)')
293
+ .option(
294
+ '--preview-type <mode>',
295
+ 'Card preview: automatic | noPreview | checklist | description | reference (PATCH after create)'
296
+ )
297
+ .option('--json', 'Output JSON')
298
+ .option('--token <token>', 'Use a specific token')
299
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
300
+ .action(
301
+ async (
302
+ opts: {
303
+ plan: string;
304
+ title: string;
305
+ bucket?: string;
306
+ due?: string;
307
+ start?: string;
308
+ label?: string[];
309
+ assign?: string[];
310
+ conversationThread?: string;
311
+ orderHint?: string;
312
+ assigneePriority?: string;
313
+ priority?: string;
314
+ previewType?: string;
315
+ json?: boolean;
316
+ token?: string;
317
+ identity?: string;
318
+ },
319
+ cmd: any
320
+ ) => {
321
+ checkReadOnly(cmd);
322
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
323
+ if (!auth.success) {
324
+ console.error(`Auth error: ${auth.error}`);
325
+ process.exit(1);
326
+ }
327
+ if (opts.priority !== undefined) {
328
+ const p = parseInt(opts.priority, 10);
329
+ if (Number.isNaN(p) || p < 0 || p > 10) {
330
+ console.error('Error: --priority must be an integer from 0 to 10');
331
+ process.exit(1);
332
+ }
333
+ }
334
+ let applied: ReturnType<typeof normalizeAppliedCategories> | undefined;
335
+ if (opts.label?.length) {
336
+ const setTrue: PlannerCategorySlot[] = [];
337
+ for (const raw of opts.label) {
338
+ const slot = parsePlannerLabelKey(raw);
339
+ if (!slot) {
340
+ console.error(`Invalid --label "${raw}". Use 1-6 or category1..category6.`);
341
+ process.exit(1);
342
+ }
343
+ setTrue.push(slot);
344
+ }
345
+ applied = normalizeAppliedCategories(undefined, { setTrue });
346
+ }
347
+ const assignments = opts.assign && opts.assign.length > 0 ? buildPlannerAssignments(opts.assign) : undefined;
348
+ const extras: CreatePlannerTaskExtras = {};
349
+ if (opts.due !== undefined) extras.dueDateTime = opts.due;
350
+ if (opts.start !== undefined) extras.startDateTime = opts.start;
351
+ if (opts.conversationThread !== undefined) extras.conversationThreadId = opts.conversationThread;
352
+ if (opts.orderHint !== undefined) extras.orderHint = opts.orderHint;
353
+ if (opts.assigneePriority !== undefined) extras.assigneePriority = opts.assigneePriority;
354
+ const extrasOut = Object.keys(extras).length > 0 ? extras : undefined;
355
+ const result = await createTask(auth.token!, opts.plan, opts.title, opts.bucket, assignments, applied, extrasOut);
356
+ if (!result.ok || !result.data) {
357
+ console.error(`Error creating task: ${result.error?.message}`);
358
+ process.exit(1);
359
+ }
360
+ if (opts.json) {
361
+ console.log(JSON.stringify(result.data, null, 2));
362
+ } else {
363
+ console.log(`Created task: ${result.data.title} (ID: ${result.data.id})`);
364
+ }
365
+ }
366
+ );
367
+
368
+ plannerCommand
369
+ .command('update-task')
370
+ .description('Update a task')
371
+ .requiredOption('-i, --id <taskId>', 'Task ID')
372
+ .option('--title <title>', 'New title')
373
+ .option('-b, --bucket <bucketId>', 'Move to Bucket ID')
374
+ .option('--percent <percentComplete>', 'Percent complete (0-100)')
375
+ .option(
376
+ '--assign <userId>',
377
+ 'Replace assignments with exactly these user IDs (repeatable)',
378
+ (v: string, prev: string[]) => [...prev, v],
379
+ [] as string[]
380
+ )
381
+ .option(
382
+ '--add-assign <userId>',
383
+ 'Add assignee, keeping others (repeatable)',
384
+ (v: string, prev: string[]) => [...prev, v],
385
+ [] as string[]
386
+ )
387
+ .option(
388
+ '--remove-assign <userId>',
389
+ 'Remove one assignee by user ID (repeatable)',
390
+ (v: string, prev: string[]) => [...prev, v],
391
+ [] as string[]
392
+ )
393
+ .option('--clear-assign', 'Remove all assignees')
394
+ .option('--order-hint <hint>', 'Task order hint within bucket')
395
+ .option('--conversation-thread <id>', 'Teams conversation thread id')
396
+ .option('--assignee-priority <hint>', 'Assignee priority order hint')
397
+ .option('--due <ISO-8601>', 'Due date/time')
398
+ .option('--start <ISO-8601>', 'Start date/time')
399
+ .option('--clear-due', 'Clear due date')
400
+ .option('--clear-start', 'Clear start date')
401
+ .option('--priority <0-10>', 'Task priority (0 highest .. 10 lowest)')
402
+ .option('--clear-priority', 'Reset priority (set to null)')
403
+ .option('--preview-type <mode>', 'Card preview: automatic | noPreview | checklist | description | reference')
404
+ .option('--clear-preview-type', 'Clear preview type (set to null)')
405
+ .option(
406
+ '--label <slot>',
407
+ 'Turn on label slot (1-6 or category1..category6); repeatable',
408
+ (v: string, prev: string[]) => [...prev, v],
409
+ [] as string[]
410
+ )
411
+ .option(
412
+ '--unlabel <slot>',
413
+ 'Turn off label slot; repeatable',
414
+ (v: string, prev: string[]) => [...prev, v],
415
+ [] as string[]
416
+ )
417
+ .option('--clear-labels', 'Clear all label slots on the task')
418
+ .option('--json', 'Output JSON')
419
+ .option('--token <token>', 'Use a specific token')
420
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
421
+ .action(
422
+ async (
423
+ opts: {
424
+ id: string;
425
+ title?: string;
426
+ bucket?: string;
427
+ percent?: string;
428
+ assign?: string[];
429
+ addAssign?: string[];
430
+ removeAssign?: string[];
431
+ clearAssign?: boolean;
432
+ orderHint?: string;
433
+ conversationThread?: string;
434
+ assigneePriority?: string;
435
+ due?: string;
436
+ start?: string;
437
+ clearDue?: boolean;
438
+ clearStart?: boolean;
439
+ priority?: string;
440
+ clearPriority?: boolean;
441
+ previewType?: string;
442
+ clearPreviewType?: boolean;
443
+ label?: string[];
444
+ unlabel?: string[];
445
+ clearLabels?: boolean;
446
+ json?: boolean;
447
+ token?: string;
448
+ identity?: string;
449
+ },
450
+ cmd: any
451
+ ) => {
452
+ checkReadOnly(cmd);
453
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
454
+ if (!auth.success) {
455
+ console.error(`Auth error: ${auth.error}`);
456
+ process.exit(1);
457
+ }
458
+
459
+ if (opts.clearPriority && opts.priority !== undefined) {
460
+ console.error('Error: use either --priority or --clear-priority, not both');
461
+ process.exit(1);
462
+ }
463
+ if (opts.clearPreviewType && opts.previewType !== undefined) {
464
+ console.error('Error: use either --preview-type or --clear-preview-type, not both');
465
+ process.exit(1);
466
+ }
467
+ if (opts.priority !== undefined) {
468
+ const p = parseInt(opts.priority, 10);
469
+ if (Number.isNaN(p) || p < 0 || p > 10) {
470
+ console.error('Error: --priority must be an integer from 0 to 10');
471
+ process.exit(1);
472
+ }
473
+ }
474
+
475
+ const assignReplace = (opts.assign?.length ?? 0) > 0;
476
+ const assignMerge = (opts.addAssign?.length ?? 0) > 0 || (opts.removeAssign?.length ?? 0) > 0;
477
+ if (assignReplace && assignMerge) {
478
+ console.error('Error: use either --assign (replace) or --add-assign/--remove-assign, not both');
479
+ process.exit(1);
480
+ }
481
+ if (assignReplace && opts.clearAssign) {
482
+ console.error('Error: use either --assign or --clear-assign, not both');
483
+ process.exit(1);
484
+ }
485
+ if (opts.clearAssign && assignMerge) {
486
+ console.error('Error: use either --clear-assign or --add-assign/--remove-assign, not both');
487
+ process.exit(1);
488
+ }
489
+ if (opts.clearDue && opts.due !== undefined) {
490
+ console.error('Error: use either --due or --clear-due, not both');
491
+ process.exit(1);
492
+ }
493
+ if (opts.clearStart && opts.start !== undefined) {
494
+ console.error('Error: use either --start or --clear-start, not both');
495
+ process.exit(1);
496
+ }
497
+
498
+ // First, we need to get the task to retrieve its ETag.
499
+ const taskRes = await getTask(auth.token!, opts.id);
500
+ if (!taskRes.ok || !taskRes.data) {
501
+ console.error(`Error fetching task: ${taskRes.error?.message}`);
502
+ process.exit(1);
503
+ }
504
+ const etag = taskRes.data['@odata.etag'];
505
+ if (!etag) {
506
+ console.error('Task does not have an ETag');
507
+ process.exit(1);
508
+ }
509
+
510
+ const updates: any = {};
511
+ if (opts.title !== undefined) updates.title = opts.title;
512
+ if (opts.bucket !== undefined) updates.bucketId = opts.bucket;
513
+ if (opts.percent !== undefined) {
514
+ const percentValue = parseInt(opts.percent, 10);
515
+ if (Number.isNaN(percentValue) || percentValue < 0 || percentValue > 100) {
516
+ console.error(`Invalid percent value: ${opts.percent}. Must be a number between 0 and 100.`);
517
+ process.exit(1);
518
+ }
519
+ updates.percentComplete = percentValue;
520
+ }
521
+ if (opts.clearAssign) {
522
+ updates.assignments = {};
523
+ } else if (assignReplace) {
524
+ updates.assignments = buildPlannerAssignments(opts.assign!);
525
+ } else if (assignMerge) {
526
+ updates.assignments = mergePlannerAssignments(
527
+ taskRes.data.assignments as Record<string, unknown> | undefined,
528
+ opts.addAssign ?? [],
529
+ opts.removeAssign ?? []
530
+ );
531
+ }
532
+
533
+ if (opts.orderHint !== undefined) updates.orderHint = opts.orderHint;
534
+ if (opts.conversationThread !== undefined) updates.conversationThreadId = opts.conversationThread;
535
+ if (opts.assigneePriority !== undefined) updates.assigneePriority = opts.assigneePriority;
536
+
537
+ if (opts.clearDue) updates.dueDateTime = null;
538
+ else if (opts.due !== undefined) updates.dueDateTime = opts.due;
539
+ if (opts.clearStart) updates.startDateTime = null;
540
+ else if (opts.start !== undefined) updates.startDateTime = opts.start;
541
+
542
+ if (opts.clearPriority) updates.priority = null;
543
+ else if (opts.priority !== undefined) updates.priority = parseInt(opts.priority, 10);
544
+ if (opts.clearPreviewType) updates.previewType = null;
545
+ else if (opts.previewType !== undefined) updates.previewType = opts.previewType;
546
+
547
+ const labelOps = (opts.label?.length ?? 0) > 0 || (opts.unlabel?.length ?? 0) > 0 || opts.clearLabels;
548
+ if (labelOps) {
549
+ const setTrue: PlannerCategorySlot[] = [];
550
+ const setFalse: PlannerCategorySlot[] = [];
551
+ for (const raw of opts.label ?? []) {
552
+ const slot = parsePlannerLabelKey(raw);
553
+ if (!slot) {
554
+ console.error(`Invalid --label "${raw}". Use 1-6 or category1..category6.`);
555
+ process.exit(1);
556
+ }
557
+ setTrue.push(slot);
558
+ }
559
+ for (const raw of opts.unlabel ?? []) {
560
+ const slot = parsePlannerLabelKey(raw);
561
+ if (!slot) {
562
+ console.error(`Invalid --unlabel "${raw}". Use 1-6 or category1..category6.`);
563
+ process.exit(1);
564
+ }
565
+ setFalse.push(slot);
566
+ }
567
+ if (opts.clearLabels && (setTrue.length > 0 || setFalse.length > 0)) {
568
+ console.error('Error: use --clear-labels alone, or use --label/--unlabel without --clear-labels');
569
+ process.exit(1);
570
+ }
571
+ updates.appliedCategories = normalizeAppliedCategories(taskRes.data.appliedCategories, {
572
+ clearAll: opts.clearLabels,
573
+ setTrue: setTrue.length ? setTrue : undefined,
574
+ setFalse: setFalse.length ? setFalse : undefined
575
+ });
576
+ }
577
+
578
+ if (Object.keys(updates).length === 0) {
579
+ console.error(
580
+ 'Error: specify at least one of --title, --bucket, --percent, --assign, --add-assign, --remove-assign, --clear-assign, --order-hint, --conversation-thread, --assignee-priority, --due, --start, --clear-due, --clear-start, --priority, --clear-priority, --preview-type, --clear-preview-type, --label, --unlabel, --clear-labels'
581
+ );
582
+ process.exit(1);
583
+ }
584
+
585
+ const result = await updateTask(auth.token!, opts.id, etag, updates);
586
+ if (!result.ok) {
587
+ console.error(`Error updating task: ${result.error?.message}`);
588
+ process.exit(1);
589
+ }
590
+
591
+ // Since PATCH returns 204 No Content, get task again to show updated state
592
+ const updatedTaskRes = await getTask(auth.token!, opts.id);
593
+ if (!updatedTaskRes.ok || !updatedTaskRes.data) {
594
+ console.error(`Error fetching updated task: ${updatedTaskRes.error?.message}`);
595
+ process.exit(1);
596
+ }
597
+
598
+ if (opts.json) {
599
+ console.log(JSON.stringify(updatedTaskRes.data, null, 2));
600
+ } else {
601
+ console.log(`Updated task: ${opts.id}`);
602
+ }
603
+ }
604
+ );
605
+
606
+ plannerCommand
607
+ .command('get-task')
608
+ .description('Fetch a Planner task by ID')
609
+ .requiredOption('-i, --id <taskId>', 'Task ID')
610
+ .option('--with-details', 'Include task details (description, checklist, references)')
611
+ .option('--json', 'Output JSON')
612
+ .option('--token <token>', 'Use a specific token')
613
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
614
+ .action(async (opts: { id: string; withDetails?: boolean; json?: boolean; token?: string; identity?: string }) => {
615
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
616
+ if (!auth.success) {
617
+ console.error(`Auth error: ${auth.error}`);
618
+ process.exit(1);
619
+ }
620
+ const taskRes = await getTask(auth.token!, opts.id);
621
+ if (!taskRes.ok || !taskRes.data) {
622
+ console.error(`Error: ${taskRes.error?.message}`);
623
+ process.exit(1);
624
+ }
625
+ const t = taskRes.data;
626
+ const td = opts.withDetails ? await getPlannerTaskDetails(auth.token!, opts.id) : undefined;
627
+ if (opts.json) {
628
+ if (opts.withDetails && td?.ok && td.data) {
629
+ console.log(JSON.stringify({ task: t, details: td.data }, null, 2));
630
+ } else {
631
+ console.log(JSON.stringify(t, null, 2));
632
+ }
633
+ } else {
634
+ const detailsR = await getPlanDetails(auth.token!, t.planId);
635
+ const descriptions = detailsR.ok ? detailsR.data?.categoryDescriptions : undefined;
636
+ const labels = formatTaskLabels(t, descriptions);
637
+ console.log(`${t.title} (ID: ${t.id})`);
638
+ console.log(` Plan: ${t.planId} | Bucket: ${t.bucketId} | ${t.percentComplete}%`);
639
+ if (t.assigneePriority) console.log(` Assignee priority: ${t.assigneePriority}`);
640
+ if (t.conversationThreadId) console.log(` Conversation thread: ${t.conversationThreadId}`);
641
+ if (t.dueDateTime) console.log(` Due: ${t.dueDateTime}`);
642
+ if (t.startDateTime) console.log(` Start: ${t.startDateTime}`);
643
+ if (t.priority !== undefined) console.log(` Priority: ${t.priority} (0=highest..10=lowest)`);
644
+ if (t.previewType) console.log(` Preview type: ${t.previewType}`);
645
+ if (labels) console.log(` Labels: ${labels}`);
646
+ if (opts.withDetails && td?.ok && td.data) {
647
+ if (td.data.description) console.log(` Description:\n${td.data.description}`);
648
+ if (td.data.checklist && Object.keys(td.data.checklist).length) {
649
+ console.log(' Checklist:');
650
+ for (const [cid, item] of Object.entries(td.data.checklist)) {
651
+ console.log(` [${item.isChecked ? 'x' : ' '}] ${item.title} (${cid})`);
652
+ }
653
+ }
654
+ }
655
+ }
656
+ });
657
+
658
+ plannerCommand
659
+ .command('get-plan')
660
+ .description('Fetch a Planner plan (for ETag before update/delete)')
661
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
662
+ .option('--json', 'Output JSON')
663
+ .option('--token <token>', 'Use a specific token')
664
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
665
+ .action(async (opts: { plan: string; json?: boolean; token?: string; identity?: string }) => {
666
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
667
+ if (!auth.success) {
668
+ console.error(`Auth error: ${auth.error}`);
669
+ process.exit(1);
670
+ }
671
+ const r = await getPlannerPlan(auth.token!, opts.plan);
672
+ if (!r.ok || !r.data) {
673
+ console.error(`Error: ${r.error?.message}`);
674
+ process.exit(1);
675
+ }
676
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
677
+ else {
678
+ console.log(`${r.data.title} (ID: ${r.data.id})`);
679
+ if (r.data.owner) console.log(` Owner (group): ${r.data.owner}`);
680
+ if (r.data['@odata.etag']) console.log(` ETag: ${r.data['@odata.etag']}`);
681
+ }
682
+ });
683
+
684
+ plannerCommand
685
+ .command('delete-task')
686
+ .description('Delete a Planner task')
687
+ .requiredOption('-i, --id <taskId>', 'Task ID')
688
+ .option('--confirm', 'Confirm deletion')
689
+ .option('--json', 'Output JSON')
690
+ .option('--token <token>', 'Use a specific token')
691
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
692
+ .action(
693
+ async (opts: { id: string; confirm?: boolean; json?: boolean; token?: string; identity?: string }, cmd: any) => {
694
+ checkReadOnly(cmd);
695
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
696
+ if (!auth.success) {
697
+ console.error(`Auth error: ${auth.error}`);
698
+ process.exit(1);
699
+ }
700
+ const taskRes = await getTask(auth.token!, opts.id);
701
+ if (!taskRes.ok || !taskRes.data) {
702
+ console.error(`Error: ${taskRes.error?.message}`);
703
+ process.exit(1);
704
+ }
705
+ const etag = taskRes.data['@odata.etag'];
706
+ if (!etag) {
707
+ console.error('Task missing ETag');
708
+ process.exit(1);
709
+ }
710
+ if (!opts.confirm) {
711
+ console.log(`Delete task "${taskRes.data.title}"? (ID: ${opts.id})`);
712
+ console.log('Run with --confirm to confirm.');
713
+ process.exit(1);
714
+ }
715
+ const del = await deletePlannerTask(auth.token!, opts.id, etag);
716
+ if (!del.ok) {
717
+ console.error(`Error: ${del.error?.message}`);
718
+ process.exit(1);
719
+ }
720
+ if (opts.json) console.log(JSON.stringify({ deleted: opts.id }, null, 2));
721
+ else console.log(`Deleted task: ${opts.id}`);
722
+ }
723
+ );
724
+
725
+ plannerCommand
726
+ .command('create-plan')
727
+ .description('Create a Planner plan in a group (v1) or in a roster (beta; use --roster)')
728
+ .option('-g, --group <groupId>', 'Microsoft 365 group that owns the plan')
729
+ .option('-r, --roster <rosterId>', 'Beta: planner roster id (container); mutually exclusive with --group')
730
+ .requiredOption('-t, --title <title>', 'Plan title')
731
+ .option('--json', 'Output JSON')
732
+ .option('--token <token>', 'Use a specific token')
733
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
734
+ .action(
735
+ async (
736
+ opts: { group?: string; roster?: string; title: string; json?: boolean; token?: string; identity?: string },
737
+ cmd: any
738
+ ) => {
739
+ checkReadOnly(cmd);
740
+ const hasGroup = Boolean(opts.group);
741
+ const hasRoster = Boolean(opts.roster);
742
+ if (hasGroup === hasRoster) {
743
+ console.error('Error: specify exactly one of --group or --roster');
744
+ process.exit(1);
745
+ }
746
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
747
+ if (!auth.success) {
748
+ console.error(`Auth error: ${auth.error}`);
749
+ process.exit(1);
750
+ }
751
+ const r = hasRoster
752
+ ? await createPlannerPlanInRoster(auth.token!, opts.roster!, opts.title)
753
+ : await createPlannerPlan(auth.token!, opts.group!, opts.title);
754
+ if (!r.ok || !r.data) {
755
+ console.error(`Error: ${r.error?.message}`);
756
+ process.exit(1);
757
+ }
758
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
759
+ else console.log(`Created plan: ${r.data.title} (ID: ${r.data.id})`);
760
+ }
761
+ );
762
+
763
+ plannerCommand
764
+ .command('update-plan')
765
+ .description('Rename a Planner plan')
766
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
767
+ .requiredOption('-t, --title <title>', 'New title')
768
+ .option('--json', 'Output JSON')
769
+ .option('--token <token>', 'Use a specific token')
770
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
771
+ .action(
772
+ async (opts: { plan: string; title: string; json?: boolean; token?: string; identity?: string }, cmd: any) => {
773
+ checkReadOnly(cmd);
774
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
775
+ if (!auth.success) {
776
+ console.error(`Auth error: ${auth.error}`);
777
+ process.exit(1);
778
+ }
779
+ const pr = await getPlannerPlan(auth.token!, opts.plan);
780
+ if (!pr.ok || !pr.data) {
781
+ console.error(`Error: ${pr.error?.message}`);
782
+ process.exit(1);
783
+ }
784
+ const etag = pr.data['@odata.etag'];
785
+ if (!etag) {
786
+ console.error('Plan missing ETag');
787
+ process.exit(1);
788
+ }
789
+ const r = await updatePlannerPlan(auth.token!, opts.plan, etag, opts.title);
790
+ if (!r.ok) {
791
+ console.error(`Error: ${r.error?.message}`);
792
+ process.exit(1);
793
+ }
794
+ const again = await getPlannerPlan(auth.token!, opts.plan);
795
+ if (opts.json && again.ok && again.data) console.log(JSON.stringify(again.data, null, 2));
796
+ else console.log(`Updated plan: ${opts.plan}`);
797
+ }
798
+ );
799
+
800
+ plannerCommand
801
+ .command('delete-plan')
802
+ .description('Delete a Planner plan')
803
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
804
+ .option('--confirm', 'Confirm deletion')
805
+ .option('--json', 'Output JSON')
806
+ .option('--token <token>', 'Use a specific token')
807
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
808
+ .action(
809
+ async (opts: { plan: string; confirm?: boolean; json?: boolean; token?: string; identity?: string }, cmd: any) => {
810
+ checkReadOnly(cmd);
811
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
812
+ if (!auth.success) {
813
+ console.error(`Auth error: ${auth.error}`);
814
+ process.exit(1);
815
+ }
816
+ const pr = await getPlannerPlan(auth.token!, opts.plan);
817
+ if (!pr.ok || !pr.data) {
818
+ console.error(`Error: ${pr.error?.message}`);
819
+ process.exit(1);
820
+ }
821
+ const etag = pr.data['@odata.etag'];
822
+ if (!etag) {
823
+ console.error('Plan missing ETag');
824
+ process.exit(1);
825
+ }
826
+ if (!opts.confirm) {
827
+ console.log(`Delete plan "${pr.data.title}"? (ID: ${opts.plan})`);
828
+ console.log('Run with --confirm to confirm.');
829
+ process.exit(1);
830
+ }
831
+ const r = await deletePlannerPlan(auth.token!, opts.plan, etag);
832
+ if (!r.ok) {
833
+ console.error(`Error: ${r.error?.message}`);
834
+ process.exit(1);
835
+ }
836
+ if (opts.json) console.log(JSON.stringify({ deleted: opts.plan }, null, 2));
837
+ else console.log(`Deleted plan: ${opts.plan}`);
838
+ }
839
+ );
840
+
841
+ plannerCommand
842
+ .command('create-bucket')
843
+ .description('Create a bucket in a plan')
844
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
845
+ .requiredOption('-n, --name <name>', 'Bucket name')
846
+ .option('--json', 'Output JSON')
847
+ .option('--token <token>', 'Use a specific token')
848
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
849
+ .action(async (opts: { plan: string; name: string; json?: boolean; token?: string; identity?: string }, cmd: any) => {
850
+ checkReadOnly(cmd);
851
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
852
+ if (!auth.success) {
853
+ console.error(`Auth error: ${auth.error}`);
854
+ process.exit(1);
855
+ }
856
+ const r = await createPlannerBucket(auth.token!, opts.plan, opts.name);
857
+ if (!r.ok || !r.data) {
858
+ console.error(`Error: ${r.error?.message}`);
859
+ process.exit(1);
860
+ }
861
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
862
+ else console.log(`Created bucket: ${r.data.name} (ID: ${r.data.id})`);
863
+ });
864
+
865
+ plannerCommand
866
+ .command('update-bucket')
867
+ .description('Rename a bucket and/or set order hint (reordering)')
868
+ .requiredOption('-i, --id <bucketId>', 'Bucket ID')
869
+ .option('-n, --name <name>', 'New name')
870
+ .option('--order-hint <hint>', 'Bucket order hint string')
871
+ .option('--json', 'Output JSON')
872
+ .option('--token <token>', 'Use a specific token')
873
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
874
+ .action(
875
+ async (
876
+ opts: { id: string; name?: string; orderHint?: string; json?: boolean; token?: string; identity?: string },
877
+ cmd: any
878
+ ) => {
879
+ checkReadOnly(cmd);
880
+ if (opts.name === undefined && opts.orderHint === undefined) {
881
+ console.error('Error: specify --name and/or --order-hint');
882
+ process.exit(1);
883
+ }
884
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
885
+ if (!auth.success) {
886
+ console.error(`Auth error: ${auth.error}`);
887
+ process.exit(1);
888
+ }
889
+ const br = await getPlannerBucket(auth.token!, opts.id);
890
+ if (!br.ok || !br.data) {
891
+ console.error(`Error: ${br.error?.message}`);
892
+ process.exit(1);
893
+ }
894
+ const etag = br.data['@odata.etag'];
895
+ if (!etag) {
896
+ console.error('Bucket missing ETag');
897
+ process.exit(1);
898
+ }
899
+ const bucketUpdates: { name?: string; orderHint?: string } = {};
900
+ if (opts.name !== undefined) bucketUpdates.name = opts.name;
901
+ if (opts.orderHint !== undefined) bucketUpdates.orderHint = opts.orderHint;
902
+ const r = await updatePlannerBucket(auth.token!, opts.id, etag, bucketUpdates);
903
+ if (!r.ok) {
904
+ console.error(`Error: ${r.error?.message}`);
905
+ process.exit(1);
906
+ }
907
+ const again = await getPlannerBucket(auth.token!, opts.id);
908
+ if (opts.json && again.ok && again.data) console.log(JSON.stringify(again.data, null, 2));
909
+ else console.log(`Updated bucket: ${opts.id}`);
910
+ }
911
+ );
912
+
913
+ plannerCommand
914
+ .command('delete-bucket')
915
+ .description('Delete a bucket')
916
+ .requiredOption('-i, --id <bucketId>', 'Bucket ID')
917
+ .option('--confirm', 'Confirm deletion')
918
+ .option('--json', 'Output JSON')
919
+ .option('--token <token>', 'Use a specific token')
920
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
921
+ .action(
922
+ async (opts: { id: string; confirm?: boolean; json?: boolean; token?: string; identity?: string }, cmd: any) => {
923
+ checkReadOnly(cmd);
924
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
925
+ if (!auth.success) {
926
+ console.error(`Auth error: ${auth.error}`);
927
+ process.exit(1);
928
+ }
929
+ const br = await getPlannerBucket(auth.token!, opts.id);
930
+ if (!br.ok || !br.data) {
931
+ console.error(`Error: ${br.error?.message}`);
932
+ process.exit(1);
933
+ }
934
+ const etag = br.data['@odata.etag'];
935
+ if (!etag) {
936
+ console.error('Bucket missing ETag');
937
+ process.exit(1);
938
+ }
939
+ if (!opts.confirm) {
940
+ console.log(`Delete bucket "${br.data.name}"? (ID: ${opts.id})`);
941
+ console.log('Run with --confirm to confirm.');
942
+ process.exit(1);
943
+ }
944
+ const r = await deletePlannerBucket(auth.token!, opts.id, etag);
945
+ if (!r.ok) {
946
+ console.error(`Error: ${r.error?.message}`);
947
+ process.exit(1);
948
+ }
949
+ if (opts.json) console.log(JSON.stringify({ deleted: opts.id }, null, 2));
950
+ else console.log(`Deleted bucket: ${opts.id}`);
951
+ }
952
+ );
953
+
954
+ plannerCommand
955
+ .command('get-task-details')
956
+ .description('Get Planner task details (description, checklist, references)')
957
+ .requiredOption('-i, --id <taskId>', 'Task ID')
958
+ .option('--json', 'Output JSON')
959
+ .option('--token <token>', 'Use a specific token')
960
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
961
+ .action(async (opts: { id: string; json?: boolean; token?: string; identity?: string }) => {
962
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
963
+ if (!auth.success) {
964
+ console.error(`Auth error: ${auth.error}`);
965
+ process.exit(1);
966
+ }
967
+ const r = await getPlannerTaskDetails(auth.token!, opts.id);
968
+ if (!r.ok || !r.data) {
969
+ console.error(`Error: ${r.error?.message}`);
970
+ process.exit(1);
971
+ }
972
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
973
+ else {
974
+ console.log(`Details ID: ${r.data.id}`);
975
+ if (r.data.description) console.log(`Description:\n${r.data.description}`);
976
+ if (r.data.checklist && Object.keys(r.data.checklist).length) {
977
+ console.log('Checklist:');
978
+ for (const [cid, item] of Object.entries(r.data.checklist)) {
979
+ console.log(` [${item.isChecked ? 'x' : ' '}] ${item.title} (${cid})`);
980
+ }
981
+ }
982
+ }
983
+ });
984
+
985
+ plannerCommand
986
+ .command('update-task-details')
987
+ .description('Update Planner task details (description and/or checklist/references JSON)')
988
+ .requiredOption('-i, --id <taskId>', 'Task ID')
989
+ .option('--description <text>', 'Task description (HTML or plain depending on client)')
990
+ .option('--patch-json <path>', 'JSON file with PATCH body (description, checklist, references, previewType)')
991
+ .option('--json', 'Output JSON')
992
+ .option('--token <token>', 'Use a specific token')
993
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
994
+ .action(
995
+ async (
996
+ opts: {
997
+ id: string;
998
+ description?: string;
999
+ patchJson?: string;
1000
+ json?: boolean;
1001
+ token?: string;
1002
+ identity?: string;
1003
+ },
1004
+ cmd: any
1005
+ ) => {
1006
+ checkReadOnly(cmd);
1007
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1008
+ if (!auth.success) {
1009
+ console.error(`Auth error: ${auth.error}`);
1010
+ process.exit(1);
1011
+ }
1012
+ if (opts.description !== undefined && opts.patchJson) {
1013
+ console.error('Error: use either --description or --patch-json, not both');
1014
+ process.exit(1);
1015
+ }
1016
+ if (opts.description === undefined && !opts.patchJson) {
1017
+ console.error('Error: specify --description and/or --patch-json');
1018
+ process.exit(1);
1019
+ }
1020
+ const dr = await getPlannerTaskDetails(auth.token!, opts.id);
1021
+ if (!dr.ok || !dr.data) {
1022
+ console.error(`Error: ${dr.error?.message}`);
1023
+ process.exit(1);
1024
+ }
1025
+ const etag = dr.data['@odata.etag'];
1026
+ const detailsId = dr.data.id;
1027
+ if (!etag) {
1028
+ console.error('Task details missing ETag');
1029
+ process.exit(1);
1030
+ }
1031
+ let body: Record<string, unknown>;
1032
+ if (opts.patchJson) {
1033
+ const raw = await readFile(opts.patchJson, 'utf-8');
1034
+ body = JSON.parse(raw) as Record<string, unknown>;
1035
+ } else {
1036
+ body = { description: opts.description };
1037
+ }
1038
+ const r = await updatePlannerTaskDetails(auth.token!, detailsId, etag, body as UpdatePlannerTaskDetailsParams);
1039
+ if (!r.ok) {
1040
+ console.error(`Error: ${r.error?.message}`);
1041
+ process.exit(1);
1042
+ }
1043
+ const again = await getPlannerTaskDetails(auth.token!, opts.id);
1044
+ if (opts.json && again.ok && again.data) console.log(JSON.stringify(again.data, null, 2));
1045
+ else console.log(`Updated task details for task: ${opts.id}`);
1046
+ }
1047
+ );
1048
+
1049
+ plannerCommand
1050
+ .command('get-plan-details')
1051
+ .description('Get plan details (label names, sharedWith, ETag)')
1052
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
1053
+ .option('--json', 'Output JSON')
1054
+ .option('--token <token>', 'Use a specific token')
1055
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1056
+ .action(async (opts: { plan: string; json?: boolean; token?: string; identity?: string }) => {
1057
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1058
+ if (!auth.success) {
1059
+ console.error(`Auth error: ${auth.error}`);
1060
+ process.exit(1);
1061
+ }
1062
+ const r = await getPlanDetails(auth.token!, opts.plan);
1063
+ if (!r.ok || !r.data) {
1064
+ console.error(`Error: ${r.error?.message}`);
1065
+ process.exit(1);
1066
+ }
1067
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1068
+ else {
1069
+ console.log(`Plan details ID: ${r.data.id}`);
1070
+ if (r.data['@odata.etag']) console.log(`ETag: ${r.data['@odata.etag']}`);
1071
+ if (r.data.categoryDescriptions) {
1072
+ for (const slot of LABEL_SLOTS) {
1073
+ const n = r.data.categoryDescriptions[slot];
1074
+ if (n) console.log(` ${slot}: ${n}`);
1075
+ }
1076
+ }
1077
+ if (r.data.sharedWith && Object.keys(r.data.sharedWith).length) {
1078
+ console.log('sharedWith:', JSON.stringify(r.data.sharedWith));
1079
+ }
1080
+ }
1081
+ });
1082
+
1083
+ plannerCommand
1084
+ .command('update-plan-details')
1085
+ .description('PATCH plan details (label names, sharedWith)')
1086
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
1087
+ .option('--names-json <path>', 'JSON: categoryDescriptions object (category1..category6)')
1088
+ .option('--shared-with-json <path>', 'JSON: sharedWith map { userId: true|false }')
1089
+ .option('--json', 'Output JSON')
1090
+ .option('--token <token>', 'Use a specific token')
1091
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1092
+ .action(
1093
+ async (
1094
+ opts: {
1095
+ plan: string;
1096
+ namesJson?: string;
1097
+ sharedWithJson?: string;
1098
+ json?: boolean;
1099
+ token?: string;
1100
+ identity?: string;
1101
+ },
1102
+ cmd: any
1103
+ ) => {
1104
+ checkReadOnly(cmd);
1105
+ if (!opts.namesJson && !opts.sharedWithJson) {
1106
+ console.error('Error: specify --names-json and/or --shared-with-json');
1107
+ process.exit(1);
1108
+ }
1109
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1110
+ if (!auth.success) {
1111
+ console.error(`Auth error: ${auth.error}`);
1112
+ process.exit(1);
1113
+ }
1114
+ const dr = await getPlanDetails(auth.token!, opts.plan);
1115
+ if (!dr.ok || !dr.data) {
1116
+ console.error(`Error: ${dr.error?.message}`);
1117
+ process.exit(1);
1118
+ }
1119
+ const etag = dr.data['@odata.etag'];
1120
+ if (!etag) {
1121
+ console.error('Plan details missing ETag');
1122
+ process.exit(1);
1123
+ }
1124
+ const body: UpdatePlannerPlanDetailsParams = {};
1125
+ if (opts.namesJson) {
1126
+ const raw = await readFile(opts.namesJson, 'utf-8');
1127
+ body.categoryDescriptions = JSON.parse(raw) as UpdatePlannerPlanDetailsParams['categoryDescriptions'];
1128
+ }
1129
+ if (opts.sharedWithJson) {
1130
+ const raw = await readFile(opts.sharedWithJson, 'utf-8');
1131
+ body.sharedWith = JSON.parse(raw) as Record<string, boolean>;
1132
+ }
1133
+ const r = await updatePlannerPlanDetails(auth.token!, opts.plan, etag, body);
1134
+ if (!r.ok) {
1135
+ console.error(`Error: ${r.error?.message}`);
1136
+ process.exit(1);
1137
+ }
1138
+ const again = await getPlanDetails(auth.token!, opts.plan);
1139
+ if (opts.json && again.ok && again.data) console.log(JSON.stringify(again.data, null, 2));
1140
+ else console.log(`Updated plan details for plan: ${opts.plan}`);
1141
+ }
1142
+ );
1143
+
1144
+ plannerCommand
1145
+ .command('list-favorite-plans')
1146
+ .description('List favorite plans (beta Graph API; see GRAPH_BETA_URL)')
1147
+ .option('--json', 'Output JSON')
1148
+ .option('--token <token>', 'Use a specific token')
1149
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1150
+ .action(async (opts: { json?: boolean; token?: string; identity?: string }) => {
1151
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1152
+ if (!auth.success) {
1153
+ console.error(`Auth error: ${auth.error}`);
1154
+ process.exit(1);
1155
+ }
1156
+ const r = await listFavoritePlans(auth.token!);
1157
+ if (!r.ok || !r.data) {
1158
+ console.error(`Error: ${r.error?.message}`);
1159
+ process.exit(1);
1160
+ }
1161
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1162
+ else for (const p of r.data) console.log(`- ${p.title} (${p.id})`);
1163
+ });
1164
+
1165
+ plannerCommand
1166
+ .command('list-roster-plans')
1167
+ .description('List plans from rosters you belong to (beta Graph API)')
1168
+ .option('--json', 'Output JSON')
1169
+ .option('--token <token>', 'Use a specific token')
1170
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1171
+ .action(async (opts: { json?: boolean; token?: string; identity?: string }) => {
1172
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1173
+ if (!auth.success) {
1174
+ console.error(`Auth error: ${auth.error}`);
1175
+ process.exit(1);
1176
+ }
1177
+ const r = await listRosterPlans(auth.token!);
1178
+ if (!r.ok || !r.data) {
1179
+ console.error(`Error: ${r.error?.message}`);
1180
+ process.exit(1);
1181
+ }
1182
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1183
+ else for (const p of r.data) console.log(`- ${p.title} (${p.id})`);
1184
+ });
1185
+
1186
+ plannerCommand
1187
+ .command('delta')
1188
+ .description('Fetch one page of Planner delta (beta /me/planner/all/delta or --url)')
1189
+ .option('--url <url>', 'Next or delta link from a previous response')
1190
+ .option('--json', 'Output JSON')
1191
+ .option('--token <token>', 'Use a specific token')
1192
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1193
+ .action(async (opts: { url?: string; json?: boolean; token?: string; identity?: string }) => {
1194
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1195
+ if (!auth.success) {
1196
+ console.error(`Auth error: ${auth.error}`);
1197
+ process.exit(1);
1198
+ }
1199
+ const r = await getPlannerDeltaPage(auth.token!, opts.url);
1200
+ if (!r.ok || !r.data) {
1201
+ console.error(`Error: ${r.error?.message}`);
1202
+ process.exit(1);
1203
+ }
1204
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1205
+ else {
1206
+ console.log(`Changes: ${(r.data.value ?? []).length} item(s)`);
1207
+ if (r.data['@odata.nextLink']) console.log(`nextLink: ${r.data['@odata.nextLink']}`);
1208
+ if (r.data['@odata.deltaLink']) console.log(`deltaLink: ${r.data['@odata.deltaLink']}`);
1209
+ }
1210
+ });
1211
+
1212
+ plannerCommand
1213
+ .command('add-checklist-item')
1214
+ .description('Add a Planner checklist item (generates id)')
1215
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1216
+ .requiredOption('-t, --title <text>', 'Checklist item text')
1217
+ .option('-c, --item-id <id>', 'Optional id (default: random UUID)')
1218
+ .option('--token <token>', 'Use a specific token')
1219
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1220
+ .action(async (opts: { id: string; title: string; itemId?: string; token?: string; identity?: string }, cmd: any) => {
1221
+ checkReadOnly(cmd);
1222
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1223
+ if (!auth.success) {
1224
+ console.error(`Auth error: ${auth.error}`);
1225
+ process.exit(1);
1226
+ }
1227
+ const r = await addPlannerChecklistItem(auth.token!, opts.id, opts.title, opts.itemId);
1228
+ if (!r.ok) {
1229
+ console.error(`Error: ${r.error?.message}`);
1230
+ process.exit(1);
1231
+ }
1232
+ console.log(`OK: checklist updated for task ${opts.id}`);
1233
+ });
1234
+
1235
+ plannerCommand
1236
+ .command('remove-checklist-item')
1237
+ .description('Remove a Planner checklist item by id')
1238
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1239
+ .requiredOption('-c, --item <checklistItemId>', 'Checklist item id')
1240
+ .option('--token <token>', 'Use a specific token')
1241
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1242
+ .action(async (opts: { id: string; item: string; token?: string; identity?: string }, cmd: any) => {
1243
+ checkReadOnly(cmd);
1244
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1245
+ if (!auth.success) {
1246
+ console.error(`Auth error: ${auth.error}`);
1247
+ process.exit(1);
1248
+ }
1249
+ const r = await removePlannerChecklistItem(auth.token!, opts.id, opts.item);
1250
+ if (!r.ok) {
1251
+ console.error(`Error: ${r.error?.message}`);
1252
+ process.exit(1);
1253
+ }
1254
+ console.log(`OK: removed checklist item ${opts.item}`);
1255
+ });
1256
+
1257
+ plannerCommand
1258
+ .command('add-reference')
1259
+ .description('Add a link reference on task details')
1260
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1261
+ .requiredOption('-u, --url <url>', 'Reference URL (key)')
1262
+ .requiredOption('-a, --alias <text>', 'Display alias')
1263
+ .option('--type <type>', 'Optional type string (e.g. PowerPoint)')
1264
+ .option('--token <token>', 'Use a specific token')
1265
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1266
+ .action(
1267
+ async (
1268
+ opts: { id: string; url: string; alias: string; type?: string; token?: string; identity?: string },
1269
+ cmd: any
1270
+ ) => {
1271
+ checkReadOnly(cmd);
1272
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1273
+ if (!auth.success) {
1274
+ console.error(`Auth error: ${auth.error}`);
1275
+ process.exit(1);
1276
+ }
1277
+ const r = await addPlannerReference(auth.token!, opts.id, opts.url, opts.alias, opts.type);
1278
+ if (!r.ok) {
1279
+ console.error(`Error: ${r.error?.message}`);
1280
+ process.exit(1);
1281
+ }
1282
+ console.log(`OK: reference added for task ${opts.id}`);
1283
+ }
1284
+ );
1285
+
1286
+ plannerCommand
1287
+ .command('remove-reference')
1288
+ .description('Remove a reference by URL key')
1289
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1290
+ .requiredOption('-u, --url <url>', 'Reference URL key')
1291
+ .option('--token <token>', 'Use a specific token')
1292
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1293
+ .action(async (opts: { id: string; url: string; token?: string; identity?: string }, cmd: any) => {
1294
+ checkReadOnly(cmd);
1295
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1296
+ if (!auth.success) {
1297
+ console.error(`Auth error: ${auth.error}`);
1298
+ process.exit(1);
1299
+ }
1300
+ const r = await removePlannerReference(auth.token!, opts.id, opts.url);
1301
+ if (!r.ok) {
1302
+ console.error(`Error: ${r.error?.message}`);
1303
+ process.exit(1);
1304
+ }
1305
+ console.log(`OK: reference removed for task ${opts.id}`);
1306
+ });
1307
+
1308
+ plannerCommand
1309
+ .command('update-checklist-item')
1310
+ .description('Rename or check/uncheck a Planner checklist item')
1311
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1312
+ .requiredOption('-c, --item <checklistItemId>', 'Checklist item id')
1313
+ .option('-t, --title <text>', 'New title')
1314
+ .option('--checked', 'Mark checked')
1315
+ .option('--unchecked', 'Mark unchecked')
1316
+ .option('--order-hint <hint>', 'Order hint string')
1317
+ .option('--token <token>', 'Use a specific token')
1318
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1319
+ .action(
1320
+ async (
1321
+ opts: {
1322
+ id: string;
1323
+ item: string;
1324
+ title?: string;
1325
+ checked?: boolean;
1326
+ unchecked?: boolean;
1327
+ orderHint?: string;
1328
+ token?: string;
1329
+ identity?: string;
1330
+ },
1331
+ cmd: any
1332
+ ) => {
1333
+ checkReadOnly(cmd);
1334
+ if (opts.checked && opts.unchecked) {
1335
+ console.error('Error: use either --checked or --unchecked, not both');
1336
+ process.exit(1);
1337
+ }
1338
+ if (opts.title === undefined && !opts.checked && !opts.unchecked && opts.orderHint === undefined) {
1339
+ console.error('Error: specify --title, --checked/--unchecked, and/or --order-hint');
1340
+ process.exit(1);
1341
+ }
1342
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1343
+ if (!auth.success) {
1344
+ console.error(`Auth error: ${auth.error}`);
1345
+ process.exit(1);
1346
+ }
1347
+ const patch: { title?: string; isChecked?: boolean; orderHint?: string } = {};
1348
+ if (opts.title !== undefined) patch.title = opts.title;
1349
+ if (opts.checked) patch.isChecked = true;
1350
+ if (opts.unchecked) patch.isChecked = false;
1351
+ if (opts.orderHint !== undefined) patch.orderHint = opts.orderHint;
1352
+ const r = await updatePlannerChecklistItem(auth.token!, opts.id, opts.item, patch);
1353
+ if (!r.ok) {
1354
+ console.error(`Error: ${r.error?.message}`);
1355
+ process.exit(1);
1356
+ }
1357
+ console.log(`OK: updated checklist item ${opts.item}`);
1358
+ }
1359
+ );
1360
+
1361
+ plannerCommand
1362
+ .command('get-task-board')
1363
+ .description('Get task board ordering (assignedTo, bucket, or progress view)')
1364
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1365
+ .requiredOption('--view <name>', 'assignedTo | bucket | progress (matches Graph task board format resources)')
1366
+ .option('--json', 'Output JSON')
1367
+ .option('--token <token>', 'Use a specific token')
1368
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1369
+ .action(async (opts: { id: string; view: string; json?: boolean; token?: string; identity?: string }) => {
1370
+ const v = opts.view.trim().toLowerCase();
1371
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1372
+ if (!auth.success) {
1373
+ console.error(`Auth error: ${auth.error}`);
1374
+ process.exit(1);
1375
+ }
1376
+ const r =
1377
+ v === 'assignedto' || v === 'assigned'
1378
+ ? await getAssignedToTaskBoardFormat(auth.token!, opts.id)
1379
+ : v === 'bucket'
1380
+ ? await getBucketTaskBoardFormat(auth.token!, opts.id)
1381
+ : v === 'progress'
1382
+ ? await getProgressTaskBoardFormat(auth.token!, opts.id)
1383
+ : null;
1384
+ if (!r) {
1385
+ console.error('Error: --view must be assignedTo, bucket, or progress');
1386
+ process.exit(1);
1387
+ }
1388
+ if (!r.ok || !r.data) {
1389
+ console.error(`Error: ${r.error?.message}`);
1390
+ process.exit(1);
1391
+ }
1392
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1393
+ else console.log(JSON.stringify(r.data, null, 2));
1394
+ });
1395
+
1396
+ plannerCommand
1397
+ .command('update-task-board')
1398
+ .description('PATCH task board ordering (use --json-file for body; etag fetched automatically)')
1399
+ .requiredOption('-i, --id <taskId>', 'Task ID')
1400
+ .requiredOption('--view <name>', 'assignedTo | bucket | progress (matches Graph task board format resources)')
1401
+ .requiredOption(
1402
+ '--json-file <path>',
1403
+ 'JSON body: assignedTo = orderHintsByAssignee + unassignedOrderHint; bucket/progress = { "orderHint": "..." }'
1404
+ )
1405
+ .option('--token <token>', 'Use a specific token')
1406
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1407
+ .action(async (opts: { id: string; view: string; jsonFile: string; token?: string; identity?: string }, cmd: any) => {
1408
+ checkReadOnly(cmd);
1409
+ const v = opts.view.trim().toLowerCase();
1410
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1411
+ if (!auth.success) {
1412
+ console.error(`Auth error: ${auth.error}`);
1413
+ process.exit(1);
1414
+ }
1415
+ const raw = await readFile(opts.jsonFile, 'utf-8');
1416
+ const body = JSON.parse(raw) as Record<string, unknown>;
1417
+ if (v === 'assignedto' || v === 'assigned') {
1418
+ const gr = await getAssignedToTaskBoardFormat(auth.token!, opts.id);
1419
+ if (!gr.ok || !gr.data) {
1420
+ console.error(`Error: ${gr.error?.message}`);
1421
+ process.exit(1);
1422
+ }
1423
+ const etag = gr.data['@odata.etag'];
1424
+ if (!etag) {
1425
+ console.error('Missing ETag on assignedTo task board format');
1426
+ process.exit(1);
1427
+ }
1428
+ const patch: {
1429
+ orderHintsByAssignee?: Record<string, string> | null;
1430
+ unassignedOrderHint?: string | null;
1431
+ } = {};
1432
+ if (Object.hasOwn(body, 'orderHintsByAssignee')) {
1433
+ patch.orderHintsByAssignee = body.orderHintsByAssignee as Record<string, string> | null;
1434
+ }
1435
+ if (Object.hasOwn(body, 'unassignedOrderHint')) {
1436
+ patch.unassignedOrderHint = body.unassignedOrderHint as string | null;
1437
+ }
1438
+ if (Object.keys(patch).length === 0) {
1439
+ console.error('Error: json-file must include orderHintsByAssignee and/or unassignedOrderHint');
1440
+ process.exit(1);
1441
+ }
1442
+ const r = await updateAssignedToTaskBoardFormat(auth.token!, opts.id, etag, patch);
1443
+ if (!r.ok) {
1444
+ console.error(`Error: ${r.error?.message}`);
1445
+ process.exit(1);
1446
+ }
1447
+ } else if (v === 'bucket') {
1448
+ const gr = await getBucketTaskBoardFormat(auth.token!, opts.id);
1449
+ if (!gr.ok || !gr.data) {
1450
+ console.error(`Error: ${gr.error?.message}`);
1451
+ process.exit(1);
1452
+ }
1453
+ const etag = gr.data['@odata.etag'];
1454
+ const orderHint = typeof body.orderHint === 'string' ? body.orderHint : null;
1455
+ if (!etag || !orderHint) {
1456
+ console.error('Error: bucket view requires ETag and json-file with { "orderHint": "..." }');
1457
+ process.exit(1);
1458
+ }
1459
+ const r = await updateBucketTaskBoardFormat(auth.token!, opts.id, etag, orderHint);
1460
+ if (!r.ok) {
1461
+ console.error(`Error: ${r.error?.message}`);
1462
+ process.exit(1);
1463
+ }
1464
+ } else if (v === 'progress') {
1465
+ const gr = await getProgressTaskBoardFormat(auth.token!, opts.id);
1466
+ if (!gr.ok || !gr.data) {
1467
+ console.error(`Error: ${gr.error?.message}`);
1468
+ process.exit(1);
1469
+ }
1470
+ const etag = gr.data['@odata.etag'];
1471
+ const orderHint = typeof body.orderHint === 'string' ? body.orderHint : null;
1472
+ if (!etag || !orderHint) {
1473
+ console.error('Error: progress view requires ETag and json-file with { "orderHint": "..." }');
1474
+ process.exit(1);
1475
+ }
1476
+ const r = await updateProgressTaskBoardFormat(auth.token!, opts.id, etag, orderHint);
1477
+ if (!r.ok) {
1478
+ console.error(`Error: ${r.error?.message}`);
1479
+ process.exit(1);
1480
+ }
1481
+ } else {
1482
+ console.error('Error: --view must be assignedTo, bucket, or progress');
1483
+ process.exit(1);
1484
+ }
1485
+ console.log('OK: task board updated');
1486
+ });
1487
+
1488
+ plannerCommand
1489
+ .command('get-me')
1490
+ .description('Get current user Planner settings (beta: favorites, recents; see GRAPH_BETA_URL)')
1491
+ .option('--json', 'Output JSON')
1492
+ .option('--token <token>', 'Use a specific token')
1493
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1494
+ .action(async (opts: { json?: boolean; token?: string; identity?: string }) => {
1495
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1496
+ if (!auth.success) {
1497
+ console.error(`Auth error: ${auth.error}`);
1498
+ process.exit(1);
1499
+ }
1500
+ const r = await getPlannerUser(auth.token!);
1501
+ if (!r.ok || !r.data) {
1502
+ console.error(`Error: ${r.error?.message}`);
1503
+ process.exit(1);
1504
+ }
1505
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1506
+ else console.log(JSON.stringify(r.data, null, 2));
1507
+ });
1508
+
1509
+ plannerCommand
1510
+ .command('add-favorite')
1511
+ .description('Add a plan to your favorites (beta PATCH /me/planner)')
1512
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
1513
+ .requiredOption('-t, --title <text>', 'Plan title (shown in favorites)')
1514
+ .option('--token <token>', 'Use a specific token')
1515
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1516
+ .action(async (opts: { plan: string; title: string; token?: string; identity?: string }, cmd: any) => {
1517
+ checkReadOnly(cmd);
1518
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1519
+ if (!auth.success) {
1520
+ console.error(`Auth error: ${auth.error}`);
1521
+ process.exit(1);
1522
+ }
1523
+ const r = await addPlannerFavoritePlan(auth.token!, opts.plan, opts.title);
1524
+ if (!r.ok) {
1525
+ console.error(`Error: ${r.error?.message}`);
1526
+ process.exit(1);
1527
+ }
1528
+ console.log(`OK: favorite added for plan ${opts.plan}`);
1529
+ });
1530
+
1531
+ plannerCommand
1532
+ .command('remove-favorite')
1533
+ .description('Remove a plan from your favorites (beta)')
1534
+ .requiredOption('-p, --plan <planId>', 'Plan ID')
1535
+ .option('--token <token>', 'Use a specific token')
1536
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1537
+ .action(async (opts: { plan: string; token?: string; identity?: string }, cmd: any) => {
1538
+ checkReadOnly(cmd);
1539
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1540
+ if (!auth.success) {
1541
+ console.error(`Auth error: ${auth.error}`);
1542
+ process.exit(1);
1543
+ }
1544
+ const r = await removePlannerFavoritePlan(auth.token!, opts.plan);
1545
+ if (!r.ok) {
1546
+ console.error(`Error: ${r.error?.message}`);
1547
+ process.exit(1);
1548
+ }
1549
+ console.log(`OK: favorite removed for plan ${opts.plan}`);
1550
+ });
1551
+
1552
+ const plannerRosterCommand = new Command('roster').description(
1553
+ 'Planner roster APIs (beta; rosters are alternate plan containers — see planner create-plan --roster)'
1554
+ );
1555
+
1556
+ plannerRosterCommand
1557
+ .command('create')
1558
+ .description('Create an empty planner roster (beta POST /planner/rosters)')
1559
+ .option('--json', 'Output JSON')
1560
+ .option('--token <token>', 'Use a specific token')
1561
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1562
+ .action(async (opts: { json?: boolean; token?: string; identity?: string }, cmd: any) => {
1563
+ checkReadOnly(cmd);
1564
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1565
+ if (!auth.success) {
1566
+ console.error(`Auth error: ${auth.error}`);
1567
+ process.exit(1);
1568
+ }
1569
+ const r = await createPlannerRoster(auth.token!);
1570
+ if (!r.ok || !r.data) {
1571
+ console.error(`Error: ${r.error?.message}`);
1572
+ process.exit(1);
1573
+ }
1574
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1575
+ else console.log(`Created roster (ID: ${r.data.id})`);
1576
+ });
1577
+
1578
+ plannerRosterCommand
1579
+ .command('get')
1580
+ .description('Get a planner roster by id (beta)')
1581
+ .requiredOption('-r, --roster <rosterId>', 'Roster ID')
1582
+ .option('--json', 'Output JSON')
1583
+ .option('--token <token>', 'Use a specific token')
1584
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1585
+ .action(async (opts: { roster: string; json?: boolean; token?: string; identity?: string }) => {
1586
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1587
+ if (!auth.success) {
1588
+ console.error(`Auth error: ${auth.error}`);
1589
+ process.exit(1);
1590
+ }
1591
+ const r = await getPlannerRoster(auth.token!, opts.roster);
1592
+ if (!r.ok || !r.data) {
1593
+ console.error(`Error: ${r.error?.message}`);
1594
+ process.exit(1);
1595
+ }
1596
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1597
+ else console.log(JSON.stringify(r.data, null, 2));
1598
+ });
1599
+
1600
+ plannerRosterCommand
1601
+ .command('list-members')
1602
+ .description('List members of a planner roster (beta)')
1603
+ .requiredOption('-r, --roster <rosterId>', 'Roster ID')
1604
+ .option('--json', 'Output JSON')
1605
+ .option('--token <token>', 'Use a specific token')
1606
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1607
+ .action(async (opts: { roster: string; json?: boolean; token?: string; identity?: string }) => {
1608
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1609
+ if (!auth.success) {
1610
+ console.error(`Auth error: ${auth.error}`);
1611
+ process.exit(1);
1612
+ }
1613
+ const r = await listPlannerRosterMembers(auth.token!, opts.roster);
1614
+ if (!r.ok || !r.data) {
1615
+ console.error(`Error: ${r.error?.message}`);
1616
+ process.exit(1);
1617
+ }
1618
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1619
+ else for (const m of r.data) console.log(`- user ${m.userId} (member id: ${m.id})`);
1620
+ });
1621
+
1622
+ plannerRosterCommand
1623
+ .command('add-member')
1624
+ .description('Add a user to a planner roster (beta)')
1625
+ .requiredOption('-r, --roster <rosterId>', 'Roster ID')
1626
+ .requiredOption('-u, --user <userId>', 'Azure AD object id of the user')
1627
+ .option('--tenant <tenantId>', 'Tenant id (optional; same-tenant only per Graph)')
1628
+ .option('--json', 'Output JSON')
1629
+ .option('--token <token>', 'Use a specific token')
1630
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1631
+ .action(
1632
+ async (
1633
+ opts: { roster: string; user: string; tenant?: string; json?: boolean; token?: string; identity?: string },
1634
+ cmd: any
1635
+ ) => {
1636
+ checkReadOnly(cmd);
1637
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1638
+ if (!auth.success) {
1639
+ console.error(`Auth error: ${auth.error}`);
1640
+ process.exit(1);
1641
+ }
1642
+ const r = await addPlannerRosterMember(auth.token!, opts.roster, opts.user, {
1643
+ tenantId: opts.tenant
1644
+ });
1645
+ if (!r.ok || !r.data) {
1646
+ console.error(`Error: ${r.error?.message}`);
1647
+ process.exit(1);
1648
+ }
1649
+ if (opts.json) console.log(JSON.stringify(r.data, null, 2));
1650
+ else console.log(`Added member: ${r.data.userId} (member id: ${r.data.id})`);
1651
+ }
1652
+ );
1653
+
1654
+ plannerRosterCommand
1655
+ .command('remove-member')
1656
+ .description(
1657
+ 'Remove a member from a planner roster (beta; removing last member may delete roster/plan after retention)'
1658
+ )
1659
+ .requiredOption('-r, --roster <rosterId>', 'Roster ID')
1660
+ .requiredOption('-m, --member <memberId>', 'Roster member resource id (from list-members)')
1661
+ .option('--token <token>', 'Use a specific token')
1662
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
1663
+ .action(async (opts: { roster: string; member: string; token?: string; identity?: string }, cmd: any) => {
1664
+ checkReadOnly(cmd);
1665
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
1666
+ if (!auth.success) {
1667
+ console.error(`Auth error: ${auth.error}`);
1668
+ process.exit(1);
1669
+ }
1670
+ const r = await removePlannerRosterMember(auth.token!, opts.roster, opts.member);
1671
+ if (!r.ok) {
1672
+ console.error(`Error: ${r.error?.message}`);
1673
+ process.exit(1);
1674
+ }
1675
+ console.log(`OK: removed roster member ${opts.member}`);
1676
+ });
1677
+
1678
+ plannerCommand.addCommand(plannerRosterCommand);