UNPKG

puter-cli

Version:

Command line interface for Puter cloud platform

1,542 lines 59.2 kB
import fs from 'node:fs';
import os from 'node:os';
import { execSync } from 'node:child_process';
import { glob } from 'glob';
import path from 'path';
import { minimatch } from 'minimatch';
import chalk from 'chalk';
import Conf from 'conf';
import fetch from 'node-fetch';
import { API_BASE, BASE_URL, PROJECT_NAME, getHeaders, showDiskSpaceUsage, resolvePath } from '../commons.js';
import { formatDateTime, formatSize, getSystemEditor } from '../utils.js';
import inquirer from 'inquirer';
import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
import { updatePrompt } from './shell.js';
import crypto from '../crypto.js';


const config = new Conf({ projectName: PROJECT_NAME });


/**
 * List files in given path
 * @param {string} path Path to the file or directory
 * @returns List of files found
 */
export async function listRemoteFiles(path) {
    const response = await fetch(`${API_BASE}/readdir`, {
        method: 'POST',
        headers: getHeaders(),
        body: JSON.stringify({ path })
    });
    return await response.json();
}

/**
 * List files in the current working directory.
 * @param {string} args Default current working directory
 */
export async function listFiles(args = []) {
  const names = args.length > 0 ? args : ['.'];
  for (let path of names)
    try {
        if (!path.startsWith('/')){
            path =  resolvePath(getCurrentDirectory(), path);
        }
        if (!(await pathExists(path))){
            console.log(chalk.yellow(`Directory ${chalk.red(path)} doesn't exists!`));
            continue;
        }
        console.log(chalk.green(`Listing files in ${chalk.dim(path)}:\n`));
        const files = await listRemoteFiles(path);
        if (Array.isArray(files) && files.length > 0) {
            console.log(chalk.cyan(`Type  Name                 Size    Modified          UID`));
            console.log(chalk.dim('----------------------------------------------------------------------------------'));
            files.forEach(file => {
                const type = file.is_dir ? 'd' : '-';
                const write = file.writable ? 'w' : '-';
                const name = file.name.padEnd(20);
                const size = file.is_dir ? '0' : formatSize(file.size);
                const modified = formatDateTime(file.modified);
                const uid = file.uid?.split('-');
                console.log(`${type}${write}   ${name} ${size.padEnd(8)} ${modified}  ${uid[0]}-...-${uid.slice(-1)}`);
            });
            console.log(chalk.green(`There are ${files.length} object(s).`));
        } else {
            console.log(chalk.red('No files or directories found.'));
        }
    } catch (error) {
        console.log(chalk.red('Failed to list files.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Create a folder in the current working directory.
 * @param {Array} args Options
 * @returns void
 */
export async function makeDirectory(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: mkdir <directory_name>'));
        return;
    }

    const directoryName = args[0];
    console.log(chalk.green(`Creating directory "${directoryName}" in "${getCurrentDirectory()}"...\n`));

    try {
        const response = await fetch(`${API_BASE}/mkdir`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                parent: getCurrentDirectory(),
                path: directoryName,
                overwrite: false,
                dedupe_name: true,
                create_missing_parents: false
            })
        });

        const data = await response.json();
        if (data && data.id) {
            console.log(chalk.green(`Directory "${directoryName}" created successfully!`));
            console.log(chalk.dim(`Path: ${data.path}`));
            console.log(chalk.dim(`UID: ${data.uid}`));
        } else {
            console.log(chalk.red('Failed to create directory. Please check your input.'));
        }
    } catch (error) {
        console.log(chalk.red('Failed to create directory.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Rename a file or directory
 * @param {Array} args Options
 * @returns void
 */
export async function renameFileOrDirectory(args = []) {
    if (args.length < 2) {
        console.log(chalk.red('Usage: mv <source> <destination>'));
        return;
    }

    const sourcePath = args[0].startsWith('/') ? args[0] : resolvePath(getCurrentDirectory(), args[0]);
    const destPath = args[1].startsWith('/') ? args[1] : resolvePath(getCurrentDirectory(), args[1]);

    console.log(chalk.green(`Moving "${sourcePath}" to "${destPath}"...\n`));

    try {
        // Step 1: Get the source file/directory info
        const statResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({ path: sourcePath })
        });

        const statData = await statResponse.json();
        if (!statData || !statData.uid) {
            console.log(chalk.red(`Could not find source "${sourcePath}".`));
            return;
        }

        const sourceUid = statData.uid;
        const sourceName = statData.name;

        // Step 2: Check if destination is an existing directory
        const destStatResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({ path: destPath })
        });

        const destData = await destStatResponse.json();
        
        // Determine if this is a rename or move operation
        const isMove = destData && destData.is_dir;
        const newName = isMove ? sourceName : path.basename(destPath);
        const destination = isMove ? destPath : path.dirname(destPath);

        if (isMove) {
            // Move operation: use /move endpoint
            const moveResponse = await fetch(`${API_BASE}/move`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    source: sourceUid,
                    destination: destination,
                    overwrite: false,
                    new_name: newName,
                    create_missing_parents: false,
                    new_metadata: {}
                })
            });

            const moveData = await moveResponse.json();
            if (moveData && moveData.moved) {
                console.log(chalk.green(`Successfully moved "${sourcePath}" to "${moveData.moved.path}"!`));
            } else {
                console.log(chalk.red('Failed to move item. Please check your input.'));
            }
        } else {
            // Rename operation: use /rename endpoint
            const renameResponse = await fetch(`${API_BASE}/rename`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    uid: sourceUid,
                    new_name: newName
                })
            });

            const renameData = await renameResponse.json();
            if (renameData && renameData.uid) {
                console.log(chalk.green(`Successfully renamed "${sourcePath}" to "${renameData.path}"!`));
            } else {
                console.log(chalk.red('Failed to rename item. Please check your input.'));
            }
        }
    } catch (error) {
        console.log(chalk.red('Failed to move/rename item.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Helper function to recursively find files matching the pattern
 * @param {Array} files List of files
 * @param {string} pattern The pattern to find
 * @param {string} basePath the base path
 * @returns array of matching files
 */
async function findMatchingFiles(files, pattern, basePath) {
    const matchedPaths = [];

    for (const file of files) {
        const filePath = path.join(basePath, file.name);

        // Check if the current file/directory matches the pattern
        if (minimatch(filePath, pattern, { dot: true })) {
            matchedPaths.push(filePath);
        }

        // If it's a directory, recursively search its contents
        if (file.is_dir) {
            const dirResponse = await fetch(`${API_BASE}/readdir`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({ path: filePath })
            });

            if (dirResponse.ok) {
                const dirFiles = await dirResponse.json();
                const dirMatches = await findMatchingFiles(dirFiles, pattern, filePath);
                matchedPaths.push(...dirMatches);
            }
        }
    }

    return matchedPaths;
}

/**
 * Find files matching the pattern in the local directory (DEPRECATED: Not used)
 * @param {string} localDir - Local directory path.
 * @param {string} pattern - File pattern (e.g., "*.html", "myapp/*").
 * @returns {Array} - Array of file objects with local and relative paths.
 */
function findLocalMatchingFiles(localDir, pattern) {
    const files = [];
    const walkDir = (dir) => {
        const entries = fs.readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                walkDir(fullPath); // Recursively traverse directories
            } else if (minimatch(fullPath, path.join(localDir, pattern), { dot: true })) {
                files.push({
                    localPath: fullPath,
                    relativePath: path.relative(localDir, fullPath)
                });
            }
        }
    };

    walkDir(localDir);
    return files;
}

