@lightnet/cli 4.6.0 → 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 +16 -0
- package/package.json +1 -1
- package/src/index.js +11 -2
- package/src/r2.js +113 -12
- package/src/support/r2.js +47 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
13
|
+
## 4.6.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#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.
|
|
18
|
+
|
|
3
19
|
## 4.6.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/package.json
CHANGED
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
|
-
|
|
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(
|
|
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
|
@@ -9,17 +9,29 @@ 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 {{
|
|
22
33
|
* force?: boolean
|
|
34
|
+
* longForce?: boolean
|
|
23
35
|
* }} R2CopyOptions
|
|
24
36
|
*/
|
|
25
37
|
|
|
@@ -42,6 +54,7 @@ const remotePathPrefix = "r2:"
|
|
|
42
54
|
* @typedef {{
|
|
43
55
|
* force?: boolean
|
|
44
56
|
* recursive?: boolean
|
|
57
|
+
* longForce?: boolean
|
|
45
58
|
* }} R2RemoveOptions
|
|
46
59
|
*/
|
|
47
60
|
|
|
@@ -77,6 +90,7 @@ export async function listR2(path, runtime = {}) {
|
|
|
77
90
|
* @param {R2Runtime} [runtime]
|
|
78
91
|
*/
|
|
79
92
|
export async function removeR2(path, options, runtime = {}) {
|
|
93
|
+
intro("r2 rm")
|
|
80
94
|
const r2Path = normalizeR2Path(path)
|
|
81
95
|
const isBucketRoot = isR2BucketRootPath(path)
|
|
82
96
|
if (isBucketRoot && !options.recursive) {
|
|
@@ -89,9 +103,19 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
const interactive = getInteractive(runtime)
|
|
92
|
-
if (isBucketRoot && !interactive) {
|
|
106
|
+
if (isBucketRoot && options.force && !options.longForce && !interactive) {
|
|
107
|
+
throw new CliError(
|
|
108
|
+
'Ignoring "-f" for R2 bucket-root cleanup. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
if (isBucketRoot && !interactive && !options.longForce) {
|
|
93
112
|
throw new CliError(
|
|
94
|
-
|
|
113
|
+
'Cleaning the R2 bucket root requires interactive confirmation. If you are sure you want to delete the entire R2 bucket, use "--force".',
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
if (isBucketRoot && options.force && !options.longForce) {
|
|
117
|
+
log.warn(
|
|
118
|
+
'Ignoring "-f" for R2 bucket-root cleanup. Use "--force" if you are sure you want to delete the entire R2 bucket.',
|
|
95
119
|
)
|
|
96
120
|
}
|
|
97
121
|
if (!options.force && !interactive) {
|
|
@@ -107,20 +131,24 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
const shouldDelete = isBucketRoot
|
|
110
|
-
?
|
|
134
|
+
? options.longForce ||
|
|
135
|
+
(await getPromptConfirm(runtime)(
|
|
111
136
|
"Delete all files in the R2 bucket? [y/N] ",
|
|
112
|
-
)
|
|
137
|
+
))
|
|
113
138
|
: options.force ||
|
|
114
139
|
(await getPromptConfirm(runtime)(`Delete R2 path "${r2Path}"? [y/N] `))
|
|
115
140
|
|
|
116
141
|
if (!shouldDelete) {
|
|
117
|
-
|
|
142
|
+
cancelPrompt()
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
const client = createR2CommandClient(runtime)
|
|
121
146
|
await client.remove(r2Path, {
|
|
122
147
|
recursive: options.recursive === true,
|
|
148
|
+
rcloneOptions:
|
|
149
|
+
options.recursive === true ? defaultRcloneOptions : undefined,
|
|
123
150
|
})
|
|
151
|
+
outro("R2 delete complete.")
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
/**
|
|
@@ -130,6 +158,7 @@ export async function removeR2(path, options, runtime = {}) {
|
|
|
130
158
|
* @param {R2Runtime} [runtime]
|
|
131
159
|
*/
|
|
132
160
|
export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
161
|
+
intro("r2 cp")
|
|
133
162
|
const sourcePath = parseCopyPath(source)
|
|
134
163
|
const destinationPath = parseCopyPath(destination)
|
|
135
164
|
if (!sourcePath.remote && !destinationPath.remote) {
|
|
@@ -161,16 +190,26 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
|
161
190
|
sourceType === "file" &&
|
|
162
191
|
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
163
192
|
|
|
164
|
-
await
|
|
165
|
-
destination,
|
|
193
|
+
const confirmedRootOverwrite = await confirmCopyRootOverwrite({
|
|
166
194
|
force: options.force === true,
|
|
195
|
+
longForce: options.longForce === true,
|
|
167
196
|
runtime,
|
|
168
|
-
sourceType,
|
|
169
197
|
targetPath,
|
|
170
198
|
targetType,
|
|
171
199
|
})
|
|
200
|
+
if (!confirmedRootOverwrite) {
|
|
201
|
+
await confirmCopyOverwrite({
|
|
202
|
+
destination,
|
|
203
|
+
force: options.force === true,
|
|
204
|
+
runtime,
|
|
205
|
+
sourceType,
|
|
206
|
+
targetPath,
|
|
207
|
+
targetType,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
172
210
|
await replaceCopyTargetBeforeCopy({
|
|
173
211
|
client,
|
|
212
|
+
rcloneOptions: defaultRcloneOptions,
|
|
174
213
|
runtime,
|
|
175
214
|
sourceType,
|
|
176
215
|
targetPath,
|
|
@@ -180,8 +219,9 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
|
180
219
|
await client.copy(
|
|
181
220
|
await toRcloneCopyPath(sourcePath, client, runtime),
|
|
182
221
|
await toRcloneCopyPath(targetPath, client, runtime),
|
|
183
|
-
{ to: shouldUseCopyTo },
|
|
222
|
+
{ rcloneOptions: defaultRcloneOptions, to: shouldUseCopyTo },
|
|
184
223
|
)
|
|
224
|
+
outro("R2 copy complete.")
|
|
185
225
|
}
|
|
186
226
|
|
|
187
227
|
/**
|
|
@@ -191,6 +231,7 @@ export async function copyR2(source, destination, options = {}, runtime = {}) {
|
|
|
191
231
|
* @param {R2Runtime} [runtime]
|
|
192
232
|
*/
|
|
193
233
|
export async function moveR2(source, destination, options, runtime = {}) {
|
|
234
|
+
intro("r2 mv")
|
|
194
235
|
const sourcePath = parseR2OnlyPath(source, "Move source")
|
|
195
236
|
const destinationPath = parseR2OnlyPath(destination, "Move destination")
|
|
196
237
|
if (isR2BucketRootPath(source) || isR2BucketRootPath(destination)) {
|
|
@@ -229,6 +270,7 @@ export async function moveR2(source, destination, options, runtime = {}) {
|
|
|
229
270
|
})
|
|
230
271
|
await replaceCopyTargetBeforeCopy({
|
|
231
272
|
client,
|
|
273
|
+
rcloneOptions: defaultRcloneOptions,
|
|
232
274
|
runtime,
|
|
233
275
|
sourceType,
|
|
234
276
|
targetPath,
|
|
@@ -238,13 +280,15 @@ export async function moveR2(source, destination, options, runtime = {}) {
|
|
|
238
280
|
await client.move(
|
|
239
281
|
await toRcloneCopyPath(sourcePath, client, runtime),
|
|
240
282
|
await toRcloneCopyPath(targetPath, client, runtime),
|
|
241
|
-
{ to: shouldUseMoveTo },
|
|
283
|
+
{ rcloneOptions: defaultRcloneOptions, to: shouldUseMoveTo },
|
|
242
284
|
)
|
|
285
|
+
outro("R2 move complete.")
|
|
243
286
|
}
|
|
244
287
|
|
|
245
288
|
/**
|
|
246
289
|
* @param {{
|
|
247
290
|
* client: ReturnType<typeof createR2CommandClient>,
|
|
291
|
+
* rcloneOptions?: RcloneOptions,
|
|
248
292
|
* runtime: R2Runtime,
|
|
249
293
|
* sourceType: CopyPathType,
|
|
250
294
|
* targetPath: CopyPath,
|
|
@@ -253,6 +297,7 @@ export async function moveR2(source, destination, options, runtime = {}) {
|
|
|
253
297
|
*/
|
|
254
298
|
async function replaceCopyTargetBeforeCopy({
|
|
255
299
|
client,
|
|
300
|
+
rcloneOptions,
|
|
256
301
|
runtime,
|
|
257
302
|
sourceType,
|
|
258
303
|
targetPath,
|
|
@@ -262,7 +307,7 @@ async function replaceCopyTargetBeforeCopy({
|
|
|
262
307
|
return
|
|
263
308
|
}
|
|
264
309
|
if (targetPath.remote) {
|
|
265
|
-
await client.remove(targetPath.path, { recursive: true })
|
|
310
|
+
await client.remove(targetPath.path, { recursive: true, rcloneOptions })
|
|
266
311
|
return
|
|
267
312
|
}
|
|
268
313
|
await rm(resolveLocalPath(targetPath.path, runtime), {
|
|
@@ -271,6 +316,55 @@ async function replaceCopyTargetBeforeCopy({
|
|
|
271
316
|
})
|
|
272
317
|
}
|
|
273
318
|
|
|
319
|
+
/**
|
|
320
|
+
* @param {{
|
|
321
|
+
* force: boolean,
|
|
322
|
+
* longForce: boolean,
|
|
323
|
+
* runtime: R2Runtime,
|
|
324
|
+
* targetPath: CopyPath,
|
|
325
|
+
* targetType: CopyPathType
|
|
326
|
+
* }} args
|
|
327
|
+
*/
|
|
328
|
+
async function confirmCopyRootOverwrite({
|
|
329
|
+
force,
|
|
330
|
+
longForce,
|
|
331
|
+
runtime,
|
|
332
|
+
targetPath,
|
|
333
|
+
targetType,
|
|
334
|
+
}) {
|
|
335
|
+
if (!isR2BucketRootCopyPath(targetPath) || targetType === "missing") {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
log.warn(
|
|
339
|
+
"This will replace ALL files in the configured R2 bucket. This cannot be undone.",
|
|
340
|
+
)
|
|
341
|
+
if (longForce) {
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
if (force && !getInteractive(runtime)) {
|
|
345
|
+
throw new CliError(
|
|
346
|
+
'Ignoring "-f" for R2 bucket-root replacement. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
if (!getInteractive(runtime)) {
|
|
350
|
+
throw new CliError(
|
|
351
|
+
'Replacing the R2 bucket root requires interactive confirmation. If you are sure you want to replace the entire R2 bucket, use "--force".',
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
if (force) {
|
|
355
|
+
log.warn(
|
|
356
|
+
'Ignoring "-f" for R2 bucket-root replacement. Use "--force" if you are sure you want to replace the entire R2 bucket.',
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
const shouldOverwrite = await getPromptConfirm(runtime)(
|
|
360
|
+
"Replace all files in the R2 bucket? [y/N] ",
|
|
361
|
+
)
|
|
362
|
+
if (!shouldOverwrite) {
|
|
363
|
+
cancelPrompt()
|
|
364
|
+
}
|
|
365
|
+
return true
|
|
366
|
+
}
|
|
367
|
+
|
|
274
368
|
/**
|
|
275
369
|
* @param {R2Runtime} runtime
|
|
276
370
|
*/
|
|
@@ -498,6 +592,13 @@ function isR2BucketRootPath(path) {
|
|
|
498
592
|
return path.trim() === "/"
|
|
499
593
|
}
|
|
500
594
|
|
|
595
|
+
/**
|
|
596
|
+
* @param {CopyPath} path
|
|
597
|
+
*/
|
|
598
|
+
function isR2BucketRootCopyPath(path) {
|
|
599
|
+
return path.remote && path.path === ""
|
|
600
|
+
}
|
|
601
|
+
|
|
501
602
|
/**
|
|
502
603
|
* @param {unknown} error
|
|
503
604
|
*/
|
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({
|
|
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 =
|
|
702
|
-
|
|
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
|
}
|