@lightnet/cli 4.7.0 → 4.7.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +7 -1
  3. package/src/r2.js +115 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @lightnet/cli
2
2
 
3
+ ## 4.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#447](https://github.com/LightNetDev/LightNet/pull/447) [`0d8fece`](https://github.com/LightNetDev/LightNet/commit/0d8fece3527a67a21c6272b422c62152948b32a1) - Only prompt for `lightnet r2 cp` and `lightnet r2 mv` when files would actually be overwritten, and list the destination files before confirmation.
8
+
3
9
  ## 4.7.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.7.0",
6
+ "version": "4.7.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -28,7 +28,13 @@
28
28
  "@clack/prompts": "^1.7.0",
29
29
  "commander": "^15.0.0"
30
30
  },
31
+ "devDependencies": {
32
+ "vitest": "^4.1.9"
33
+ },
31
34
  "engines": {
32
35
  "node": ">=22"
36
+ },
37
+ "scripts": {
38
+ "test": "vitest"
33
39
  }
34
40
  }
package/src/r2.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import { stat } from "node:fs/promises"
3
+ import { readdir, stat } from "node:fs/promises"
4
4
  import { basename, join, posix, resolve } from "node:path"
5
5
  import {
6
6
  cwd as processCwd,
@@ -78,6 +78,7 @@ const defaultRcloneOptions = {
78
78
  * isInteractive?: boolean
79
79
  * promptConfirm?: (message: string) => Promise<boolean>
80
80
  * promptText?: (message: string) => Promise<string>
81
+ * createR2Client?: typeof createR2Client
81
82
  * writeStdout?: (message: string) => void
82
83
  * }} R2Runtime
83
84
  */
