8bitsapps-gcp-utils 1.0.3 → 1.0.5

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.
@@ -180,6 +180,53 @@ class Storage {
180
180
  resumable: false
181
181
  });
182
182
  }
183
+ //
184
+ /**
185
+ * Creates a folder (empty placeholder object) in the bucket.
186
+ * @param {string} bucketName - Name of the bucket.
187
+ * @param {string} folderPath - Path of the folder (must end with /).
188
+ * @returns {Promise<void>}
189
+ */
190
+ async createFolder(bucketName, folderPath) {
191
+ await this.loadConfiguration();
192
+ //
193
+ const bucket = this.storage.bucket(bucketName);
194
+ const normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
195
+ const file = bucket.file(normalizedPath);
196
+ await file.save("", { contentType: "application/x-directory" });
197
+ }
198
+ //
199
+ /**
200
+ * Deletes a file from the bucket.
201
+ * @param {string} bucketName - Name of the bucket.
202
+ * @param {string} filePath - Path of the file to delete.
203
+ * @returns {Promise<void>}
204
+ */
205
+ async deleteFile(bucketName, filePath) {
206
+ await this.loadConfiguration();
207
+ //
208
+ const bucket = this.storage.bucket(bucketName);
209
+ const file = bucket.file(filePath);
210
+ await file.delete();
211
+ }
212
+ //
213
+ /**
214
+ * Deletes a folder and all its contents from the bucket.
215
+ * @param {string} bucketName - Name of the bucket.
216
+ * @param {string} folderPath - Path of the folder to delete.
217
+ * @returns {Promise<number>} Number of deleted files.
218
+ */
219
+ async deleteFolder(bucketName, folderPath) {
220
+ await this.loadConfiguration();
221
+ //
222
+ const bucket = this.storage.bucket(bucketName);
223
+ const [files] = await bucket.getFiles({ prefix: folderPath });
224
+ //
225
+ for (const file of files) {
226
+ await file.delete();
227
+ }
228
+ return files.length;
229
+ }
183
230
  }
184
231
 
185
232
  module.exports = Storage;
@@ -1,9 +1,11 @@
1
1
  const Network = require("./Network.js");
2
2
  const GCPUStorage = require("./Storage.js");
3
-
3
+ const packageJson = require("../package.json");
4
+ //
4
5
  const GCPUtils = {
5
6
  Network: Network,
6
- Storage: GCPUStorage
7
+ Storage: GCPUStorage,
8
+ version: packageJson.version
7
9
  };
8
10
 
9
11
  module.exports = GCPUtils;
package/README.md CHANGED
@@ -20,6 +20,9 @@ Interactive navigator for Google Cloud Storage:
20
20
  - View files with size information.
21
21
  - Download files to current directory.
22
22
  - Upload local files to GCS.
23
+ - Create folders in GCS.
24
+ - Delete files from GCS.
25
+ - Delete folders from GCS (recursive).
23
26
  - Support for multiple buckets per configuration.
24
27
 
25
28
  ### Initialize configuration
@@ -108,20 +108,25 @@ async function showNavigationMenu(bucketName, currentPath, contents, options) {
108
108
  choices.push(new inquirer.Separator(chalk.yellow(`(showing first ${maxItems} items)`)));
109
109
  }
110
110
  choices.push({
111
- name: chalk.green("Upload file here"),
111
+ name: chalk.green("Upload file here"),
112
112
  value: { action: "upload", value: currentPath }
113
113
  });
114
+ choices.push({
115
+ name: chalk.green("+ Create folder here"),
116
+ value: { action: "createFolder", value: currentPath }
117
+ });
114
118
  //
115
119
  const message = backEnabled
116
- ? `${bucketName}:${displayPath} (← back, ESC exit)`
117
- : `${bucketName}:${displayPath} (ESC exit)`;
120
+ ? `${bucketName}:${displayPath} (← back, DEL delete, ESC exit)`
121
+ : `${bucketName}:${displayPath} (DEL delete, ESC exit)`;
118
122
  //