/**
 * Move a file/directory to the Trash
 * @param {Array} args Options:
 * -f: Force delete (no confirmation)
 * @returns void
 */
export async function removeFileOrDirectory(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: rm <name> [-f]'));
        return;
    }

    const skipConfirmation = args.includes('-f'); // Check the flag if provided
    const names = skipConfirmation ? args.filter(option => option !== '-f') : args;

    try {
        // Step 1: Fetch the list of files and directories from the server
        const listResponse = await fetch(`${API_BASE}/readdir`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({ path: getCurrentDirectory() })
        });

        if (!listResponse.ok) {
            console.error(chalk.red('Failed to list files from the server.'));
            return;
        }

        const files = await listResponse.json();
        if (!Array.isArray(files) || files.length == 0) {
            console.error(chalk.red('No files or directories found on the server.'));
            return;
        }

        // Step 2: Find all files/directories matching the provided patterns
        const matchedPaths = [];
        for (const name of names) {
            if (name.startsWith('/')){
                const pattern = resolvePath('/', name);
                matchedPaths.push(pattern);
                continue;
            }
            const pattern = resolvePath(getCurrentDirectory(), name);
            const matches = await findMatchingFiles(files, pattern, getCurrentDirectory());
            matchedPaths.push(...matches);
        }

        if (matchedPaths.length === 0) {
            console.error(chalk.red('No files or directories found matching the pattern.'));
            return;
        }

        // Step 3: Prompt for confirmation (unless -f flag is provided)
        if (!skipConfirmation) {
            console.log(chalk.yellow(`The following items will be moved to Trash:`));
            console.log(chalk.cyan('Hint: Execute "clean" to empty the Trash.'));
            matchedPaths.forEach(path => console.log(chalk.dim(`- ${path}`)));

            const { confirm } = await inquirer.prompt([
                {
                    type: 'confirm',
                    name: 'confirm',
                    message: `Are you sure you want to move these ${matchedPaths.length} item(s) to Trash?`,
                    default: false
                }
            ]);

            if (!confirm) {
                console.log(chalk.yellow('Operation canceled.'));
                return;
            }
        }

        // Step 4: Move each matched file/directory to Trash
        for (const path of matchedPaths) {
            try {
                console.log(chalk.green(`Preparing to remove "${path}"...`));

                // Step 4.1: Get the UID of the file/directory
                const statResponse = await fetch(`${API_BASE}/stat`, {
                    method: 'POST',
                    headers: getHeaders(),
                    body: JSON.stringify({ path })
                });

                const statData = await statResponse.json();
                if (!statData || !statData.uid) {
                    console.error(chalk.red(`Could not find file or directory with path "${path}".`));
                    continue;
                }

                const uid = statData.uid;
                const originalPath = statData.path;

                // Step 4.2: Perform the move operation to Trash
                const moveResponse = await fetch(`${API_BASE}/move`, {
                    method: 'POST',
                    headers: getHeaders(),
                    body: JSON.stringify({
                        source: uid,
                        destination: `/${getCurrentUserName()}/Trash`,
                        overwrite: false,
                        new_name: uid, // Use the UID as the new name in Trash
                        create_missing_parents: false,
                        new_metadata: {
                            original_name: path.split('/').pop(),
                            original_path: originalPath,
                            trashed_ts: Math.floor(Date.now() / 1000) // Current timestamp
                        }
                    })
                });

                const moveData = await moveResponse.json();
                if (moveData && moveData.moved) {
                    console.log(chalk.green(`Successfully moved "${path}" to Trash!`));
                    console.log(chalk.dim(`Moved to: ${moveData.moved.path}`));
                } else {
                    console.error(chalk.red(`Failed to move "${path}" to Trash.`));
                }
            } catch (error) {
                console.error(chalk.red(`Failed to remove "${path}".`));
                console.error(chalk.red(`Error: ${error.message}`));
            }
        }
    } catch (error) {
        console.error(chalk.red('Failed to remove items.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Delete a folder and its contents (PREVENTED BY PUTER API)
 * @param {string} folderPath - The path of the folder to delete (defaults to Trash).
 * @param {boolean} skipConfirmation - Whether to skip the confirmation prompt.
 */
export async function deleteFolder(folderPath, skipConfirmation = false) {
    console.log(chalk.green(`Preparing to delete "${folderPath}"...\n`));

    try {
        // Step 1: Prompt for confirmation (unless skipConfirmation is true)
        if (!skipConfirmation) {
            const { confirm } = await inquirer.prompt([
                {
                    type: 'confirm',
                    name: 'confirm',
                    message: `Are you sure you want to delete all contents of "${folderPath}"?`,
                    default: false
                }
            ]);

            if (!confirm) {
                console.log(chalk.yellow('Operation canceled.'));
                return;
            }
        }

        // Step 2: Perform the delete operation
        const deleteResponse = await fetch(`${API_BASE}/delete`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                paths: [folderPath],
                descendants_only: true, // Delete only the contents, not the folder itself
                recursive: true // Delete all subdirectories and files
            })
        });

        const deleteData = await deleteResponse.json();
        if (deleteResponse.ok && Object.keys(deleteData).length == 0) {
            console.log(chalk.green(`Successfully deleted all contents from: ${chalk.cyan(folderPath)}`));
        } else {
            console.log(chalk.red('Failed to delete folder. Please check your input.'));
        }
    } catch (error) {
        console.log(chalk.red('Failed to delete folder.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Empty the Trash (wrapper for deleteFolder).
 * @param {boolean} skipConfirmation - Whether to skip the confirmation prompt.
 */
export async function emptyTrash(skipConfirmation = true) {
    const trashPath = `/${getCurrentUserName()}/Trash`;
    await deleteFolder(trashPath, skipConfirmation);
}

/**
 * Show statistical information about the current working directory.
 * @param {Array} args array of path names
 */
export async function getInfo(args = []) {
    const names = args.length > 0 ? args : ['.'];
    for (let name of names)
        try {
            name = `${getCurrentDirectory()}/${name}`;
            console.log(chalk.green(`Getting stat info for: "${name}"...\n`));
            const response = await fetch(`${API_BASE}/stat`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    path: name
                })
            });
            const data = await response.json();
            if (response.ok && data) {
                console.log(chalk.cyan('File/Directory Information:'));
                console.log(chalk.dim('----------------------------------------'));
                console.log(chalk.cyan(`Name: `) + chalk.white(data.name));
                console.log(chalk.cyan(`Path: `) + chalk.white(data.path));
                console.log(chalk.cyan(`Type: `) + chalk.white(data.is_dir ? 'Directory' : 'File'));
                console.log(chalk.cyan(`Size: `) + chalk.white(data.size ? formatSize(data.size) : 'N/A'));
                console.log(chalk.cyan(`Created: `) + chalk.white(new Date(data.created * 1000).toLocaleString()));
                console.log(chalk.cyan(`Modified: `) + chalk.white(new Date(data.modified * 1000).toLocaleString()));
                console.log(chalk.cyan(`Writable: `) + chalk.white(data.writable ? 'Yes' : 'No'));
                console.log(chalk.cyan(`Owner: `) + chalk.white(data.owner.username));
                console.log(chalk.dim('----------------------------------------'));
            } else {
                console.error(chalk.red('Unable to get stat info. Please check your credentials.'));
            }
        } catch (error) {
            console.error(chalk.red(`Failed to get stat info.\nError: ${error.message}`));
        }
}

/**
 * Show the current working directory
 */
export async function showCwd() {
    console.log(chalk.green(`${config.get('cwd')}`));
}

/**
 * Change the current working directory
 * @param {Array} args - The path arguments
 * @returns void
 */
export async function changeDirectory(args) {
    let currentPath = config.get('cwd');
    // If no arguments, print the current directory
    if (!args.length) {
        console.log(chalk.green(currentPath));
        return;
    }

    const path = args[0];
    // Handle "/","~",".." and deeper navigation
    const newPath = path.startsWith('/')? path: (path === '~'? `/${getCurrentUserName()}` :resolvePath(currentPath, path));
    try {
        // Check if the new path is a valid directory
        const response = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                path: newPath
            })
        });

        const data = await response.json();
        if (response.ok && data && data.is_dir) {
            // Update the newPath to use the correct name from the response
            const arrayDirs = newPath.split('/');
            arrayDirs.pop();
            arrayDirs.push(data.name);
            updatePrompt(arrayDirs.join('/')); // Update the shell prompt
        } else {
            console.log(chalk.red(`"${newPath}" is not a directory`));
        }
    } catch (error) {
        console.log(chalk.red(`Cannot access "${newPath}": ${error.message}`));
    }
}

