@lightnet/cli 4.6.0 → 4.6.1

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
@@ -1,5 +1,11 @@
1
1
  # @lightnet/cli
2
2
 
3
+ ## 4.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#441](https://github.com/LightNetDev/LightNet/pull/441) [`b643414`](https://github.com/LightNetDev/LightNet/commit/b643414852ed1e6d80fa013dff9e1189c9f7ba35) - Protect R2 bucket-root delete and copy replacement with a visible warning and require the long `--force` flag to bypass confirmation.
8
+
3
9
  ## 4.6.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "CLI for managing LightNet projects",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "4.6.0",
6
+ "version": "4.6.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
package/src/index.js CHANGED
@@ -96,10 +96,11 @@ r2Command
96
96
  .option("-r, --recursive", "delete a directory/prefix recursively")
97
97
  .option(
98
98
  "-f, --force",
99
- "delete without confirmation, except when cleaning the bucket root",
99
+ 'delete without confirmation; use "--force" to clean the bucket root without confirmation',
100
100
  )
101
101
  .action(async (path, options) => {
102
102
  try {
103
+ options.longForce = hasLongForceFlag()
103
104
  await removeR2(path, options)
104
105
  } catch (error) {
105
106
  handleCommandError(error)
@@ -113,9 +114,13 @@ r2Command
113
114
  )
114
115
  .argument("<source>", 'source path; use "r2:<path>" for R2')
115
116
  .argument("<destination>", 'destination path; use "r2:<path>" for R2')
116
- .option("-f, --force", "overwrite existing files without confirmation")
117
+ .option(
118
+ "-f, --force",
119
+ 'overwrite without confirmation; use "--force" to replace the bucket root without confirmation',
120
+ )
117
121
  .action(async (source, destination, options) => {
118
122
  try {
123
+ options.longForce = hasLongForceFlag()
119
124
  await copyR2(source, destination, options)
120
125
  } catch (error) {
121
126
  handleCommandError(error)
@@ -154,3 +159,7 @@ function handleCommandError(error) {
154
159
  }
155
160
  throw error
156
161
  }
162
+
163
+ function hasLongForceFlag() {
164
+ return process.argv.includes("--force")
165
+ }
package/src/r2.js CHANGED
@@ -20,6 +20,7 @@ const remotePathPrefix = "r2:"
20
20
  /**
21
21
  * @typedef {{
22
22
  * force?: boolean
23
+ * longForce?: boolean
23
24
  * }} R2CopyOptions
24
25
  */
25
26
 
@@ -42,6 +43,7 @@ const remotePathPrefix = "r2:"
42
43
  * @typedef {{
43
44
  * force?: boolean
44
45
  * recursive?: boolean
46
+ * longForce?: boolean
45
47
  * }} R2RemoveOptions
46
48
  */
47
49
 
@@ -89,9 +91,19 @@ export async function removeR2(path, options, runtime = {}) {
89
91
  }
90
92
 
91
93
  const interactive = getInteractive(runtime)
92
- if (isBucketRoot && !interactive) {
94
+ if (isBucketRoot && options.force && !options.longForce && !interactive) {
93
95
  throw new CliError(
94
- "Cleaning the R2 bucket root requires interactive confirmation.",
96
+ 'Ignoring "-f" for R2 bucket-root cleanup. If you are sure you want to delete the entire R2 bucket, use "--force".',
97
+ )
98
+ }
99
+ if (isBucketRoot && !interactive && !options.longForce) {
100
+ throw new CliError(
101
+ 'Cleaning the R2 bucket root requires interactive confirmation. If you are sure you want to delete the entire R2 bucket, use "--force".',
102
+ )
103
+ }
104
+ if (isBucketRoot && options.force && !options.longForce) {
105
+ log.warn(
106
+ 'Ignoring "-f" for R2 bucket-root cleanup. Use "--force" if you are sure you want to delete the entire R2 bucket.',
95
107
  )
96
108
  }
97
109
  if (!options.force && !interactive) {
@@ -107,14 +119,15 @@ export async function removeR2(path, options, runtime = {}) {
107
119
  }
108
120
 
109
121
  const shouldDelete = isBucketRoot
110
- ? await getPromptConfirm(runtime)(
122
+ ? options.longForce ||
123
+ (await getPromptConfirm(runtime)(
111
124
  "Delete all files in the R2 bucket? [y/N] ",
112
- )
125
+ ))
113
126
  : options.force ||
114
127
  (await getPromptConfirm(runtime)(`Delete R2 path "${r2Path}"? [y/N] `))
115
128
 
116
129
  if (!shouldDelete) {
117
- return
130
+ cancelPrompt()
118
131
  }
119
132
 
120
133
  const client = createR2CommandClient(runtime)
@@ -161,14 +174,23 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
161
174
  sourceType === "file" &&
162
175
  (targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
163
176
 
164
- await confirmCopyOverwrite({
165
- destination,
177
+ const confirmedRootOverwrite = await confirmCopyRootOverwrite({
166
178
  force: options.force === true,
179
+ longForce: options.longForce === true,
167
180
  runtime,
168
- sourceType,
169
181
  targetPath,
170
182
  targetType,
171
183
  })
184
+ if (!confirmedRootOverwrite) {
185
+ await confirmCopyOverwrite({
186
+ destination,
187
+ force: options.force === true,
188
+ runtime,
189
+ sourceType,
190
+ targetPath,
191
+ targetType,
192
+ })
193
+ }
172
194
  await replaceCopyTargetBeforeCopy({
173
195
  client,
174
196
  runtime,
@@ -271,6 +293,55 @@ async function replaceCopyTargetBeforeCopy({
271
293
  })
272
294
  }
273
295
 
296
+ /**
297
+ * @param {{
298
+ * force: boolean,
299
+ * longForce: boolean,
300
+ * runtime: R2Runtime,
301
+ * targetPath: CopyPath,
302
+ * targetType: CopyPathType
303
+ * }} args
304
+ */
305
+ async function confirmCopyRootOverwrite({
306
+ force,
307
+ longForce,
308
+ runtime,
309
+ targetPath,
310
+ targetType,
311
+ }) {
312
+ if (!isR2BucketRootCopyPath(targetPath) || targetType === "missing") {
313
+ return false
314
+ }
315
+ log.warn(
316
+ "This will replace ALL files in the configured R2 bucket. This cannot be undone.",
317
+ )
318
+ if (longForce) {
319
+ return true
320
+ }
321
+ if (force && !getInteractive(runtime)) {
322
+ throw new CliError(
323
+ 'Ignoring "-f" for R2 bucket-root replacement. If you are sure you want to replace the entire R2 bucket, use "--force".',
324
+ )
325
+ }
326
+ if (!getInteractive(runtime)) {
327
+ throw new CliError(
328
+ 'Replacing the R2 bucket root requires interactive confirmation. If you are sure you want to replace the entire R2 bucket, use "--force".',
329
+ )
330
+ }
331
+ if (force) {
332
+ log.warn(
333
+ 'Ignoring "-f" for R2 bucket-root replacement. Use "--force" if you are sure you want to replace the entire R2 bucket.',
334
+ )
335
+ }
336
+ const shouldOverwrite = await getPromptConfirm(runtime)(
337
+ "Replace all files in the R2 bucket? [y/N] ",
338
+ )
339
+ if (!shouldOverwrite) {
340
+ cancelPrompt()
341
+ }
342
+ return true
343
+ }
344
+
274
345
  /**
275
346
  * @param {R2Runtime} runtime
276
347
  */
@@ -498,6 +569,13 @@ function isR2BucketRootPath(path) {
498
569
  return path.trim() === "/"
499
570
  }
500
571
 
572
+ /**
573
+ * @param {CopyPath} path
574
+ */
575
+ function isR2BucketRootCopyPath(path) {
576
+ return path.remote && path.path === ""
577
+ }
578
+
501
579
  /**
502
580
  * @param {unknown} error
503
581
  */