puter-cli 1.7.3 → 1.8.1
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 +12 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/commands/files.js +163 -2
- package/src/commons.js +40 -10
- package/src/executor.js +10 -2
- package/src/utils.js +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,20 @@ 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.1](https://github.com/HeyPuter/puter-cli/compare/v1.8.0...v1.8.1)
|
|
8
|
+
|
|
9
|
+
- fix: better version status handling and error resilience [`ab467e8`](https://github.com/HeyPuter/puter-cli/commit/ab467e8e57cb4f82619424b12136724904df0302)
|
|
10
|
+
|
|
11
|
+
#### [v1.8.0](https://github.com/HeyPuter/puter-cli/compare/v1.7.3...v1.8.0)
|
|
12
|
+
|
|
13
|
+
> 2 April 2025
|
|
14
|
+
|
|
15
|
+
- feat(files): add edit command to modify remote files with local editor [`220b07c`](https://github.com/HeyPuter/puter-cli/commit/220b07c79fa9e9ab2e0e668cfbd7e5260c9746a2)
|
|
16
|
+
|
|
7
17
|
#### [v1.7.3](https://github.com/HeyPuter/puter-cli/compare/v1.7.2...v1.7.3)
|
|
8
18
|
|
|
19
|
+
> 16 March 2025
|
|
20
|
+
|
|
9
21
|
- fix: absolute remote path treated as relative in update #10 [`cb716a3`](https://github.com/HeyPuter/puter-cli/commit/cb716a37afdd9f552c53244a3eb63d7a649e244c)
|
|
10
22
|
|
|
11
23
|
#### [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
package/src/commands/files.js
CHANGED
|
@@ -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 {
|
|
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/commons.js
CHANGED
|
@@ -345,16 +345,46 @@ export async function getVersionFromPackage() {
|
|
|
345
345
|
* Get latest package info from npm registery
|
|
346
346
|
*/
|
|
347
347
|
export async function getLatestVersion(packageName) {
|
|
348
|
+
let currentVersion = 'unknown';
|
|
349
|
+
let latestVersion = null;
|
|
350
|
+
let status = 'offline'; // Default status
|
|
351
|
+
|
|
348
352
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
// Attempt to get the current version first
|
|
354
|
+
currentVersion = await getVersionFromPackage();
|
|
355
|
+
if (!currentVersion) {
|
|
356
|
+
currentVersion = 'unknown'; // Fallback if local version fetch fails
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Attempt to fetch the latest version from npm
|
|
360
|
+
try {
|
|
361
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
362
|
+
if (response.ok) {
|
|
363
|
+
const data = await response.json();
|
|
364
|
+
latestVersion = data.version;
|
|
365
|
+
}
|
|
366
|
+
} catch (fetchError) {
|
|
367
|
+
// Ignore fetch errors
|
|
368
|
+
// console.warn(chalk.yellow(`Could not fetch latest version for ${packageName}: ${fetchError.message}`));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Determine the status based on fetched versions
|
|
372
|
+
if (latestVersion) {
|
|
373
|
+
if (currentVersion !== 'unknown' && latestVersion === currentVersion) {
|
|
374
|
+
status = 'up-to-date';
|
|
375
|
+
} else if (currentVersion !== 'unknown' && latestVersion !== currentVersion) {
|
|
376
|
+
status = `latest: ${latestVersion}`;
|
|
377
|
+
} else {
|
|
378
|
+
// If currentVersion is unknown but we got latest, show latest
|
|
379
|
+
status = `latest: ${latestVersion}`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// status remains 'offline'...
|
|
383
|
+
|
|
356
384
|
} catch (error) {
|
|
357
|
-
|
|
358
|
-
|
|
385
|
+
// Catch errors from getVersionFromPackage or other unexpected issues
|
|
386
|
+
console.error(chalk.red(`Error determining version status: ${error.message}`));
|
|
387
|
+
status = 'error'; // Indicate an error occurred
|
|
359
388
|
}
|
|
360
|
-
|
|
389
|
+
return `v${currentVersion} (${status})`;
|
|
390
|
+
}
|
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
|
}
|