/**
 * Fetch disk usage information
 * @param {Object} body - Optional arguments to include in the request body.
 */
export async function getDiskUsage(body = null) {
    console.log(chalk.green('Fetching disk usage information...\n'));
    try {
        const response = await fetch(`${API_BASE}/df`, {
            method: 'POST',
            headers: getHeaders(),
            body: body ? JSON.stringify(body) : null
        });

        const data = await response.json();
        if (response.ok && data) {
            showDiskSpaceUsage(data);
        } else {
            console.error(chalk.red('Unable to fetch disk usage information.'));
        }
    } catch (error) {
        console.error(chalk.red(`Failed to fetch disk usage information.\nError: ${error.message}`));
    }
}

/**
 * Check if a path exists
 * @param {string} filePath List of files/directories
 */
export async function pathExists(filePath) {
    if (filePath.length < 1) {
        console.log(chalk.red('No path provided.'));
        return false;
    }
    try {
        // Step 1: Check if the file already exists
        const statResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                path: filePath
            })
        });

        return statResponse.ok;
    } catch (error){
        console.error(chalk.red('Failed to check if file exists.'));
            return false;
    }    
}

/**
 * Create a new file (similar to Unix "touch" command).
 * @param {Array} args - The arguments passed to the command (file name and optional content).
 * @returns {boolean} - True if the file was created successfully, false otherwise.
 */
