@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 CHANGED
@@ -1,28 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import mri from "mri"
3
+ import { parseArgs } from "node:util"
4
4
  import { runCreate } from "../src/index.js"
5
5
 
6
6
  async function main() {
7
- const args = process.argv.slice(2)
8
- const parsed = mri(args, {
9
- alias: {
10
- h: "help",
11
- v: "version",
12
- g: "git",
13
- i: "install",
14
- d: "domain"
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
- boolean: ["help", "version", "git", "install"],
17
- string: ["package-manager", "domain"]
17
+ allowPositionals: true,
18
+ strict: false
18
19
  })
19
20
 
20
- if (parsed.version) {
21
+ if (values.version) {
21
22
  console.log("rimelight v0.1.0")
22
23
  process.exit(0)
23
24
  }
24
25
 
25
- if (parsed.help) {
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 = parsed._[0]
51
+ const command = positionals[0]
51
52
 
52
53
  if (command === "compare") {
53
- const targetDir = parsed._[1] || "."
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 = parsed._[1] || "."
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" ? parsed._[1] : parsed._[0]
62
+ const projectName = command === "create" ? positionals[1] : positionals[0]
62
63
  await runCreate({
63
64
  projectName,
64
- domain: parsed.domain,
65
- git: parsed.git,
66
- install: parsed.install,
67
- packageManager: parsed["package-manager"]
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.13",
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
- "dependencies": {
31
- "@clack/prompts": "1.8.1",
32
- "cross-spawn": "7.0.6",
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": "0.0.20",
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
- "scripts": {
46
- "create": "node ./bin/rimelight.js create",
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 * as prompts from "@clack/prompts"
5
- import pc from "picocolors"
6
- import spawn from "cross-spawn"
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 starterPath = path.resolve(import.meta.dirname, "../../starter")
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 = spawn.sync("git", ["init"], { cwd: resolvedTargetDir })
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 = spawn.sync(packageManager, ["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 = spawn.sync("git", ["init"], { cwd: resolvedTargetDir })
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 = spawn.crossSpawn
337
- ? spawn.crossSpawn(packageManager, ["install"], {
338
- cwd: resolvedTargetDir,
339
- stdio: "ignore"
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 = spawn.sync("vp", ["install"], { cwd: targetDir, stdio: "ignore" })
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.