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.
@@ -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
+ };