export async function createFile(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: touch <file_name> [content]'));
        return false;
    }

    const filePath = args[0]; // File path (e.g., "app/index.html")
    const content = args.length > 1 ? args.slice(1).join(' ') : ''; // Optional content
    let fullPath = filePath;
    if (!filePath.startsWith(`/${getCurrentUserName()}/`)){
        fullPath = resolvePath(getCurrentDirectory(), filePath); // Resolve the full path
    }
    const dirName = path.dirname(fullPath); // Extract the directory name
    const fileName = path.basename(fullPath); // Extract the file name
    const dedupeName = false; // Default: false
    const overwrite = true; // Default: true

    console.log(chalk.green(`Creating file:\nFileName: "${chalk.dim(fileName)}"\nPath: "${chalk.dim(dirName)}"\nContent Length: ${chalk.dim(content.length)}`));
    try {
        // Step 1: Check if the file already exists
        const statResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                path: fullPath
            })
        });

        if (statResponse.ok) {
            const statData = await statResponse.json();
            if (statData && statData.id) {
                if (!overwrite) {
                    console.error(chalk.red(`File "${filePath}" already exists. Use --overwrite=true to replace it.`));
                    return false;
                }
                console.log(chalk.yellow(`File "${filePath}" already exists. It will be overwritten.`));
            }
        } else if (statResponse.status !== 404) {
            console.error(chalk.red('Failed to check if file exists.'));
            return false;
        }

        // Step 2: Check disk space
        const dfResponse = await fetch(`${API_BASE}/df`, {
            method: 'POST',
            headers: getHeaders(),
            body: null
        });

        if (!dfResponse.ok) {
            console.error(chalk.red('Unable to check disk space.'));
            return false;
        }

        const dfData = await dfResponse.json();
        if (dfData.used >= dfData.capacity) {
            console.error(chalk.red('Not enough disk space to create the file.'));
            showDiskSpaceUsage(dfData); // Display disk usage info
            return false;
        }

        // Step 3: Create the nested directories if they don't exist
        const dirStatResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                path: dirName
            })
        });

        if (!dirStatResponse.ok || dirStatResponse.status === 404) {
            // Create the directory if it doesn't exist
            await fetch(`${API_BASE}/mkdir`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    parent: path.dirname(dirName),
                    path: path.basename(dirName),
                    overwrite: false,
                    dedupe_name: true,
                    create_missing_parents: true
                })
            });
        }

        // Step 4: Create the file using /batch
        const operationId = crypto.randomUUID(); // Generate a unique operation ID
        const socketId = 'undefined'; // Placeholder socket ID
        const boundary = `----WebKitFormBoundary${crypto.randomUUID().replace(/-/g, '')}`;
        const fileBlob = new Blob([content || ''], { type: 'text/plain' });

        const formData = `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="operation_id"\r\n\r\n${operationId}\r\n` +
            `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="socket_id"\r\n\r\n${socketId}\r\n` +
            `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${socketId}\r\n` +
            `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${JSON.stringify({
                name: fileName,
                type: 'text/plain',
                size: fileBlob.size
            })}\r\n` +
            `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="operation"\r\n\r\n${JSON.stringify({
                op: 'write',
                dedupe_name: dedupeName,
                overwrite: overwrite,
                operation_id: operationId,
                path: dirName,
                name: fileName,
                item_upload_id: 0
            })}\r\n` +
            `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
            `Content-Type: text/plain\r\n\r\n${content || ''}\r\n` +
            `--${boundary}--\r\n`;

        // Send the request
        const createResponse = await fetch(`${API_BASE}/batch`, {
            method: 'POST',
            headers: getHeaders(`multipart/form-data; boundary=${boundary}`),
            body: formData
        });

        if (!createResponse.ok) {
            const errorText = await createResponse.text();
            console.error(chalk.red(`Failed to create file. Server response: ${errorText}. status: ${createResponse.status}`));
            return false;
        }

        const createData = await createResponse.json();
        if (createData && createData.results && createData.results.length > 0) {
            const file = createData.results[0];
            console.log(chalk.green(`File "${filePath}" created successfully!`));
            console.log(chalk.dim(`Path: ${file.path}`));
            console.log(chalk.dim(`UID: ${file.uid}`));
        } else {
            console.error(chalk.red('Failed to create file. Invalid response from server.'));
            return false;
        }
    } catch (error) {
        console.error(chalk.red(`Failed to create file.\nError: ${error.message}`));
        return false;
    }
    return true;
}

/**
 * Read and display the content of a file (similar to Unix "cat" command).
 * @param {Array} args - The arguments passed to the command (file path).
 */
export async function readFile(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: cat <file_path>'));
        return;
    }

    const filePath = resolvePath(getCurrentDirectory(), args[0]);
    console.log(chalk.green(`Reading file "${filePath}"...\n`));

    try {
        // Step 1: Fetch the file content
        const response = await fetch(`${API_BASE}/read?file=${encodeURIComponent(filePath)}`, {
            method: 'GET',
            headers: getHeaders()
        });

        if (!response.ok) {
            console.error(chalk.red(`Failed to read file. Server response: ${response.statusText}`));
            return;
        }

        const data = await response.text();

        // Step 2: Dispaly the content
        if (data.length) {
            console.log(chalk.cyan(data));
        } else {
            console.error(chalk.red('File is empty.'));
        }
    } catch (error) {
        console.error(chalk.red(`Failed to read file.\nError: ${error.message}`));
    }
}

/**
 * Upload a file from the host machine to the Puter server
 * @param {Array} args - The arguments passed to the command: (<local_path> [remote_path] [dedupe_name] [overwrite])
 */
export async function uploadFile(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: push <local_path> [remote_path] [dedupe_name] [overwrite]'));
        return;
    }

    const localPath = args[0];
    let remotePath = '';
    if (args.length > 1){
        remotePath = args[1].startsWith('/')? args[1]: resolvePath(getCurrentDirectory(), args[1]);
    } else {
        remotePath = resolvePath(getCurrentDirectory(), '.');
    }
    const dedupeName = args.length > 2 ? args[2] === 'true' : true; // Default: true
    const overwrite = args.length > 3 ? args[3] === 'true' : false; // Default: false

    console.log(chalk.green(`Uploading files from "${localPath}" to "${remotePath}"...\n`));
    try {
        // Step 1: Find all matching files (excluding hidden files)
        const files = glob.sync(localPath, { nodir: true, dot: false });

        if (files.length === 0) {
            console.error(chalk.red('No files found to upload.'));
            return;
        }

        // Step 2: Check disk space
        const dfResponse = await fetch(`${API_BASE}/df`, {
            method: 'POST',
            headers: getHeaders(),
            body: null
        });

        if (!dfResponse.ok) {
            console.error(chalk.red('Unable to check disk space.'));
            return;
        }

        const dfData = await dfResponse.json();
        if (dfData.used >= dfData.capacity) {
            console.error(chalk.red('Not enough disk space to upload the files.'));
            showDiskSpaceUsage(dfData); // Display disk usage info
            return;
        }

        // Step 3: Upload each file
        for (const filePath of files) {
            const fileName = path.basename(filePath);
            const fileContent = fs.readFileSync(filePath, 'utf8');

            // Prepare the upload request
            const operationId = crypto.randomUUID(); // Generate a unique operation ID
            const socketId = 'undefined'; // Placeholder socket ID
            const boundary = `----WebKitFormBoundary${crypto.randomUUID().replace(/-/g, '')}`;

            // Prepare FormData
            const formData = `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="operation_id"\r\n\r\n${operationId}\r\n` +
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="socket_id"\r\n\r\n${socketId}\r\n` +
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${socketId}\r\n` +
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${JSON.stringify({
                    name: fileName,
                    type: 'text/plain',
                    size: Buffer.byteLength(fileContent, 'utf8')
                })}\r\n` +
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="operation"\r\n\r\n${JSON.stringify({
                    op: 'write',
                    dedupe_name: dedupeName,
                    overwrite: overwrite,
                    operation_id: operationId,
                    path: remotePath,
                    name: fileName,
                    item_upload_id: 0
                })}\r\n` +
                `--${boundary}\r\n` +
                `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
                `Content-Type: text/plain\r\n\r\n${fileContent}\r\n` +
                `--${boundary}--\r\n`;

            // Send the upload request
            const uploadResponse = await fetch(`${API_BASE}/batch`, {
                method: 'POST',
                headers: getHeaders(`multipart/form-data; boundary=${boundary}`),
                body: formData
            });

            if (!uploadResponse.ok) {
                const errorText = await uploadResponse.text();
                console.error(chalk.red(`Failed to upload file "${fileName}". Server response: ${errorText}`));
                continue;
            }

            const uploadData = await uploadResponse.json();
            if (uploadData && uploadData.results && uploadData.results.length > 0) {
                const file = uploadData.results[0];
                console.log(chalk.green(`File "${fileName}" uploaded successfully!`));
                console.log(chalk.dim(`Path: ${file.path}`));
                console.log(chalk.dim(`UID: ${file.uid}`));
            } else {
                console.error(chalk.red(`Failed to upload file "${fileName}". Invalid response from server.`));
            }
        }
    } catch (error) {
        console.error(chalk.red(`Failed to upload files.\nError: ${error.message}`));
    }
}

