puter-cli 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +22 -1
- package/package.json +1 -1
- package/src/commands/files.js +123 -41
- package/src/commons.js +1 -1
- package/src/executor.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v1.7.2](https://github.com/HeyPuter/puter-cli/compare/v1.7.1...v1.7.2)
|
|
8
|
+
|
|
9
|
+
- fix: mv command for both rename/move files [`0b944c8`](https://github.com/HeyPuter/puter-cli/commit/0b944c8295f615427bd90d19a6058b2053c1b3dc)
|
|
10
|
+
|
|
11
|
+
#### [v1.7.1](https://github.com/HeyPuter/puter-cli/compare/v1.7.0...v1.7.1)
|
|
12
|
+
|
|
13
|
+
> 4 March 2025
|
|
14
|
+
|
|
15
|
+
- fix: update command [`38ca9e8`](https://github.com/HeyPuter/puter-cli/commit/38ca9e824642cbf8b1ffdb0d2a6e5426f84c7371)
|
|
16
|
+
- docs: update README [`6e658f6`](https://github.com/HeyPuter/puter-cli/commit/6e658f6a117ee1ba206768905d53fb61c78e136d)
|
|
17
|
+
|
|
7
18
|
#### [v1.7.0](https://github.com/HeyPuter/puter-cli/compare/v1.6.1...v1.7.0)
|
|
8
19
|
|
|
20
|
+
> 16 February 2025
|
|
21
|
+
|
|
9
22
|
- feat: save auth token when login [`55b32b7`](https://github.com/HeyPuter/puter-cli/commit/55b32b7feca050902f4470f06af38f81d3299e6a)
|
|
10
23
|
- fix: create app from host shell [`30e5028`](https://github.com/HeyPuter/puter-cli/commit/30e5028d831d26349e3ae2fc8e34921693b5702c)
|
|
11
24
|
|
package/README.md
CHANGED
|
@@ -63,8 +63,10 @@ Then just follow the prompts, this command doesn't require you to log in.
|
|
|
63
63
|
#### Authentication
|
|
64
64
|
- **Login**: Log in to your Puter account.
|
|
65
65
|
```bash
|
|
66
|
-
puter login
|
|
66
|
+
puter login [--save]
|
|
67
67
|
```
|
|
68
|
+
P.S. You can add `--save` to save your authentication `token` to `.env` file as `PUTER_API_KEY` variable.
|
|
69
|
+
|
|
68
70
|
- **Logout**: Log out of your Puter account.
|
|
69
71
|
```bash
|
|
70
72
|
puter logout
|
|
@@ -127,6 +129,19 @@ Think of it as `git [push|pull]` commands, they're basically simplified equivale
|
|
|
127
129
|
```
|
|
128
130
|
P.S. These commands consider the current directory as the base path for every operation, basic wildcards are supported: e.g. `push myapp/*.html`.
|
|
129
131
|
|
|
132
|
+
- **Synchronize Files**: Bidirectional synchronization between local and remote directories.
|
|
133
|
+
```bash
|
|
134
|
+
puter> update <local_directory> <remote_directory> [--delete] [-r]
|
|
135
|
+
```
|
|
136
|
+
P.S. The `--delete` flag removes files in the remote directory that don't exist locally. The `-r` flag enables recursive synchronization of subdirectories.
|
|
137
|
+
|
|
138
|
+
#### User Information
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The addition describes the `update` command which allows for bidirectional synchronization between local and remote directories, including the optional flags for deleting files and recursive synchronization.
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
|
|
130
145
|
#### User Information
|
|
131
146
|
- **Get User Info**: Display user information.
|
|
132
147
|
```bash
|
|
@@ -157,8 +172,14 @@ P.S. Please check the help command `help apps` for more details about any argume
|
|
|
157
172
|
```bash
|
|
158
173
|
puter> app:create <name> [<directory>] [--description="My App Description"] [--url=<url>]
|
|
159
174
|
```
|
|
175
|
+
- This command works also from your system's terminal:
|
|
176
|
+
```bash
|
|
177
|
+
$> puter app:create <name> [<directory>] [--description="My App Description"] [--url=<url>]
|
|
178
|
+
```
|
|
179
|
+
|
|
160
180
|
P.S. By default a new `index.html` with basic content will be created, but you can set a directory when you create a new application as follows: `app:create nameOfApp ./appDir`, so all files will be copied to the `AppData` directory, you can then update your app using `app:update <name> <remote_dir>`. This command will attempt to create a subdomain with a random `uid` prefixed with the name of the app.
|
|
161
181
|
|
|
182
|
+
|
|
162
183
|
- **Update Application**: Update an application.
|
|
163
184
|
```bash
|
|
164
185
|
puter> app:update <name> <remote_dir>
|
package/package.json
CHANGED
package/src/commands/files.js
CHANGED
|
@@ -117,54 +117,87 @@ export async function makeDirectory(args = []) {
|
|
|
117
117
|
*/
|
|
118
118
|
export async function renameFileOrDirectory(args = []) {
|
|
119
119
|
if (args.length < 2) {
|
|
120
|
-
console.log(chalk.red('Usage: mv <
|
|
120
|
+
console.log(chalk.red('Usage: mv <source> <destination>'));
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const newName = args[1];
|
|
124
|
+
const sourcePath = args[0].startsWith('/') ? args[0] : resolvePath(getCurrentDirectory(), args[0]);
|
|
125
|
+
const destPath = args[1].startsWith('/') ? args[1] : resolvePath(getCurrentDirectory(), args[1]);
|
|
127
126
|
|
|
128
|
-
console.log(chalk.green(`
|
|
127
|
+
console.log(chalk.green(`Moving "${sourcePath}" to "${destPath}"...\n`));
|
|
129
128
|
|
|
130
129
|
try {
|
|
131
|
-
// Step 1: Get the
|
|
130
|
+
// Step 1: Get the source file/directory info
|
|
132
131
|
const statResponse = await fetch(`${API_BASE}/stat`, {
|
|
133
132
|
method: 'POST',
|
|
134
133
|
headers: getHeaders(),
|
|
135
|
-
body: JSON.stringify({
|
|
136
|
-
path: `${currentPath}/${oldName}`
|
|
137
|
-
})
|
|
134
|
+
body: JSON.stringify({ path: sourcePath })
|
|
138
135
|
});
|
|
139
136
|
|
|
140
137
|
const statData = await statResponse.json();
|
|
141
138
|
if (!statData || !statData.uid) {
|
|
142
|
-
console.log(chalk.red(`Could not find
|
|
139
|
+
console.log(chalk.red(`Could not find source "${sourcePath}".`));
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
const
|
|
143
|
+
const sourceUid = statData.uid;
|
|
144
|
+
const sourceName = statData.name;
|
|
147
145
|
|
|
148
|
-
// Step 2:
|
|
149
|
-
const
|
|
146
|
+
// Step 2: Check if destination is an existing directory
|
|
147
|
+
const destStatResponse = await fetch(`${API_BASE}/stat`, {
|
|
150
148
|
method: 'POST',
|
|
151
149
|
headers: getHeaders(),
|
|
152
|
-
body: JSON.stringify({
|
|
153
|
-
uid: uid,
|
|
154
|
-
new_name: newName
|
|
155
|
-
})
|
|
150
|
+
body: JSON.stringify({ path: destPath })
|
|
156
151
|
});
|
|
157
152
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
const destData = await destStatResponse.json();
|
|
154
|
+
|
|
155
|
+
// Determine if this is a rename or move operation
|
|
156
|
+
const isMove = destData && destData.is_dir;
|
|
157
|
+
const newName = isMove ? sourceName : path.basename(destPath);
|
|
158
|
+
const destination = isMove ? destPath : path.dirname(destPath);
|
|
159
|
+
|
|
160
|
+
if (isMove) {
|
|
161
|
+
// Move operation: use /move endpoint
|
|
162
|
+
const moveResponse = await fetch(`${API_BASE}/move`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: getHeaders(),
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
source: sourceUid,
|
|
167
|
+
destination: destination,
|
|
168
|
+
overwrite: false,
|
|
169
|
+
new_name: newName,
|
|
170
|
+
create_missing_parents: false,
|
|
171
|
+
new_metadata: {}
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const moveData = await moveResponse.json();
|
|
176
|
+
if (moveData && moveData.moved) {
|
|
177
|
+
console.log(chalk.green(`Successfully moved "${sourcePath}" to "${moveData.moved.path}"!`));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.red('Failed to move item. Please check your input.'));
|
|
180
|
+
}
|
|
163
181
|
} else {
|
|
164
|
-
|
|
182
|
+
// Rename operation: use /rename endpoint
|
|
183
|
+
const renameResponse = await fetch(`${API_BASE}/rename`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: getHeaders(),
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
uid: sourceUid,
|
|
188
|
+
new_name: newName
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const renameData = await renameResponse.json();
|
|
193
|
+
if (renameData && renameData.uid) {
|
|
194
|
+
console.log(chalk.green(`Successfully renamed "${sourcePath}" to "${renameData.path}"!`));
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.red('Failed to rename item. Please check your input.'));
|
|
197
|
+
}
|
|
165
198
|
}
|
|
166
199
|
} catch (error) {
|
|
167
|
-
console.log(chalk.red('Failed to rename item.'));
|
|
200
|
+
console.log(chalk.red('Failed to move/rename item.'));
|
|
168
201
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
169
202
|
}
|
|
170
203
|
}
|
|
@@ -206,7 +239,6 @@ async function findMatchingFiles(files, pattern, basePath) {
|
|
|
206
239
|
return matchedPaths;
|
|
207
240
|
}
|
|
208
241
|
|
|
209
|
-
|
|
210
242
|
/**
|
|
211
243
|
* Find files matching the pattern in the local directory (DEPRECATED: Not used)
|
|
212
244
|
* @param {string} localDir - Local directory path.
|
|
@@ -1075,28 +1107,34 @@ export async function copyFile(args = []) {
|
|
|
1075
1107
|
/**
|
|
1076
1108
|
* List all files in a local directory.
|
|
1077
1109
|
* @param {string} localDir - The local directory path.
|
|
1110
|
+
* @param {boolean} recursive - Whether to recursively list files in subdirectories
|
|
1078
1111
|
* @returns {Array} - Array of local file objects.
|
|
1079
1112
|
*/
|
|
1080
|
-
function listLocalFiles(localDir) {
|
|
1113
|
+
function listLocalFiles(localDir, recursive = false) {
|
|
1081
1114
|
const files = [];
|
|
1082
|
-
const walkDir = (dir) => {
|
|
1115
|
+
const walkDir = (dir, baseDir) => {
|
|
1116
|
+
|
|
1083
1117
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1084
1118
|
for (const entry of entries) {
|
|
1085
1119
|
const fullPath = path.join(dir, entry.name);
|
|
1120
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
1086
1121
|
if (entry.isDirectory()) {
|
|
1087
|
-
|
|
1122
|
+
if (recursive) {
|
|
1123
|
+
walkDir(fullPath, baseDir); // Recursively traverse directories if flag is set
|
|
1124
|
+
}
|
|
1088
1125
|
} else {
|
|
1089
1126
|
files.push({
|
|
1090
|
-
relativePath:
|
|
1127
|
+
relativePath: relativePath,
|
|
1091
1128
|
localPath: fullPath,
|
|
1092
1129
|
size: fs.statSync(fullPath).size,
|
|
1093
1130
|
modified: fs.statSync(fullPath).mtime.getTime()
|
|
1094
1131
|
});
|
|
1095
1132
|
}
|
|
1096
1133
|
}
|
|
1134
|
+
|
|
1097
1135
|
};
|
|
1098
1136
|
|
|
1099
|
-
walkDir(localDir);
|
|
1137
|
+
walkDir(localDir, localDir);
|
|
1100
1138
|
return files;
|
|
1101
1139
|
}
|
|
1102
1140
|
|
|
@@ -1190,12 +1228,39 @@ async function resolveLocalDirectory(localPath) {
|
|
|
1190
1228
|
return absolutePath;
|
|
1191
1229
|
}
|
|
1192
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Ensure a remote directory exists, creating it if necessary
|
|
1233
|
+
* @param {string} remotePath - The remote directory path
|
|
1234
|
+
*/
|
|
1235
|
+
async function ensureRemoteDirectoryExists(remotePath) {
|
|
1236
|
+
try {
|
|
1237
|
+
const exists = await pathExists(remotePath);
|
|
1238
|
+
if (!exists) {
|
|
1239
|
+
// Create the directory and any missing parents
|
|
1240
|
+
await fetch(`${API_BASE}/mkdir`, {
|
|
1241
|
+
method: 'POST',
|
|
1242
|
+
headers: getHeaders(),
|
|
1243
|
+
body: JSON.stringify({
|
|
1244
|
+
parent: path.dirname(remotePath),
|
|
1245
|
+
path: path.basename(remotePath),
|
|
1246
|
+
overwrite: false,
|
|
1247
|
+
dedupe_name: true,
|
|
1248
|
+
create_missing_parents: true
|
|
1249
|
+
})
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
console.error(chalk.red(`Failed to create remote directory: ${remotePath}`));
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1193
1258
|
/**
|
|
1194
1259
|
* Synchronize a local directory with a remote directory on Puter.
|
|
1195
|
-
* @param {string[]} args - Command-line arguments (e.g., [localDir, remoteDir]).
|
|
1260
|
+
* @param {string[]} args - Command-line arguments (e.g., [localDir, remoteDir, --delete, -r]).
|
|
1196
1261
|
*/
|
|
1197
1262
|
export async function syncDirectory(args = []) {
|
|
1198
|
-
const usageMessage = 'Usage: update <local_directory> <remote_directory> [--delete]';
|
|
1263
|
+
const usageMessage = 'Usage: update <local_directory> <remote_directory> [--delete] [-r]';
|
|
1199
1264
|
if (args.length < 2) {
|
|
1200
1265
|
console.log(chalk.red(usageMessage));
|
|
1201
1266
|
return;
|
|
@@ -1204,10 +1269,12 @@ export async function syncDirectory(args = []) {
|
|
|
1204
1269
|
let localDir = '';
|
|
1205
1270
|
let remoteDir = '';
|
|
1206
1271
|
let deleteFlag = '';
|
|
1272
|
+
let recursiveFlag = false;
|
|
1207
1273
|
try {
|
|
1208
1274
|
localDir = await resolveLocalDirectory(args[0]);
|
|
1209
1275
|
remoteDir = resolvePath(getCurrentDirectory(), args[1]);
|
|
1210
1276
|
deleteFlag = args.includes('--delete'); // Whether to delete extra files
|
|
1277
|
+
recursiveFlag = args.includes('-r'); // Whether to recursively process subdirectories
|
|
1211
1278
|
} catch (error) {
|
|
1212
1279
|
console.error(chalk.red(error.message));
|
|
1213
1280
|
console.log(chalk.green(usageMessage));
|
|
@@ -1231,14 +1298,17 @@ export async function syncDirectory(args = []) {
|
|
|
1231
1298
|
}
|
|
1232
1299
|
|
|
1233
1300
|
// Step 3: List local files
|
|
1234
|
-
const localFiles = listLocalFiles(localDir);
|
|
1301
|
+
const localFiles = listLocalFiles(localDir, recursiveFlag);
|
|
1235
1302
|
|
|
1236
1303
|
// Step 4: Compare local and remote files
|
|
1237
1304
|
let { toUpload, toDownload, toDelete } = compareFiles(localFiles, remoteFiles, localDir, remoteDir);
|
|
1305
|
+
let filteredToUpload = [...toUpload];
|
|
1306
|
+
let filteredToDownload = [...toDownload];
|
|
1238
1307
|
|
|
1239
1308
|
// Step 5: Handle conflicts (if any)
|
|
1240
1309
|
const conflicts = findConflicts(toUpload, toDownload);
|
|
1241
1310
|
if (conflicts.length > 0) {
|
|
1311
|
+
|
|
1242
1312
|
console.log(chalk.yellow('The following files have conflicts:'));
|
|
1243
1313
|
conflicts.forEach(file => console.log(chalk.dim(`- ${file}`)));
|
|
1244
1314
|
|
|
@@ -1256,12 +1326,12 @@ export async function syncDirectory(args = []) {
|
|
|
1256
1326
|
]);
|
|
1257
1327
|
|
|
1258
1328
|
if (resolve === 'local') {
|
|
1259
|
-
|
|
1329
|
+
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
|
|
1260
1330
|
} else if (resolve === 'remote') {
|
|
1261
|
-
|
|
1331
|
+
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
|
|
1262
1332
|
} else {
|
|
1263
|
-
|
|
1264
|
-
|
|
1333
|
+
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
|
|
1334
|
+
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
|
|
1265
1335
|
}
|
|
1266
1336
|
}
|
|
1267
1337
|
|
|
@@ -1269,18 +1339,30 @@ export async function syncDirectory(args = []) {
|
|
|
1269
1339
|
console.log(chalk.green('Starting synchronization...'));
|
|
1270
1340
|
|
|
1271
1341
|
// Upload new/updated files
|
|
1272
|
-
for (const file of
|
|
1342
|
+
for (const file of filteredToUpload) {
|
|
1273
1343
|
console.log(chalk.cyan(`Uploading "${file.relativePath}"...`));
|
|
1274
1344
|
const dedupeName = 'false';
|
|
1275
1345
|
const overwrite = 'true';
|
|
1276
|
-
|
|
1346
|
+
|
|
1347
|
+
// Create parent directories if needed
|
|
1348
|
+
const remoteFilePath = path.join(remoteDir, file.relativePath);
|
|
1349
|
+
const remoteFileDir = path.dirname(remoteFilePath);
|
|
1350
|
+
|
|
1351
|
+
// Ensure remote directory exists
|
|
1352
|
+
await ensureRemoteDirectoryExists(remoteFileDir);
|
|
1353
|
+
|
|
1354
|
+
await uploadFile([file.localPath, remoteFileDir, dedupeName, overwrite]);
|
|
1277
1355
|
}
|
|
1278
1356
|
|
|
1279
1357
|
// Download new/updated files
|
|
1280
|
-
for (const file of
|
|
1358
|
+
for (const file of filteredToDownload) {
|
|
1281
1359
|
console.log(chalk.cyan(`Downloading "${file.relativePath}"...`));
|
|
1282
1360
|
const overwrite = 'true';
|
|
1283
|
-
|
|
1361
|
+
// Create local parent directories if needed
|
|
1362
|
+
const localFilePath = path.join(localDir, file.relativePath);
|
|
1363
|
+
// const localFileDir = path.dirname(localFilePath);
|
|
1364
|
+
|
|
1365
|
+
await downloadFile([file.remotePath, localFilePath, overwrite]);
|
|
1284
1366
|
}
|
|
1285
1367
|
|
|
1286
1368
|
// Delete extra files (if --delete flag is set)
|
package/src/commons.js
CHANGED
|
@@ -106,7 +106,7 @@ export function showDiskSpaceUsage(data) {
|
|
|
106
106
|
console.log(chalk.cyan(`Usage Percentage: `) + chalk.white(`${usagePercentage.toFixed(2)}%`));
|
|
107
107
|
console.log(chalk.dim('----------------------------------------'));
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
/**
|
|
111
111
|
* Resolve a relative path to an absolute path
|
|
112
112
|
* @param {string} currentPath - The current working directory
|
package/src/executor.js
CHANGED
|
@@ -323,7 +323,7 @@ function showHelp(command) {
|
|
|
323
323
|
Example: pull /path/to/file
|
|
324
324
|
`,
|
|
325
325
|
update: `
|
|
326
|
-
${chalk.cyan('update <src> <dest>')}
|
|
326
|
+
${chalk.cyan('update <src> <dest> [--delete] [-r]')}
|
|
327
327
|
Sync local directory with remote cloud.
|
|
328
328
|
Example: update /local/path /remote/path
|
|
329
329
|
`,
|