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
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import graphClient from '../graph/client.js';
|
|
2
|
+
import {
|
|
3
|
+
outputOneDriveList,
|
|
4
|
+
outputOneDriveDetail,
|
|
5
|
+
outputOneDriveResult,
|
|
6
|
+
outputOneDriveSearchResults,
|
|
7
|
+
outputOneDriveShareResult,
|
|
8
|
+
outputOneDriveInviteResult,
|
|
9
|
+
} from '../utils/output.js';
|
|
10
|
+
import { handleError } from '../utils/error.js';
|
|
11
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
12
|
+
import { basename } from 'path';
|
|
13
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
14
|
+
import { stat } from 'fs/promises';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* OneDrive commands
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List files and folders
|
|
22
|
+
*/
|
|
23
|
+
export async function listFiles(path = '', options = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const { top = 100, json = false } = options;
|
|
26
|
+
|
|
27
|
+
const items = await graphClient.onedrive.list(path, { top });
|
|
28
|
+
|
|
29
|
+
outputOneDriveList(items, { json, path });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error, { json: options.json });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get file/folder metadata
|
|
37
|
+
*/
|
|
38
|
+
export async function getMetadata(path, options = {}) {
|
|
39
|
+
try {
|
|
40
|
+
const { json = false } = options;
|
|
41
|
+
|
|
42
|
+
if (!path) {
|
|
43
|
+
throw new Error('Path is required');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const item = await graphClient.onedrive.getMetadata(path);
|
|
47
|
+
|
|
48
|
+
outputOneDriveDetail(item, { json });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
handleError(error, { json: options.json });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Download file
|
|
56
|
+
*/
|
|
57
|
+
export async function downloadFile(remotePath, localPath, options = {}) {
|
|
58
|
+
try {
|
|
59
|
+
const { json = false } = options;
|
|
60
|
+
|
|
61
|
+
if (!remotePath) {
|
|
62
|
+
throw new Error('Remote path is required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Get metadata first to check if it's a file and get the name
|
|
66
|
+
const metadata = await graphClient.onedrive.getMetadata(remotePath);
|
|
67
|
+
|
|
68
|
+
if (metadata.folder) {
|
|
69
|
+
throw new Error('Cannot download folders. Please specify a file.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine local path
|
|
73
|
+
const targetPath = localPath || metadata.name;
|
|
74
|
+
|
|
75
|
+
if (!json) {
|
|
76
|
+
console.log(`⬇️ Downloading: ${metadata.name}`);
|
|
77
|
+
console.log(` Size: ${formatFileSize(metadata.size)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Download file
|
|
81
|
+
const response = await graphClient.onedrive.download(remotePath);
|
|
82
|
+
|
|
83
|
+
// Write to file
|
|
84
|
+
const fileStream = createWriteStream(targetPath);
|
|
85
|
+
const reader = response.body.getReader();
|
|
86
|
+
|
|
87
|
+
let downloadedBytes = 0;
|
|
88
|
+
const totalBytes = metadata.size;
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done) break;
|
|
93
|
+
|
|
94
|
+
fileStream.write(Buffer.from(value));
|
|
95
|
+
downloadedBytes += value.length;
|
|
96
|
+
|
|
97
|
+
// Show progress (only in non-json mode)
|
|
98
|
+
if (!json && totalBytes > 0) {
|
|
99
|
+
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
100
|
+
process.stdout.write(`\r Progress: ${percent}% (${formatFileSize(downloadedBytes)} / ${formatFileSize(totalBytes)})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fileStream.end();
|
|
105
|
+
|
|
106
|
+
if (!json && totalBytes > 0) {
|
|
107
|
+
console.log(''); // New line after progress
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result = {
|
|
111
|
+
status: 'downloaded',
|
|
112
|
+
name: metadata.name,
|
|
113
|
+
path: targetPath,
|
|
114
|
+
size: metadata.size,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
outputOneDriveResult(result, { json });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
handleError(error, { json: options.json });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Upload file
|
|
125
|
+
*/
|
|
126
|
+
export async function uploadFile(localPath, remotePath, options = {}) {
|
|
127
|
+
try {
|
|
128
|
+
const { json = false } = options;
|
|
129
|
+
|
|
130
|
+
if (!localPath) {
|
|
131
|
+
throw new Error('Local path is required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get file stats
|
|
135
|
+
const stats = await stat(localPath);
|
|
136
|
+
if (!stats.isFile()) {
|
|
137
|
+
throw new Error('Local path must be a file');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileName = basename(localPath);
|
|
141
|
+
const targetPath = remotePath || fileName;
|
|
142
|
+
|
|
143
|
+
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
144
|
+
|
|
145
|
+
if (!json) {
|
|
146
|
+
console.log(`⬆️ Uploading: ${fileName}`);
|
|
147
|
+
console.log(` Size: ${formatFileSize(stats.size)}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Small file upload (< 4MB)
|
|
151
|
+
if (fileSizeInMB < 4) {
|
|
152
|
+
const content = await readFile(localPath);
|
|
153
|
+
const result = await graphClient.onedrive.upload(targetPath, content);
|
|
154
|
+
|
|
155
|
+
outputOneDriveResult({
|
|
156
|
+
status: 'uploaded',
|
|
157
|
+
name: result.name,
|
|
158
|
+
path: targetPath,
|
|
159
|
+
size: result.size,
|
|
160
|
+
webUrl: result.webUrl,
|
|
161
|
+
}, { json });
|
|
162
|
+
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Large file upload with session
|
|
167
|
+
if (!json) {
|
|
168
|
+
console.log(' Using upload session for large file...');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const session = await graphClient.onedrive.createUploadSession(targetPath);
|
|
172
|
+
const uploadUrl = session.uploadUrl;
|
|
173
|
+
|
|
174
|
+
// Read file and upload in chunks
|
|
175
|
+
const chunkSize = 320 * 1024 * 10; // 3.2MB chunks
|
|
176
|
+
const fileContent = await readFile(localPath);
|
|
177
|
+
const totalSize = fileContent.length;
|
|
178
|
+
|
|
179
|
+
let start = 0;
|
|
180
|
+
let uploadedBytes = 0;
|
|
181
|
+
|
|
182
|
+
while (start < totalSize) {
|
|
183
|
+
const end = Math.min(start + chunkSize, totalSize);
|
|
184
|
+
const chunk = fileContent.slice(start, end);
|
|
185
|
+
|
|
186
|
+
const result = await graphClient.onedrive.uploadChunk(
|
|
187
|
+
uploadUrl,
|
|
188
|
+
chunk,
|
|
189
|
+
start,
|
|
190
|
+
end,
|
|
191
|
+
totalSize
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
uploadedBytes = end;
|
|
195
|
+
|
|
196
|
+
// Show progress
|
|
197
|
+
if (!json) {
|
|
198
|
+
const percent = ((uploadedBytes / totalSize) * 100).toFixed(1);
|
|
199
|
+
process.stdout.write(`\r Progress: ${percent}% (${formatFileSize(uploadedBytes)} / ${formatFileSize(totalSize)})`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
start = end;
|
|
203
|
+
|
|
204
|
+
// Check if upload is complete
|
|
205
|
+
if (result.id) {
|
|
206
|
+
if (!json) {
|
|
207
|
+
console.log(''); // New line after progress
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
outputOneDriveResult({
|
|
211
|
+
status: 'uploaded',
|
|
212
|
+
name: result.name,
|
|
213
|
+
path: targetPath,
|
|
214
|
+
size: result.size,
|
|
215
|
+
webUrl: result.webUrl,
|
|
216
|
+
}, { json });
|
|
217
|
+
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
handleError(error, { json: options.json });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Search files
|
|
228
|
+
*/
|
|
229
|
+
export async function searchFiles(query, options = {}) {
|
|
230
|
+
try {
|
|
231
|
+
const { top = 50, json = false } = options;
|
|
232
|
+
|
|
233
|
+
if (!query) {
|
|
234
|
+
throw new Error('Search query is required');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const results = await graphClient.onedrive.search(query, { top });
|
|
238
|
+
|
|
239
|
+
outputOneDriveSearchResults(results, { json, query });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
handleError(error, { json: options.json });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create sharing link
|
|
247
|
+
*/
|
|
248
|
+
export async function shareFile(path, options = {}) {
|
|
249
|
+
try {
|
|
250
|
+
const { type = 'view', scope = 'organization', json = false } = options;
|
|
251
|
+
|
|
252
|
+
if (!path) {
|
|
253
|
+
throw new Error('Path is required');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate type
|
|
257
|
+
if (!['view', 'edit'].includes(type)) {
|
|
258
|
+
throw new Error('Type must be "view" or "edit"');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Validate scope
|
|
262
|
+
if (!['organization', 'anonymous', 'users'].includes(scope)) {
|
|
263
|
+
throw new Error('Scope must be "organization", "anonymous", or "users"');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await graphClient.onedrive.share(path, { type, scope });
|
|
267
|
+
|
|
268
|
+
outputOneDriveShareResult(result, { json, path, type });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
handleError(error, { json: options.json });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Invite users to access file (external sharing)
|
|
276
|
+
*/
|
|
277
|
+
export async function inviteFile(path, email, options = {}) {
|
|
278
|
+
try {
|
|
279
|
+
const { role = 'read', message = '', notify = true, json = false } = options;
|
|
280
|
+
|
|
281
|
+
if (!path) {
|
|
282
|
+
throw new Error('文件路径不能为空');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!email) {
|
|
286
|
+
throw new Error('邮箱地址不能为空');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate role
|
|
290
|
+
if (!['read', 'write'].includes(role)) {
|
|
291
|
+
throw new Error('权限类型必须是 "read"(查看)或 "write"(编辑)');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Parse email (support multiple emails separated by comma)
|
|
295
|
+
const recipients = email.split(',').map(e => e.trim()).filter(e => e);
|
|
296
|
+
|
|
297
|
+
if (recipients.length === 0) {
|
|
298
|
+
throw new Error('至少需要提供一个有效的邮箱地址');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!json) {
|
|
302
|
+
console.log(`📤 正在创建分享邀请...`);
|
|
303
|
+
console.log(` 文件: ${path}`);
|
|
304
|
+
console.log(` 受邀人: ${recipients.join(', ')}`);
|
|
305
|
+
console.log(` 权限: ${role === 'write' ? '编辑' : '查看'}`);
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const result = await graphClient.onedrive.invite(path, {
|
|
310
|
+
recipients,
|
|
311
|
+
role,
|
|
312
|
+
message,
|
|
313
|
+
sendInvitation: notify,
|
|
314
|
+
requireSignIn: false, // Allow external users with one-time code
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
outputOneDriveInviteResult(result, {
|
|
318
|
+
json,
|
|
319
|
+
path,
|
|
320
|
+
recipients,
|
|
321
|
+
role,
|
|
322
|
+
sendInvitation: notify
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
handleError(error, { json: options.json });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create folder
|
|
331
|
+
*/
|
|
332
|
+
export async function createFolder(path, options = {}) {
|
|
333
|
+
try {
|
|
334
|
+
const { json = false } = options;
|
|
335
|
+
|
|
336
|
+
if (!path) {
|
|
337
|
+
throw new Error('Folder path is required');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = await graphClient.onedrive.mkdir(path);
|
|
341
|
+
|
|
342
|
+
outputOneDriveResult({
|
|
343
|
+
status: 'created',
|
|
344
|
+
name: result.name,
|
|
345
|
+
path: path,
|
|
346
|
+
type: 'folder',
|
|
347
|
+
}, { json });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
handleError(error, { json: options.json });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Delete file or folder
|
|
355
|
+
*/
|
|
356
|
+
export async function deleteItem(path, options = {}) {
|
|
357
|
+
try {
|
|
358
|
+
const { force = false, json = false } = options;
|
|
359
|
+
|
|
360
|
+
if (!path) {
|
|
361
|
+
throw new Error('Path is required');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Confirmation prompt (unless --force)
|
|
365
|
+
if (!force && !json) {
|
|
366
|
+
const readline = await import('readline');
|
|
367
|
+
const rl = readline.createInterface({
|
|
368
|
+
input: process.stdin,
|
|
369
|
+
output: process.stdout,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const answer = await new Promise((resolve) => {
|
|
373
|
+
rl.question(`⚠️ Delete "${path}"? This cannot be undone. (y/N): `, resolve);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
rl.close();
|
|
377
|
+
|
|
378
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
379
|
+
console.log('Cancelled.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await graphClient.onedrive.remove(path);
|
|
385
|
+
|
|
386
|
+
outputOneDriveResult({
|
|
387
|
+
status: 'deleted',
|
|
388
|
+
path: path,
|
|
389
|
+
}, { json });
|
|
390
|
+
} catch (error) {
|
|
391
|
+
handleError(error, { json: options.json });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Format file size
|
|
397
|
+
*/
|
|
398
|
+
function formatFileSize(bytes) {
|
|
399
|
+
if (!bytes) return '0 B';
|
|
400
|
+
|
|
401
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
402
|
+
let size = bytes;
|
|
403
|
+
let unitIndex = 0;
|
|
404
|
+
|
|
405
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
406
|
+
size /= 1024;
|
|
407
|
+
unitIndex++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export default {
|
|
414
|
+
ls: listFiles,
|
|
415
|
+
get: getMetadata,
|
|
416
|
+
download: downloadFile,
|
|
417
|
+
upload: uploadFile,
|
|
418
|
+
search: searchFiles,
|
|
419
|
+
share: shareFile,
|
|
420
|
+
invite: inviteFile,
|
|
421
|
+
mkdir: createFolder,
|
|
422
|
+
rm: deleteItem,
|
|
423
|
+
};
|