@@ -285,10 +286,12 @@ export async function copyR2(paths, options = {}, runtime = {}) {
285
286
  (targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
286
287
 
287
288
  await confirmCopyOverwrite({
289
+ client,
288
290
  destination,
289
291
  force: options.force === true,
290
292
  noClobber: options.noClobber === true,
291
293
  runtime,
294
+ sourcePath,
292
295
  sourceType,
293
296
  targetPath,
294
297
  targetType,
@@ -383,10 +386,12 @@ export async function moveR2(paths, options = {}, runtime = {}) {
383
386
  (targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
384
387
 
385
388
  await confirmCopyOverwrite({
389
+ client,
386
390
  destination,
387
391
  force: options.force === true,
388
392
  noClobber: options.noClobber === true,
389
393
  runtime,
394
+ sourcePath,
390
395
  sourceType,
391
396
  targetPath,
392
397
  targetType,
@@ -426,7 +431,7 @@ export async function moveR2(paths, options = {}, runtime = {}) {
426
431
  * @param {R2Runtime} runtime
427
432
  */
428
433
  function createR2CommandClient(runtime) {
429
- return createR2Client({
434
+ return (runtime.createR2Client ?? createR2Client)({
430
435
  cwd: runtime.cwd ?? processCwd(),
431
436
  interactive: getInteractive(runtime),
432
437
  promptText: getPromptText(runtime),
@@ -580,20 +585,24 @@ function getCopyTargetPath({
580
585
 
581
586
  /**
582
587
  * @param {{
588
+ * client: ReturnType<typeof createR2CommandClient>,
583
589
  * destination: string,
584
590
  * force: boolean,
585
591
  * noClobber: boolean,
586
592
  * runtime: R2Runtime,
593
+ * sourcePath: CopyPath,
587
594
  * sourceType: CopyPathType,
588
595
  * targetPath: CopyPath,
589
596
  * targetType: CopyPathType
590
597
  * }} args
591
598
  */
592
599
  async function confirmCopyOverwrite({
600
+ client,
593
601
  destination,
594
602
  force,
595
603
  noClobber,
596
604
  runtime,
605
+ sourcePath,
597
606
  sourceType,
598
607
  targetPath,
599
608
  targetType,
@@ -617,11 +626,27 @@ async function confirmCopyOverwrite({
617
626
  if (noClobber) {
618
627
  return
619
628
  }
629
+ const overwrittenPaths =
630
+ sourceType === "directory"
631
+ ? await getDirectoryOverwritePaths({
632
+ client,
633
+ runtime,
634
+ sourcePath,
635
+ targetPath,
636
+ })
637
+ : [formatCopyPath(targetPath)]
638
+ if (overwrittenPaths.length === 0) {
639
+ return
640
+ }
620
641
  if (!getInteractive(runtime)) {
621
642
  throw new CliError(
622
643
  'Transfer would overwrite existing files. Re-run with "--force" in non-interactive environments.',
623
644
  )
624
645
  }
646
+ log.warn(`Files to overwrite (${overwrittenPaths.length})`)
647
+ for (const overwrittenPath of overwrittenPaths) {
648
+ log.message(`• ${overwrittenPath}`)
649
+ }
625
650
  const message =
626
651
  sourceType === "directory"
627
652
  ? `Overwrite any existing files under destination "${destination}"? [y/N] `
@@ -632,6 +657,94 @@ async function confirmCopyOverwrite({
632
657
  }
633
658
  }
634
659
 
660
+ /**
661
+ * @param {{
662
+ * client: ReturnType<typeof createR2CommandClient>
663
+ * runtime: R2Runtime
664
+ * sourcePath: CopyPath
665
+ * targetPath: CopyPath
666
+ * }} args
667
+ */
668
+ async function getDirectoryOverwritePaths({
669
+ client,
670
+ runtime,
671
+ sourcePath,
672
+ targetPath,
673
+ }) {
674
+ const sourceFiles = await listCopyFiles(sourcePath, client, runtime)
675
+ if (sourceFiles.length === 0) {
676
+ return []
677
+ }
678
+ const targetFiles = new Set(await listCopyFiles(targetPath, client, runtime))
679
+ return sourceFiles
680
+ .filter((relativePath) => targetFiles.has(relativePath))
681
+ .map((relativePath) =>
682
+ formatCopyPath({
683
+ ...targetPath,
684
+ path: joinCopyPath(targetPath, relativePath),
685
+ }),
686
+ )
687
+ }
688
+
689
+ /**
690
+ * @param {CopyPath} copyPath
691
+ * @param {ReturnType<typeof createR2CommandClient>} client
692
+ * @param {R2Runtime} runtime
693
+ */
694
+ async function listCopyFiles(copyPath, client, runtime) {
695
+ if (copyPath.remote) {
696
+ return parseRcloneFileList(await client.list(copyPath.path))
697
+ }
698
+ return listLocalFiles(resolveLocalPath(copyPath.path, runtime))
699
+ }
700
+
701
+ /**
702
+ * @param {string} root
703
+ * @param {string} [prefix]
704
+ * @returns {Promise<string[]>}
705
+ */
706
+ async function listLocalFiles(root, prefix = "") {
707
+ const entries = await readdir(root, { withFileTypes: true })
708
+ const files = []
709
+ for (const entry of entries) {
710
+ const relativePath = prefix
711
+ ? joinRelativePath(prefix, entry.name)
712
+ : entry.name
713
+ const absolutePath = join(root, entry.name)
714
+ if (entry.isDirectory()) {
715
+ files.push(...(await listLocalFiles(absolutePath, relativePath)))
716
+ } else if (entry.isFile()) {
717
+ files.push(relativePath)
718
+ }
719
+ }
720
+ return files
721
+ }
722
+
723
+ /**
724
+ * @param {string} output
725
+ */
726
+ function parseRcloneFileList(output) {
727
+ return output
728
+ .split(/\r?\n/)
729
+ .map((line) => line.trim())
730
+ .filter(Boolean)
731
+ }
732
+
733
+ /**
734
+ * @param {string} parent
735
+ * @param {string} child
736
+ */
737
+ function joinRelativePath(parent, child) {
738
+ return parent ? posix.join(parent, child) : child
739
+ }
740
+
741
+ /**
742
+ * @param {CopyPath} copyPath
743
+ */
744
+ function formatCopyPath(copyPath) {
745
+ return copyPath.remote ? `${remotePathPrefix}${copyPath.path}` : copyPath.path
746
+ }
747
+
635
748
  /**
636
749
  * @param {CopyPath} copyPath
637
750
  * @param {ReturnType<typeof createR2CommandClient>} client