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