119
123
  const { selected } = await inquirer.prompt([{
120
124
  type: "listWithEscape",
121
125
  name: "selected",
122
126
  message,
123
127
  choices,
124
- enableBack: backEnabled
128
+ enableBack: backEnabled,
129
+ deleteFilter: (value) => value?.action === "file" || value?.action === "folder"
125
130
  }]);
126
131
  //
127
132
  return selected;
@@ -141,6 +146,41 @@ async function promptUploadPath() {
141
146
  return filePath || null;
142
147
  }
143
148
  //
149
+ /**
150
+ * Prompts for folder name to create.
151
+ * @returns {Promise<string|null>} Folder name or null if cancelled.
152
+ */
153
+ async function promptFolderName() {
154
+ const { folderName } = await inquirer.prompt([{
155
+ type: "input",
156
+ name: "folderName",
157
+ message: "Enter folder name (or leave empty to cancel):",
158
+ validate: (input) => {
159
+ if (!input.trim()) return true;
160
+ if (input.includes("/")) return "Folder name cannot contain /";
161
+ return true;
162
+ }
163
+ }]);
164
+ //
165
+ return folderName.trim() || null;
166
+ }
167
+ //
168
+ /**
169
+ * Prompts for delete confirmation.
170
+ * @param {string} itemName - Name of the item to delete.
171
+ * @param {string} itemType - Type of item ("file" or "folder").
172
+ * @returns {Promise<boolean>} True if confirmed.
173
+ */
174
+ async function confirmDelete(itemName, itemType) {
175
+ const { confirmed } = await inquirer.prompt([{
176
+ type: "confirm",
177
+ name: "confirmed",
178
+ message: `Delete ${itemType} "${itemName}"?`,
179
+ default: false
180
+ }]);
181
+ return confirmed;
182
+ }
183
+ //
144
184
  /**
145
185
  * Shows the operation log section.
146
186
  * @param {Array<{type: string, message: string}>} logs - Array of log entries.
@@ -183,6 +223,9 @@ const command = {
183
223
  if (bucketName === null || bucketName?.action === "back") {
184
224
  return; // ESC or back arrow pressed.
185
225
  }
226
+ // Position cursor at end of bucket selection line.
227
+ process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
228
+ process.stdout.write(`? Select bucket (ESC exit): ${bucketName}`);
186
229
  //
187
230
  // Navigation loop.
188
231
  const pathHistory = [""];
@@ -213,7 +256,7 @@ const command = {
213
256
  if (result === null) {
214
257
  // ESC pressed - exit storage navigator completely.
215
258
  const exitDisplayPath = currentPath || "/";
216
- const exitHint = backEnabled ? "(← back, ESC exit)" : "(ESC exit)";
259
+ const exitHint = backEnabled ? "(← back, DEL delete, ESC exit)" : "(DEL delete, ESC exit)";
217
260
  process.stdout.write(`\x1b[1A\x1b[2K\x1b[G? ${bucketName}:${exitDisplayPath} ${exitHint} ${chalk.red("<- exit")}`);
218
261
  console.log();
219
262
  return;
@@ -222,18 +265,51 @@ const command = {
222
265
  if (result.action === "back") {
223
266
  // Left arrow pressed - go back one level (only possible when backEnabled is true).
224
267
  const backDisplayPath = currentPath || "/";
225
- process.stdout.write(`\x1b[1A\x1b[2K\x1b[G? ${bucketName}:${backDisplayPath} (← back, ESC exit) ${chalk.cyan("<- back")}`);
268
+ process.stdout.write(`\x1b[1A\x1b[2K\x1b[G? ${bucketName}:${backDisplayPath} (← back, DEL delete, ESC exit) ${chalk.cyan("<- back")}`);
226
269
  pathHistory.pop();
227
270
  continue;
228
271
  }
229
272
  //
273
+ if (result.action === "delete") {
274
+ const selectedValue = result.value;
275
+ // Skip if not a file or folder (e.g., upload/createFolder option).
276
+ if (!selectedValue?.action || (selectedValue.action !== "file" && selectedValue.action !== "folder")) {
277
+ continue;
278
+ }
279
+ //
280
+ const isFolder = selectedValue.action === "folder";
281
+ const itemPath = selectedValue.value;
282
+ const itemName = getDisplayName(itemPath, currentPath);
283
+ //
284
+ const confirmed = await confirmDelete(itemName, isFolder ? "folder" : "file");
285
+ if (confirmed) {
286
+ const itemType = isFolder ? "folder" : "file";
287
+ process.stdout.write(chalk.yellow(`\n⏳ Deleting ${itemType} ${itemName}...`));
288
+ try {
289
+ if (isFolder) {
290
+ const count = await storage.deleteFolder(bucketName, itemPath);
291
+ process.stdout.write("\x1b[2K\x1b[G");
292
+ operationLogs.push({ type: "success", message: `Deleted folder "${itemName}" (${count} files)` });
293
+ } else {
294
+ await storage.deleteFile(bucketName, itemPath);
295
+ process.stdout.write("\x1b[2K\x1b[G");
296
+ operationLogs.push({ type: "success", message: `Deleted file "${itemName}"` });
297
+ }
298
+ } catch (err) {
299
+ process.stdout.write("\x1b[2K\x1b[G");
300
+ operationLogs.push({ type: "error", message: `Delete failed: ${itemName} - ${err.message}` });
301
+ }
302
+ }
303
+ continue;
304
+ }
305
+ //
230
306
  switch (result.action) {
231
307
  case "folder": {
232
308
  // Force newline, then go back up and overwrite inquirer's colored line.
233
309
  const folderDisplayName = getDisplayName(result.value, currentPath);
234
310
  const folderDisplayPath = currentPath || "/";
235
311
  process.stdout.write("\x1b[1A\x1b[2K\x1b[G");
236
- process.stdout.write(`? ${bucketName}:${folderDisplayPath} (← back, ESC exit) ${chalk.cyan(`[D] ${folderDisplayName}`)}`);
312
+ process.stdout.write(`? ${bucketName}:${folderDisplayPath} (← back, DEL delete, ESC exit) ${chalk.cyan(`[D] ${folderDisplayName}`)}`);
237
313
  // Enter folder.
238
314
  pathHistory.push(result.value);
239
315
  break;
@@ -279,6 +355,27 @@ const command = {
279
355
  }
280
356
  break;
281
357
  }
358
+ //
359
+ case "createFolder": {
360
+ // Create folder.
361
+ const folderName = await promptFolderName();
362
+ if (folderName) {
363
+ const remotePath = currentPath + folderName + "/";
364
+ //
365
+ process.stdout.write(chalk.yellow(`\n⏳ Creating folder ${folderName}...`));
366
+ try {
367
+ await storage.createFolder(bucketName, remotePath);
368
+ // Clear the "Creating..." line.
369
+ process.stdout.write("\x1b[2K\x1b[G");
370
+ operationLogs.push({ type: "success", message: `Folder created: ${folderName} at ${bucketName}:${currentPath || "/"}` });
371
+ } catch (err) {
372
+ // Clear the "Creating..." line.
373
+ process.stdout.write("\x1b[2K\x1b[G");
374
+ operationLogs.push({ type: "error", message: `Create folder failed: ${folderName} - ${err.message}` });
375
+ }
376
+ }
377
+ break;
378
+ }
282
379
  }
283
380
  }
284
381
  }
package/gcpUtils.js CHANGED
@@ -6,8 +6,10 @@ const { isLocalMode, getConfigurationsDir } = require("./utils/paths.js");
6
6
  const { listConfigurations } = require("./utils/configLoader.js");
7
7
  const commands = require("./commands/index.js");
8
8
  const ListWithEscapePrompt = require("./utils/prompts/listWithEscape.js");
9
+ const { checkForUpdates } = require("./utils/updateChecker.js");
9
10
  //
10
- const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
11
+ const packageJson = require("./package.json");
12
+ const boxWidth = 55;
11
13
  //
12
14
  // Register custom prompt with ESC support.
13
15
  inquirer.registerPrompt("listWithEscape", ListWithEscapePrompt);
@@ -109,18 +111,34 @@ async function waitForKeypress() {
109
111
  function showBanner() {
110
112
  const mode = isLocalMode() ? "local" : "global";
111
113
  console.log(
112
- `=================================================
113
- ${chalk.hex("#FFFFFF")(" █ █ ")} |
114
- ${chalk.hex("#ffff00")(" █ █ ")} |
115
- ${chalk.hex("#fffF00")(" ███████ ")} | ${chalk.hex("#F77B00").bold("GCP Utils")} ${chalk.gray(`v${packageJson.version}`)}
116
- ${chalk.hex("#FFCE00")(" ███ █ ███ ")} | by ${chalk.whiteBright.bold("8BitsApps")}
117
- ${chalk.hex("#FFCE00")("█████████████ ")} |
118
- ${chalk.hex("#F77B00")("█ ███████ █ ")} | ${chalk.bgHex("#FFCE00").hex("#000000")("We made app for fun! (ツ)")}
119
- ${chalk.hex("#F77B00")("█ ███████ █ ")} | ${chalk.hex("#FFCE00")("https://8bitsapps.com")}
120
- ${chalk.hex("#E73100")(" █ █ ")} |
121
- ${chalk.hex("#E73100")(" ██ ██ ")} | ${chalk.gray(`Mode:${mode}`)}
122
- _________________________________________________
123
- `);
114
+ `${chalk.hex("#F77B00")("=".repeat(boxWidth))}
115
+ ${chalk.hex("#FFFFFF")(" █ █ ")} |
116
+ ${chalk.hex("#ffff00")(" █ █ ")} |
117
+ ${chalk.hex("#fffF00")(" ███████ ")} | ${chalk.hex("#F77B00").bold("GCP Utils")} ${chalk.gray(`v${packageJson.version}`)}
118
+ ${chalk.hex("#FFCE00")(" ███ █ ███ ")} | by ${chalk.whiteBright.bold("8BitsApps")}
119
+ ${chalk.hex("#FFCE00")(" █████████████ ")} |
120
+ ${chalk.hex("#F77B00")(" █ ███████ █ ")} | ${chalk.bgHex("#FFCE00").hex("#000000")("We make app for fun! (ツ)")}
121
+ ${chalk.hex("#F77B00")(" █ ███████ █ ")} | ${chalk.hex("#FFCE00")("https://8bitsapps.com")}
122
+ ${chalk.hex("#E73100")(" █ █ ")} |
123
+ ${chalk.hex("#E73100")(" ██ ██ ")} | ${chalk.gray(`Mode:${mode}`)}
124
+ ${chalk.hex("#F77B00")("_".repeat(boxWidth))}\n`);
125
+ }
126
+ //
127
+ /**
128
+ * Shows update notification if a new version is available.
129
+ * @param {{available: boolean, current: string, latest: string}|null} updateInfo
130
+ */
131
+ function showUpdateNotification(updateInfo) {
132
+ if (!updateInfo || !updateInfo.available) {
133
+ return;
134
+ }
135
+ //
136
+ const msg = `Update available: ${updateInfo.current} → ${updateInfo.latest}`;
137
+ const cmd = `npm install -g ${packageJson.name}`;
138
+ //
139
+ console.log(chalk.white(` ${msg}`));
140
+ console.log(chalk.gray(` Run: ${chalk.cyan(cmd)}`));
141
+ console.log(chalk.hex("#FFCE00")("_".repeat(boxWidth) + "\n"));
124
142
  }