/**
 * Get a temporary CSRF Token
 * @returns The CSRF token
 */
async function getCsrfToken() {
    const csrfResponse = await fetch(`${BASE_URL}/get-anticsrf-token`, {
        method: 'GET',
        headers: getHeaders()
    });

    if (!csrfResponse.ok) {
        console.error(chalk.red('Failed to fetch CSRF token.'));
        return;
    }

    const csrfData = await csrfResponse.json();
    if (!csrfData || !csrfData.token) {
        console.error(chalk.red('Failed to fetch anti-CSRF token.'));
        return;
    }

    return csrfData.token;
}

/**
 * Download a file from the Puter server to the host machine 
 * @param {Array} args - The arguments passed to the command (remote file path, Optional: local path).
 */
export async function downloadFile(args = []) {
    if (args.length < 1) {
        console.log(chalk.red('Usage: pull <remote_file_path> [local_path] [overwrite]'));
        return;
    }

    const remotePathPattern = resolvePath(getCurrentDirectory(), args[0]); // Resolve the remote file path pattern
    const localBasePath = path.dirname(args.length > 1 ? args[1] : '.'); // Default to the current directory
    const overwrite = args.length > 2 ? args[2] === 'true' : false; // Default: false

    console.log(chalk.green(`Downloading files matching "${remotePathPattern}" to "${localBasePath}"...\n`));

    try {
        // Step 1: Fetch the list of files and directories from the server
        const listResponse = await fetch(`${API_BASE}/readdir`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({ path: getCurrentDirectory() })
        });

        if (!listResponse.ok) {
            console.error(chalk.red('Failed to list files from the server.'));
            return;
        }

        const files = await listResponse.json();
        if (!Array.isArray(files) || files.length === 0) {
            console.error(chalk.red('No files or directories found on the server.'));
            return;
        }

        // Step 2: Recursively find files matching the pattern
        const matchedFiles = await findMatchingFiles(files, remotePathPattern, getCurrentDirectory());

        if (matchedFiles.length === 0) {
            console.error(chalk.red('No files found matching the pattern.'));
            return;
        }

        // Step 3: Download each matched file
        for (const remoteFilePath of matchedFiles) {
            const relativePath = path.relative(getCurrentDirectory(), remoteFilePath);
            const localFilePath = path.join(localBasePath, relativePath);

            // Ensure the local directory exists
            if (!fs.existsSync(path.dirname(localFilePath))){
                fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
            }

            console.log(chalk.green(`Downloading file "${remoteFilePath}" to "${localFilePath}"...`));

                    // Fetch the anti-CSRF token
            const antiCsrfToken = await getCsrfToken();

            const downloadResponse = await fetch(`${BASE_URL}/down?path=${remoteFilePath}`, {
                method: 'POST',
                headers: {
                    ...getHeaders('application/x-www-form-urlencoded'),
                    "cookie": `puter_auth_token=${getAuthToken()};`
                },
                "referrerPolicy": "strict-origin-when-cross-origin",
                body: `anti_csrf=${antiCsrfToken}`
            });

            if (!downloadResponse.ok) {
                console.error(chalk.red(`Failed to download file "${remoteFilePath}". Server response: ${downloadResponse.statusText}`));
                continue;
            }

            // Step 5: Save the file content to the local filesystem
            const fileContent = await downloadResponse.text();

            // Check if the file exists, if so then delete it before writing.
            if (overwrite && fs.existsSync(localFilePath)) {
                fs.unlinkSync(localFilePath);
                console.log(chalk.yellow(`File "${localFilePath}" already exists. Overwriting...`));
            }

            fs.writeFileSync(localFilePath, fileContent, 'utf8');
            const fileSize = fs.statSync(localFilePath).size;
            console.log(chalk.green(`File: "${remoteFilePath}" downloaded to "${localFilePath}" (size: ${formatSize(fileSize)})`));
        }
    } catch (error) {
        console.error(chalk.red(`Failed to download files.\nError: ${error.message}`));
    }
}

