puter-cli 1.7.3 → 1.8.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.
package/CHANGELOG.md CHANGED
@@ -4,8 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v1.8.0](https://github.com/HeyPuter/puter-cli/compare/v1.7.3...v1.8.0)
8
+
9
+ - feat(files): add edit command to modify remote files with local editor [`220b07c`](https://github.com/HeyPuter/puter-cli/commit/220b07c79fa9e9ab2e0e668cfbd7e5260c9746a2)
10
+
7
11
  #### [v1.7.3](https://github.com/HeyPuter/puter-cli/compare/v1.7.2...v1.7.3)
8
12
 
13
+ > 16 March 2025
14
+
9
15
  - fix: absolute remote path treated as relative in update #10 [`cb716a3`](https://github.com/HeyPuter/puter-cli/commit/cb716a37afdd9f552c53244a3eb63d7a649e244c)
10
16
 
11
17
  #### [v1.7.2](https://github.com/HeyPuter/puter-cli/compare/v1.7.1...v1.7.2)
package/README.md CHANGED
@@ -135,6 +135,12 @@ P.S. These commands consider the current directory as the base path for every op
135
135
  ```
136
136
  P.S. The `--delete` flag removes files in the remote directory that don't exist locally. The `-r` flag enables recursive synchronization of subdirectories.
137
137
 
138
+ **Edit a file**: Edit remote text files using your preferred local text editor.
139
+ ```bash
140
+ puter> edit <file>
141
+ ```
142
+ P.S. This command will download the remote file to your local machine, open it in your default editor, and then upload the changes back to the remote instance. It uses `vim` by default, but you can change it by setting the `EDITOR` environment variable.
143
+
138
144
  #### User Information
139
145
  ```
140
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puter-cli",
3
- "version": "1.7.3",
3
+ "version": "1.8.0",
4
4
  "description": "Command line interface for Puter cloud platform",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,18 +1,20 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { execSync } from 'node:child_process';
2
4
  import { glob } from 'glob';
3
5
  import path from 'path';
4
6
  import { minimatch } from 'minimatch';
5
7
  import chalk from 'chalk';
6
- import ora from 'ora';
7
8
  import Conf from 'conf';
8
9
  import fetch from 'node-fetch';
9
10
  import { 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';
11
12
  import inquirer from 'inquirer';
12
13
  import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
13
14
  import { updatePrompt } from './shell.js';
14
15
  import crypto from '../crypto.js';
15
16
 
17
+
16
18
  const config = new Conf({ projectName: PROJECT_NAME });
17
19
 
18
20
 
@@ -1379,3 +1381,162 @@ export async function syncDirectory(args = []) {
1379
1381
  console.error(chalk.red(`Error: ${error.message}`));
1380
1382
  }
1381
1383
  }
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
+ }
package/src/executor.js CHANGED
@@ -5,12 +5,12 @@ import { listSites, createSite, deleteSite, infoSite } from './commands/sites.js
5
5
  import { listFiles, makeDirectory, renameFileOrDirectory,
6
6
  removeFileOrDirectory, emptyTrash, changeDirectory, showCwd,
7
7
  getInfo, getDiskUsage, createFile, readFile, uploadFile,
8
- downloadFile, copyFile, syncDirectory } from './commands/files.js';
8
+ downloadFile, copyFile, syncDirectory, editFile } from './commands/files.js';
9
9
  import { getUserInfo, getUsageInfo } from './commands/auth.js';
10
10
  import { PROJECT_NAME, API_BASE, getHeaders } from './commons.js';
11
11
  import inquirer from 'inquirer';
12
12
  import { exec } from 'node:child_process';
13
- import { parseArgs } from './utils.js';
13
+ import { parseArgs, getSystemEditor } from './utils.js';
14
14
  import { rl } from './commands/shell.js';
15
15
  import { ErrorAPI } from './modules/ErrorModule.js';
16
16
 
@@ -144,6 +144,7 @@ const commands = {
144
144
  push: uploadFile,
145
145
  pull: downloadFile,
146
146
  update: syncDirectory,
147
+ edit: editFile,
147
148
  sites: listSites,
148
149
  site: infoSite,
149
150
  'site:delete': deleteSite,
@@ -327,6 +328,13 @@ function showHelp(command) {
327
328
  Sync local directory with remote cloud.
328
329
  Example: update /local/path /remote/path
329
330
  `,
331
+ edit: `
332
+ ${chalk.cyan('edit <file>')}
333
+ Edit a remote file using your local text editor.
334
+ Example: edit /path/to/file
335
+
336
+ System editor: ${chalk.green(getSystemEditor())}
337
+ `,
330
338
  sites: `
331
339
  ${chalk.cyan('sites')}
332
340
  List sites and subdomains.
package/src/utils.js CHANGED
@@ -120,4 +120,16 @@ export function isValidAppUuid (uuid) {
120
120
  export function is_valid_uuid4 (uuid) {
121
121
  const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
122
122
  return uuidV4Regex.test(uuid);
123
+ }
124
+
125
+ /**
126
+ * Get system editor
127
+ * @returns {string} - System editor
128
+ * @example
129
+ * getSystemEditor()
130
+ * // => 'nano'
131
+ */
132
+ export function getSystemEditor() {
133
+ return process.env.EDITOR || process.env.VISUAL ||
134
+ (process.platform === 'win32' ? 'notepad' : 'vi')
123
135
  }