@lightnet/cli 4.2.0 → 4.3.0
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 +14 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/src/check-files.js +201 -508
- package/src/check-links.js +456 -0
- package/src/check-translations.js +81 -18
- package/src/index.js +19 -0
- package/src/support/cli-config.js +57 -0
- package/src/support/content-collections.js +191 -0
- package/src/support/filesystem.js +216 -0
- package/src/support/r2.js +141 -288
package/src/support/r2.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { execFile } from "node:child_process"
|
|
4
|
+
import { env as processEnv } from "node:process"
|
|
5
|
+
import { promisify } from "node:util"
|
|
6
6
|
|
|
7
|
+
import { isCancel, log, password } from "@clack/prompts"
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
cliConfigFileName,
|
|
11
|
+
readCliConfig,
|
|
12
|
+
writeCliConfig,
|
|
13
|
+
} from "./cli-config.js"
|
|
7
14
|
import { CliError } from "./cli-error.js"
|
|
8
15
|
|
|
9
|
-
const
|
|
10
|
-
const gitIgnoreEntry = `${cliConfigFileName}\n`
|
|
16
|
+
const execFileAsync = promisify(execFile)
|
|
11
17
|
const sessionSecretEnvName = "LIGHTNET_R2_SECRET_ACCESS_KEY"
|
|
12
18
|
|
|
13
19
|
/**
|
|
@@ -19,26 +25,14 @@ const sessionSecretEnvName = "LIGHTNET_R2_SECRET_ACCESS_KEY"
|
|
|
19
25
|
* }} R2Config
|
|
20
26
|
*/
|
|
21
27
|
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {{
|
|
24
|
-
* path: string
|
|
25
|
-
* sources: string[]
|
|
26
|
-
* }} MissingReference
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @typedef {Map<string, {key: string, sources: Set<string>}>} R2ReferenceMap
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
28
|
/**
|
|
34
29
|
* @param {{
|
|
35
30
|
* cwd: string
|
|
36
31
|
* interactive: boolean
|
|
37
|
-
* logger: {error: (...args: unknown[]) => void}
|
|
38
32
|
* promptText: (message: string) => Promise<string>
|
|
39
33
|
* }} args
|
|
40
34
|
*/
|
|
41
|
-
|
|
35
|
+
async function initR2Config({ cwd, interactive, promptText }) {
|
|
42
36
|
const config = await readCliConfig(cwd)
|
|
43
37
|
const parsed = parseR2Config(config)
|
|
44
38
|
if (parsed) {
|
|
@@ -51,7 +45,7 @@ export async function ensureR2Config({ cwd, interactive, logger, promptText }) {
|
|
|
51
45
|
)
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
log.error(`Missing R2 config in "${cliConfigFileName}".`)
|
|
55
49
|
return promptForR2Config({ cwd, promptText })
|
|
56
50
|
}
|
|
57
51
|
|
|
@@ -59,157 +53,131 @@ export async function ensureR2Config({ cwd, interactive, logger, promptText }) {
|
|
|
59
53
|
* @param {{
|
|
60
54
|
* cwd: string
|
|
61
55
|
* interactive: boolean
|
|
62
|
-
* logger: {error: (...args: unknown[]) => void}
|
|
63
56
|
* promptConfirm: (message: string) => Promise<boolean>
|
|
64
57
|
* promptText: (message: string) => Promise<string>
|
|
65
|
-
* promptSecret?: (message: string) => Promise<string>
|
|
66
|
-
* config: R2Config
|
|
67
|
-
* references: R2ReferenceMap
|
|
68
|
-
* runRclone: (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<{stdout:string, stderr:string}>
|
|
69
58
|
* }} args
|
|
70
59
|
*/
|
|
71
|
-
export
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
if (!shouldRefresh) {
|
|
88
|
-
throw error
|
|
60
|
+
export function createR2FileStorage({
|
|
61
|
+
cwd,
|
|
62
|
+
interactive,
|
|
63
|
+
promptConfirm,
|
|
64
|
+
promptText,
|
|
65
|
+
}) {
|
|
66
|
+
/** @type {R2Config|undefined} */
|
|
67
|
+
let config
|
|
68
|
+
/** @type {string|undefined} */
|
|
69
|
+
let secretAccessKey
|
|
70
|
+
|
|
71
|
+
/** @returns {Promise<R2Config>} */
|
|
72
|
+
const getConfig = async () => {
|
|
73
|
+
if (config) {
|
|
74
|
+
return config
|
|
89
75
|
}
|
|
90
|
-
const
|
|
91
|
-
cwd
|
|
92
|
-
|
|
76
|
+
const initializedConfig = await initR2Config({
|
|
77
|
+
cwd,
|
|
78
|
+
interactive,
|
|
79
|
+
promptText,
|
|
93
80
|
})
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
...args,
|
|
97
|
-
config: refreshedConfig,
|
|
98
|
-
})),
|
|
99
|
-
config: refreshedConfig,
|
|
100
|
-
}
|
|
81
|
+
config = initializedConfig
|
|
82
|
+
return initializedConfig
|
|
101
83
|
}
|
|
102
|
-
}
|
|
103
84
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* objectKeys: string[]
|
|
109
|
-
* promptSecret?: (message: string) => Promise<string>
|
|
110
|
-
* runRclone: (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<{stdout:string, stderr:string}>
|
|
111
|
-
* }} args
|
|
112
|
-
*/
|
|
113
|
-
export async function deleteR2Objects({
|
|
114
|
-
cwd,
|
|
115
|
-
config,
|
|
116
|
-
objectKeys,
|
|
117
|
-
promptSecret,
|
|
118
|
-
runRclone,
|
|
119
|
-
}) {
|
|
120
|
-
/** @type {string} */
|
|
121
|
-
const secretAccessKey = await promptRequiredSecret(promptSecret)
|
|
122
|
-
/** @type {string[]} */
|
|
123
|
-
const removed = []
|
|
124
|
-
|
|
125
|
-
for (const objectKey of objectKeys) {
|
|
126
|
-
try {
|
|
127
|
-
await runConfiguredRclone({
|
|
128
|
-
args: ["deletefile", makeR2Path(config, objectKey)],
|
|
129
|
-
config,
|
|
130
|
-
cwd,
|
|
131
|
-
promptSecret: async () => secretAccessKey,
|
|
132
|
-
runRclone,
|
|
133
|
-
})
|
|
134
|
-
removed.push(objectKey)
|
|
135
|
-
} catch {
|
|
136
|
-
// caller prints a per-file failure message
|
|
85
|
+
/** @returns {Promise<string>} */
|
|
86
|
+
const getSecretAccessKey = async () => {
|
|
87
|
+
if (!secretAccessKey) {
|
|
88
|
+
secretAccessKey = await promptRequiredSecret()
|
|
137
89
|
}
|
|
90
|
+
return secretAccessKey
|
|
138
91
|
}
|
|
139
92
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
missingReferences: /** @type {MissingReference[]} */ ([]),
|
|
162
|
-
orphanedObjects: /** @type {string[]} */ ([]),
|
|
163
|
-
referenceCount: 0,
|
|
93
|
+
/**
|
|
94
|
+
* @template T
|
|
95
|
+
* @param {(config: R2Config) => Promise<T>} operation
|
|
96
|
+
*/
|
|
97
|
+
const runWithRefresh = async (operation) => {
|
|
98
|
+
try {
|
|
99
|
+
return await operation(await getConfig())
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!interactive) {
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
log.error(error instanceof Error ? error.message : "R2 operation failed.")
|
|
105
|
+
const shouldRefresh = await promptConfirm(
|
|
106
|
+
"Refresh saved R2 settings and try again? [y/N] ",
|
|
107
|
+
)
|
|
108
|
+
if (!shouldRefresh) {
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
const refreshedConfig = await promptForR2Config({ cwd, promptText })
|
|
112
|
+
config = refreshedConfig
|
|
113
|
+
return operation(refreshedConfig)
|
|
164
114
|
}
|
|
165
115
|
}
|
|
166
116
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
117
|
+
const storage = {
|
|
118
|
+
async init() {
|
|
119
|
+
await getConfig()
|
|
120
|
+
return storage
|
|
121
|
+
},
|
|
122
|
+
async list() {
|
|
123
|
+
return runWithRefresh(async (config) => {
|
|
124
|
+
const secretAccessKey = await getSecretAccessKey()
|
|
125
|
+
const sharedArgs = {
|
|
126
|
+
cwd,
|
|
127
|
+
config,
|
|
128
|
+
secretAccessKey,
|
|
129
|
+
}
|
|
130
|
+
await assertR2Access(sharedArgs)
|
|
131
|
+
const bucketObjects = await listR2Objects(sharedArgs)
|
|
132
|
+
return bucketObjects.map((item) => item.key).sort()
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* @param {string[]} paths
|
|
137
|
+
*/
|
|
138
|
+
async delete(paths) {
|
|
139
|
+
const r2Config = await getConfig()
|
|
140
|
+
const secretAccessKey = await getSecretAccessKey()
|
|
141
|
+
/** @type {string[]} */
|
|
142
|
+
const removed = []
|
|
143
|
+
for (const path of paths) {
|
|
144
|
+
try {
|
|
145
|
+
await runConfiguredRclone({
|
|
146
|
+
args: ["deletefile", makeR2Path(r2Config, path)],
|
|
147
|
+
cwd,
|
|
148
|
+
config: r2Config,
|
|
149
|
+
secretAccessKey,
|
|
150
|
+
})
|
|
151
|
+
removed.push(path)
|
|
152
|
+
} catch {
|
|
153
|
+
// The caller prints per-file failures based on the returned paths.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return removed
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} url
|
|
160
|
+
*/
|
|
161
|
+
toPath(url) {
|
|
162
|
+
if (!config) {
|
|
163
|
+
throw new CliError("R2 storage must be initialized before use.")
|
|
164
|
+
}
|
|
165
|
+
const normalizedBase = config.publicUrl.replace(/\/+$/, "")
|
|
166
|
+
if (!url.startsWith(normalizedBase)) {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
const path = url.slice(normalizedBase.length).replace(/^\/+/, "")
|
|
170
|
+
return path || undefined
|
|
171
|
+
},
|
|
204
172
|
}
|
|
173
|
+
return storage
|
|
205
174
|
}
|
|
206
175
|
|
|
207
176
|
/**
|
|
208
177
|
* @param {{
|
|
209
178
|
* cwd: string
|
|
210
179
|
* config: R2Config
|
|
211
|
-
*
|
|
212
|
-
* runRclone: (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<{stdout:string, stderr:string}>
|
|
180
|
+
* secretAccessKey: string
|
|
213
181
|
* }} args
|
|
214
182
|
*/
|
|
215
183
|
async function assertR2Access(args) {
|
|
@@ -231,8 +199,7 @@ async function assertR2Access(args) {
|
|
|
231
199
|
* @param {{
|
|
232
200
|
* cwd: string
|
|
233
201
|
* config: R2Config
|
|
234
|
-
*
|
|
235
|
-
* runRclone: (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<{stdout:string, stderr:string}>
|
|
202
|
+
* secretAccessKey: string
|
|
236
203
|
* }} args
|
|
237
204
|
*/
|
|
238
205
|
async function listR2Objects(args) {
|
|
@@ -273,25 +240,17 @@ async function listR2Objects(args) {
|
|
|
273
240
|
* args: string[]
|
|
274
241
|
* cwd: string
|
|
275
242
|
* config: R2Config
|
|
276
|
-
*
|
|
277
|
-
* runRclone: (args: string[], cwd: string, env?: NodeJS.ProcessEnv) => Promise<{stdout:string, stderr:string}>
|
|
243
|
+
* secretAccessKey: string
|
|
278
244
|
* }} args
|
|
279
245
|
*/
|
|
280
|
-
async function runConfiguredRclone({
|
|
281
|
-
args,
|
|
282
|
-
cwd,
|
|
283
|
-
config,
|
|
284
|
-
promptSecret,
|
|
285
|
-
runRclone,
|
|
286
|
-
}) {
|
|
287
|
-
const secretAccessKey = await promptSecret("R2 secretAccessKey: ")
|
|
246
|
+
async function runConfiguredRclone({ args, cwd, config, secretAccessKey }) {
|
|
288
247
|
const env = {
|
|
289
248
|
...processEnv,
|
|
290
249
|
RCLONE_S3_SECRET_ACCESS_KEY: secretAccessKey,
|
|
291
250
|
}
|
|
292
251
|
|
|
293
252
|
try {
|
|
294
|
-
return await
|
|
253
|
+
return await defaultRunRclone(
|
|
295
254
|
[
|
|
296
255
|
...args,
|
|
297
256
|
"--s3-provider",
|
|
@@ -330,37 +289,27 @@ async function promptForR2Config({ cwd, promptText }) {
|
|
|
330
289
|
},
|
|
331
290
|
}
|
|
332
291
|
|
|
333
|
-
await
|
|
334
|
-
resolve(cwd, cliConfigFileName),
|
|
335
|
-
`${JSON.stringify(nextConfig, null, 2)}\n`,
|
|
336
|
-
"utf8",
|
|
337
|
-
)
|
|
338
|
-
await ensureGitignoreContainsConfig(cwd)
|
|
292
|
+
await writeCliConfig(cwd, nextConfig)
|
|
339
293
|
|
|
340
294
|
return nextConfig.r2
|
|
341
295
|
}
|
|
342
296
|
|
|
343
297
|
/**
|
|
344
|
-
* @param {((message:string)=>Promise<string>) | undefined} promptSecret
|
|
345
298
|
* @returns {Promise<string>}
|
|
346
299
|
*/
|
|
347
|
-
async function promptRequiredSecret(
|
|
300
|
+
async function promptRequiredSecret() {
|
|
348
301
|
const sessionSecret = processEnv[sessionSecretEnvName]
|
|
349
302
|
if (typeof sessionSecret === "string" && sessionSecret.trim()) {
|
|
350
303
|
return sessionSecret
|
|
351
304
|
}
|
|
352
305
|
|
|
353
|
-
/** @type {(message:string)=>Promise<string>} */
|
|
354
|
-
const prompt =
|
|
355
|
-
promptSecret ??
|
|
356
|
-
((message) =>
|
|
357
|
-
defaultPromptSecret(message, {
|
|
358
|
-
input: stdin,
|
|
359
|
-
output: stdout,
|
|
360
|
-
}))
|
|
361
|
-
|
|
362
306
|
while (true) {
|
|
363
|
-
const value = await
|
|
307
|
+
const value = await password({
|
|
308
|
+
message: "R2 secretAccessKey:",
|
|
309
|
+
})
|
|
310
|
+
if (isCancel(value)) {
|
|
311
|
+
throw new CliError("Secret access key prompt cancelled.")
|
|
312
|
+
}
|
|
364
313
|
if (value.trim()) {
|
|
365
314
|
processEnv[sessionSecretEnvName] = value
|
|
366
315
|
return value
|
|
@@ -381,22 +330,6 @@ async function promptRequiredText(promptText, message) {
|
|
|
381
330
|
}
|
|
382
331
|
}
|
|
383
332
|
|
|
384
|
-
/**
|
|
385
|
-
* @param {string} cwd
|
|
386
|
-
*/
|
|
387
|
-
async function readCliConfig(cwd) {
|
|
388
|
-
const filePath = resolve(cwd, cliConfigFileName)
|
|
389
|
-
if (!(await pathExists(filePath))) {
|
|
390
|
-
return undefined
|
|
391
|
-
}
|
|
392
|
-
const contents = await readFile(filePath, "utf8")
|
|
393
|
-
try {
|
|
394
|
-
return JSON.parse(contents)
|
|
395
|
-
} catch {
|
|
396
|
-
throw new CliError(`Invalid JSON in "${cliConfigFileName}".`)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
333
|
/**
|
|
401
334
|
* @param {unknown} config
|
|
402
335
|
* @returns {R2Config|undefined}
|
|
@@ -426,23 +359,6 @@ function parseR2Config(config) {
|
|
|
426
359
|
return { publicUrl, accountId, accessKeyId, bucket }
|
|
427
360
|
}
|
|
428
361
|
|
|
429
|
-
/**
|
|
430
|
-
* @param {string} cwd
|
|
431
|
-
*/
|
|
432
|
-
async function ensureGitignoreContainsConfig(cwd) {
|
|
433
|
-
const filePath = resolve(cwd, ".gitignore")
|
|
434
|
-
const contents = (await pathExists(filePath))
|
|
435
|
-
? await readFile(filePath, "utf8")
|
|
436
|
-
: ""
|
|
437
|
-
if (contents.includes(cliConfigFileName)) {
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
const next = contents
|
|
441
|
-
? `${contents}${contents.endsWith("\n") ? "" : "\n"}${gitIgnoreEntry}`
|
|
442
|
-
: gitIgnoreEntry
|
|
443
|
-
await writeFile(filePath, next, "utf8")
|
|
444
|
-
}
|
|
445
|
-
|
|
446
362
|
/**
|
|
447
363
|
* @param {R2Config} config
|
|
448
364
|
* @param {string} [objectKey]
|
|
@@ -460,6 +376,18 @@ function getR2Endpoint(accountId) {
|
|
|
460
376
|
return `https://${accountId}.r2.cloudflarestorage.com`
|
|
461
377
|
}
|
|
462
378
|
|
|
379
|
+
/**
|
|
380
|
+
* @param {string[]} args
|
|
381
|
+
* @param {string} cwd
|
|
382
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
383
|
+
*/
|
|
384
|
+
async function defaultRunRclone(args, cwd, env) {
|
|
385
|
+
return execFileAsync("rclone", args, {
|
|
386
|
+
cwd,
|
|
387
|
+
env: env ?? processEnv,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
463
391
|
/**
|
|
464
392
|
* @param {unknown} error
|
|
465
393
|
*/
|
|
@@ -478,84 +406,9 @@ function getSanitizedRcloneError(error) {
|
|
|
478
406
|
return "rclone command failed."
|
|
479
407
|
}
|
|
480
408
|
|
|
481
|
-
/**
|
|
482
|
-
* @param {string} filePath
|
|
483
|
-
*/
|
|
484
|
-
async function pathExists(filePath) {
|
|
485
|
-
try {
|
|
486
|
-
await access(filePath)
|
|
487
|
-
return true
|
|
488
|
-
} catch {
|
|
489
|
-
return false
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
409
|
/**
|
|
494
410
|
* @param {unknown} value
|
|
495
411
|
*/
|
|
496
412
|
function isPlainObject(value) {
|
|
497
413
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
498
414
|
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* @param {string} message
|
|
502
|
-
* @param {{input: NodeJS.ReadStream, output: NodeJS.WriteStream}} io
|
|
503
|
-
*/
|
|
504
|
-
async function defaultPromptSecret(message, io) {
|
|
505
|
-
if (
|
|
506
|
-
!io.input.isTTY ||
|
|
507
|
-
!io.output.isTTY ||
|
|
508
|
-
typeof io.input.setRawMode !== "function"
|
|
509
|
-
) {
|
|
510
|
-
throw new CliError(
|
|
511
|
-
"Secret access key prompt requires an interactive terminal.",
|
|
512
|
-
)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return await new Promise((resolve, reject) => {
|
|
516
|
-
let value = ""
|
|
517
|
-
const wasRaw = io.input.isRaw
|
|
518
|
-
|
|
519
|
-
io.output.write(message)
|
|
520
|
-
io.input.resume()
|
|
521
|
-
io.input.setEncoding("utf8")
|
|
522
|
-
io.input.setRawMode(true)
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* @param {string} chunk
|
|
526
|
-
*/
|
|
527
|
-
const onData = (chunk) => {
|
|
528
|
-
for (const character of chunk) {
|
|
529
|
-
if (character === "\r" || character === "\n") {
|
|
530
|
-
cleanup()
|
|
531
|
-
io.output.write("\n")
|
|
532
|
-
resolve(value)
|
|
533
|
-
return
|
|
534
|
-
}
|
|
535
|
-
if (character === "\u0003" || character === "\u0004") {
|
|
536
|
-
cleanup()
|
|
537
|
-
io.output.write("\n")
|
|
538
|
-
reject(new CliError("Secret access key prompt cancelled."))
|
|
539
|
-
return
|
|
540
|
-
}
|
|
541
|
-
if (
|
|
542
|
-
character === "\u007f" ||
|
|
543
|
-
character === "\b" ||
|
|
544
|
-
character === "\x08"
|
|
545
|
-
) {
|
|
546
|
-
value = value.slice(0, -1)
|
|
547
|
-
continue
|
|
548
|
-
}
|
|
549
|
-
value += character
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const cleanup = () => {
|
|
554
|
-
io.input.off("data", onData)
|
|
555
|
-
io.input.setRawMode(Boolean(wasRaw))
|
|
556
|
-
io.input.pause()
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
io.input.on("data", onData)
|
|
560
|
-
})
|
|
561
|
-
}
|