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,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
+ };