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,312 @@
|
|
|
1
|
+
import graphClient from '../graph/client.js';
|
|
2
|
+
import {
|
|
3
|
+
outputSharePointSiteList,
|
|
4
|
+
outputSharePointLists,
|
|
5
|
+
outputSharePointItems,
|
|
6
|
+
outputSharePointSearchResults,
|
|
7
|
+
outputOneDriveList,
|
|
8
|
+
outputOneDriveDetail,
|
|
9
|
+
outputOneDriveResult,
|
|
10
|
+
} from '../utils/output.js';
|
|
11
|
+
import { handleError } from '../utils/error.js';
|
|
12
|
+
import { readFile } from 'fs/promises';
|
|
13
|
+
import { basename } from 'path';
|
|
14
|
+
import { createWriteStream } from 'fs';
|
|
15
|
+
import { stat } from 'fs/promises';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SharePoint commands
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* List accessible SharePoint sites
|
|
23
|
+
*/
|
|
24
|
+
export async function listSites(options = {}) {
|
|
25
|
+
try {
|
|
26
|
+
const { search, top = 50, json = false } = options;
|
|
27
|
+
|
|
28
|
+
const sites = await graphClient.sharepoint.sites({ search, top });
|
|
29
|
+
|
|
30
|
+
outputSharePointSiteList(sites, { json, search });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
handleError(error, { json: options.json });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List site lists and document libraries
|
|
38
|
+
*/
|
|
39
|
+
export async function listLists(site, options = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const { top = 100, json = false } = options;
|
|
42
|
+
|
|
43
|
+
if (!site) {
|
|
44
|
+
throw new Error('Site parameter is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lists = await graphClient.sharepoint.lists(site, { top });
|
|
48
|
+
|
|
49
|
+
outputSharePointLists(lists, { json, site });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
handleError(error, { json: options.json });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List items in a SharePoint list
|
|
57
|
+
*/
|
|
58
|
+
export async function listItems(site, listId, options = {}) {
|
|
59
|
+
try {
|
|
60
|
+
const { top = 100, json = false } = options;
|
|
61
|
+
|
|
62
|
+
if (!site || !listId) {
|
|
63
|
+
throw new Error('Site and list ID are required');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const items = await graphClient.sharepoint.items(site, listId, { top });
|
|
67
|
+
|
|
68
|
+
outputSharePointItems(items, { json });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
handleError(error, { json: options.json });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* List files in SharePoint document library
|
|
76
|
+
*/
|
|
77
|
+
export async function listFiles(site, path = '', options = {}) {
|
|
78
|
+
try {
|
|
79
|
+
const { top = 100, json = false } = options;
|
|
80
|
+
|
|
81
|
+
if (!site) {
|
|
82
|
+
throw new Error('Site parameter is required');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = await graphClient.sharepoint.files(site, path, { top });
|
|
86
|
+
|
|
87
|
+
outputOneDriveList(files, { json, path: path || '/' });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
handleError(error, { json: options.json });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Download file from SharePoint
|
|
95
|
+
*/
|
|
96
|
+
export async function downloadFile(site, remotePath, localPath, options = {}) {
|
|
97
|
+
try {
|
|
98
|
+
const { json = false } = options;
|
|
99
|
+
|
|
100
|
+
if (!site || !remotePath) {
|
|
101
|
+
throw new Error('Site and remote path are required');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get metadata first to check if it's a file and get the name
|
|
105
|
+
const siteId = await graphClient.sharepoint._parseSite(site);
|
|
106
|
+
const cleanPath = remotePath.replace(/^\/+|\/+$/g, '');
|
|
107
|
+
const metadata = await graphClient.get(`/sites/${siteId}/drive/root:/${cleanPath}`);
|
|
108
|
+
|
|
109
|
+
if (metadata.folder) {
|
|
110
|
+
throw new Error('Cannot download folders. Please specify a file.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine local path
|
|
114
|
+
const targetPath = localPath || metadata.name;
|
|
115
|
+
|
|
116
|
+
if (!json) {
|
|
117
|
+
console.log(`⬇️ Downloading: ${metadata.name}`);
|
|
118
|
+
console.log(` Size: ${formatFileSize(metadata.size)}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Download file
|
|
122
|
+
const response = await graphClient.sharepoint.download(site, remotePath);
|
|
123
|
+
|
|
124
|
+
// Write to file
|
|
125
|
+
const fileStream = createWriteStream(targetPath);
|
|
126
|
+
const reader = response.body.getReader();
|
|
127
|
+
|
|
128
|
+
let downloadedBytes = 0;
|
|
129
|
+
const totalBytes = metadata.size;
|
|
130
|
+
|
|
131
|
+
while (true) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
if (done) break;
|
|
134
|
+
|
|
135
|
+
fileStream.write(Buffer.from(value));
|
|
136
|
+
downloadedBytes += value.length;
|
|
137
|
+
|
|
138
|
+
// Show progress (only in non-json mode)
|
|
139
|
+
if (!json && totalBytes > 0) {
|
|
140
|
+
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
141
|
+
process.stdout.write(`\r Progress: ${percent}% (${formatFileSize(downloadedBytes)} / ${formatFileSize(totalBytes)})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fileStream.end();
|
|
146
|
+
|
|
147
|
+
if (!json && totalBytes > 0) {
|
|
148
|
+
console.log(''); // New line after progress
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const result = {
|
|
152
|
+
status: 'downloaded',
|
|
153
|
+
name: metadata.name,
|
|
154
|
+
path: targetPath,
|
|
155
|
+
size: metadata.size,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
outputOneDriveResult(result, { json });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
handleError(error, { json: options.json });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Upload file to SharePoint
|
|
166
|
+
*/
|
|
167
|
+
export async function uploadFile(site, localPath, remotePath, options = {}) {
|
|
168
|
+
try {
|
|
169
|
+
const { json = false } = options;
|
|
170
|
+
|
|
171
|
+
if (!site || !localPath) {
|
|
172
|
+
throw new Error('Site and local path are required');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get file stats
|
|
176
|
+
const stats = await stat(localPath);
|
|
177
|
+
if (!stats.isFile()) {
|
|
178
|
+
throw new Error('Local path must be a file');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fileName = basename(localPath);
|
|
182
|
+
const targetPath = remotePath || fileName;
|
|
183
|
+
|
|
184
|
+
const fileSizeInMB = stats.size / (1024 * 1024);
|
|
185
|
+
|
|
186
|
+
if (!json) {
|
|
187
|
+
console.log(`⬆️ Uploading: ${fileName}`);
|
|
188
|
+
console.log(` Size: ${formatFileSize(stats.size)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Small file upload (< 4MB)
|
|
192
|
+
if (fileSizeInMB < 4) {
|
|
193
|
+
const content = await readFile(localPath);
|
|
194
|
+
const result = await graphClient.sharepoint.upload(site, targetPath, content);
|
|
195
|
+
|
|
196
|
+
outputOneDriveResult({
|
|
197
|
+
status: 'uploaded',
|
|
198
|
+
name: result.name,
|
|
199
|
+
path: targetPath,
|
|
200
|
+
size: result.size,
|
|
201
|
+
webUrl: result.webUrl,
|
|
202
|
+
}, { json });
|
|
203
|
+
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Large file upload with session
|
|
208
|
+
if (!json) {
|
|
209
|
+
console.log(' Using upload session for large file...');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const session = await graphClient.sharepoint.createUploadSession(site, targetPath);
|
|
213
|
+
const uploadUrl = session.uploadUrl;
|
|
214
|
+
|
|
215
|
+
// Read file and upload in chunks
|
|
216
|
+
const chunkSize = 320 * 1024 * 10; // 3.2MB chunks
|
|
217
|
+
const fileContent = await readFile(localPath);
|
|
218
|
+
const totalSize = fileContent.length;
|
|
219
|
+
|
|
220
|
+
let start = 0;
|
|
221
|
+
let uploadedBytes = 0;
|
|
222
|
+
|
|
223
|
+
while (start < totalSize) {
|
|
224
|
+
const end = Math.min(start + chunkSize, totalSize);
|
|
225
|
+
const chunk = fileContent.slice(start, end);
|
|
226
|
+
|
|
227
|
+
const result = await graphClient.onedrive.uploadChunk(
|
|
228
|
+
uploadUrl,
|
|
229
|
+
chunk,
|
|
230
|
+
start,
|
|
231
|
+
end,
|
|
232
|
+
totalSize
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
uploadedBytes = end;
|
|
236
|
+
|
|
237
|
+
// Show progress
|
|
238
|
+
if (!json) {
|
|
239
|
+
const percent = ((uploadedBytes / totalSize) * 100).toFixed(1);
|
|
240
|
+
process.stdout.write(`\r Progress: ${percent}% (${formatFileSize(uploadedBytes)} / ${formatFileSize(totalSize)})`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
start = end;
|
|
244
|
+
|
|
245
|
+
// Check if upload is complete
|
|
246
|
+
if (result.id) {
|
|
247
|
+
if (!json) {
|
|
248
|
+
console.log(''); // New line after progress
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
outputOneDriveResult({
|
|
252
|
+
status: 'uploaded',
|
|
253
|
+
name: result.name,
|
|
254
|
+
path: targetPath,
|
|
255
|
+
size: result.size,
|
|
256
|
+
webUrl: result.webUrl,
|
|
257
|
+
}, { json });
|
|
258
|
+
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
handleError(error, { json: options.json });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Search SharePoint content
|
|
269
|
+
*/
|
|
270
|
+
export async function searchContent(query, options = {}) {
|
|
271
|
+
try {
|
|
272
|
+
const { top = 50, json = false } = options;
|
|
273
|
+
|
|
274
|
+
if (!query) {
|
|
275
|
+
throw new Error('Search query is required');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const results = await graphClient.sharepoint.search(query, { top });
|
|
279
|
+
|
|
280
|
+
outputSharePointSearchResults(results, { json, query });
|
|
281
|
+
} catch (error) {
|
|
282
|
+
handleError(error, { json: options.json });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Format file size
|
|
288
|
+
*/
|
|
289
|
+
function formatFileSize(bytes) {
|
|
290
|
+
if (!bytes) return '0 B';
|
|
291
|
+
|
|
292
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
293
|
+
let size = bytes;
|
|
294
|
+
let unitIndex = 0;
|
|
295
|
+
|
|
296
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
297
|
+
size /= 1024;
|
|
298
|
+
unitIndex++;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default {
|
|
305
|
+
sites: listSites,
|
|
306
|
+
lists: listLists,
|
|
307
|
+
items: listItems,
|
|
308
|
+
files: listFiles,
|
|
309
|
+
download: downloadFile,
|
|
310
|
+
upload: uploadFile,
|
|
311
|
+
search: searchContent,
|
|
312
|
+
};
|