@rimelight/cli 0.0.13 → 0.0.14
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/bin/rimelight.js +22 -21
- package/package.json +7 -13
- package/src/index.js +25 -17
- package/src/prompts.js +380 -0
- package/LICENSE +0 -21
package/bin/rimelight.js
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { parseArgs } from "node:util"
|
|
4
4
|
import { runCreate } from "../src/index.js"
|
|
5
5
|
|
|
6
6
|
async function main() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
const { values, positionals } = parseArgs({
|
|
8
|
+
args: process.argv.slice(2),
|
|
9
|
+
options: {
|
|
10
|
+
"domain": { type: "string", short: "d" },
|
|
11
|
+
"git": { type: "boolean", short: "g" },
|
|
12
|
+
"install": { type: "boolean", short: "i" },
|
|
13
|
+
"package-manager": { type: "string" },
|
|
14
|
+
"help": { type: "boolean", short: "h" },
|
|
15
|
+
"version": { type: "boolean", short: "v" }
|
|
15
16
|
},
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
strict: false
|
|
18
19
|
})
|
|
19
20
|
|
|
20
|
-
if (
|
|
21
|
+
if (values.version) {
|
|
21
22
|
console.log("rimelight v0.1.0")
|
|
22
23
|
process.exit(0)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
if (
|
|
26
|
+
if (values.help) {
|
|
26
27
|
console.log(`
|
|
27
28
|
Rimelight CLI - Manage and scaffold premium Rimelight projects.
|
|
28
29
|
|
|
@@ -47,24 +48,24 @@ Options:
|
|
|
47
48
|
process.exit(0)
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
const command =
|
|
51
|
+
const command = positionals[0]
|
|
51
52
|
|
|
52
53
|
if (command === "compare") {
|
|
53
|
-
const targetDir =
|
|
54
|
+
const targetDir = positionals[1] || "."
|
|
54
55
|
const { runCompare } = await import("../src/index.js")
|
|
55
56
|
await runCompare({ targetDir })
|
|
56
57
|
} else if (command === "update") {
|
|
57
|
-
const targetDir =
|
|
58
|
+
const targetDir = positionals[1] || "."
|
|
58
59
|
const { runUpdate } = await import("../src/index.js")
|
|
59
60
|
await runUpdate({ targetDir })
|
|
60
61
|
} else if (!command || command === "create") {
|
|
61
|
-
const projectName = command === "create" ?
|
|
62
|
+
const projectName = command === "create" ? positionals[1] : positionals[0]
|
|
62
63
|
await runCreate({
|
|
63
64
|
projectName,
|
|
64
|
-
domain:
|
|
65
|
-
git:
|
|
66
|
-
install:
|
|
67
|
-
packageManager:
|
|
65
|
+
domain: values.domain,
|
|
66
|
+
git: values.git,
|
|
67
|
+
install: values.install,
|
|
68
|
+
packageManager: values["package-manager"]
|
|
68
69
|
})
|
|
69
70
|
} else {
|
|
70
71
|
console.error(`Unknown command: ${command}. Use "rimelight --help" to see all options.`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Command Line Interface",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -27,23 +27,17 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"mri": "1.2.0",
|
|
34
|
-
"picocolors": "1.1.1"
|
|
30
|
+
"scripts": {
|
|
31
|
+
"create": "node ./bin/rimelight.js create",
|
|
32
|
+
"check": "vp check --fix"
|
|
35
33
|
},
|
|
36
34
|
"devDependencies": {
|
|
37
|
-
"@rimelight/config": "
|
|
38
|
-
"@types/node": "26.6.1",
|
|
35
|
+
"@rimelight/config": "workspace:*",
|
|
39
36
|
"typescript": "6.0.3",
|
|
40
37
|
"vite-plus": "0.3.3"
|
|
41
38
|
},
|
|
42
39
|
"engines": {
|
|
43
40
|
"node": ">=26.9.0"
|
|
44
41
|
},
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
"check": "vp check --fix"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
42
|
+
"packageManager": "pnpm@12.4.2"
|
|
43
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import fs from "node:fs/promises"
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs"
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import { spawnSync } from "node:child_process"
|
|
5
|
+
import * as prompts from "./prompts.js"
|
|
6
|
+
import { pc } from "./prompts.js"
|
|
7
|
+
|
|
8
|
+
function runCommand(cmd, args, options = {}) {
|
|
9
|
+
return spawnSync(cmd, args, {
|
|
10
|
+
...options,
|
|
11
|
+
shell: process.platform === "win32"
|
|
12
|
+
})
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
const
|
|
15
|
+
const candidateStarterPaths = [
|
|
16
|
+
path.resolve(import.meta.dirname, "../../../apps/starter.rimelight.com"),
|
|
17
|
+
path.resolve(import.meta.dirname, "../../starter"),
|
|
18
|
+
path.resolve(import.meta.dirname, "../starter")
|
|
19
|
+
]
|
|
20
|
+
export const starterPath =
|
|
21
|
+
candidateStarterPaths.find((p) => existsSync(p)) || candidateStarterPaths[0]
|
|
9
22
|
|
|
10
23
|
// Helper to parse JSON with comments (JSONC)
|
|
11
24
|
export function parseJsonc(content) {
|
|
@@ -242,7 +255,7 @@ export async function runCreate(options) {
|
|
|
242
255
|
if (initGit) {
|
|
243
256
|
const gitSpinner = prompts.spinner()
|
|
244
257
|
gitSpinner.start("Initializing Git repository...")
|
|
245
|
-
const gitInit =
|
|
258
|
+
const gitInit = runCommand("git", ["init"], { cwd: resolvedTargetDir })
|
|
246
259
|
if (gitInit.status === 0) {
|
|
247
260
|
gitSpinner.stop(pc.green("✔ Git repository initialized!"))
|
|
248
261
|
} else {
|
|
@@ -255,7 +268,7 @@ export async function runCreate(options) {
|
|
|
255
268
|
const installSpinner = prompts.spinner()
|
|
256
269
|
installSpinner.start(`Installing dependencies with ${packageManager}...`)
|
|
257
270
|
|
|
258
|
-
const install =
|
|
271
|
+
const install = runCommand(packageManager, ["install"], {
|
|
259
272
|
cwd: resolvedTargetDir,
|
|
260
273
|
stdio: "ignore"
|
|
261
274
|
})
|
|
@@ -323,7 +336,7 @@ export async function runCreate(options) {
|
|
|
323
336
|
|
|
324
337
|
if (initGit) {
|
|
325
338
|
console.log("Initializing Git repository...")
|
|
326
|
-
const gitInit =
|
|
339
|
+
const gitInit = runCommand("git", ["init"], { cwd: resolvedTargetDir })
|
|
327
340
|
if (gitInit.status === 0) {
|
|
328
341
|
console.log(pc.green("✔ Git repository initialized!"))
|
|
329
342
|
} else {
|
|
@@ -333,15 +346,10 @@ export async function runCreate(options) {
|
|
|
333
346
|
|
|
334
347
|
if (installDeps) {
|
|
335
348
|
console.log(`Installing dependencies using ${packageManager}...`)
|
|
336
|
-
const install =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
})
|
|
341
|
-
: spawn.sync(packageManager, ["install"], {
|
|
342
|
-
cwd: resolvedTargetDir,
|
|
343
|
-
stdio: "ignore"
|
|
344
|
-
})
|
|
349
|
+
const install = runCommand(packageManager, ["install"], {
|
|
350
|
+
cwd: resolvedTargetDir,
|
|
351
|
+
stdio: "ignore"
|
|
352
|
+
})
|
|
345
353
|
if (install.status === 0) {
|
|
346
354
|
console.log(pc.green("✔ Dependencies successfully installed!"))
|
|
347
355
|
} else {
|
|
@@ -1288,7 +1296,7 @@ export async function runUpdate(options) {
|
|
|
1288
1296
|
if (runInstall) {
|
|
1289
1297
|
const runVpInstall = prompts.spinner()
|
|
1290
1298
|
runVpInstall.start("Running vp install...")
|
|
1291
|
-
const vpInstall =
|
|
1299
|
+
const vpInstall = runCommand("vp", ["install"], { cwd: targetDir, stdio: "ignore" })
|
|
1292
1300
|
if (vpInstall.status === 0) {
|
|
1293
1301
|
runVpInstall.stop(pc.green("✔ vp install completed successfully!"))
|
|
1294
1302
|
} else {
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import readline from "node:readline"
|
|
2
|
+
import readlinePromises from "node:readline/promises"
|
|
3
|
+
import { styleText } from "node:util"
|
|
4
|
+
|
|
5
|
+
// --- Terminal Styling (Node 22+ styleText with graceful fallback) ---
|
|
6
|
+
|
|
7
|
+
export const pc = {
|
|
8
|
+
magenta: (text) => styleTextSafe("magenta", text),
|
|
9
|
+
bold: (text) => styleTextSafe("bold", text),
|
|
10
|
+
green: (text) => styleTextSafe("green", text),
|
|
11
|
+
cyan: (text) => styleTextSafe("cyan", text),
|
|
12
|
+
yellow: (text) => styleTextSafe("yellow", text),
|
|
13
|
+
red: (text) => styleTextSafe("red", text),
|
|
14
|
+
dim: (text) => styleTextSafe("dim", text)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function styleTextSafe(format, text) {
|
|
18
|
+
if (typeof styleText === "function") {
|
|
19
|
+
try {
|
|
20
|
+
return styleText(format, String(text))
|
|
21
|
+
} catch {
|
|
22
|
+
// fallback to raw ANSI if format is unsupported
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const codes = {
|
|
26
|
+
bold: ["\x1b[1m", "\x1b[22m"],
|
|
27
|
+
dim: ["\x1b[2m", "\x1b[22m"],
|
|
28
|
+
red: ["\x1b[31m", "\x1b[39m"],
|
|
29
|
+
green: ["\x1b[32m", "\x1b[39m"],
|
|
30
|
+
yellow: ["\x1b[33m", "\x1b[39m"],
|
|
31
|
+
magenta: ["\x1b[35m", "\x1b[39m"],
|
|
32
|
+
cyan: ["\x1b[36m", "\x1b[39m"]
|
|
33
|
+
}
|
|
34
|
+
const [open, close] = codes[format] || ["", ""]
|
|
35
|
+
return `${open}${text}${close}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Cancellation Symbol & Global Terminal Cleanup ---
|
|
39
|
+
|
|
40
|
+
const CANCEL_SYMBOL = Symbol("rimelight:cancel")
|
|
41
|
+
|
|
42
|
+
if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
43
|
+
process.on("exit", () => {
|
|
44
|
+
if (process.stdout?.isTTY) {
|
|
45
|
+
process.stdout.write("\x1b[?25h")
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isCancel(value) {
|
|
51
|
+
return value === CANCEL_SYMBOL
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function intro(title) {
|
|
55
|
+
console.log(`\n${title}\n`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function outro(message) {
|
|
59
|
+
console.log(`\n${message}\n`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cancel(message) {
|
|
63
|
+
console.log(`\n${pc.red(message)}\n`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Interactive Prompts ---
|
|
67
|
+
|
|
68
|
+
export async function text(options) {
|
|
69
|
+
const { message, placeholder, validate, defaultValue = "" } = options
|
|
70
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
|
71
|
+
|
|
72
|
+
if (!isTTY) {
|
|
73
|
+
return defaultValue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const promptMsg = placeholder ? `${message} ${pc.dim(`(${placeholder})`)}: ` : `${message}: `
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const rl = readlinePromises.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const answer = await rl.question(promptMsg)
|
|
86
|
+
rl.close()
|
|
87
|
+
|
|
88
|
+
const val = answer.trim() || defaultValue
|
|
89
|
+
if (validate) {
|
|
90
|
+
const error = validate(val)
|
|
91
|
+
if (error) {
|
|
92
|
+
console.log(pc.yellow(`⚠ ${error}`))
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return val
|
|
97
|
+
} catch {
|
|
98
|
+
rl.close()
|
|
99
|
+
return CANCEL_SYMBOL
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function confirm(options) {
|
|
105
|
+
const { message, initialValue = true } = options
|
|
106
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
|
107
|
+
|
|
108
|
+
if (!isTTY) {
|
|
109
|
+
return initialValue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hint = initialValue ? "[Y/n]" : "[y/N]"
|
|
113
|
+
const promptMsg = `${message} ${pc.dim(hint)}: `
|
|
114
|
+
|
|
115
|
+
const rl = readlinePromises.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stdout
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const answer = await rl.question(promptMsg)
|
|
122
|
+
rl.close()
|
|
123
|
+
|
|
124
|
+
const trimmed = answer.trim().toLowerCase()
|
|
125
|
+
if (!trimmed) return initialValue
|
|
126
|
+
return trimmed === "y" || trimmed === "yes"
|
|
127
|
+
} catch {
|
|
128
|
+
rl.close()
|
|
129
|
+
return CANCEL_SYMBOL
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function select(options) {
|
|
134
|
+
const { message, options: choices, initialValue } = options
|
|
135
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
|
136
|
+
|
|
137
|
+
if (!isTTY || choices.length === 0) {
|
|
138
|
+
return initialValue ?? choices[0]?.value
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let selectedIndex = Math.max(
|
|
142
|
+
0,
|
|
143
|
+
choices.findIndex((c) => c.value === initialValue)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const { stdin, stdout } = process
|
|
148
|
+
let cleaned = false
|
|
149
|
+
|
|
150
|
+
const render = (first = false) => {
|
|
151
|
+
if (!first) {
|
|
152
|
+
// Move cursor up by (choices.length + 1) lines
|
|
153
|
+
stdout.write(`\x1b[${choices.length + 1}A\r\x1b[J`)
|
|
154
|
+
}
|
|
155
|
+
stdout.write(`${pc.magenta("?")} ${pc.bold(message)}\n`)
|
|
156
|
+
choices.forEach((choice, idx) => {
|
|
157
|
+
const isSelected = idx === selectedIndex
|
|
158
|
+
const pointer = isSelected ? pc.cyan("❯") : " "
|
|
159
|
+
const label = isSelected ? pc.cyan(choice.label) : choice.label
|
|
160
|
+
const hint = choice.hint ? ` ${pc.dim(`(${choice.hint})`)}` : ""
|
|
161
|
+
stdout.write(` ${pointer} ${label}${hint}\n`)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (cleaned) return
|
|
167
|
+
cleaned = true
|
|
168
|
+
stdin.removeListener("keypress", onKeyPress)
|
|
169
|
+
stdin.removeListener("error", onStdinError)
|
|
170
|
+
process.removeListener("exit", onProcessExit)
|
|
171
|
+
if (stdin.isTTY) stdin.setRawMode(false)
|
|
172
|
+
stdout.write("\x1b[?25h") // show cursor
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const onStdinError = () => {
|
|
176
|
+
cleanup()
|
|
177
|
+
resolve(CANCEL_SYMBOL)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const onProcessExit = () => {
|
|
181
|
+
cleanup()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const onKeyPress = (_, key) => {
|
|
185
|
+
if (key) {
|
|
186
|
+
if (key.name === "c" && key.ctrl) {
|
|
187
|
+
cleanup()
|
|
188
|
+
resolve(CANCEL_SYMBOL)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
if (key.name === "up" || key.name === "k") {
|
|
192
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length
|
|
193
|
+
render()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (key.name === "down" || key.name === "j") {
|
|
197
|
+
selectedIndex = (selectedIndex + 1) % choices.length
|
|
198
|
+
render()
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
if (key.name === "return" || key.name === "enter") {
|
|
202
|
+
cleanup()
|
|
203
|
+
resolve(choices[selectedIndex]?.value)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (key.name === "escape") {
|
|
207
|
+
cleanup()
|
|
208
|
+
resolve(CANCEL_SYMBOL)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
stdin.once("error", onStdinError)
|
|
215
|
+
process.once("exit", onProcessExit)
|
|
216
|
+
readline.emitKeypressEvents(stdin)
|
|
217
|
+
if (stdin.isTTY) stdin.setRawMode(true)
|
|
218
|
+
stdout.write("\x1b[?25l") // hide cursor
|
|
219
|
+
render(true)
|
|
220
|
+
stdin.on("keypress", onKeyPress)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function multiselect(options) {
|
|
225
|
+
const { message, options: choices, initialValues = [], required = false } = options
|
|
226
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY
|
|
227
|
+
|
|
228
|
+
if (!isTTY || choices.length === 0) {
|
|
229
|
+
return initialValues.length > 0 ? initialValues : choices.map((c) => c.value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let selectedIndex = 0
|
|
233
|
+
const selectedSet = new Set(initialValues)
|
|
234
|
+
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const { stdin, stdout } = process
|
|
237
|
+
let cleaned = false
|
|
238
|
+
|
|
239
|
+
const render = (first = false) => {
|
|
240
|
+
if (!first) {
|
|
241
|
+
// Move cursor up by (choices.length + 2) lines
|
|
242
|
+
stdout.write(`\x1b[${choices.length + 2}A\r\x1b[J`)
|
|
243
|
+
}
|
|
244
|
+
stdout.write(
|
|
245
|
+
`${pc.magenta("?")} ${pc.bold(message)} ${pc.dim("(Space to toggle, Enter to submit)")}\n`
|
|
246
|
+
)
|
|
247
|
+
choices.forEach((choice, idx) => {
|
|
248
|
+
const isCurrent = idx === selectedIndex
|
|
249
|
+
const isChecked = selectedSet.has(choice.value)
|
|
250
|
+
const pointer = isCurrent ? pc.cyan("❯") : " "
|
|
251
|
+
const checkbox = isChecked ? pc.green("[✔]") : pc.dim("[ ]")
|
|
252
|
+
const label = isCurrent ? pc.cyan(choice.label) : choice.label
|
|
253
|
+
const hint = choice.hint ? ` ${pc.dim(`(${choice.hint})`)}` : ""
|
|
254
|
+
stdout.write(` ${pointer} ${checkbox} ${label}${hint}\n`)
|
|
255
|
+
})
|
|
256
|
+
stdout.write(` ${pc.dim(`${selectedSet.size} selected`)}\n`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const cleanup = () => {
|
|
260
|
+
if (cleaned) return
|
|
261
|
+
cleaned = true
|
|
262
|
+
stdin.removeListener("keypress", onKeyPress)
|
|
263
|
+
stdin.removeListener("error", onStdinError)
|
|
264
|
+
process.removeListener("exit", onProcessExit)
|
|
265
|
+
if (stdin.isTTY) stdin.setRawMode(false)
|
|
266
|
+
stdout.write("\x1b[?25h") // show cursor
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const onStdinError = () => {
|
|
270
|
+
cleanup()
|
|
271
|
+
resolve(CANCEL_SYMBOL)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const onProcessExit = () => {
|
|
275
|
+
cleanup()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const onKeyPress = (_, key) => {
|
|
279
|
+
if (key) {
|
|
280
|
+
if (key.name === "c" && key.ctrl) {
|
|
281
|
+
cleanup()
|
|
282
|
+
resolve(CANCEL_SYMBOL)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
if (key.name === "up" || key.name === "k") {
|
|
286
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length
|
|
287
|
+
render()
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (key.name === "down" || key.name === "j") {
|
|
291
|
+
selectedIndex = (selectedIndex + 1) % choices.length
|
|
292
|
+
render()
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (key.name === "space") {
|
|
296
|
+
const val = choices[selectedIndex]?.value
|
|
297
|
+
if (selectedSet.has(val)) {
|
|
298
|
+
selectedSet.delete(val)
|
|
299
|
+
} else {
|
|
300
|
+
selectedSet.add(val)
|
|
301
|
+
}
|
|
302
|
+
render()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
if (key.name === "return" || key.name === "enter") {
|
|
306
|
+
if (required && selectedSet.size === 0) {
|
|
307
|
+
render()
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
cleanup()
|
|
311
|
+
resolve(Array.from(selectedSet))
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if (key.name === "escape") {
|
|
315
|
+
cleanup()
|
|
316
|
+
resolve(CANCEL_SYMBOL)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
stdin.once("error", onStdinError)
|
|
323
|
+
process.once("exit", onProcessExit)
|
|
324
|
+
readline.emitKeypressEvents(stdin)
|
|
325
|
+
if (stdin.isTTY) stdin.setRawMode(true)
|
|
326
|
+
stdout.write("\x1b[?25l") // hide cursor
|
|
327
|
+
render(true)
|
|
328
|
+
stdin.on("keypress", onKeyPress)
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- Terminal Spinner ---
|
|
333
|
+
|
|
334
|
+
export function spinner() {
|
|
335
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
336
|
+
let frameIndex = 0
|
|
337
|
+
let intervalId = null
|
|
338
|
+
let currentMessage = ""
|
|
339
|
+
const isTTY = process.stdout.isTTY
|
|
340
|
+
|
|
341
|
+
const onProcessExit = () => {
|
|
342
|
+
if (isTTY) {
|
|
343
|
+
process.stdout.write("\x1b[?25h") // restore cursor
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
start(msg = "") {
|
|
349
|
+
currentMessage = msg
|
|
350
|
+
if (isTTY) {
|
|
351
|
+
process.once("exit", onProcessExit)
|
|
352
|
+
process.stdout.write("\x1b[?25l") // hide cursor
|
|
353
|
+
intervalId = setInterval(() => {
|
|
354
|
+
const frame = pc.cyan(frames[frameIndex] || "⠋")
|
|
355
|
+
frameIndex = (frameIndex + 1) % frames.length
|
|
356
|
+
process.stdout.write(`\r${frame} ${currentMessage}`)
|
|
357
|
+
}, 80)
|
|
358
|
+
} else {
|
|
359
|
+
console.log(`ℹ ${msg}`)
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
message(msg = "") {
|
|
363
|
+
currentMessage = msg
|
|
364
|
+
},
|
|
365
|
+
stop(finalMessage = "") {
|
|
366
|
+
if (intervalId) {
|
|
367
|
+
clearInterval(intervalId)
|
|
368
|
+
intervalId = null
|
|
369
|
+
}
|
|
370
|
+
if (isTTY) {
|
|
371
|
+
process.removeListener("exit", onProcessExit)
|
|
372
|
+
process.stdout.write(`\r\x1b[K`) // clear line
|
|
373
|
+
process.stdout.write("\x1b[?25h") // show cursor
|
|
374
|
+
}
|
|
375
|
+
if (finalMessage) {
|
|
376
|
+
console.log(finalMessage)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Rimelight Entertainment
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|