11import fs from 'node:fs' ;
2+ import os from 'node:os' ;
3+ import { execSync } from 'node:child_process' ;
24import { glob } from 'glob' ;
35import path from 'path' ;
46import { minimatch } from 'minimatch' ;
57import chalk from 'chalk' ;
6- import ora from 'ora' ;
78import Conf from 'conf' ;
89import fetch from 'node-fetch' ;
910import { API_BASE , BASE_URL , PROJECT_NAME , getHeaders , showDiskSpaceUsage , resolvePath } from '../commons.js' ;
10- import { formatDate , formatDateTime , formatSize } from '../utils.js' ;
11+ import { formatDateTime , formatSize , getSystemEditor } from '../utils.js' ;
1112import inquirer from 'inquirer' ;
1213import { getAuthToken , getCurrentDirectory , getCurrentUserName } from './auth.js' ;
1314import { updatePrompt } from './shell.js' ;
1415import crypto from '../crypto.js' ;
1516
17+
1618const config = new Conf ( { projectName : PROJECT_NAME } ) ;
1719
1820
@@ -1379,3 +1381,162 @@ export async function syncDirectory(args = []) {
13791381 console . error ( chalk . red ( `Error: ${ error . message } ` ) ) ;
13801382 }
13811383}
1384+
1385+ /**
1386+ * Edit a remote file using the local system editor
1387+ * @param {Array } args - The file path to edit
1388+ * @returns {Promise<void> }
1389+ */
1390+ export async function editFile ( args = [ ] ) {
1391+ if ( args . length < 1 ) {
1392+ console . log ( chalk . red ( 'Usage: edit <file>' ) ) ;
1393+ return ;
1394+ }
1395+
1396+ const filePath = args [ 0 ] . startsWith ( '/' ) ? args [ 0 ] : resolvePath ( getCurrentDirectory ( ) , args [ 0 ] ) ;
1397+ console . log ( chalk . green ( `Fetching file: ${ filePath } ` ) ) ;
1398+
1399+ try {
1400+ // Step 1: Check if file exists
1401+ const statResponse = await fetch ( `${ API_BASE } /stat` , {
1402+ method : 'POST' ,
1403+ headers : getHeaders ( ) ,
1404+ body : JSON . stringify ( { path : filePath } )
1405+ } ) ;
1406+
1407+ const statData = await statResponse . json ( ) ;
1408+ if ( ! statData || ! statData . uid || statData . is_dir ) {
1409+ console . log ( chalk . red ( `File not found or is a directory: ${ filePath } ` ) ) ;
1410+ return ;
1411+ }
1412+
1413+ // Step 2: Download the file content
1414+ const downloadResponse = await fetch ( `${ API_BASE } /read?file=${ encodeURIComponent ( filePath ) } ` , {
1415+ method : 'GET' ,
1416+ headers : getHeaders ( )
1417+ } ) ;
1418+
1419+ if ( ! downloadResponse . ok ) {
1420+ console . log ( chalk . red ( `Failed to download file: ${ filePath } ` ) ) ;
1421+ return ;
1422+ }
1423+
1424+ const fileContent = await downloadResponse . text ( ) ;
1425+ console . log ( chalk . green ( `File fetched: ${ filePath } (${ formatSize ( fileContent . length ) } bytes)` ) ) ;
1426+
1427+ // Step 3: Create a temporary file
1428+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'puter-' ) ) ;
1429+ const tempFilePath = path . join ( tempDir , path . basename ( filePath ) ) ;
1430+ fs . writeFileSync ( tempFilePath , fileContent , 'utf-8' ) ;
1431+
1432+ // Step 4: Determine the editor to use
1433+ const editor = getSystemEditor ( ) ;
1434+ console . log ( chalk . cyan ( `Opening file with ${ editor } ...` ) ) ;
1435+
1436+ // Step 5: Open the file in the editor using execSync instead of spawn
1437+ // This will block until the editor is closed, which is better for terminal-based editors
1438+ try {
1439+ execSync ( `${ editor } "${ tempFilePath } "` , {
1440+ stdio : 'inherit' ,
1441+ env : process . env
1442+ } ) ;
1443+
1444+ // Read the updated content after editor closes
1445+ const updatedContent = fs . readFileSync ( tempFilePath , 'utf8' ) ;
1446+ console . log ( chalk . cyan ( 'Uploading changes...' ) ) ;
1447+
1448+ // Step 7: Upload the updated file content
1449+ // Step 7.1: Check disk space
1450+ const dfResponse = await fetch ( `${ API_BASE } /df` , {
1451+ method : 'POST' ,
1452+ headers : getHeaders ( ) ,
1453+ body : null
1454+ } ) ;
1455+
1456+ if ( ! dfResponse . ok ) {
1457+ console . log ( chalk . red ( 'Unable to check disk space.' ) ) ;
1458+ return ;
1459+ }
1460+
1461+ const dfData = await dfResponse . json ( ) ;
1462+ if ( dfData . used >= dfData . capacity ) {
1463+ console . log ( chalk . red ( 'Not enough disk space to upload the file.' ) ) ;
1464+ showDiskSpaceUsage ( dfData ) ; // Display disk usage info
1465+ return ;
1466+ }
1467+
1468+ // Step 7.2: Uploading the updated file
1469+ const operationId = crypto . randomUUID ( ) ; // Generate a unique operation ID
1470+ const socketId = 'undefined' ; // Placeholder socket ID
1471+ const boundary = `----WebKitFormBoundary${ crypto . randomUUID ( ) . replace ( / - / g, '' ) } ` ;
1472+ const fileName = path . basename ( filePath ) ;
1473+ const dirName = path . dirname ( filePath ) ;
1474+
1475+ // Prepare FormData
1476+ const formData = `--${ boundary } \r\n` +
1477+ `Content-Disposition: form-data; name="operation_id"\r\n\r\n${ operationId } \r\n` +
1478+ `--${ boundary } \r\n` +
1479+ `Content-Disposition: form-data; name="socket_id"\r\n\r\n${ socketId } \r\n` +
1480+ `--${ boundary } \r\n` +
1481+ `Content-Disposition: form-data; name="original_client_socket_id"\r\n\r\n${ socketId } \r\n` +
1482+ `--${ boundary } \r\n` +
1483+ `Content-Disposition: form-data; name="fileinfo"\r\n\r\n${ JSON . stringify ( {
1484+ name : fileName ,
1485+ type : 'text/plain' ,
1486+ size : Buffer . byteLength ( updatedContent , 'utf8' )
1487+ } ) } \r\n` +
1488+ `--${ boundary } \r\n` +
1489+ `Content-Disposition: form-data; name="operation"\r\n\r\n${ JSON . stringify ( {
1490+ op : 'write' ,
1491+ dedupe_name : false ,
1492+ overwrite : true ,
1493+ operation_id : operationId ,
1494+ path : dirName ,
1495+ name : fileName ,
1496+ item_upload_id : 0
1497+ } ) } \r\n` +
1498+ `--${ boundary } \r\n` +
1499+ `Content-Disposition: form-data; name="file"; filename="${ fileName } "\r\n` +
1500+ `Content-Type: text/plain\r\n\r\n${ updatedContent } \r\n` +
1501+ `--${ boundary } --\r\n` ;
1502+
1503+ // Send the upload request
1504+ const uploadResponse = await fetch ( `${ API_BASE } /batch` , {
1505+ method : 'POST' ,
1506+ headers : getHeaders ( `multipart/form-data; boundary=${ boundary } ` ) ,
1507+ body : formData
1508+ } ) ;
1509+
1510+ if ( ! uploadResponse . ok ) {
1511+ const errorText = await uploadResponse . text ( ) ;
1512+ console . log ( chalk . red ( `Failed to save file. Server response: ${ errorText } ` ) ) ;
1513+ return ;
1514+ }
1515+
1516+ const uploadData = await uploadResponse . json ( ) ;
1517+ if ( uploadData && uploadData . results && uploadData . results . length > 0 ) {
1518+ const file = uploadData . results [ 0 ] ;
1519+ console . log ( chalk . green ( `File saved: ${ file . path } ` ) ) ;
1520+ } else {
1521+ console . log ( chalk . red ( 'Failed to save file. Invalid response from server.' ) ) ;
1522+ }
1523+ } catch ( error ) {
1524+ if ( error . status === 130 ) {
1525+ // This is a SIGINT (Ctrl+C), which is normal for some editors
1526+ console . log ( chalk . yellow ( 'Editor closed without saving.' ) ) ;
1527+ } else {
1528+ console . log ( chalk . red ( `Error during editing: ${ error . message } ` ) ) ;
1529+ }
1530+ } finally {
1531+ // Clean up temporary files
1532+ try {
1533+ fs . unlinkSync ( tempFilePath ) ;
1534+ fs . rmdirSync ( tempDir ) ;
1535+ } catch ( e ) {
1536+ console . error ( chalk . dim ( `Failed to clean up temporary files: ${ e . message } ` ) ) ;
1537+ }
1538+ }
1539+ } catch ( error ) {
1540+ console . log ( chalk . red ( `Error: ${ error . message } ` ) ) ;
1541+ }
1542+ }
0 commit comments