/**
 * Copy files or directories from one location to another on the Puter server (similar to Unix "cp" command).
 * @param {Array} args - The arguments passed to the command (source path, destination path).
 */
export async function copyFile(args = []) {
    if (args.length < 2) {
        console.log(chalk.red('Usage: cp <source_path> <destination_path>'));
        return;
    }

    const sourcePath = args[0].startsWith(`/${getCurrentUserName()}`) ? args[0] : resolvePath(getCurrentDirectory(), args[0]); // Resolve the source path
    const destinationPath = args[1].startsWith(`/${getCurrentUserName()}`) ? args[1] : resolvePath(getCurrentDirectory(), args[1]); // Resolve the destination path

    console.log(chalk.green(`Copy: "${chalk.dim(sourcePath)}" to: "${chalk.dim(destinationPath)}"...\n`));
    try {
        // Step 1: Check if the source is a directory or a file
        const statResponse = await fetch(`${API_BASE}/stat`, {
            method: 'POST',
            headers: getHeaders(),
            body: JSON.stringify({
                path: sourcePath
            })
        });

        if (!statResponse.ok) {
            console.error(chalk.red(`Failed to check source path. Server response: ${await statResponse.text()}`));
            return;
        }

        const statData = await statResponse.json();
        if (!statData || !statData.id) {
            console.error(chalk.red(`Source path "${sourcePath}" does not exist.`));
            return;
        }

        if (statData.is_dir) {
            // Step 2: If source is a directory, copy all files recursively
            const files = await listFiles([sourcePath]);
            for (const file of files) {
                const relativePath = file.path.replace(sourcePath, '');
                const destPath = path.join(destinationPath, relativePath);

                const copyResponse = await fetch(`${API_BASE}/copy`, {
                    method: 'POST',
                    headers: getHeaders(),
                    body: JSON.stringify({
                        source: file.path,
                        destination: destPath
                    })
                });

                if (!copyResponse.ok) {
                    console.error(chalk.red(`Failed to copy file "${file.path}". Server response: ${await copyResponse.text()}`));
                    continue;
                }

                const copyData = await copyResponse.json();
                if (copyData && copyData.length > 0 && copyData[0].copied) {
                    console.log(chalk.green(`File "${chalk.dim(file.path)}" copied successfully to "${chalk.dim(copyData[0].copied.path)}"!`));
                } else {
                    console.error(chalk.red(`Failed to copy file "${file.path}". Invalid response from server.`));
                }
            }
        } else {
            // Step 3: If source is a file, copy it directly
            const copyResponse = await fetch(`${API_BASE}/copy`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    source: sourcePath,
                    destination: destinationPath
                })
            });

            if (!copyResponse.ok) {
                console.error(chalk.red(`Failed to copy file. Server response: ${await copyResponse.text()}`));
                return;
            }

            const copyData = await copyResponse.json();
            if (copyData && copyData.length > 0 && copyData[0].copied) {
                console.log(chalk.green(`File "${sourcePath}" copied successfully to "${copyData[0].copied.path}"!`));
                console.log(chalk.dim(`UID: ${copyData[0].copied.uid}`));
            } else {
                console.error(chalk.red('Failed to copy file. Invalid response from server.'));
            }
        }
    } catch (error) {
        console.error(chalk.red(`Failed to copy file.\nError: ${error.message}`));
    }
}


/**
 * List all files in a local directory.
 * @param {string} localDir - The local directory path.
 * @param {boolean} recursive - Whether to recursively list files in subdirectories
 * @returns {Array} - Array of local file objects.
 */
function listLocalFiles(localDir, recursive = false) {
    const files = [];
    const walkDir = (dir, baseDir) => {

        const entries = fs.readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dir, entry.name);
            const relativePath = path.relative(baseDir, fullPath);
            if (entry.isDirectory()) {
                if (recursive) {
                    walkDir(fullPath, baseDir); // Recursively traverse directories if flag is set
                }
            } else {
                files.push({
                    relativePath: relativePath,
                    localPath: fullPath,
                    size: fs.statSync(fullPath).size,
                    modified: fs.statSync(fullPath).mtime.getTime()
                });
            }
        }

    };

    walkDir(localDir, localDir);
    return files;
}

/**
 * Compare local and remote files to determine actions.
 * @param {Array} localFiles - Array of local file objects.
 * @param {Array} remoteFiles - Array of remote file objects.
 * @param {string} localDir - Local directory path.
 * @param {string} remoteDir - Remote directory path.
 * @returns {Object} - Object containing files to upload, download, and delete.
 */
function compareFiles(localFiles, remoteFiles, localDir, remoteDir) {
    const toUpload = []; // Files to upload to remote
    const toDownload = []; // Files to download from remote
    const toDelete = []; // Files to delete from remote

    // Create a map of remote files for quick lookup
    const remoteFileMap = new Map();
    remoteFiles.forEach(file => {
        remoteFileMap.set(file.name, {
            size: file.size,
            modified: new Date(file.modified).getTime()
        });
    });

    // Check local files
    for (const file of localFiles) {
        const remoteFile = remoteFileMap.get(file.relativePath);
        if (!remoteFile || file.modified > remoteFile.modified) {
            toUpload.push(file); // New or updated file
        }
    }

    // Check remote files
    for (const file of remoteFiles) {
        const localFile = localFiles.find(f => f.relativePath === file.name);
        if (localFile){
            console.log(`localFile: ${localFile.relativePath}, modified: ${localFile.modified}`);
        }
        console.log(`file: ${file.name}, modified: ${file.modified}`);
        if (!localFile) {
            toDelete.push({ relativePath: file.name }); // Extra file in remote
        } else if (file.modified > parseInt(localFile.modified / 1000)) {
            toDownload.push(localFile); // New or updated file in remote
        }
    }

    return { toUpload, toDownload, toDelete };
}

