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