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