/**
 * Find conflicts where the same file has been modified in both locations.
 * @param {Array} toUpload - Files to upload.
 * @param {Array} toDownload - Files to download.
 * @returns {Array} - Array of conflicting file paths.
 */
function findConflicts(toUpload, toDownload) {
    const conflicts = [];
    const uploadPaths = toUpload.map(file => file.relativePath);
    const downloadPaths = toDownload.map(file => file.relativePath);

    for (const path of uploadPaths) {
        if (downloadPaths.includes(path)) {
            conflicts.push(path);
        }
    }

    return conflicts;
}

/**
 * Resolve given local path directory
 * @param {string} localPath The local path to resolve
 * @returns {Promise<string>} The resolved absolute path
 * @throws {Error} If the path does not exist or is not a directory
 */
async function resolveLocalDirectory(localPath) {
    // Resolve the path to an absolute path
    const absolutePath = path.resolve(localPath);
  
    // Check if the path exists
    if (!fs.existsSync(absolutePath)) {
      throw new Error(`Path does not exist: ${absolutePath}`);
    }
  
    // Check if the path is a directory
    const stats = await fs.promises.stat(absolutePath);
    if (!stats.isDirectory()) {
      throw new Error(`Path is not a directory: ${absolutePath}`);
    }
    return absolutePath;
}

/**
 * Ensure a remote directory exists, creating it if necessary
 * @param {string} remotePath - The remote directory path
 */
async function ensureRemoteDirectoryExists(remotePath) {
    try {
        const exists = await pathExists(remotePath);
        if (!exists) {
            // Create the directory and any missing parents
            await fetch(`${API_BASE}/mkdir`, {
                method: 'POST',
                headers: getHeaders(),
                body: JSON.stringify({
                    parent: path.dirname(remotePath),
                    path: path.basename(remotePath),
                    overwrite: false,
                    dedupe_name: true,
                    create_missing_parents: true
                })
            });
        }
    } catch (error) {
        console.error(chalk.red(`Failed to create remote directory: ${remotePath}`));
        throw error;
    }
}

/**
 * Synchronize a local directory with a remote directory on Puter.
 * @param {string[]} args - Command-line arguments (e.g., [localDir, remoteDir, --delete, -r]).
 */
export async function syncDirectory(args = []) {
    const usageMessage = 'Usage: update <local_directory> <remote_directory> [--delete] [-r]';
    if (args.length < 2) {
        console.log(chalk.red(usageMessage));
        return;
    }

    let localDir = '';
    let remoteDir = '';
    let deleteFlag = '';
    let recursiveFlag = false;
    try {
        localDir = await resolveLocalDirectory(args[0]);
        remoteDir = resolvePath(getCurrentDirectory(), args[1]);
        deleteFlag = args.includes('--delete'); // Whether to delete extra files
        recursiveFlag = args.includes('-r'); // Whether to recursively process subdirectories
    } catch (error) {
        console.error(chalk.red(error.message));
        console.log(chalk.green(usageMessage));
        return;
    }

    console.log(chalk.green(`Syncing local directory ${chalk.cyan(localDir)}" with remote directory ${chalk.cyan(remoteDir)}"...\n`));

    try {
        // Step 1: Validate local directory
        if (!fs.existsSync(localDir)) {
            console.error(chalk.red(`Local directory "${localDir}" does not exist.`));
            return;
        }

        // Step 2: Fetch remote directory contents
        const remoteFiles = await listRemoteFiles(remoteDir);
        if (!Array.isArray(remoteFiles)) {
            console.error(chalk.red('Failed to fetch remote directory contents.'));
            return;
        }

        // Step 3: List local files
        const localFiles = listLocalFiles(localDir, recursiveFlag);

        // Step 4: Compare local and remote files
        let { toUpload, toDownload, toDelete } = compareFiles(localFiles, remoteFiles, localDir, remoteDir);
        let filteredToUpload = [...toUpload];
        let filteredToDownload = [...toDownload];

        // Step 5: Handle conflicts (if any)
        const conflicts = findConflicts(toUpload, toDownload);
        if (conflicts.length > 0) {

            console.log(chalk.yellow('The following files have conflicts:'));
            conflicts.forEach(file => console.log(chalk.dim(`- ${file}`)));

            const { resolve } = await inquirer.prompt([
                {
                    type: 'list',
                    name: 'resolve',
                    message: 'How would you like to resolve conflicts?',
                    choices: [
                        { name: 'Keep local version', value: 'local' },
                        { name: 'Keep remote version', value: 'remote' },
                        { name: 'Skip conflicting files', value: 'skip' }
                    ]
                }
            ]);

            if (resolve === 'local') {
                filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
            } else if (resolve === 'remote') {
                filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
            } else {
                filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
                filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
            }
        }

        // Step 6: Perform synchronization
        console.log(chalk.green('Starting synchronization...'));

        // Upload new/updated files
        for (const file of filteredToUpload) {
            console.log(chalk.cyan(`Uploading "${file.relativePath}"...`));
            const dedupeName = 'false';
            const overwrite = 'true';

            // Create parent directories if needed
            const remoteFilePath = path.join(remoteDir, file.relativePath);
            const remoteFileDir = path.dirname(remoteFilePath);
            
            // Ensure remote directory exists
            await ensureRemoteDirectoryExists(remoteFileDir);

            await uploadFile([file.localPath, remoteFileDir, dedupeName, overwrite]);
        }

        // Download new/updated files
        for (const file of filteredToDownload) {
            console.log(chalk.cyan(`Downloading "${file.relativePath}"...`));
            const overwrite = 'true';
            // Create local parent directories if needed
            const localFilePath = path.join(localDir, file.relativePath);
            // const localFileDir = path.dirname(localFilePath);

            await downloadFile([file.remotePath, localFilePath, overwrite]);
        }

        // Delete extra files (if --delete flag is set)
        if (deleteFlag) {
            for (const file of toDelete) {
                console.log(chalk.yellow(`Deleting "${file.relativePath}"...`));
                await removeFileOrDirectory([path.join(remoteDir, file.relativePath), '-f']);
            }
        }

        console.log(chalk.green('Synchronization complete!'));
    } catch (error) {
        console.error(chalk.red('Failed to synchronize directories.'));
        console.error(chalk.red(`Error: ${error.message}`));
    }
}