125
143
  //
126
144
  /**
@@ -128,6 +146,10 @@ _________________________________________________
128
146
  */
129
147
  async function main() {
130
148
  console.clear();
149
+ //
150
+ // Start update check in background (non-blocking).
151
+ const updateCheckPromise = checkForUpdates();
152
+ //
131
153
  // Check if initialized.
132
154
  if (!(await isInitialized())) {
133
155
  console.log(chalk.yellow("Configuration not found."));
@@ -147,9 +169,27 @@ async function main() {
147
169
  return;
148
170
  }
149
171
  //
172
+ // Wait for update check with timeout (max 2s).
173
+ let updateInfo = null;
174
+ try {
175
+ updateInfo = await Promise.race([
176
+ updateCheckPromise,
177
+ new Promise(resolve => setTimeout(() => resolve(null), 2000))
178
+ ]);
179
+ } catch {
180
+ // Ignore errors.
181
+ }
182
+ //
150
183
  while (true) {
151
184
  console.clear();
152
185
  showBanner();
186
+ //
187
+ // Show update notification only on first iteration.
188
+ if (updateInfo) {
189
+ showUpdateNotification(updateInfo);
190
+ updateInfo = null;
191
+ }
192
+ //
153
193
  const cmdName = await showMainMenu();
154
194
  //
155
195
  // ESC pressed in main menu - exit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "8bitsapps-gcp-utils",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "GCP Utility CLI for firewall and storage operations",
5
5
  "main": "gcpUtils.js",
6
6
  "bin": {
@@ -39,6 +39,24 @@ class ListWithEscapePrompt extends ListPrompt {
39
39
  });
40
40
  }
41
41
  //
42
+ // Handle delete key - delete selected item (only if deleteFilter allows).
43
+ events.keypress
44
+ .pipe(
45
+ takeUntil(events.line),
46
+ filter(({ key }) => key && key.name === "delete")
47
+ )
48
+ .forEach(() => {
49
+ const selectedValue = this.opt.choices.getChoice(this.selected).value;
50
+ // Check if delete is allowed for this item.
51
+ const deleteFilter = this.opt.deleteFilter;
52
+ if (deleteFilter && !deleteFilter(selectedValue)) {
53
+ return; // Ignore delete key for this item.
54
+ }
55
+ process.stdout.write(`\x1b[${(this.screen.height || 1) - 1}A\x1b[J\x1b[G`);
56
+ this.screen.done();
57
+ this.done({ action: "delete", value: selectedValue });
58
+ });
59
+ //
42
60
  return super._run(cb);
43
61
  }
44
62
  }
