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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puter-cli",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Command line interface for Puter cloud platform",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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 <old_name> <new_name>'));
120
+ console.log(chalk.red('Usage: mv <source> <destination>'));
121
121
  return;
122
122
  }
123
123
 
124
- const currentPath = getCurrentDirectory();
125
- const oldName = args[0];
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(`Renaming "${oldName}" to "${newName}"...\n`));
127
+ console.log(chalk.green(`Moving "${sourcePath}" to "${destPath}"...\n`));
129
128
 
130
129
  try {
131
- // Step 1: Get the UID of the file/directory using the old name
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 file or directory with name "${oldName}".`));
139
+ console.log(chalk.red(`Could not find source "${sourcePath}".`));
143
140
  return;
144
141
  }
145
142
 
146
- const uid = statData.uid;
143
+ const sourceUid = statData.uid;
144
+ const sourceName = statData.name;
147
145
 
148
- // Step 2: Perform the rename operation using the UID
149
- const renameResponse = await fetch(`${API_BASE}/rename`, {
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 renameData = await renameResponse.json();
159
- if (renameData && renameData.uid) {
160
- console.log(chalk.green(`Successfully renamed "${oldName}" to "${newName}"!`));
161
- console.log(chalk.dim(`Path: ${renameData.path}`));
162
- console.log(chalk.dim(`UID: ${renameData.uid}`));
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
- console.log(chalk.red('Failed to rename item. Please check your input.'));
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
- walkDir(fullPath);
1122
+ if (recursive) {
1123
+ walkDir(fullPath, baseDir); // Recursively traverse directories if flag is set
1124
+ }
1088
1125
  } else {
1089
1126
  files.push({
1090
- relativePath: path.relative(localDir, fullPath),
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
- toDownload = toDownload.filter(file => !conflicts.includes(file.relativePath));
1329
+ filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
1260
1330
  } else if (resolve === 'remote') {
1261
- toUpload = toUpload.filter(file => !conflicts.includes(file.relativePath));
1331
+ filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
1262
1332
  } else {
1263
- toUpload = toUpload.filter(file => !conflicts.includes(file.relativePath));
1264
- toDownload = toDownload.filter(file => !conflicts.includes(file.relativePath));
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 toUpload) {
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
- await uploadFile([file.localPath, remoteDir, dedupeName, overwrite]);
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 toDownload) {
1358
+ for (const file of filteredToDownload) {
1281
1359
  console.log(chalk.cyan(`Downloading "${file.relativePath}"...`));
1282
1360
  const overwrite = 'true';
1283
- await downloadFile([file.relativePath, file.localPath, overwrite]);
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
  `,