/**
 * Edit a remote file using the local system editor
 * @param {Array} args - The file path to edit
 * @returns {Promise<void>}
 */
export async function editFile(args = []) {
  if (args.length < 1) {
    console.log(chalk.red('Usage: edit <file>'));
    return;
  }

  const filePath = args[0].startsWith('/') ? args[0] : resolvePath(getCurrentDirectory(), args[0]);
  console.log(chalk.green(`Fetching file: ${filePath}`));

  try {
    // Step 1: Check if file exists
    const statResponse = await fetch(`${API_BASE}/stat`, {
      method: 'POST',
      headers: getHeaders(),
      body: JSON.stringify({ path: filePath })
    });
    
    const statData = await statResponse.json();
    if (!statData || !statData.uid || statData.is_dir) {
      console.log(chalk.red(`File not found or is a directory: ${filePath}`));
      return;
    }

    // Step 2: Download the file content
    const downloadResponse = await fetch(`${API_BASE}/read?file=${encodeURIComponent(filePath)}`, {
      method: 'GET',
      headers: getHeaders()
    });
    
    if (!downloadResponse.ok) {
      console.log(chalk.red(`Failed to download file: ${filePath}`));
      return;
    }
    
    const fileContent = await downloadResponse.text();
    console.log(chalk.green(`File fetched: ${filePath} (${formatSize(fileContent.length)} bytes)`));

    // Step 3: Create a temporary file
    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puter-'));
    const tempFilePath = path.join(tempDir, path.basename(filePath));
    fs.writeFileSync(tempFilePath, fileContent, 'utf-8');

    // Step 4: Determine the editor to use
    const editor = getSystemEditor();
    console.log(chalk.cyan(`Opening file with ${editor}...`));
    
    // Step 5: Open the file in the editor using execSync instead of spawn
    // This will block until the editor is closed, which is better for terminal-based editors
    try {
      execSync(`${editor} "${tempFilePath}"`, { 
        stdio: 'inherit',
        env: process.env
      });
      
      // Read the updated content after editor closes
      const updatedContent = fs.readFileSync(tempFilePath, 'utf8');
      console.log(chalk.cyan('Uploading changes...'));
      
      // Step 7: Upload the updated file content
      // Step 7.1: Check disk space
      const dfResponse = await fetch(`${API_BASE}/df`, {
          method: 'POST',
          headers: getHeaders(),
          body: null
      });

      if (!dfResponse.ok) {
          console.log(chalk.red('Unable to check disk space.'));
          return;
      }

      const dfData = await dfResponse.json();
      if (dfData.used >= dfData.capacity) {
          console.log(chalk.red('Not enough disk space to upload the file.'));
          showDiskSpaceUsage(dfData); // Display disk usage info
          return;
      }

      // Step 7.2: Uploading the updated file
      const operationId = crypto.randomUUID(); // Generate a unique operation ID
      const socketId = 'undefined'; // Placeholder socket ID
      const boundary = `----WebKitFormBoundary${crypto.randomUUID().replace(/-/g, '')}`;
      const fileName = path.basename(filePath);
      const dirName = path.dirname(filePath);

      // Prepare FormData
      const formData = `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="operation_id"\r\n\r\n${operationId}\r\n` +
          `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="socket_id"\r\n\r\n${socketId}\r\n` +
          `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${socketId}\r\n` +
          `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${JSON.stringify({
              name: fileName,
              type: 'text/plain',
              size: Buffer.byteLength(updatedContent, 'utf8')
          })}\r\n` +
          `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="operation"\r\n\r\n${JSON.stringify({
              op: 'write',
              dedupe_name: false,
              overwrite: true,
              operation_id: operationId,
              path: dirName,
              name: fileName,
              item_upload_id: 0
          })}\r\n` +
          `--${boundary}\r\n` +
          `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
          `Content-Type: text/plain\r\n\r\n${updatedContent}\r\n` +
          `--${boundary}--\r\n`;

      // Send the upload request
      const uploadResponse = await fetch(`${API_BASE}/batch`, {
          method: 'POST',
          headers: getHeaders(`multipart/form-data; boundary=${boundary}`),
          body: formData
      });

      if (!uploadResponse.ok) {
          const errorText = await uploadResponse.text();
          console.log(chalk.red(`Failed to save file. Server response: ${errorText}`));
          return;
      }

      const uploadData = await uploadResponse.json();
      if (uploadData && uploadData.results && uploadData.results.length > 0) {
          const file = uploadData.results[0];
          console.log(chalk.green(`File saved: ${file.path}`));
      } else {
          console.log(chalk.red('Failed to save file. Invalid response from server.'));
      }
    } catch (error) {
      if (error.status === 130) {
        // This is a SIGINT (Ctrl+C), which is normal for some editors
        console.log(chalk.yellow('Editor closed without saving.'));
      } else {
        console.log(chalk.red(`Error during editing: ${error.message}`));
      }
    } finally {
      // Clean up temporary files
      try {
        fs.unlinkSync(tempFilePath);
        fs.rmdirSync(tempDir);
      } catch (e) {
        console.error(chalk.dim(`Failed to clean up temporary files: ${e.message}`));
      }
    }
  } catch (error) {
    console.log(chalk.red(`Error: ${error.message}`));
  }
}