@@ -0,0 +1,66 @@
1
+ const axios = require("axios");
2
+ const packageJson = require("../package.json");
3
+ //
4
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
5
+ const PACKAGE_NAME = packageJson.name;
6
+ const CURRENT_VERSION = packageJson.version;
7
+ //
8
+ /**
9
+ * Compares two semver versions.
10
+ * @param {string} current - Current version.
11
+ * @param {string} latest - Latest version from registry.
12
+ * @returns {boolean} True if latest is newer than current.
13
+ */
14
+ function isNewerVersion(current, latest) {
15
+ const currentParts = current.split(".").map(Number);
16
+ const latestParts = latest.split(".").map(Number);
17
+ //
18
+ for (let i = 0; i < 3; i++) {
19
+ if (latestParts[i] > currentParts[i]) return true;
20
+ if (latestParts[i] < currentParts[i]) return false;
21
+ }
22
+ return false;
23
+ }
24
+ //
25
+ /**
26
+ * Fetches the latest version from npm registry.
27
+ * @returns {Promise<string|null>} Latest version or null on error.
28
+ */
29
+ async function fetchLatestVersion() {
30
+ try {
31
+ const url = `${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`;
32
+ const response = await axios.get(url, { timeout: 3000 });
33
+ return response.data.version || null;
34
+ } catch {
35
+ // Silently fail - network issues should not block the CLI.
36
+ return null;
37
+ }
38
+ }
39
+ //
40
+ /**
41
+ * Checks for updates and returns update info.
42
+ * @returns {Promise<{available: boolean, current: string, latest: string}|null>}
43
+ */
44
+ async function checkForUpdates() {
45
+ const latestVersion = await fetchLatestVersion();
46
+ //
47
+ if (!latestVersion) {
48
+ return null;
49
+ }
50
+ //
51
+ const updateAvailable = isNewerVersion(CURRENT_VERSION, latestVersion);
52
+ //
53
+ return {
54
+ available: updateAvailable,
55
+ current: CURRENT_VERSION,
56
+ latest: latestVersion
57
+ };
58
+ }
59
+ //
60
+ module.exports = {
61
+ checkForUpdates,
62
+ isNewerVersion,
63
+ fetchLatestVersion,
64
+ CURRENT_VERSION,
65
+ PACKAGE_NAME
66
+ };