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.
- package/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- 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);
|