@lightnet/cli 4.6.1 → 4.6.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
@@ -1,5 +1,15 @@
1
1
  # @lightnet/cli
2
2
 
3
+ ## 4.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#443](https://github.com/LightNetDev/LightNet/pull/443) [`6593139`](https://github.com/LightNetDev/LightNet/commit/65931398471107c03bca6bf4de675599b11db5e5) - Add Clack intro and completion messages to mutating `lightnet r2` commands while keeping `lightnet r2 ls` pipe-friendly.
8
+
9
+ - [#443](https://github.com/LightNetDev/LightNet/pull/443) [`6593139`](https://github.com/LightNetDev/LightNet/commit/65931398471107c03bca6bf4de675599b11db5e5) - Improve `lightnet r2 cp`, `lightnet r2 mv`, and recursive `lightnet r2 rm` performance by passing tuned rclone concurrency settings internally.
10
+
11
+ - [#443](https://github.com/LightNetDev/LightNet/pull/443) [`6593139`](https://github.com/LightNetDev/LightNet/commit/65931398471107c03bca6bf4de675599b11db5e5) - Show a clear install message when `lightnet r2` commands cannot find `rclone` on PATH.
12
+
3
13
  ## 4.6.1
4
14
 
5
15
  ### Patch 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.1",
6
+ "version": "4.6.2",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
package/src/r2.js CHANGED
@@ -9,13 +9,24 @@ import {
9
9
  stdout,
10
10
  } from "node:process"
11
11
 
12
- import { confirm, isCancel, log, text } from "@clack/prompts"
12
+ import { confirm, intro, isCancel, log, outro, text } from "@clack/prompts"
13
13
 
14
14
  import { CliError } from "./support/cli-error.js"
15
15
  import { cancelPrompt } from "./support/prompt-cancel.js"
16
16
  import { createR2Client } from "./support/r2.js"
17
17
 
18
18
  const remotePathPrefix = "r2:"
19
+ const defaultRcloneOptions = {
20
+ checkers: 16,
21
+ transfers: 8,
22
+ }
23
+
24
+ /**
25
+ * @typedef {{
26
+ * transfers?: number
27
+ * checkers?: number
28
+ * }} RcloneOptions
29
+ */
19
30
 
20
31
  /**
21
32
  * @typedef {{
@@ -79,6 +90,7 @@ export async function listR2(path, runtime = {}) {
79
90
  * @param {R2Runtime} [runtime]
80
91
  */
81
92
  export async function removeR2(path, options, runtime = {}) {
93
+ intro("r2 rm")
82
94
  const r2Path = normalizeR2Path(path)
83
95
  const isBucketRoot = isR2BucketRootPath(path)
84
96
  if (isBucketRoot && !options.recursive) {
@@ -133,7 +145,10 @@ export async function removeR2(path, options, runtime = {}) {
133
145
  const client = createR2CommandClient(runtime)
134
146
  await client.remove(r2Path, {
135
147
  recursive: options.recursive === true,
148
+ rcloneOptions:
149
+ options.recursive === true ? defaultRcloneOptions : undefined,
136
150
  })
151
+ outro("R2 delete complete.")
137
152
  }
138
153
 
139
154
  /**
@@ -143,6 +158,7 @@ export async function removeR2(path, options, runtime = {}) {
143
158
  * @param {R2Runtime} [runtime]
144
159
  */
145
160
  export async function copyR2(source, destination, options = {}, runtime = {}) {
161
+ intro("r2 cp")
146
162
  const sourcePath = parseCopyPath(source)
147
163
  const destinationPath = parseCopyPath(destination)
148
164
  if (!sourcePath.remote && !destinationPath.remote) {
@@ -193,6 +209,7 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
193
209
  }
194
210
  await replaceCopyTargetBeforeCopy({
195
211
  client,
212
+ rcloneOptions: defaultRcloneOptions,
196
213
  runtime,
197
214
  sourceType,
198
215
  targetPath,
@@ -202,8 +219,9 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
202
219
  await client.copy(
203
220
  await toRcloneCopyPath(sourcePath, client, runtime),
204
221
  await toRcloneCopyPath(targetPath, client, runtime),
205
- { to: shouldUseCopyTo },
222
+ { rcloneOptions: defaultRcloneOptions, to: shouldUseCopyTo },
206
223
  )
224
+ outro("R2 copy complete.")
207
225
  }
208
226
 
209
227
  /**
@@ -213,6 +231,7 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
213
231
  * @param {R2Runtime} [runtime]
214
232
  */
215
233
  export async function moveR2(source, destination, options, runtime = {}) {
234
+ intro("r2 mv")
216
235
  const sourcePath = parseR2OnlyPath(source, "Move source")
217
236
  const destinationPath = parseR2OnlyPath(destination, "Move destination")
218
237
  if (isR2BucketRootPath(source) || isR2BucketRootPath(destination)) {
@@ -251,6 +270,7 @@ export async function moveR2(source, destination, options, runtime = {}) {
251
270
  })
252
271
  await replaceCopyTargetBeforeCopy({
253
272
  client,
273
+ rcloneOptions: defaultRcloneOptions,
254
274
  runtime,
255
275
  sourceType,
256
276
  targetPath,
@@ -260,13 +280,15 @@ export async function moveR2(source, destination, options, runtime = {}) {
260
280
  await client.move(
261
281
  await toRcloneCopyPath(sourcePath, client, runtime),
262
282
  await toRcloneCopyPath(targetPath, client, runtime),
263
- { to: shouldUseMoveTo },
283
+ { rcloneOptions: defaultRcloneOptions, to: shouldUseMoveTo },
264
284
  )
285
+ outro("R2 move complete.")
265
286
  }
266
287
 
267
288
  /**
268
289
  * @param {{
269
290
  * client: ReturnType<typeof createR2CommandClient>,
291
+ * rcloneOptions?: RcloneOptions,
270
292
  * runtime: R2Runtime,
271
293
  * sourceType: CopyPathType,
272
294
  * targetPath: CopyPath,
@@ -275,6 +297,7 @@ export async function moveR2(source, destination, options, runtime = {}) {
275
297
  */
276
298
  async function replaceCopyTargetBeforeCopy({
277
299
  client,
300
+ rcloneOptions,
278
301
  runtime,
279
302
  sourceType,
280
303
  targetPath,
@@ -284,7 +307,7 @@ async function replaceCopyTargetBeforeCopy({
284
307
  return
285
308
  }
286
309
  if (targetPath.remote) {
287
- await client.remove(targetPath.path, { recursive: true })
310
+ await client.remove(targetPath.path, { recursive: true, rcloneOptions })
288
311
  return
289
312
  }
290
313
  await rm(resolveLocalPath(targetPath.path, runtime), {
package/src/support/r2.js CHANGED
@@ -21,6 +21,13 @@ const execFileAsync = promisify(execFile)
21
21
  const sessionSecretEnvName = "LIGHTNET_R2_SECRET_ACCESS_KEY"
22
22
  const r2DeleteBatchSize = 1000
23
23
 
24
+ /**
25
+ * @typedef {{
26
+ * transfers?: number
27
+ * checkers?: number
28
+ * }} RcloneOptions
29
+ */
30
+
24
31
  /**
25
32
  * @typedef {{
26
33
  * publicUrl: string
@@ -222,7 +229,7 @@ export function createR2Client({ cwd, interactive, promptText }) {
222
229
  },
223
230
  /**
224
231
  * @param {string} path
225
- * @param {{recursive?: boolean}} [options]
232
+ * @param {{recursive?: boolean, rcloneOptions?: RcloneOptions}} [options]
226
233
  */
227
234
  async remove(path, options = {}) {
228
235
  const r2Config = await getConfig()
@@ -234,13 +241,14 @@ export function createR2Client({ cwd, interactive, promptText }) {
234
241
  : ["deletefile", r2Path],
235
242
  cwd,
236
243
  config: r2Config,
244
+ rcloneOptions: options.rcloneOptions,
237
245
  secretAccessKey,
238
246
  })
239
247
  },
240
248
  /**
241
249
  * @param {string} source
242
250
  * @param {string} destination
243
- * @param {{to?: boolean}} [options]
251
+ * @param {{to?: boolean, rcloneOptions?: RcloneOptions}} [options]
244
252
  */
245
253
  async copy(source, destination, options = {}) {
246
254
  const r2Config = await getConfig()
@@ -248,13 +256,14 @@ export function createR2Client({ cwd, interactive, promptText }) {
248
256
  args: [options.to ? "copyto" : "copy", source, destination],
249
257
  cwd,
250
258
  config: r2Config,
259
+ rcloneOptions: options.rcloneOptions,
251
260
  secretAccessKey: await getSecretAccessKey(),
252
261
  })
253
262
  },
254
263
  /**
255
264
  * @param {string} source
256
265
  * @param {string} destination
257
- * @param {{to?: boolean}} [options]
266
+ * @param {{to?: boolean, rcloneOptions?: RcloneOptions}} [options]
258
267
  */
259
268
  async move(source, destination, options = {}) {
260
269
  const r2Config = await getConfig()
@@ -262,6 +271,7 @@ export function createR2Client({ cwd, interactive, promptText }) {
262
271
  args: [options.to ? "moveto" : "move", source, destination],
263
272
  cwd,
264
273
  config: r2Config,
274
+ rcloneOptions: options.rcloneOptions,
265
275
  secretAccessKey: await getSecretAccessKey(),
266
276
  })
267
277
  },
@@ -496,10 +506,17 @@ async function listR2Objects(args) {
496
506
  * args: string[]
497
507
  * cwd: string
498
508
  * config: R2Config
509
+ * rcloneOptions?: RcloneOptions
499
510
  * secretAccessKey: string
500
511
  * }} args
501
512
  */
502
- async function runConfiguredRclone({ args, cwd, config, secretAccessKey }) {
513
+ async function runConfiguredRclone({
514
+ args,
515
+ cwd,
516
+ config,
517
+ rcloneOptions,
518
+ secretAccessKey,
519
+ }) {
503
520
  const env = {
504
521
  ...processEnv,
505
522
  RCLONE_S3_SECRET_ACCESS_KEY: secretAccessKey,
@@ -518,6 +535,7 @@ async function runConfiguredRclone({ args, cwd, config, secretAccessKey }) {
518
535
  "--s3-endpoint",
519
536
  getR2Endpoint(config.accountId),
520
537
  "--s3-no-check-bucket",
538
+ ...formatRcloneOptions(rcloneOptions),
521
539
  ],
522
540
  cwd,
523
541
  env,
@@ -527,6 +545,21 @@ async function runConfiguredRclone({ args, cwd, config, secretAccessKey }) {
527
545
  }
528
546
  }
529
547
 
548
+ /**
549
+ * @param {RcloneOptions|undefined} options
550
+ */
551
+ function formatRcloneOptions(options) {
552
+ /** @type {string[]} */
553
+ const args = []
554
+ if (options?.transfers !== undefined) {
555
+ args.push("--transfers", String(options.transfers))
556
+ }
557
+ if (options?.checkers !== undefined) {
558
+ args.push("--checkers", String(options.checkers))
559
+ }
560
+ return args
561
+ }
562
+
530
563
  /**
531
564
  * @param {{cwd:string, promptText:(message:string)=>Promise<string>}} args
532
565
  */
@@ -698,9 +731,16 @@ async function defaultRunRclone(args, cwd, env) {
698
731
  */
699
732
  function getSanitizedRcloneError(error) {
700
733
  if (isPlainObject(error)) {
701
- const processError = /** @type {{stderr?: unknown, stdout?: unknown}} */ (
702
- error
703
- )
734
+ const processError =
735
+ /** @type {{code?: unknown, stderr?: unknown, stdout?: unknown}} */ (
736
+ error
737
+ )
738
+ if (processError.code === "ENOENT") {
739
+ return [
740
+ "rclone is required for lightnet r2 commands but was not found on PATH.",
741
+ "Install rclone: https://rclone.org/install/",
742
+ ].join("\n")
743
+ }
704
744
  if (typeof processError.stderr === "string" && processError.stderr.trim()) {
705
745
  return processError.stderr.trim()
706
746
  }