puter-cli 1.0.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,1290 @@
1
+ import fs from 'node:fs';
2
+ import { glob } from 'glob';
3
+ import path from 'path';
4
+ import { minimatch } from 'minimatch';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import Conf from 'conf';
8
+ import fetch from 'node-fetch';
9
+ import { API_BASE, BASE_URL, PROJECT_NAME, getHeaders, showDiskSpaceUsage, resolvePath } from './commons.js';
10
+ import { formatDate, formatDateTime, formatSize } from './utils.js';
11
+ import inquirer from 'inquirer';
12
+ import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
13
+ import { updatePrompt } from './shell.js';
14
+
15
+ const config = new Conf({ projectName: PROJECT_NAME });
16
+
17
+
18
+ /**
19
+ * List files in given path
20
+ * @param {string} path Path to the file or directory
21
+ * @returns List of files found
22
+ */
23
+ export async function listRemoteFiles(path) {
24
+ const response = await fetch(`${API_BASE}/readdir`, {
25
+ method: 'POST',
26
+ headers: getHeaders(),
27
+ body: JSON.stringify({ path })
28
+ });
29
+ return await response.json();
30
+ }
31
+
32
+ /**
33
+ * List files in the current working directory.
34
+ * @param {string} args Default current working directory
35
+ */
36
+ export async function listFiles(args = []) {
37
+ const names = args.length > 0 ? args : ['.'];
38
+ for (let path of names)
39
+ try {
40
+ if (!path.startsWith('/')){
41
+ path = resolvePath(getCurrentDirectory(), path);
42
+ }
43
+ if (!(await pathExists(path))){
44
+ console.log(chalk.yellow(`Directory ${chalk.red(path)} doesn't exists!`));
45
+ continue;
46
+ }
47
+ console.log(chalk.green(`Listing files in ${chalk.dim(path)}:\n`));
48
+ const files = await listRemoteFiles(path);
49
+ if (Array.isArray(files) && files.length > 0) {
50
+ console.log(chalk.cyan(`Type Name Size Modified UID`));
51
+ console.log(chalk.dim('----------------------------------------------------------------------------------'));
52
+ files.forEach(file => {
53
+ const type = file.is_dir ? 'd' : '-';
54
+ const write = file.writable ? 'w' : '-';
55
+ const name = file.name.padEnd(20);
56
+ const size = file.is_dir ? '0' : formatSize(file.size);
57
+ const modified = formatDateTime(file.modified);
58
+ const uid = file.uid;
59
+ console.log(`${type}${write} ${name} ${size.padEnd(8)} ${modified} ${uid}`);
60
+ });
61
+ console.log(chalk.green(`There are ${files.length} object(s).`));
62
+ } else {
63
+ console.log(chalk.red('No files or directories found.'));
64
+ }
65
+ } catch (error) {
66
+ console.log(chalk.red('Failed to list files.'));
67
+ console.error(chalk.red(`Error: ${error.message}`));
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Create a folder in the current working directory.
73
+ * @param {Array} args Options
74
+ * @returns void
75
+ */
76
+ export async function makeDirectory(args = []) {
77
+ if (args.length < 1) {
78
+ console.log(chalk.red('Usage: mkdir <directory_name>'));
79
+ return;
80
+ }
81
+
82
+ const directoryName = args[0];
83
+ console.log(chalk.green(`Creating directory "${directoryName}" in "${getCurrentDirectory()}"...\n`));
84
+
85
+ try {
86
+ const response = await fetch(`${API_BASE}/mkdir`, {
87
+ method: 'POST',
88
+ headers: getHeaders(),
89
+ body: JSON.stringify({
90
+ parent: getCurrentDirectory(),
91
+ path: directoryName,
92
+ overwrite: false,
93
+ dedupe_name: true,
94
+ create_missing_parents: false
95
+ })
96
+ });
97
+
98
+ const data = await response.json();
99
+ if (data && data.id) {
100
+ console.log(chalk.green(`Directory "${directoryName}" created successfully!`));
101
+ console.log(chalk.dim(`Path: ${data.path}`));
102
+ console.log(chalk.dim(`UID: ${data.uid}`));
103
+ } else {
104
+ console.log(chalk.red('Failed to create directory. Please check your input.'));
105
+ }
106
+ } catch (error) {
107
+ console.log(chalk.red('Failed to create directory.'));
108
+ console.error(chalk.red(`Error: ${error.message}`));
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Rename a file or directory
114
+ * @param {Array} args Options
115
+ * @returns void
116
+ */
117
+ export async function renameFileOrDirectory(args = []) {
118
+ if (args.length < 2) {
119
+ console.log(chalk.red('Usage: mv <old_name> <new_name>'));
120
+ return;
121
+ }
122
+
123
+ const currentPath = getCurrentDirectory();
124
+ const oldName = args[0];
125
+ const newName = args[1];
126
+
127
+ console.log(chalk.green(`Renaming "${oldName}" to "${newName}"...\n`));
128
+
129
+ try {
130
+ // Step 1: Get the UID of the file/directory using the old name
131
+ const statResponse = await fetch(`${API_BASE}/stat`, {
132
+ method: 'POST',
133
+ headers: getHeaders(),
134
+ body: JSON.stringify({
135
+ path: `${currentPath}/${oldName}`
136
+ })
137
+ });
138
+
139
+ const statData = await statResponse.json();
140
+ if (!statData || !statData.uid) {
141
+ console.log(chalk.red(`Could not find file or directory with name "${oldName}".`));
142
+ return;
143
+ }
144
+
145
+ const uid = statData.uid;
146
+
147
+ // Step 2: Perform the rename operation using the UID
148
+ const renameResponse = await fetch(`${API_BASE}/rename`, {
149
+ method: 'POST',
150
+ headers: getHeaders(),
151
+ body: JSON.stringify({
152
+ uid: uid,
153
+ new_name: newName
154
+ })
155
+ });
156
+
157
+ const renameData = await renameResponse.json();
158
+ if (renameData && renameData.uid) {
159
+ console.log(chalk.green(`Successfully renamed "${oldName}" to "${newName}"!`));
160
+ console.log(chalk.dim(`Path: ${renameData.path}`));
161
+ console.log(chalk.dim(`UID: ${renameData.uid}`));
162
+ } else {
163
+ console.log(chalk.red('Failed to rename item. Please check your input.'));
164
+ }
165
+ } catch (error) {
166
+ console.log(chalk.red('Failed to rename item.'));
167
+ console.error(chalk.red(`Error: ${error.message}`));
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Helper function to recursively find files matching the pattern
173
+ * @param {Array} files List of files
174
+ * @param {string} pattern The pattern to find
175
+ * @param {string} basePath the base path
176
+ * @returns array of matching files
177
+ */
178
+ async function findMatchingFiles(files, pattern, basePath) {
179
+ const matchedPaths = [];
180
+
181
+ for (const file of files) {
182
+ const filePath = path.join(basePath, file.name);
183
+
184
+ // Check if the current file/directory matches the pattern
185
+ if (minimatch(filePath, pattern, { dot: true })) {
186
+ matchedPaths.push(filePath);
187
+ }
188
+
189
+ // If it's a directory, recursively search its contents
190
+ if (file.is_dir) {
191
+ const dirResponse = await fetch(`${API_BASE}/readdir`, {
192
+ method: 'POST',
193
+ headers: getHeaders(),
194
+ body: JSON.stringify({ path: filePath })
195
+ });
196
+
197
+ if (dirResponse.ok) {
198
+ const dirFiles = await dirResponse.json();
199
+ const dirMatches = await findMatchingFiles(dirFiles, pattern, filePath);
200
+ matchedPaths.push(...dirMatches);
201
+ }
202
+ }
203
+ }
204
+
205
+ return matchedPaths;
206
+ }
207
+
208
+
209
+ /**
210
+ * Find files matching the pattern in the local directory (DEPRECATED: Not used)
211
+ * @param {string} localDir - Local directory path.
212
+ * @param {string} pattern - File pattern (e.g., "*.html", "myapp/*").
213
+ * @returns {Array} - Array of file objects with local and relative paths.
214
+ */
215
+ function findLocalMatchingFiles(localDir, pattern) {
216
+ const files = [];
217
+ const walkDir = (dir) => {
218
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
219
+ for (const entry of entries) {
220
+ const fullPath = path.join(dir, entry.name);
221
+ if (entry.isDirectory()) {
222
+ walkDir(fullPath); // Recursively traverse directories
223
+ } else if (minimatch(fullPath, path.join(localDir, pattern), { dot: true })) {
224
+ files.push({
225
+ localPath: fullPath,
226
+ relativePath: path.relative(localDir, fullPath)
227
+ });
228
+ }
229
+ }
230
+ };
231
+
232
+ walkDir(localDir);
233
+ return files;
234
+ }
235
+
236
+ /**
237
+ * Move a file/directory to the Trash
238
+ * @param {Array} args Options:
239
+ * -f: Force delete (no confirmation)
240
+ * @returns void
241
+ */
242
+ export async function removeFileOrDirectory(args = []) {
243
+ if (args.length < 1) {
244
+ console.log(chalk.red('Usage: rm <name> [-f]'));
245
+ return;
246
+ }
247
+
248
+ const skipConfirmation = args.includes('-f'); // Check the flag if provided
249
+ const names = skipConfirmation ? args.filter(option => option !== '-f') : args;
250
+
251
+ try {
252
+ // Step 1: Fetch the list of files and directories from the server
253
+ const listResponse = await fetch(`${API_BASE}/readdir`, {
254
+ method: 'POST',
255
+ headers: getHeaders(),
256
+ body: JSON.stringify({ path: getCurrentDirectory() })
257
+ });
258
+
259
+ if (!listResponse.ok) {
260
+ console.error(chalk.red('Failed to list files from the server.'));
261
+ return;
262
+ }
263
+
264
+ const files = await listResponse.json();
265
+ if (!Array.isArray(files) || files.length == 0) {
266
+ console.error(chalk.red('No files or directories found on the server.'));
267
+ return;
268
+ }
269
+
270
+ // Step 2: Find all files/directories matching the provided patterns
271
+ const matchedPaths = [];
272
+ for (const name of names) {
273
+ if (name.startsWith('/')){
274
+ const pattern = resolvePath('/', name);
275
+ matchedPaths.push(pattern);
276
+ continue;
277
+ }
278
+ const pattern = resolvePath(getCurrentDirectory(), name);
279
+ const matches = await findMatchingFiles(files, pattern, getCurrentDirectory());
280
+ matchedPaths.push(...matches);
281
+ }
282
+
283
+ if (matchedPaths.length === 0) {
284
+ console.error(chalk.red('No files or directories found matching the pattern.'));
285
+ return;
286
+ }
287
+
288
+ // Step 3: Prompt for confirmation (unless -f flag is provided)
289
+ if (!skipConfirmation) {
290
+ console.log(chalk.yellow(`The following items will be moved to Trash:`));
291
+ console.log(chalk.cyan('Hint: Execute "clean" to empty the Trash.'));
292
+ matchedPaths.forEach(path => console.log(chalk.dim(`- ${path}`)));
293
+
294
+ const { confirm } = await inquirer.prompt([
295
+ {
296
+ type: 'confirm',
297
+ name: 'confirm',
298
+ message: `Are you sure you want to move these ${matchedPaths.length} item(s) to Trash?`,
299
+ default: false
300
+ }
301
+ ]);
302
+
303
+ if (!confirm) {
304
+ console.log(chalk.yellow('Operation canceled.'));
305
+ return;
306
+ }
307
+ }
308
+
309
+ // Step 4: Move each matched file/directory to Trash
310
+ for (const path of matchedPaths) {
311
+ try {
312
+ console.log(chalk.green(`Preparing to remove "${path}"...`));
313
+
314
+ // Step 4.1: Get the UID of the file/directory
315
+ const statResponse = await fetch(`${API_BASE}/stat`, {
316
+ method: 'POST',
317
+ headers: getHeaders(),
318
+ body: JSON.stringify({ path })
319
+ });
320
+
321
+ const statData = await statResponse.json();
322
+ if (!statData || !statData.uid) {
323
+ console.error(chalk.red(`Could not find file or directory with path "${path}".`));
324
+ continue;
325
+ }
326
+
327
+ const uid = statData.uid;
328
+ const originalPath = statData.path;
329
+
330
+ // Step 4.2: Perform the move operation to Trash
331
+ const moveResponse = await fetch(`${API_BASE}/move`, {
332
+ method: 'POST',
333
+ headers: getHeaders(),
334
+ body: JSON.stringify({
335
+ source: uid,
336
+ destination: `/${getCurrentUserName()}/Trash`,
337
+ overwrite: false,
338
+ new_name: uid, // Use the UID as the new name in Trash
339
+ create_missing_parents: false,
340
+ new_metadata: {
341
+ original_name: path.split('/').pop(),
342
+ original_path: originalPath,
343
+ trashed_ts: Math.floor(Date.now() / 1000) // Current timestamp
344
+ }
345
+ })
346
+ });
347
+
348
+ const moveData = await moveResponse.json();
349
+ if (moveData && moveData.moved) {
350
+ console.log(chalk.green(`Successfully moved "${path}" to Trash!`));
351
+ console.log(chalk.dim(`Moved to: ${moveData.moved.path}`));
352
+ } else {
353
+ console.error(chalk.red(`Failed to move "${path}" to Trash.`));
354
+ }
355
+ } catch (error) {
356
+ console.error(chalk.red(`Failed to remove "${path}".`));
357
+ console.error(chalk.red(`Error: ${error.message}`));
358
+ }
359
+ }
360
+ } catch (error) {
361
+ console.error(chalk.red('Failed to remove items.'));
362
+ console.error(chalk.red(`Error: ${error.message}`));
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Delete a folder and its contents (PREVENTED BY PUTER API)
368
+ * @param {string} folderPath - The path of the folder to delete (defaults to Trash).
369
+ * @param {boolean} skipConfirmation - Whether to skip the confirmation prompt.
370
+ */
371
+ export async function deleteFolder(folderPath, skipConfirmation = false) {
372
+ console.log(chalk.green(`Preparing to delete "${folderPath}"...\n`));
373
+
374
+ try {
375
+ // Step 1: Prompt for confirmation (unless skipConfirmation is true)
376
+ if (!skipConfirmation) {
377
+ const { confirm } = await inquirer.prompt([
378
+ {
379
+ type: 'confirm',
380
+ name: 'confirm',
381
+ message: `Are you sure you want to delete all contents of "${folderPath}"?`,
382
+ default: false
383
+ }
384
+ ]);
385
+
386
+ if (!confirm) {
387
+ console.log(chalk.yellow('Operation canceled.'));
388
+ return;
389
+ }
390
+ }
391
+
392
+ // Step 2: Perform the delete operation
393
+ const deleteResponse = await fetch(`${API_BASE}/delete`, {
394
+ method: 'POST',
395
+ headers: getHeaders(),
396
+ body: JSON.stringify({
397
+ paths: [folderPath],
398
+ descendants_only: true, // Delete only the contents, not the folder itself
399
+ recursive: true // Delete all subdirectories and files
400
+ })
401
+ });
402
+
403
+ const deleteData = await deleteResponse.json();
404
+ if (deleteResponse.ok) {
405
+ console.log(chalk.green(`Successfully deleted all contents of "${folderPath}"!`));
406
+ } else {
407
+ console.log(chalk.red('Failed to delete folder. Please check your input.'));
408
+ }
409
+ } catch (error) {
410
+ console.log(chalk.red('Failed to delete folder.'));
411
+ console.error(chalk.red(`Error: ${error.message}`));
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Empty the Trash (wrapper for deleteFolder).
417
+ * @param {boolean} skipConfirmation - Whether to skip the confirmation prompt.
418
+ */
419
+ export async function emptyTrash(skipConfirmation = true) {
420
+ const trashPath = `/${getCurrentUserName()}/Trash`;
421
+ await deleteFolder(trashPath, skipConfirmation);
422
+ }
423
+
424
+ /**
425
+ * Show statistical information about the current working directory.
426
+ * @param {Array} args array of path names
427
+ */
428
+ export async function getInfo(args = []) {
429
+ const names = args.length > 0 ? args : ['.'];
430
+ for (let name of names)
431
+ try {
432
+ name = `${getCurrentDirectory()}/${name}`;
433
+ console.log(chalk.green(`Getting stat info for: "${name}"...\n`));
434
+ const response = await fetch(`${API_BASE}/stat`, {
435
+ method: 'POST',
436
+ headers: getHeaders(),
437
+ body: JSON.stringify({
438
+ path: name
439
+ })
440
+ });
441
+ const data = await response.json();
442
+ if (response.ok && data) {
443
+ console.log(chalk.cyan('File/Directory Information:'));
444
+ console.log(chalk.dim('----------------------------------------'));
445
+ console.log(chalk.cyan(`Name: `) + chalk.white(data.name));
446
+ console.log(chalk.cyan(`Path: `) + chalk.white(data.path));
447
+ console.log(chalk.cyan(`Type: `) + chalk.white(data.is_dir ? 'Directory' : 'File'));
448
+ console.log(chalk.cyan(`Size: `) + chalk.white(data.size ? formatSize(data.size) : 'N/A'));
449
+ console.log(chalk.cyan(`Created: `) + chalk.white(new Date(data.created * 1000).toLocaleString()));
450
+ console.log(chalk.cyan(`Modified: `) + chalk.white(new Date(data.modified * 1000).toLocaleString()));
451
+ console.log(chalk.cyan(`Writable: `) + chalk.white(data.writable ? 'Yes' : 'No'));
452
+ console.log(chalk.cyan(`Owner: `) + chalk.white(data.owner.username));
453
+ console.log(chalk.dim('----------------------------------------'));
454
+ console.log(chalk.green('Done.'));
455
+ } else {
456
+ console.error(chalk.red('Unable to get stat info. Please check your credentials.'));
457
+ }
458
+ } catch (error) {
459
+ console.error(chalk.red(`Failed to get stat info.\nError: ${error.message}`));
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Show the current working directory
465
+ */
466
+ export async function showCwd() {
467
+ console.log(chalk.green(`${config.get('cwd')}`));
468
+ }
469
+
470
+ /**
471
+ * Change the current working directory
472
+ * @param {Array} args - The path arguments
473
+ * @returns void
474
+ */
475
+ export async function changeDirectory(args) {
476
+ let currentPath = config.get('cwd');
477
+ // If no arguments, print the current directory
478
+ if (!args.length) {
479
+ console.log(chalk.green(currentPath));
480
+ return;
481
+ }
482
+
483
+ const path = args[0];
484
+ // Handle "/","~",".." and deeper navigation
485
+ const newPath = path.startsWith('/')? path: (path === '~'? `/${getCurrentUserName()}` :resolvePath(currentPath, path));
486
+ try {
487
+ // Check if the new path is a valid directory
488
+ const response = await fetch(`${API_BASE}/stat`, {
489
+ method: 'POST',
490
+ headers: getHeaders(),
491
+ body: JSON.stringify({
492
+ path: newPath
493
+ })
494
+ });
495
+
496
+ const data = await response.json();
497
+ if (response.ok && data && data.is_dir) {
498
+ // Update the newPath to use the correct name from the response
499
+ const arrayDirs = newPath.split('/');
500
+ arrayDirs.pop();
501
+ arrayDirs.push(data.name);
502
+ updatePrompt(arrayDirs.join('/')); // Update the shell prompt
503
+ } else {
504
+ console.log(chalk.red(`"${newPath}" is not a directory`));
505
+ }
506
+ } catch (error) {
507
+ console.log(chalk.red(`Cannot access "${newPath}": ${error.message}`));
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Fetch disk usage information
513
+ * @param {Object} body - Optional arguments to include in the request body.
514
+ */
515
+ export async function getDiskUsage(body = null) {
516
+ console.log(chalk.green('Fetching disk usage information...\n'));
517
+ try {
518
+ const response = await fetch(`${API_BASE}/df`, {
519
+ method: 'POST',
520
+ headers: getHeaders(),
521
+ body: body ? JSON.stringify(body) : null
522
+ });
523
+
524
+ const data = await response.json();
525
+ console.log(data);
526
+ if (response.ok && data) {
527
+ showDiskSpaceUsage(data);
528
+ } else {
529
+ console.error(chalk.red('Unable to fetch disk usage information.'));
530
+ }
531
+ } catch (error) {
532
+ console.error(chalk.red(`Failed to fetch disk usage information.\nError: ${error.message}`));
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Check if a path exists
538
+ * @param {string} filePath List of files/directories
539
+ */
540
+ export async function pathExists(filePath) {
541
+ if (filePath.length < 1) {
542
+ console.log(chalk.red('No path provided.'));
543
+ return false;
544
+ }
545
+ try {
546
+ // Step 1: Check if the file already exists
547
+ const statResponse = await fetch(`${API_BASE}/stat`, {
548
+ method: 'POST',
549
+ headers: getHeaders(),
550
+ body: JSON.stringify({
551
+ path: filePath
552
+ })
553
+ });
554
+
555
+ return statResponse.ok;
556
+ } catch (error){
557
+ console.error(chalk.red('Failed to check if file exists.'));
558
+ return false;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Create a new file (similar to Unix "touch" command).
564
+ * @param {Array} args - The arguments passed to the command (file name and optional content).
565
+ * @returns {boolean} - True if the file was created successfully, false otherwise.
566
+ */
567
+ export async function createFile(args = []) {
568
+ if (args.length < 1) {
569
+ console.log(chalk.red('Usage: touch <file_name> [content]'));
570
+ return false;
571
+ }
572
+
573
+ const filePath = args[0]; // File path (e.g., "app/index.html")
574
+ const content = args.length > 1 ? args.slice(1).join(' ') : ''; // Optional content
575
+ let fullPath = filePath;
576
+ if (!filePath.startsWith(`/${getCurrentUserName()}/`)){
577
+ fullPath = resolvePath(getCurrentDirectory(), filePath); // Resolve the full path
578
+ }
579
+ const dirName = path.dirname(fullPath); // Extract the directory name
580
+ const fileName = path.basename(fullPath); // Extract the file name
581
+ const dedupeName = false; // Default: false
582
+ const overwrite = true; // Default: true
583
+
584
+ console.log(chalk.green(`Creating file:\nFileName: "${chalk.dim(fileName)}"\nPath: "${chalk.dim(dirName)}"\nContent Length: ${chalk.dim(content.length)}`));
585
+ try {
586
+ // Step 1: Check if the file already exists
587
+ const statResponse = await fetch(`${API_BASE}/stat`, {
588
+ method: 'POST',
589
+ headers: getHeaders(),
590
+ body: JSON.stringify({
591
+ path: fullPath
592
+ })
593
+ });
594
+
595
+ if (statResponse.ok) {
596
+ const statData = await statResponse.json();
597
+ if (statData && statData.id) {
598
+ if (!overwrite) {
599
+ console.error(chalk.red(`File "${filePath}" already exists. Use --overwrite=true to replace it.`));
600
+ return false;
601
+ }
602
+ console.log(chalk.yellow(`File "${filePath}" already exists. It will be overwritten.`));
603
+ }
604
+ } else if (statResponse.status !== 404) {
605
+ console.error(chalk.red('Failed to check if file exists.'));
606
+ return false;
607
+ }
608
+
609
+ // Step 2: Check disk space
610
+ const dfResponse = await fetch(`${API_BASE}/df`, {
611
+ method: 'POST',
612
+ headers: getHeaders(),
613
+ body: null
614
+ });
615
+
616
+ if (!dfResponse.ok) {
617
+ console.error(chalk.red('Unable to check disk space.'));
618
+ return false;
619
+ }
620
+
621
+ const dfData = await dfResponse.json();
622
+ if (dfData.used >= dfData.capacity) {
623
+ console.error(chalk.red('Not enough disk space to create the file.'));
624
+ showDiskSpaceUsage(dfData); // Display disk usage info
625
+ return false;
626
+ }
627
+
628
+ // Step 3: Create the nested directories if they don't exist
629
+ const dirStatResponse = await fetch(`${API_BASE}/stat`, {
630
+ method: 'POST',
631
+ headers: getHeaders(),
632
+ body: JSON.stringify({
633
+ path: dirName
634
+ })
635
+ });
636
+
637
+ if (!dirStatResponse.ok || dirStatResponse.status === 404) {
638
+ // Create the directory if it doesn't exist
639
+ await fetch(`${API_BASE}/mkdir`, {
640
+ method: 'POST',
641
+ headers: getHeaders(),
642
+ body: JSON.stringify({
643
+ parent: path.dirname(dirName),
644
+ path: path.basename(dirName),
645
+ overwrite: false,
646
+ dedupe_name: true,
647
+ create_missing_parents: true
648
+ })
649
+ });
650
+ }
651
+
652
+ // Step 4: Create the file using /batch
653
+ const operationId = crypto.randomUUID(); // Generate a unique operation ID
654
+ const socketId = 'undefined'; // Placeholder socket ID
655
+ const boundary = `----WebKitFormBoundary${crypto.randomUUID().replace(/-/g, '')}`;
656
+ const fileBlob = new Blob([content || ''], { type: 'text/plain' });
657
+
658
+ const formData = `--${boundary}\r\n` +
659
+ `Content-Disposition: form-data; name="operation_id"\r\n\r\n${operationId}\r\n` +
660
+ `--${boundary}\r\n` +
661
+ `Content-Disposition: form-data; name="socket_id"\r\n\r\n${socketId}\r\n` +
662
+ `--${boundary}\r\n` +
663
+ `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${socketId}\r\n` +
664
+ `--${boundary}\r\n` +
665
+ `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${JSON.stringify({
666
+ name: fileName,
667
+ type: 'text/plain',
668
+ size: fileBlob.size
669
+ })}\r\n` +
670
+ `--${boundary}\r\n` +
671
+ `Content-Disposition: form-data; name="operation"\r\n\r\n${JSON.stringify({
672
+ op: 'write',
673
+ dedupe_name: dedupeName,
674
+ overwrite: overwrite,
675
+ operation_id: operationId,
676
+ path: dirName,
677
+ name: fileName,
678
+ item_upload_id: 0
679
+ })}\r\n` +
680
+ `--${boundary}\r\n` +
681
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
682
+ `Content-Type: text/plain\r\n\r\n${content || ''}\r\n` +
683
+ `--${boundary}--\r\n`;
684
+
685
+ // Send the request
686
+ const createResponse = await fetch(`${API_BASE}/batch`, {
687
+ method: 'POST',
688
+ headers: getHeaders(`multipart/form-data; boundary=${boundary}`),
689
+ body: formData
690
+ });
691
+
692
+ if (!createResponse.ok) {
693
+ const errorText = await createResponse.text();
694
+ console.error(chalk.red(`Failed to create file. Server response: ${errorText}. status: ${createResponse.status}`));
695
+ return false;
696
+ }
697
+
698
+ const createData = await createResponse.json();
699
+ if (createData && createData.results && createData.results.length > 0) {
700
+ const file = createData.results[0];
701
+ console.log(chalk.green(`File "${filePath}" created successfully!`));
702
+ console.log(chalk.dim(`Path: ${file.path}`));
703
+ console.log(chalk.dim(`UID: ${file.uid}`));
704
+ } else {
705
+ console.error(chalk.red('Failed to create file. Invalid response from server.'));
706
+ return false;
707
+ }
708
+ } catch (error) {
709
+ console.error(chalk.red(`Failed to create file.\nError: ${error.message}`));
710
+ return false;
711
+ }
712
+ return true;
713
+ }
714
+
715
+ /**
716
+ * Read and display the content of a file (similar to Unix "cat" command).
717
+ * @param {Array} args - The arguments passed to the command (file path).
718
+ */
719
+ export async function readFile(args = []) {
720
+ if (args.length < 1) {
721
+ console.log(chalk.red('Usage: cat <file_path>'));
722
+ return;
723
+ }
724
+
725
+ const filePath = resolvePath(getCurrentDirectory(), args[0]);
726
+ console.log(chalk.green(`Reading file "${filePath}"...\n`));
727
+
728
+ try {
729
+ // Step 1: Fetch the file content
730
+ const response = await fetch(`${API_BASE}/read?file=${encodeURIComponent(filePath)}`, {
731
+ method: 'GET',
732
+ headers: getHeaders()
733
+ });
734
+
735
+ if (!response.ok) {
736
+ console.error(chalk.red(`Failed to read file. Server response: ${response.statusText}`));
737
+ return;
738
+ }
739
+
740
+ const data = await response.text();
741
+
742
+ // Step 2: Dispaly the content
743
+ if (data.length) {
744
+ console.log(chalk.cyan(data));
745
+ } else {
746
+ console.error(chalk.red('File is empty.'));
747
+ }
748
+ } catch (error) {
749
+ console.error(chalk.red(`Failed to read file.\nError: ${error.message}`));
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Upload a file from the host machine to the Puter server
755
+ * @param {Array} args - The arguments passed to the command: (<local_path> [remote_path] [dedupe_name] [overwrite])
756
+ */
757
+ export async function uploadFile(args = []) {
758
+ if (args.length < 1) {
759
+ console.log(chalk.red('Usage: push <local_path> [remote_path] [dedupe_name] [overwrite]'));
760
+ return;
761
+ }
762
+
763
+ const localPath = args[0];
764
+ let remotePath = '';
765
+ if (args.length > 1){
766
+ remotePath = args[1].startsWith('/')? args[1]: resolvePath(getCurrentDirectory(), args[1]);
767
+ } else {
768
+ remotePath = resolvePath(getCurrentDirectory(), '.');
769
+ }
770
+ const dedupeName = args.length > 2 ? args[2] === 'true' : true; // Default: true
771
+ const overwrite = args.length > 3 ? args[3] === 'true' : false; // Default: false
772
+
773
+ console.log(chalk.green(`Uploading files from "${localPath}" to "${remotePath}"...\n`));
774
+ try {
775
+ // Step 1: Find all matching files (excluding hidden files)
776
+ const files = glob.sync(localPath, { nodir: true, dot: false });
777
+
778
+ if (files.length === 0) {
779
+ console.error(chalk.red('No files found to upload.'));
780
+ return;
781
+ }
782
+
783
+ // Step 2: Check disk space
784
+ const dfResponse = await fetch(`${API_BASE}/df`, {
785
+ method: 'POST',
786
+ headers: getHeaders(), // Use a dummy boundary for non-multipart requests
787
+ body: null
788
+ });
789
+
790
+ if (!dfResponse.ok) {
791
+ console.error(chalk.red('Unable to check disk space.'));
792
+ return;
793
+ }
794
+
795
+ const dfData = await dfResponse.json();
796
+ if (dfData.used >= dfData.capacity) {
797
+ console.error(chalk.red('Not enough disk space to upload the files.'));
798
+ showDiskSpaceUsage(dfData); // Display disk usage info
799
+ return;
800
+ }
801
+
802
+ // Step 3: Upload each file
803
+ for (const filePath of files) {
804
+ const fileName = path.basename(filePath);
805
+ const fileContent = fs.readFileSync(filePath, 'utf8');
806
+
807
+ // Prepare the upload request
808
+ const operationId = crypto.randomUUID(); // Generate a unique operation ID
809
+ const socketId = 'undefined'; // Placeholder socket ID
810
+ const boundary = `----WebKitFormBoundary${crypto.randomUUID().replace(/-/g, '')}`;
811
+
812
+ // Prepare FormData
813
+ const formData = `--${boundary}\r\n` +
814
+ `Content-Disposition: form-data; name="operation_id"\r\n\r\n${operationId}\r\n` +
815
+ `--${boundary}\r\n` +
816
+ `Content-Disposition: form-data; name="socket_id"\r\n\r\n${socketId}\r\n` +
817
+ `--${boundary}\r\n` +
818
+ `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${socketId}\r\n` +
819
+ `--${boundary}\r\n` +
820
+ `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${JSON.stringify({
821
+ name: fileName,
822
+ type: 'text/plain',
823
+ size: Buffer.byteLength(fileContent, 'utf8')
824
+ })}\r\n` +
825
+ `--${boundary}\r\n` +
826
+ `Content-Disposition: form-data; name="operation"\r\n\r\n${JSON.stringify({
827
+ op: 'write',
828
+ dedupe_name: dedupeName,
829
+ overwrite: overwrite,
830
+ operation_id: operationId,
831
+ path: remotePath,
832
+ name: fileName,
833
+ item_upload_id: 0
834
+ })}\r\n` +
835
+ `--${boundary}\r\n` +
836
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
837
+ `Content-Type: text/plain\r\n\r\n${fileContent}\r\n` +
838
+ `--${boundary}--\r\n`;
839
+
840
+ // Send the upload request
841
+ const uploadResponse = await fetch(`${API_BASE}/batch`, {
842
+ method: 'POST',
843
+ headers: getHeaders(`multipart/form-data; boundary=${boundary}`),
844
+ body: formData
845
+ });
846
+
847
+ if (!uploadResponse.ok) {
848
+ const errorText = await uploadResponse.text();
849
+ console.error(chalk.red(`Failed to upload file "${fileName}". Server response: ${errorText}`));
850
+ continue;
851
+ }
852
+
853
+ const uploadData = await uploadResponse.json();
854
+ if (uploadData && uploadData.results && uploadData.results.length > 0) {
855
+ const file = uploadData.results[0];
856
+ console.log(chalk.green(`File "${fileName}" uploaded successfully!`));
857
+ console.log(chalk.dim(`Path: ${file.path}`));
858
+ console.log(chalk.dim(`UID: ${file.uid}`));
859
+ } else {
860
+ console.error(chalk.red(`Failed to upload file "${fileName}". Invalid response from server.`));
861
+ }
862
+ }
863
+ } catch (error) {
864
+ console.error(chalk.red(`Failed to upload files.\nError: ${error.message}`));
865
+ }
866
+ }
867
+
868
+ /**
869
+ * Get a temporary CSRF Token
870
+ * @returns The CSRF token
871
+ */
872
+ async function getCsrfToken() {
873
+ const csrfResponse = await fetch(`${BASE_URL}/get-anticsrf-token`, {
874
+ method: 'GET',
875
+ headers: getHeaders()
876
+ });
877
+
878
+ if (!csrfResponse.ok) {
879
+ console.error(chalk.red('Failed to fetch CSRF token.'));
880
+ return;
881
+ }
882
+
883
+ const csrfData = await csrfResponse.json();
884
+ if (!csrfData || !csrfData.token) {
885
+ console.error(chalk.red('Failed to fetch anti-CSRF token.'));
886
+ return;
887
+ }
888
+
889
+ return csrfData.token;
890
+ }
891
+
892
+ /**
893
+ * Download a file from the Puter server to the host machine
894
+ * @param {Array} args - The arguments passed to the command (remote file path, Optional: local path).
895
+ */
896
+ export async function downloadFile(args = []) {
897
+ if (args.length < 1) {
898
+ console.log(chalk.red('Usage: pull <remote_file_path> [local_path] [overwrite]'));
899
+ return;
900
+ }
901
+
902
+ const remotePathPattern = resolvePath(getCurrentDirectory(), args[0]); // Resolve the remote file path pattern
903
+ const localBasePath = path.dirname(args.length > 1 ? args[1] : '.'); // Default to the current directory
904
+ const overwrite = args.length > 2 ? args[2] === 'true' : false; // Default: false
905
+
906
+ console.log(chalk.green(`Downloading files matching "${remotePathPattern}" to "${localBasePath}"...\n`));
907
+
908
+ try {
909
+ // Step 1: Fetch the list of files and directories from the server
910
+ const listResponse = await fetch(`${API_BASE}/readdir`, {
911
+ method: 'POST',
912
+ headers: getHeaders(),
913
+ body: JSON.stringify({ path: getCurrentDirectory() })
914
+ });
915
+
916
+ if (!listResponse.ok) {
917
+ console.error(chalk.red('Failed to list files from the server.'));
918
+ return;
919
+ }
920
+
921
+ const files = await listResponse.json();
922
+ if (!Array.isArray(files) || files.length === 0) {
923
+ console.error(chalk.red('No files or directories found on the server.'));
924
+ return;
925
+ }
926
+
927
+ // Step 2: Recursively find files matching the pattern
928
+ const matchedFiles = await findMatchingFiles(files, remotePathPattern, getCurrentDirectory());
929
+
930
+ if (matchedFiles.length === 0) {
931
+ console.error(chalk.red('No files found matching the pattern.'));
932
+ return;
933
+ }
934
+
935
+ // Step 3: Download each matched file
936
+ for (const remoteFilePath of matchedFiles) {
937
+ const relativePath = path.relative(getCurrentDirectory(), remoteFilePath);
938
+ const localFilePath = path.join(localBasePath, relativePath);
939
+
940
+ // Ensure the local directory exists
941
+ if (!fs.existsSync(path.dirname(localFilePath))){
942
+ fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
943
+ }
944
+
945
+ console.log(chalk.green(`Downloading file "${remoteFilePath}" to "${localFilePath}"...`));
946
+
947
+ // Fetch the anti-CSRF token
948
+ const antiCsrfToken = await getCsrfToken();
949
+
950
+ const downloadResponse = await fetch(`${BASE_URL}/down?path=${remoteFilePath}`, {
951
+ method: 'POST',
952
+ headers: {
953
+ ...getHeaders('application/x-www-form-urlencoded'),
954
+ "cookie": `puter_auth_token=${getAuthToken()};`
955
+ },
956
+ "referrerPolicy": "strict-origin-when-cross-origin",
957
+ body: `anti_csrf=${antiCsrfToken}`
958
+ });
959
+
960
+ if (!downloadResponse.ok) {
961
+ console.error(chalk.red(`Failed to download file "${remoteFilePath}". Server response: ${downloadResponse.statusText}`));
962
+ continue;
963
+ }
964
+
965
+ // Step 5: Save the file content to the local filesystem
966
+ const fileContent = await downloadResponse.text();
967
+
968
+ // Check if the file exists, if so then delete it before writing.
969
+ if (overwrite && fs.existsSync(localFilePath)) {
970
+ fs.unlinkSync(localFilePath);
971
+ console.log(chalk.yellow(`File "${localFilePath}" already exists. Overwriting...`));
972
+ }
973
+
974
+ fs.writeFileSync(localFilePath, fileContent, 'utf8');
975
+ const fileSize = fs.statSync(localFilePath).size;
976
+ console.log(chalk.green(`File: "${remoteFilePath}" downloaded to "${localFilePath}" (size: ${formatSize(fileSize)})`));
977
+ }
978
+ } catch (error) {
979
+ console.error(chalk.red(`Failed to download files.\nError: ${error.message}`));
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Copy files or directories from one location to another on the Puter server (similar to Unix "cp" command).
985
+ * @param {Array} args - The arguments passed to the command (source path, destination path).
986
+ */
987
+ export async function copyFile(args = []) {
988
+ if (args.length < 2) {
989
+ console.log(chalk.red('Usage: cp <source_path> <destination_path>'));
990
+ return;
991
+ }
992
+
993
+ const sourcePath = args[0].startsWith(`/${getCurrentUserName()}`) ? args[0] : resolvePath(getCurrentDirectory(), args[0]); // Resolve the source path
994
+ const destinationPath = args[1].startsWith(`/${getCurrentUserName()}`) ? args[1] : resolvePath(getCurrentDirectory(), args[1]); // Resolve the destination path
995
+
996
+ console.log(chalk.green(`Copy: "${chalk.dim(sourcePath)}" to: "${chalk.dim(destinationPath)}"...\n`));
997
+ try {
998
+ // Step 1: Check if the source is a directory or a file
999
+ const statResponse = await fetch(`${API_BASE}/stat`, {
1000
+ method: 'POST',
1001
+ headers: getHeaders(),
1002
+ body: JSON.stringify({
1003
+ path: sourcePath
1004
+ })
1005
+ });
1006
+
1007
+ if (!statResponse.ok) {
1008
+ console.error(chalk.red(`Failed to check source path. Server response: ${await statResponse.text()}`));
1009
+ return;
1010
+ }
1011
+
1012
+ const statData = await statResponse.json();
1013
+ if (!statData || !statData.id) {
1014
+ console.error(chalk.red(`Source path "${sourcePath}" does not exist.`));
1015
+ return;
1016
+ }
1017
+
1018
+ if (statData.is_dir) {
1019
+ // Step 2: If source is a directory, copy all files recursively
1020
+ const files = await listFiles([sourcePath]);
1021
+ for (const file of files) {
1022
+ const relativePath = file.path.replace(sourcePath, '');
1023
+ const destPath = path.join(destinationPath, relativePath);
1024
+
1025
+ const copyResponse = await fetch(`${API_BASE}/copy`, {
1026
+ method: 'POST',
1027
+ headers: getHeaders(),
1028
+ body: JSON.stringify({
1029
+ source: file.path,
1030
+ destination: destPath
1031
+ })
1032
+ });
1033
+
1034
+ if (!copyResponse.ok) {
1035
+ console.error(chalk.red(`Failed to copy file "${file.path}". Server response: ${await copyResponse.text()}`));
1036
+ continue;
1037
+ }
1038
+
1039
+ const copyData = await copyResponse.json();
1040
+ if (copyData && copyData.length > 0 && copyData[0].copied) {
1041
+ console.log(chalk.green(`File "${chalk.dim(file.path)}" copied successfully to "${chalk.dim(copyData[0].copied.path)}"!`));
1042
+ } else {
1043
+ console.error(chalk.red(`Failed to copy file "${file.path}". Invalid response from server.`));
1044
+ }
1045
+ }
1046
+ } else {
1047
+ // Step 3: If source is a file, copy it directly
1048
+ const copyResponse = await fetch(`${API_BASE}/copy`, {
1049
+ method: 'POST',
1050
+ headers: getHeaders(),
1051
+ body: JSON.stringify({
1052
+ source: sourcePath,
1053
+ destination: destinationPath
1054
+ })
1055
+ });
1056
+
1057
+ if (!copyResponse.ok) {
1058
+ console.error(chalk.red(`Failed to copy file. Server response: ${await copyResponse.text()}`));
1059
+ return;
1060
+ }
1061
+
1062
+ const copyData = await copyResponse.json();
1063
+ if (copyData && copyData.length > 0 && copyData[0].copied) {
1064
+ console.log(chalk.green(`File "${sourcePath}" copied successfully to "${copyData[0].copied.path}"!`));
1065
+ console.log(chalk.dim(`UID: ${copyData[0].copied.uid}`));
1066
+ } else {
1067
+ console.error(chalk.red('Failed to copy file. Invalid response from server.'));
1068
+ }
1069
+ }
1070
+ } catch (error) {
1071
+ console.error(chalk.red(`Failed to copy file.\nError: ${error.message}`));
1072
+ }
1073
+ }
1074
+
1075
+
1076
+ /**
1077
+ * List all files in a local directory.
1078
+ * @param {string} localDir - The local directory path.
1079
+ * @returns {Array} - Array of local file objects.
1080
+ */
1081
+ function listLocalFiles(localDir) {
1082
+ const files = [];
1083
+ const walkDir = (dir) => {
1084
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1085
+ for (const entry of entries) {
1086
+ const fullPath = path.join(dir, entry.name);
1087
+ if (entry.isDirectory()) {
1088
+ walkDir(fullPath);
1089
+ } else {
1090
+ files.push({
1091
+ relativePath: path.relative(localDir, fullPath),
1092
+ localPath: fullPath,
1093
+ size: fs.statSync(fullPath).size,
1094
+ modified: fs.statSync(fullPath).mtime.getTime()
1095
+ });
1096
+ }
1097
+ }
1098
+ };
1099
+
1100
+ walkDir(localDir);
1101
+ return files;
1102
+ }
1103
+
1104
+ /**
1105
+ * Compare local and remote files to determine actions.
1106
+ * @param {Array} localFiles - Array of local file objects.
1107
+ * @param {Array} remoteFiles - Array of remote file objects.
1108
+ * @param {string} localDir - Local directory path.
1109
+ * @param {string} remoteDir - Remote directory path.
1110
+ * @returns {Object} - Object containing files to upload, download, and delete.
1111
+ */
1112
+ function compareFiles(localFiles, remoteFiles, localDir, remoteDir) {
1113
+ const toUpload = []; // Files to upload to remote
1114
+ const toDownload = []; // Files to download from remote
1115
+ const toDelete = []; // Files to delete from remote
1116
+
1117
+ // Create a map of remote files for quick lookup
1118
+ const remoteFileMap = new Map();
1119
+ remoteFiles.forEach(file => {
1120
+ remoteFileMap.set(file.name, {
1121
+ size: file.size,
1122
+ modified: new Date(file.modified).getTime()
1123
+ });
1124
+ });
1125
+
1126
+ // Check local files
1127
+ for (const file of localFiles) {
1128
+ const remoteFile = remoteFileMap.get(file.relativePath);
1129
+ if (!remoteFile || file.modified > remoteFile.modified) {
1130
+ toUpload.push(file); // New or updated file
1131
+ }
1132
+ }
1133
+
1134
+ // Check remote files
1135
+ for (const file of remoteFiles) {
1136
+ const localFile = localFiles.find(f => f.relativePath === file.name);
1137
+ if (localFile){
1138
+ console.log(`localFile: ${localFile.relativePath}, modified: ${localFile.modified}`);
1139
+ }
1140
+ console.log(`file: ${file.name}, modified: ${file.modified}`);
1141
+ if (!localFile) {
1142
+ toDelete.push({ relativePath: file.name }); // Extra file in remote
1143
+ } else if (file.modified > parseInt(localFile.modified / 1000)) {
1144
+ toDownload.push(localFile); // New or updated file in remote
1145
+ }
1146
+ }
1147
+
1148
+ return { toUpload, toDownload, toDelete };
1149
+ }
1150
+
1151
+ /**
1152
+ * Find conflicts where the same file has been modified in both locations.
1153
+ * @param {Array} toUpload - Files to upload.
1154
+ * @param {Array} toDownload - Files to download.
1155
+ * @returns {Array} - Array of conflicting file paths.
1156
+ */
1157
+ function findConflicts(toUpload, toDownload) {
1158
+ const conflicts = [];
1159
+ const uploadPaths = toUpload.map(file => file.relativePath);
1160
+ const downloadPaths = toDownload.map(file => file.relativePath);
1161
+
1162
+ for (const path of uploadPaths) {
1163
+ if (downloadPaths.includes(path)) {
1164
+ conflicts.push(path);
1165
+ }
1166
+ }
1167
+
1168
+ return conflicts;
1169
+ }
1170
+
1171
+ /**
1172
+ * Resolve given local path directory
1173
+ * @param {string} localPath The local path to resolve
1174
+ * @returns {Promise<string>} The resolved absolute path
1175
+ * @throws {Error} If the path does not exist or is not a directory
1176
+ */
1177
+ async function resolveLocalDirectory(localPath) {
1178
+ // Resolve the path to an absolute path
1179
+ const absolutePath = path.resolve(localPath);
1180
+
1181
+ // Check if the path exists
1182
+ if (!fs.existsSync(absolutePath)) {
1183
+ throw new Error(`Path does not exist: ${absolutePath}`);
1184
+ }
1185
+
1186
+ // Check if the path is a directory
1187
+ const stats = await fs.promises.stat(absolutePath);
1188
+ if (!stats.isDirectory()) {
1189
+ throw new Error(`Path is not a directory: ${absolutePath}`);
1190
+ }
1191
+ return absolutePath;
1192
+ }
1193
+
1194
+ /**
1195
+ * Synchronize a local directory with a remote directory on Puter.
1196
+ * @param {string[]} args - Command-line arguments (e.g., [localDir, remoteDir]).
1197
+ */
1198
+ export async function syncDirectory(args = []) {
1199
+ if (args.length < 2) {
1200
+ console.log(chalk.red('Usage: update <local_directory> <remote_directory> [--delete]'));
1201
+ return;
1202
+ }
1203
+
1204
+ const localDir = await resolveLocalDirectory(args[0]);
1205
+ const remoteDir = resolvePath(getCurrentDirectory(), args[1]);
1206
+ const deleteFlag = args.includes('--delete'); // Whether to delete extra files
1207
+
1208
+ console.log(chalk.green(`Syncing local directory "${localDir}" with remote directory "${remoteDir}"...\n`));
1209
+
1210
+ try {
1211
+ // Step 1: Validate local directory
1212
+ if (!fs.existsSync(localDir)) {
1213
+ console.error(chalk.red(`Local directory "${localDir}" does not exist.`));
1214
+ return;
1215
+ }
1216
+
1217
+ // Step 2: Fetch remote directory contents
1218
+ const remoteFiles = await listRemoteFiles(remoteDir);
1219
+ if (!Array.isArray(remoteFiles)) {
1220
+ console.error(chalk.red('Failed to fetch remote directory contents.'));
1221
+ return;
1222
+ }
1223
+
1224
+ // Step 3: List local files
1225
+ const localFiles = listLocalFiles(localDir);
1226
+
1227
+ // Step 4: Compare local and remote files
1228
+ let { toUpload, toDownload, toDelete } = compareFiles(localFiles, remoteFiles, localDir, remoteDir);
1229
+
1230
+ // Step 5: Handle conflicts (if any)
1231
+ const conflicts = findConflicts(toUpload, toDownload);
1232
+ if (conflicts.length > 0) {
1233
+ console.log(chalk.yellow('The following files have conflicts:'));
1234
+ conflicts.forEach(file => console.log(chalk.dim(`- ${file}`)));
1235
+
1236
+ const { resolve } = await inquirer.prompt([
1237
+ {
1238
+ type: 'list',
1239
+ name: 'resolve',
1240
+ message: 'How would you like to resolve conflicts?',
1241
+ choices: [
1242
+ { name: 'Keep local version', value: 'local' },
1243
+ { name: 'Keep remote version', value: 'remote' },
1244
+ { name: 'Skip conflicting files', value: 'skip' }
1245
+ ]
1246
+ }
1247
+ ]);
1248
+
1249
+ if (resolve === 'local') {
1250
+ toDownload = toDownload.filter(file => !conflicts.includes(file.relativePath));
1251
+ } else if (resolve === 'remote') {
1252
+ toUpload = toUpload.filter(file => !conflicts.includes(file.relativePath));
1253
+ } else {
1254
+ toUpload = toUpload.filter(file => !conflicts.includes(file.relativePath));
1255
+ toDownload = toDownload.filter(file => !conflicts.includes(file.relativePath));
1256
+ }
1257
+ }
1258
+
1259
+ // Step 6: Perform synchronization
1260
+ console.log(chalk.green('Starting synchronization...'));
1261
+
1262
+ // Upload new/updated files
1263
+ for (const file of toUpload) {
1264
+ console.log(chalk.cyan(`Uploading "${file.relativePath}"...`));
1265
+ const dedupeName = 'false';
1266
+ const overwrite = 'true';
1267
+ await uploadFile([file.localPath, remoteDir, dedupeName, overwrite]);
1268
+ }
1269
+
1270
+ // Download new/updated files
1271
+ for (const file of toDownload) {
1272
+ console.log(chalk.cyan(`Downloading "${file.relativePath}"...`));
1273
+ const overwrite = 'true';
1274
+ await downloadFile([file.relativePath, file.localPath, overwrite]);
1275
+ }
1276
+
1277
+ // Delete extra files (if --delete flag is set)
1278
+ if (deleteFlag) {
1279
+ for (const file of toDelete) {
1280
+ console.log(chalk.yellow(`Deleting "${file.relativePath}"...`));
1281
+ await removeFileOrDirectory([path.join(remoteDir, file.relativePath), '-f']);
1282
+ }
1283
+ }
1284
+
1285
+ console.log(chalk.green('Synchronization complete!'));
1286
+ } catch (error) {
1287
+ console.error(chalk.red('Failed to synchronize directories.'));
1288
+ console.error(chalk.red(`Error: ${error.message}`));
1289
+ }
1290
+ }