m365-cli 0.1.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/README.md +683 -0
- package/bin/m365.js +489 -0
- package/config/default.json +18 -0
- package/package.json +36 -0
- package/src/auth/device-flow.js +154 -0
- package/src/auth/token-manager.js +237 -0
- package/src/commands/calendar.js +279 -0
- package/src/commands/mail.js +353 -0
- package/src/commands/onedrive.js +423 -0
- package/src/commands/sharepoint.js +312 -0
- package/src/graph/client.js +875 -0
- package/src/utils/config.js +60 -0
- package/src/utils/error.js +114 -0
- package/src/utils/output.js +850 -0
- package/src/utils/trusted-senders.js +190 -0
package/bin/m365.js
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { login, logout } from '../src/auth/token-manager.js';
|
|
8
|
+
import mailCommands from '../src/commands/mail.js';
|
|
9
|
+
import calendarCommands from '../src/commands/calendar.js';
|
|
10
|
+
import onedriveCommands from '../src/commands/onedrive.js';
|
|
11
|
+
import sharepointCommands from '../src/commands/sharepoint.js';
|
|
12
|
+
import { handleError } from '../src/utils/error.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Load package.json for version
|
|
18
|
+
const packageJson = JSON.parse(
|
|
19
|
+
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('m365')
|
|
26
|
+
.description('Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive')
|
|
27
|
+
.version(packageJson.version);
|
|
28
|
+
|
|
29
|
+
// Login command
|
|
30
|
+
program
|
|
31
|
+
.command('login')
|
|
32
|
+
.description('Authenticate with Microsoft 365')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await login();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
handleError(error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Logout command
|
|
42
|
+
program
|
|
43
|
+
.command('logout')
|
|
44
|
+
.description('Clear stored credentials')
|
|
45
|
+
.action(async () => {
|
|
46
|
+
try {
|
|
47
|
+
await logout();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
handleError(error);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Mail commands
|
|
54
|
+
const mailCommand = program
|
|
55
|
+
.command('mail')
|
|
56
|
+
.description('Manage emails');
|
|
57
|
+
|
|
58
|
+
mailCommand
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List emails')
|
|
61
|
+
.option('-t, --top <number>', 'Number of emails to list', '10')
|
|
62
|
+
.option('-f, --folder <name>', 'Folder name (inbox, sent, drafts)', 'inbox')
|
|
63
|
+
.option('--json', 'Output as JSON')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
await mailCommands.list({
|
|
66
|
+
top: parseInt(options.top),
|
|
67
|
+
folder: options.folder,
|
|
68
|
+
json: options.json,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
mailCommand
|
|
73
|
+
.command('read')
|
|
74
|
+
.description('Read email by ID')
|
|
75
|
+
.argument('<id>', 'Email ID')
|
|
76
|
+
.option('--force', 'Skip whitelist check and show full content')
|
|
77
|
+
.option('--json', 'Output as JSON')
|
|
78
|
+
.action(async (id, options) => {
|
|
79
|
+
await mailCommands.read(id, {
|
|
80
|
+
force: options.force,
|
|
81
|
+
json: options.json,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
mailCommand
|
|
86
|
+
.command('send')
|
|
87
|
+
.description('Send an email')
|
|
88
|
+
.argument('<to>', 'Recipient email address(es) (comma-separated)')
|
|
89
|
+
.argument('<subject>', 'Email subject')
|
|
90
|
+
.argument('<body>', 'Email body (HTML supported)')
|
|
91
|
+
.option('-a, --attach <files...>', 'Attach files')
|
|
92
|
+
.option('--cc <emails>', 'CC recipients (comma-separated)')
|
|
93
|
+
.option('--bcc <emails>', 'BCC recipients (comma-separated)')
|
|
94
|
+
.option('--json', 'Output as JSON')
|
|
95
|
+
.action(async (to, subject, body, options) => {
|
|
96
|
+
await mailCommands.send(to, subject, body, {
|
|
97
|
+
attach: options.attach || [],
|
|
98
|
+
cc: options.cc,
|
|
99
|
+
bcc: options.bcc,
|
|
100
|
+
json: options.json,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
mailCommand
|
|
105
|
+
.command('search')
|
|
106
|
+
.description('Search emails')
|
|
107
|
+
.argument('<query>', 'Search query')
|
|
108
|
+
.option('-t, --top <number>', 'Number of results', '10')
|
|
109
|
+
.option('--json', 'Output as JSON')
|
|
110
|
+
.action(async (query, options) => {
|
|
111
|
+
await mailCommands.search(query, {
|
|
112
|
+
top: parseInt(options.top),
|
|
113
|
+
json: options.json,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
mailCommand
|
|
118
|
+
.command('attachments')
|
|
119
|
+
.description('List email attachments')
|
|
120
|
+
.argument('<id>', 'Email ID')
|
|
121
|
+
.option('--json', 'Output as JSON')
|
|
122
|
+
.action(async (id, options) => {
|
|
123
|
+
await mailCommands.attachments(id, {
|
|
124
|
+
json: options.json,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
mailCommand
|
|
129
|
+
.command('download-attachment')
|
|
130
|
+
.description('Download email attachment')
|
|
131
|
+
.argument('<message-id>', 'Email ID')
|
|
132
|
+
.argument('<attachment-id>', 'Attachment ID')
|
|
133
|
+
.argument('[local-path]', 'Local file path (default: attachment name)')
|
|
134
|
+
.option('--json', 'Output as JSON')
|
|
135
|
+
.action(async (messageId, attachmentId, localPath, options) => {
|
|
136
|
+
await mailCommands.downloadAttachment(messageId, attachmentId, localPath, {
|
|
137
|
+
json: options.json,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
mailCommand
|
|
142
|
+
.command('trust')
|
|
143
|
+
.description('Add email or domain to whitelist')
|
|
144
|
+
.argument('<email>', 'Email address or domain (e.g., user@example.com or @example.com)')
|
|
145
|
+
.option('--json', 'Output as JSON')
|
|
146
|
+
.action(async (email, options) => {
|
|
147
|
+
await mailCommands.trust(email, {
|
|
148
|
+
json: options.json,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
mailCommand
|
|
153
|
+
.command('untrust')
|
|
154
|
+
.description('Remove email or domain from whitelist')
|
|
155
|
+
.argument('<email>', 'Email address or domain to remove')
|
|
156
|
+
.option('--json', 'Output as JSON')
|
|
157
|
+
.action(async (email, options) => {
|
|
158
|
+
await mailCommands.untrust(email, {
|
|
159
|
+
json: options.json,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
mailCommand
|
|
164
|
+
.command('trusted')
|
|
165
|
+
.description('List trusted senders whitelist')
|
|
166
|
+
.option('--json', 'Output as JSON')
|
|
167
|
+
.action(async (options) => {
|
|
168
|
+
await mailCommands.trusted({
|
|
169
|
+
json: options.json,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Calendar commands
|
|
174
|
+
const calendarCommand = program
|
|
175
|
+
.command('calendar')
|
|
176
|
+
.alias('cal')
|
|
177
|
+
.description('Manage calendar events');
|
|
178
|
+
|
|
179
|
+
calendarCommand
|
|
180
|
+
.command('list')
|
|
181
|
+
.description('List calendar events')
|
|
182
|
+
.option('-d, --days <number>', 'Number of days to look ahead', '7')
|
|
183
|
+
.option('-t, --top <number>', 'Maximum number of events', '50')
|
|
184
|
+
.option('--json', 'Output as JSON')
|
|
185
|
+
.action(async (options) => {
|
|
186
|
+
await calendarCommands.list({
|
|
187
|
+
days: parseInt(options.days),
|
|
188
|
+
top: parseInt(options.top),
|
|
189
|
+
json: options.json,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
calendarCommand
|
|
194
|
+
.command('get')
|
|
195
|
+
.description('Get calendar event by ID')
|
|
196
|
+
.argument('<id>', 'Event ID')
|
|
197
|
+
.option('--json', 'Output as JSON')
|
|
198
|
+
.action(async (id, options) => {
|
|
199
|
+
await calendarCommands.get(id, {
|
|
200
|
+
json: options.json,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
calendarCommand
|
|
205
|
+
.command('create')
|
|
206
|
+
.description('Create calendar event')
|
|
207
|
+
.argument('<title>', 'Event title')
|
|
208
|
+
.requiredOption('-s, --start <datetime>', 'Start date/time (YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD)')
|
|
209
|
+
.requiredOption('-e, --end <datetime>', 'End date/time (YYYY-MM-DDTHH:MM:SS or YYYY-MM-DD)')
|
|
210
|
+
.option('-l, --location <location>', 'Event location')
|
|
211
|
+
.option('-b, --body <body>', 'Event description')
|
|
212
|
+
.option('-a, --attendees <emails>', 'Attendee emails (comma-separated)', (val) => val.split(','))
|
|
213
|
+
.option('--allday', 'All-day event')
|
|
214
|
+
.option('--json', 'Output as JSON')
|
|
215
|
+
.action(async (title, options) => {
|
|
216
|
+
await calendarCommands.create(title, {
|
|
217
|
+
start: options.start,
|
|
218
|
+
end: options.end,
|
|
219
|
+
location: options.location,
|
|
220
|
+
body: options.body,
|
|
221
|
+
attendees: options.attendees || [],
|
|
222
|
+
allday: options.allday || false,
|
|
223
|
+
json: options.json,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
calendarCommand
|
|
228
|
+
.command('update')
|
|
229
|
+
.description('Update calendar event')
|
|
230
|
+
.argument('<id>', 'Event ID')
|
|
231
|
+
.option('-t, --title <title>', 'Event title')
|
|
232
|
+
.option('-s, --start <datetime>', 'Start date/time (YYYY-MM-DDTHH:MM:SS)')
|
|
233
|
+
.option('-e, --end <datetime>', 'End date/time (YYYY-MM-DDTHH:MM:SS)')
|
|
234
|
+
.option('-l, --location <location>', 'Event location')
|
|
235
|
+
.option('-b, --body <body>', 'Event description')
|
|
236
|
+
.option('--json', 'Output as JSON')
|
|
237
|
+
.action(async (id, options) => {
|
|
238
|
+
await calendarCommands.update(id, {
|
|
239
|
+
title: options.title,
|
|
240
|
+
start: options.start,
|
|
241
|
+
end: options.end,
|
|
242
|
+
location: options.location,
|
|
243
|
+
body: options.body,
|
|
244
|
+
json: options.json,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
calendarCommand
|
|
249
|
+
.command('delete')
|
|
250
|
+
.description('Delete calendar event')
|
|
251
|
+
.argument('<id>', 'Event ID')
|
|
252
|
+
.option('--json', 'Output as JSON')
|
|
253
|
+
.action(async (id, options) => {
|
|
254
|
+
await calendarCommands.delete(id, {
|
|
255
|
+
json: options.json,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// OneDrive commands
|
|
260
|
+
const onedriveCommand = program
|
|
261
|
+
.command('onedrive')
|
|
262
|
+
.alias('od')
|
|
263
|
+
.description('Manage OneDrive files and folders');
|
|
264
|
+
|
|
265
|
+
onedriveCommand
|
|
266
|
+
.command('ls')
|
|
267
|
+
.description('List files and folders')
|
|
268
|
+
.argument('[path]', 'Path to list (default: root)', '')
|
|
269
|
+
.option('-t, --top <number>', 'Maximum number of items', '100')
|
|
270
|
+
.option('--json', 'Output as JSON')
|
|
271
|
+
.action(async (path, options) => {
|
|
272
|
+
await onedriveCommands.ls(path, {
|
|
273
|
+
top: parseInt(options.top),
|
|
274
|
+
json: options.json,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
onedriveCommand
|
|
279
|
+
.command('get')
|
|
280
|
+
.description('Get file/folder metadata')
|
|
281
|
+
.argument('<path>', 'Path to file or folder')
|
|
282
|
+
.option('--json', 'Output as JSON')
|
|
283
|
+
.action(async (path, options) => {
|
|
284
|
+
await onedriveCommands.get(path, {
|
|
285
|
+
json: options.json,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
onedriveCommand
|
|
290
|
+
.command('download')
|
|
291
|
+
.description('Download file from OneDrive')
|
|
292
|
+
.argument('<remote-path>', 'Remote file path')
|
|
293
|
+
.argument('[local-path]', 'Local destination path (default: current directory)')
|
|
294
|
+
.option('--json', 'Output as JSON')
|
|
295
|
+
.action(async (remotePath, localPath, options) => {
|
|
296
|
+
await onedriveCommands.download(remotePath, localPath, {
|
|
297
|
+
json: options.json,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
onedriveCommand
|
|
302
|
+
.command('upload')
|
|
303
|
+
.description('Upload file to OneDrive')
|
|
304
|
+
.argument('<local-path>', 'Local file path')
|
|
305
|
+
.argument('[remote-path]', 'Remote destination path (default: root with same name)')
|
|
306
|
+
.option('--json', 'Output as JSON')
|
|
307
|
+
.action(async (localPath, remotePath, options) => {
|
|
308
|
+
await onedriveCommands.upload(localPath, remotePath, {
|
|
309
|
+
json: options.json,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
onedriveCommand
|
|
314
|
+
.command('search')
|
|
315
|
+
.description('Search files in OneDrive')
|
|
316
|
+
.argument('<query>', 'Search query')
|
|
317
|
+
.option('-t, --top <number>', 'Maximum number of results', '50')
|
|
318
|
+
.option('--json', 'Output as JSON')
|
|
319
|
+
.action(async (query, options) => {
|
|
320
|
+
await onedriveCommands.search(query, {
|
|
321
|
+
top: parseInt(options.top),
|
|
322
|
+
json: options.json,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
onedriveCommand
|
|
327
|
+
.command('share')
|
|
328
|
+
.description('Create sharing link')
|
|
329
|
+
.argument('<path>', 'Path to file or folder')
|
|
330
|
+
.option('--type <type>', 'Link type: view or edit', 'view')
|
|
331
|
+
.option('--scope <scope>', 'Share scope: organization, anonymous, or users', 'organization')
|
|
332
|
+
.option('--json', 'Output as JSON')
|
|
333
|
+
.action(async (path, options) => {
|
|
334
|
+
await onedriveCommands.share(path, {
|
|
335
|
+
type: options.type,
|
|
336
|
+
scope: options.scope,
|
|
337
|
+
json: options.json,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
onedriveCommand
|
|
342
|
+
.command('invite')
|
|
343
|
+
.description('Invite users to access file (external sharing)')
|
|
344
|
+
.argument('<path>', 'Path to file')
|
|
345
|
+
.argument('<email>', 'Email address(es), comma-separated')
|
|
346
|
+
.option('--role <role>', 'Permission: read or write', 'read')
|
|
347
|
+
.option('--message <msg>', 'Invitation message')
|
|
348
|
+
.option('--no-notify', 'Do not send email notification')
|
|
349
|
+
.option('--json', 'Output as JSON')
|
|
350
|
+
.action(async (path, email, options) => {
|
|
351
|
+
await onedriveCommands.invite(path, email, {
|
|
352
|
+
role: options.role,
|
|
353
|
+
message: options.message,
|
|
354
|
+
notify: options.notify,
|
|
355
|
+
json: options.json,
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
onedriveCommand
|
|
360
|
+
.command('mkdir')
|
|
361
|
+
.description('Create folder')
|
|
362
|
+
.argument('<path>', 'Folder path')
|
|
363
|
+
.option('--json', 'Output as JSON')
|
|
364
|
+
.action(async (path, options) => {
|
|
365
|
+
await onedriveCommands.mkdir(path, {
|
|
366
|
+
json: options.json,
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
onedriveCommand
|
|
371
|
+
.command('rm')
|
|
372
|
+
.description('Delete file or folder')
|
|
373
|
+
.argument('<path>', 'Path to file or folder')
|
|
374
|
+
.option('--force', 'Skip confirmation')
|
|
375
|
+
.option('--json', 'Output as JSON')
|
|
376
|
+
.action(async (path, options) => {
|
|
377
|
+
await onedriveCommands.rm(path, {
|
|
378
|
+
force: options.force,
|
|
379
|
+
json: options.json,
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// SharePoint commands
|
|
384
|
+
const sharepointCommand = program
|
|
385
|
+
.command('sharepoint')
|
|
386
|
+
.alias('sp')
|
|
387
|
+
.description('Manage SharePoint sites and content');
|
|
388
|
+
|
|
389
|
+
sharepointCommand
|
|
390
|
+
.command('sites')
|
|
391
|
+
.description('List accessible SharePoint sites')
|
|
392
|
+
.option('--search <query>', 'Search for sites')
|
|
393
|
+
.option('-t, --top <number>', 'Maximum number of sites', '50')
|
|
394
|
+
.option('--json', 'Output as JSON')
|
|
395
|
+
.action(async (options) => {
|
|
396
|
+
await sharepointCommands.sites({
|
|
397
|
+
search: options.search,
|
|
398
|
+
top: parseInt(options.top),
|
|
399
|
+
json: options.json,
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
sharepointCommand
|
|
404
|
+
.command('lists')
|
|
405
|
+
.description('List site lists and document libraries')
|
|
406
|
+
.argument('<site>', 'Site URL (hostname:/path) or site ID')
|
|
407
|
+
.option('-t, --top <number>', 'Maximum number of lists', '100')
|
|
408
|
+
.option('--json', 'Output as JSON')
|
|
409
|
+
.action(async (site, options) => {
|
|
410
|
+
await sharepointCommands.lists(site, {
|
|
411
|
+
top: parseInt(options.top),
|
|
412
|
+
json: options.json,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
sharepointCommand
|
|
417
|
+
.command('items')
|
|
418
|
+
.description('List items in a SharePoint list')
|
|
419
|
+
.argument('<site>', 'Site URL (hostname:/path) or site ID')
|
|
420
|
+
.argument('<list>', 'List ID')
|
|
421
|
+
.option('-t, --top <number>', 'Maximum number of items', '100')
|
|
422
|
+
.option('--json', 'Output as JSON')
|
|
423
|
+
.action(async (site, list, options) => {
|
|
424
|
+
await sharepointCommands.items(site, list, {
|
|
425
|
+
top: parseInt(options.top),
|
|
426
|
+
json: options.json,
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
sharepointCommand
|
|
431
|
+
.command('files')
|
|
432
|
+
.description('List files in site document library')
|
|
433
|
+
.argument('<site>', 'Site URL (hostname:/path) or site ID')
|
|
434
|
+
.argument('[path]', 'Path in document library (default: root)', '')
|
|
435
|
+
.option('-t, --top <number>', 'Maximum number of files', '100')
|
|
436
|
+
.option('--json', 'Output as JSON')
|
|
437
|
+
.action(async (site, path, options) => {
|
|
438
|
+
await sharepointCommands.files(site, path, {
|
|
439
|
+
top: parseInt(options.top),
|
|
440
|
+
json: options.json,
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
sharepointCommand
|
|
445
|
+
.command('download')
|
|
446
|
+
.description('Download file from SharePoint')
|
|
447
|
+
.argument('<site>', 'Site URL (hostname:/path) or site ID')
|
|
448
|
+
.argument('<file-path>', 'Remote file path')
|
|
449
|
+
.argument('[local-path]', 'Local destination path (default: current directory)')
|
|
450
|
+
.option('--json', 'Output as JSON')
|
|
451
|
+
.action(async (site, filePath, localPath, options) => {
|
|
452
|
+
await sharepointCommands.download(site, filePath, localPath, {
|
|
453
|
+
json: options.json,
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
sharepointCommand
|
|
458
|
+
.command('upload')
|
|
459
|
+
.description('Upload file to SharePoint')
|
|
460
|
+
.argument('<site>', 'Site URL (hostname:/path) or site ID')
|
|
461
|
+
.argument('<local-path>', 'Local file path')
|
|
462
|
+
.argument('[remote-path]', 'Remote destination path (default: root with same name)')
|
|
463
|
+
.option('--json', 'Output as JSON')
|
|
464
|
+
.action(async (site, localPath, remotePath, options) => {
|
|
465
|
+
await sharepointCommands.upload(site, localPath, remotePath, {
|
|
466
|
+
json: options.json,
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
sharepointCommand
|
|
471
|
+
.command('search')
|
|
472
|
+
.description('Search SharePoint content')
|
|
473
|
+
.argument('<query>', 'Search query')
|
|
474
|
+
.option('-t, --top <number>', 'Maximum number of results', '50')
|
|
475
|
+
.option('--json', 'Output as JSON')
|
|
476
|
+
.action(async (query, options) => {
|
|
477
|
+
await sharepointCommands.search(query, {
|
|
478
|
+
top: parseInt(options.top),
|
|
479
|
+
json: options.json,
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Parse arguments
|
|
484
|
+
program.parse(process.argv);
|
|
485
|
+
|
|
486
|
+
// Show help if no arguments
|
|
487
|
+
if (!process.argv.slice(2).length) {
|
|
488
|
+
program.outputHelp();
|
|
489
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tenantId": "5b4c4b46-4279-4f19-9e5d-84ea285f9b9c",
|
|
3
|
+
"clientId": "091b3d7b-e217-4410-868c-01c3ee6189b6",
|
|
4
|
+
"scopes": [
|
|
5
|
+
"https://graph.microsoft.com/Mail.ReadWrite",
|
|
6
|
+
"https://graph.microsoft.com/Mail.Send",
|
|
7
|
+
"https://graph.microsoft.com/Calendars.ReadWrite",
|
|
8
|
+
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
9
|
+
"https://graph.microsoft.com/Sites.ReadWrite.All",
|
|
10
|
+
"offline_access"
|
|
11
|
+
],
|
|
12
|
+
"graphApiUrl": "https://graph.microsoft.com/v1.0",
|
|
13
|
+
"authUrl": "https://login.microsoftonline.com",
|
|
14
|
+
"credsPath": "~/.m365-cli/credentials.json",
|
|
15
|
+
"deviceCodePollInterval": 5,
|
|
16
|
+
"deviceCodeTimeout": 900,
|
|
17
|
+
"tokenRefreshBuffer": 60
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "m365-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Microsoft 365 CLI - Manage Mail, Calendar, and OneDrive from the command line",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": ["bin", "src", "config", "README.md"],
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"m365": "./bin/m365.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
|
+
"link": "npm link",
|
|
14
|
+
"unlink": "npm unlink -g m365-cli"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"microsoft365",
|
|
18
|
+
"office365",
|
|
19
|
+
"graph-api",
|
|
20
|
+
"cli",
|
|
21
|
+
"email",
|
|
22
|
+
"calendar",
|
|
23
|
+
"onedrive"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"repository": { "type": "git", "url": "" },
|
|
27
|
+
"bugs": { "url": "" },
|
|
28
|
+
"homepage": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import config from '../utils/config.js';
|
|
2
|
+
import { AuthError } from '../utils/error.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Device Code Flow authentication
|
|
6
|
+
* https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Request device code from Microsoft
|
|
11
|
+
*/
|
|
12
|
+
export async function requestDeviceCode() {
|
|
13
|
+
const tenantId = config.get('tenantId');
|
|
14
|
+
const clientId = config.get('clientId');
|
|
15
|
+
const scopes = config.get('scopes').join(' ');
|
|
16
|
+
const authUrl = config.get('authUrl');
|
|
17
|
+
|
|
18
|
+
const url = `${authUrl}/${tenantId}/oauth2/v2.0/devicecode`;
|
|
19
|
+
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
24
|
+
},
|
|
25
|
+
body: new URLSearchParams({
|
|
26
|
+
client_id: clientId,
|
|
27
|
+
scope: scopes,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new AuthError('Failed to request device code', await response.json());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
deviceCode: data.device_code,
|
|
39
|
+
userCode: data.user_code,
|
|
40
|
+
verificationUri: data.verification_uri,
|
|
41
|
+
expiresIn: data.expires_in || 900,
|
|
42
|
+
interval: data.interval || 5,
|
|
43
|
+
message: data.message,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Poll for access token
|
|
49
|
+
*/
|
|
50
|
+
export async function pollForToken(deviceCode) {
|
|
51
|
+
const tenantId = config.get('tenantId');
|
|
52
|
+
const clientId = config.get('clientId');
|
|
53
|
+
const authUrl = config.get('authUrl');
|
|
54
|
+
|
|
55
|
+
const url = `${authUrl}/${tenantId}/oauth2/v2.0/token`;
|
|
56
|
+
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
},
|
|
62
|
+
body: new URLSearchParams({
|
|
63
|
+
client_id: clientId,
|
|
64
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
65
|
+
device_code: deviceCode,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
|
|
71
|
+
// Check for errors
|
|
72
|
+
if (data.error) {
|
|
73
|
+
if (data.error === 'authorization_pending') {
|
|
74
|
+
return { pending: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (data.error === 'slow_down') {
|
|
78
|
+
return { slowDown: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new AuthError(
|
|
82
|
+
data.error_description || data.error,
|
|
83
|
+
{ error: data.error }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Success - return token data
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
accessToken: data.access_token,
|
|
91
|
+
refreshToken: data.refresh_token,
|
|
92
|
+
expiresIn: data.expires_in || 3600,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Full device code flow
|
|
98
|
+
*/
|
|
99
|
+
export async function deviceCodeFlow() {
|
|
100
|
+
// Step 1: Request device code
|
|
101
|
+
console.log('š Starting authentication...\n');
|
|
102
|
+
const deviceCodeData = await requestDeviceCode();
|
|
103
|
+
|
|
104
|
+
// Step 2: Show user instructions
|
|
105
|
+
console.log('ā'.repeat(60));
|
|
106
|
+
console.log('š± Please authenticate:');
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(` 1. Open: ${deviceCodeData.verificationUri}`);
|
|
109
|
+
console.log(` 2. Enter code: ${deviceCodeData.userCode}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('ā'.repeat(60));
|
|
112
|
+
console.log('\nā³ Waiting for authentication...\n');
|
|
113
|
+
|
|
114
|
+
// Step 3: Poll for token
|
|
115
|
+
const startTime = Date.now();
|
|
116
|
+
const expiresAt = startTime + deviceCodeData.expiresIn * 1000;
|
|
117
|
+
let interval = deviceCodeData.interval * 1000;
|
|
118
|
+
|
|
119
|
+
while (Date.now() < expiresAt) {
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await pollForToken(deviceCodeData.deviceCode);
|
|
124
|
+
|
|
125
|
+
if (result.success) {
|
|
126
|
+
return {
|
|
127
|
+
accessToken: result.accessToken,
|
|
128
|
+
refreshToken: result.refreshToken,
|
|
129
|
+
expiresIn: result.expiresIn,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.slowDown) {
|
|
134
|
+
// Increase polling interval
|
|
135
|
+
interval += 1000;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Keep waiting if pending
|
|
139
|
+
if (result.pending) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new AuthError('Authentication timed out. Please try again.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default {
|
|
151
|
+
requestDeviceCode,
|
|
152
|
+
pollForToken,
|
|
153
|
+
deviceCodeFlow,
|
|
154
|
+
};
|