reset-framework-cli 0.2.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +31 -0
  4. package/src/commands/build.js +115 -0
  5. package/src/commands/dev.js +160 -0
  6. package/src/commands/doctor.js +89 -0
  7. package/src/commands/init.js +629 -0
  8. package/src/commands/package.js +63 -0
  9. package/src/index.js +214 -0
  10. package/src/lib/context.js +66 -0
  11. package/src/lib/framework.js +55 -0
  12. package/src/lib/logger.js +11 -0
  13. package/src/lib/output.js +65 -0
  14. package/src/lib/process.js +165 -0
  15. package/src/lib/project.js +357 -0
  16. package/src/lib/toolchain.js +62 -0
  17. package/src/lib/ui.js +244 -0
  18. package/templates/basic/README.md +15 -0
  19. package/templates/basic/frontend/README.md +73 -0
  20. package/templates/basic/frontend/eslint.config.js +23 -0
  21. package/templates/basic/frontend/index.html +13 -0
  22. package/templates/basic/frontend/package.json +31 -0
  23. package/templates/basic/frontend/public/favicon.svg +1 -0
  24. package/templates/basic/frontend/public/icons.svg +24 -0
  25. package/templates/basic/frontend/src/App.css +138 -0
  26. package/templates/basic/frontend/src/App.tsx +72 -0
  27. package/templates/basic/frontend/src/assets/hero.png +0 -0
  28. package/templates/basic/frontend/src/assets/react.svg +1 -0
  29. package/templates/basic/frontend/src/assets/vite.svg +1 -0
  30. package/templates/basic/frontend/src/index.css +111 -0
  31. package/templates/basic/frontend/src/lib/reset.ts +16 -0
  32. package/templates/basic/frontend/src/main.tsx +10 -0
  33. package/templates/basic/frontend/tsconfig.app.json +24 -0
  34. package/templates/basic/frontend/tsconfig.json +7 -0
  35. package/templates/basic/frontend/tsconfig.node.json +26 -0
  36. package/templates/basic/frontend/vite.config.ts +6 -0
  37. package/templates/basic/reset.config.json +29 -0
@@ -0,0 +1,357 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { createRequire } from "node:module"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const require = createRequire(import.meta.url)
7
+
8
+ function requireString(object, key, scope) {
9
+ if (typeof object?.[key] !== "string" || object[key].trim() === "") {
10
+ throw new Error(`Missing or invalid string field '${scope}.${key}'`)
11
+ }
12
+
13
+ return object[key]
14
+ }
15
+
16
+ function optionalString(object, key, fallback) {
17
+ if (object == null || !(key in object)) {
18
+ return fallback
19
+ }
20
+
21
+ if (typeof object[key] !== "string" || object[key].trim() === "") {
22
+ throw new Error(`Missing or invalid string field '${key}'`)
23
+ }
24
+
25
+ return object[key]
26
+ }
27
+
28
+ function optionalObject(object, key) {
29
+ if (object == null || !(key in object)) {
30
+ return {}
31
+ }
32
+
33
+ if (typeof object[key] !== "object" || object[key] === null || Array.isArray(object[key])) {
34
+ throw new Error(`Missing or invalid object field '${key}'`)
35
+ }
36
+
37
+ return object[key]
38
+ }
39
+
40
+ function sanitizeName(value) {
41
+ return value
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9]+/g, "-")
44
+ .replace(/^-+|-+$/g, "") || "reset-app"
45
+ }
46
+
47
+ function normalizeFrontendDir(value) {
48
+ const trimmed = value.trim()
49
+
50
+ if (trimmed === "." || trimmed === "./") {
51
+ return "."
52
+ }
53
+
54
+ if (path.isAbsolute(trimmed)) {
55
+ throw new Error("project.frontendDir must be a relative path")
56
+ }
57
+
58
+ const normalized = path.normalize(trimmed).replace(/\\/g, "/").replace(/\/+$/g, "")
59
+
60
+ if (normalized === "" || normalized === ".") {
61
+ return "."
62
+ }
63
+
64
+ if (normalized.startsWith("../")) {
65
+ throw new Error("project.frontendDir cannot point outside the app root")
66
+ }
67
+
68
+ return normalized
69
+ }
70
+
71
+ function normalizeStyling(value) {
72
+ if (value === "css" || value === "tailwindcss") {
73
+ return value
74
+ }
75
+
76
+ throw new Error("project.styling must be either 'css' or 'tailwindcss'")
77
+ }
78
+
79
+ function toTitleCase(value) {
80
+ return value
81
+ .split(/[^a-zA-Z0-9]+/)
82
+ .filter(Boolean)
83
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
84
+ .join(" ") || "Reset App"
85
+ }
86
+
87
+ function readJsonFile(filePath) {
88
+ return JSON.parse(readFileSync(filePath, "utf8"))
89
+ }
90
+
91
+ function isPathInside(parentDir, candidatePath) {
92
+ const relative = path.relative(parentDir, candidatePath)
93
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
94
+ }
95
+
96
+ function resolvePackageInfo(packageName, fallbackRoot) {
97
+ try {
98
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
99
+ const packageRoot = path.dirname(packageJsonPath)
100
+ const manifest = readJsonFile(packageJsonPath)
101
+
102
+ return {
103
+ packageName: manifest.name ?? packageName,
104
+ packageRoot,
105
+ packageJsonPath,
106
+ version: manifest.version ?? "0.0.0",
107
+ localFallback: false
108
+ }
109
+ } catch {
110
+ const packageJsonPath = path.join(fallbackRoot, "package.json")
111
+ const manifest = readJsonFile(packageJsonPath)
112
+
113
+ return {
114
+ packageName: manifest.name ?? packageName,
115
+ packageRoot: fallbackRoot,
116
+ packageJsonPath,
117
+ version: manifest.version ?? "0.0.0",
118
+ localFallback: true
119
+ }
120
+ }
121
+ }
122
+
123
+ export function resolveFrameworkPaths() {
124
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url))
125
+ const cliDir = path.resolve(moduleDir, "../..")
126
+ const packagesDir = path.resolve(cliDir, "..")
127
+ const nativePackage = resolvePackageInfo(
128
+ "@reset-framework/native",
129
+ path.join(packagesDir, "native")
130
+ )
131
+ const sdkPackage = resolvePackageInfo(
132
+ "@reset-framework/sdk",
133
+ path.join(packagesDir, "sdk")
134
+ )
135
+ const schemaPackage = resolvePackageInfo(
136
+ "@reset-framework/schema",
137
+ path.join(packagesDir, "schema")
138
+ )
139
+ const isWorkspaceLayout = [nativePackage, sdkPackage, schemaPackage].every((pkg) =>
140
+ isPathInside(packagesDir, pkg.packageRoot)
141
+ )
142
+
143
+ return {
144
+ cliDir,
145
+ packagesDir,
146
+ frameworkRoot: nativePackage.packageRoot,
147
+ frameworkPackage: nativePackage,
148
+ isWorkspaceLayout,
149
+ sdkPackage,
150
+ schemaPackage,
151
+ runtimeDir: path.join(nativePackage.packageRoot, "runtime"),
152
+ rootCMakePath: path.join(nativePackage.packageRoot, "CMakeLists.txt"),
153
+ cmakePresetsPath: path.join(nativePackage.packageRoot, "CMakePresets.json"),
154
+ vcpkgManifestPath: path.join(nativePackage.packageRoot, "vcpkg.json"),
155
+ templatesDir: path.join(cliDir, "templates"),
156
+ }
157
+ }
158
+
159
+ export function resolveFrameworkBuildPaths(appPaths) {
160
+ const frameworkCacheRoot = path.join(appPaths.appRoot, ".reset", "framework")
161
+
162
+ return {
163
+ frameworkCacheRoot,
164
+ devBuildDir: path.join(frameworkCacheRoot, "build", "dev"),
165
+ releaseBuildDir: path.join(frameworkCacheRoot, "build", "release"),
166
+ devAppBinary: path.join(
167
+ frameworkCacheRoot,
168
+ "build",
169
+ "dev",
170
+ "runtime",
171
+ "reset-framework.app",
172
+ "Contents",
173
+ "MacOS",
174
+ "reset-framework"
175
+ ),
176
+ releaseAppTemplate: path.join(
177
+ frameworkCacheRoot,
178
+ "build",
179
+ "release",
180
+ "runtime",
181
+ "reset-framework.app"
182
+ )
183
+ }
184
+ }
185
+
186
+ export function resolveAppPaths(appRoot) {
187
+ return {
188
+ appRoot,
189
+ frontendDir: path.join(appRoot, "frontend"),
190
+ resetConfigPath: path.join(appRoot, "reset.config.json"),
191
+ legacyResetConfigPath: path.join(appRoot, "frontend", "reset.config.json")
192
+ }
193
+ }
194
+
195
+ export function resolveConfigPath(appPaths) {
196
+ if (existsSync(appPaths.resetConfigPath)) {
197
+ return appPaths.resetConfigPath
198
+ }
199
+
200
+ if (existsSync(appPaths.legacyResetConfigPath)) {
201
+ return appPaths.legacyResetConfigPath
202
+ }
203
+
204
+ return appPaths.resetConfigPath
205
+ }
206
+
207
+ export function getFrameworkChecks(frameworkPaths) {
208
+ return [
209
+ ["native", frameworkPaths.frameworkPackage.packageJsonPath],
210
+ ["sdk", frameworkPaths.sdkPackage.packageJsonPath],
211
+ ["schema", frameworkPaths.schemaPackage.packageJsonPath],
212
+ ["cmake", frameworkPaths.rootCMakePath],
213
+ ["runtime", frameworkPaths.runtimeDir],
214
+ ["vcpkg", frameworkPaths.vcpkgManifestPath],
215
+ ["templates", frameworkPaths.templatesDir]
216
+ ]
217
+ }
218
+
219
+ export function getAppChecks(appPaths) {
220
+ return [["config", resolveConfigPath(appPaths)]]
221
+ }
222
+
223
+ export function assertFrameworkInstall(frameworkPaths) {
224
+ const missing = getFrameworkChecks(frameworkPaths)
225
+ .filter(([, filePath]) => !existsSync(filePath))
226
+ .map(([label]) => label)
227
+
228
+ if (missing.length > 0) {
229
+ throw new Error(
230
+ `Reset CLI installation is incomplete. Missing: ${missing.join(", ")}`
231
+ )
232
+ }
233
+ }
234
+
235
+ export function assertAppProject(appPaths, config) {
236
+ const checks = [...getAppChecks(appPaths)]
237
+
238
+ if (config) {
239
+ checks.unshift(["frontend", resolveFrontendDir(appPaths, config)])
240
+ }
241
+
242
+ const missing = checks
243
+ .filter(([, filePath]) => !existsSync(filePath))
244
+ .map(([label]) => label)
245
+
246
+ if (missing.length > 0) {
247
+ throw new Error(
248
+ `Current directory does not look like a Reset app. Missing: ${missing.join(", ")}`
249
+ )
250
+ }
251
+ }
252
+
253
+ export function validateResetConfig(rawConfig) {
254
+ const frontend = optionalObject(rawConfig, "frontend")
255
+ const build = optionalObject(rawConfig, "build")
256
+ const project = optionalObject(rawConfig, "project")
257
+ const windowConfig = optionalObject(rawConfig, "window")
258
+
259
+ return {
260
+ ...rawConfig,
261
+ name: sanitizeName(requireString(rawConfig, "name", "config")),
262
+ productName: requireString(rawConfig, "productName", "config"),
263
+ appId: requireString(rawConfig, "appId", "config"),
264
+ version: requireString(rawConfig, "version", "config"),
265
+ window: {
266
+ title: optionalString(windowConfig, "title", "Reset App")
267
+ },
268
+ frontend: {
269
+ devUrl: requireString(frontend, "devUrl", "frontend"),
270
+ distDir: optionalString(frontend, "distDir", "dist"),
271
+ entryHtml: optionalString(frontend, "entryHtml", "index.html")
272
+ },
273
+ project: {
274
+ frontendDir: normalizeFrontendDir(optionalString(project, "frontendDir", "frontend")),
275
+ styling: normalizeStyling(optionalString(project, "styling", "css"))
276
+ },
277
+ build: {
278
+ outputDir: optionalString(build, "outputDir", ".reset/build")
279
+ }
280
+ }
281
+ }
282
+
283
+ export function loadResetConfig(appPaths) {
284
+ const configPath = resolveConfigPath(appPaths)
285
+ const raw = readFileSync(configPath, "utf8")
286
+
287
+ try {
288
+ return validateResetConfig(JSON.parse(raw))
289
+ } catch (error) {
290
+ if (error instanceof SyntaxError) {
291
+ throw new Error(`Invalid JSON in ${configPath}: ${error.message}`)
292
+ }
293
+
294
+ throw error
295
+ }
296
+ }
297
+
298
+ export function resolveAppOutputPaths(appPaths, config) {
299
+ const outputRoot = path.resolve(appPaths.appRoot, config.build.outputDir)
300
+ const frontendDir = resolveFrontendDir(appPaths, config)
301
+ const appBundleName = `${config.productName}.app`
302
+ const macosDir = path.join(outputRoot, "macos")
303
+ const appBundlePath = path.join(macosDir, appBundleName)
304
+ const resourcesDir = path.join(appBundlePath, "Contents", "Resources")
305
+ const packagesDir = path.join(outputRoot, "packages")
306
+
307
+ return {
308
+ outputRoot,
309
+ macosDir,
310
+ appBundlePath,
311
+ resourcesDir,
312
+ bundledConfigPath: path.join(resourcesDir, "reset.config.json"),
313
+ bundledFrontendDir: path.join(resourcesDir, config.frontend.distDir),
314
+ frontendDistDir: path.resolve(frontendDir, config.frontend.distDir),
315
+ frontendEntryFile: path.resolve(
316
+ frontendDir,
317
+ config.frontend.distDir,
318
+ config.frontend.entryHtml
319
+ ),
320
+ packagesDir,
321
+ zipPath: path.join(packagesDir, `${config.name}-macos.zip`)
322
+ }
323
+ }
324
+
325
+ export function resolveFrontendDir(appPaths, config) {
326
+ return path.resolve(appPaths.appRoot, config.project.frontendDir)
327
+ }
328
+
329
+ export function resolveDevServerOptions(config) {
330
+ const url = new URL(config.frontend.devUrl)
331
+
332
+ return {
333
+ host: url.hostname,
334
+ port: url.port || "5173",
335
+ url: url.toString()
336
+ }
337
+ }
338
+
339
+ export function makeAppMetadata(appName) {
340
+ const name = sanitizeName(appName)
341
+ const productName = toTitleCase(appName)
342
+
343
+ return {
344
+ name,
345
+ productName,
346
+ appId: `com.example.${name}`,
347
+ title: productName
348
+ }
349
+ }
350
+
351
+ export function resolveSdkDependencySpec(frameworkPaths) {
352
+ if (frameworkPaths.isWorkspaceLayout || frameworkPaths.sdkPackage.localFallback) {
353
+ return `file:${frameworkPaths.sdkPackage.packageRoot}`
354
+ }
355
+
356
+ return `^${frameworkPaths.sdkPackage.version}`
357
+ }
@@ -0,0 +1,62 @@
1
+ import { existsSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import path from "node:path"
4
+
5
+ import { runCommand } from "./process.js"
6
+
7
+ function toToolchainPath(root) {
8
+ return path.join(root, "scripts", "buildsystems", "vcpkg.cmake")
9
+ }
10
+
11
+ export function getVcpkgToolchainCandidates() {
12
+ const candidates = []
13
+
14
+ if (typeof process.env.CMAKE_TOOLCHAIN_FILE === "string" && process.env.CMAKE_TOOLCHAIN_FILE.trim() !== "") {
15
+ candidates.push(process.env.CMAKE_TOOLCHAIN_FILE)
16
+ }
17
+
18
+ if (typeof process.env.VCPKG_ROOT === "string" && process.env.VCPKG_ROOT.trim() !== "") {
19
+ candidates.push(toToolchainPath(process.env.VCPKG_ROOT))
20
+ }
21
+
22
+ candidates.push(toToolchainPath(path.join(homedir(), ".vcpkg")))
23
+ candidates.push(toToolchainPath(path.join(homedir(), ".reset-framework-cli", "vcpkg")))
24
+
25
+ return [...new Set(candidates)]
26
+ }
27
+
28
+ export function findVcpkgToolchainFile() {
29
+ return getVcpkgToolchainCandidates().find((candidate) => existsSync(candidate)) ?? null
30
+ }
31
+
32
+ export async function ensureVcpkgToolchain(options = {}) {
33
+ const { dryRun = false } = options
34
+ const existing = findVcpkgToolchainFile()
35
+
36
+ if (existing) {
37
+ return existing
38
+ }
39
+
40
+ const vcpkgRoot = path.join(homedir(), ".reset-framework-cli", "vcpkg")
41
+ const toolchainFile = toToolchainPath(vcpkgRoot)
42
+
43
+ if (!existsSync(vcpkgRoot)) {
44
+ await runCommand("git", ["clone", "https://github.com/microsoft/vcpkg", vcpkgRoot], {
45
+ dryRun
46
+ })
47
+ }
48
+
49
+ if (process.platform === "win32") {
50
+ await runCommand("cmd", ["/c", "bootstrap-vcpkg.bat", "-disableMetrics"], {
51
+ cwd: vcpkgRoot,
52
+ dryRun
53
+ })
54
+ } else {
55
+ await runCommand("bash", ["./bootstrap-vcpkg.sh", "-disableMetrics"], {
56
+ cwd: vcpkgRoot,
57
+ dryRun
58
+ })
59
+ }
60
+
61
+ return toolchainFile
62
+ }
package/src/lib/ui.js ADDED
@@ -0,0 +1,244 @@
1
+ import readline from "node:readline/promises"
2
+ import { stdin as input, stdout as output } from "node:process"
3
+
4
+ const ansi = {
5
+ reset: "\u001b[0m",
6
+ bold: "\u001b[1m",
7
+ dim: "\u001b[2m",
8
+ cyan: "\u001b[36m",
9
+ gray: "\u001b[90m",
10
+ green: "\u001b[32m",
11
+ yellow: "\u001b[33m",
12
+ red: "\u001b[31m"
13
+ }
14
+
15
+ function style(text, code) {
16
+ if (!output.isTTY) {
17
+ return text
18
+ }
19
+
20
+ return `${code}${text}${ansi.reset}`
21
+ }
22
+
23
+ export function isInteractiveSession() {
24
+ return Boolean(input.isTTY && output.isTTY)
25
+ }
26
+
27
+ export function bold(text) {
28
+ return style(text, ansi.bold)
29
+ }
30
+
31
+ export function dim(text) {
32
+ return style(text, ansi.dim)
33
+ }
34
+
35
+ export function accent(text) {
36
+ return style(text, ansi.cyan)
37
+ }
38
+
39
+ export function success(text) {
40
+ return style(text, ansi.green)
41
+ }
42
+
43
+ export function warning(text) {
44
+ return style(text, ansi.yellow)
45
+ }
46
+
47
+ export function danger(text) {
48
+ return style(text, ansi.red)
49
+ }
50
+
51
+ export function printBanner(title, subtitle) {
52
+ console.log(bold(title))
53
+ if (subtitle) {
54
+ console.log(dim(subtitle))
55
+ }
56
+ console.log("")
57
+ }
58
+
59
+ export function printSection(title) {
60
+ console.log(accent(title))
61
+ }
62
+
63
+ export function printMutedLine(text = "") {
64
+ console.log(dim(text))
65
+ }
66
+
67
+ export function printKeyValueTable(entries) {
68
+ if (entries.length === 0) {
69
+ return
70
+ }
71
+
72
+ const width = entries.reduce((max, [key]) => Math.max(max, key.length), 0)
73
+
74
+ for (const [key, value] of entries) {
75
+ console.log(` ${dim(key.padEnd(width))} ${value}`)
76
+ }
77
+ }
78
+
79
+ function formatStatus(status, width) {
80
+ const normalized = String(status).toLowerCase()
81
+ const padded = normalized.toUpperCase().padEnd(width)
82
+
83
+ if (normalized === "ok" || normalized === "done" || normalized === "ready") {
84
+ return success(padded)
85
+ }
86
+
87
+ if (normalized === "warn" || normalized === "skip") {
88
+ return warning(padded)
89
+ }
90
+
91
+ if (normalized === "missing" || normalized === "error") {
92
+ return danger(padded)
93
+ }
94
+
95
+ return accent(padded)
96
+ }
97
+
98
+ export function printStatusTable(entries) {
99
+ if (entries.length === 0) {
100
+ return
101
+ }
102
+
103
+ const statusWidth = entries.reduce((max, [status]) => Math.max(max, String(status).length), 0)
104
+ const labelWidth = entries.reduce((max, [, label]) => Math.max(max, label.length), 0)
105
+
106
+ for (const [status, label, detail = ""] of entries) {
107
+ const suffix = detail ? ` ${detail}` : ""
108
+ console.log(` ${formatStatus(status, statusWidth)} ${label.padEnd(labelWidth)}${suffix}`)
109
+ }
110
+ }
111
+
112
+ export function printCommandList(commands) {
113
+ if (commands.length === 0) {
114
+ return
115
+ }
116
+
117
+ const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0)
118
+
119
+ for (const command of commands) {
120
+ console.log(` ${bold(command.name.padEnd(width))} ${command.description}`)
121
+ }
122
+ }
123
+
124
+ export function createProgress(total, label = "Progress") {
125
+ let current = 0
126
+ let lastWidth = 0
127
+
128
+ function render(message) {
129
+ const width = 24
130
+ const ratio = total === 0 ? 1 : current / total
131
+ const filled = Math.round(ratio * width)
132
+ const percentage = `${Math.round(ratio * 100)}`.padStart(3, " ")
133
+ const bar = `${"#".repeat(filled)}${"-".repeat(width - filled)}`
134
+ const line = `${dim(label)} [${bar}] ${percentage}% ${message}`
135
+
136
+ if (output.isTTY) {
137
+ const padded = line.padEnd(lastWidth, " ")
138
+ output.write(`\r${padded}`)
139
+ lastWidth = Math.max(lastWidth, line.length)
140
+ if (current >= total) {
141
+ output.write("\n")
142
+ }
143
+ return
144
+ }
145
+
146
+ console.log(line)
147
+ }
148
+
149
+ return {
150
+ tick(message) {
151
+ current += 1
152
+ render(message)
153
+ },
154
+ complete(message) {
155
+ current = total
156
+ render(message)
157
+ }
158
+ }
159
+ }
160
+
161
+ export async function withPromptSession(callback) {
162
+ const rl = readline.createInterface({ input, output })
163
+
164
+ try {
165
+ return await callback(rl)
166
+ } finally {
167
+ rl.close()
168
+ }
169
+ }
170
+
171
+ export async function promptText(rl, options) {
172
+ const { label, defaultValue = "", validate } = options
173
+
174
+ while (true) {
175
+ const suffix = defaultValue ? ` ${dim(`[${defaultValue}]`)}` : ""
176
+ const answer = (await rl.question(`${label}${suffix}: `)).trim()
177
+ const value = answer === "" ? defaultValue : answer
178
+
179
+ if (!validate) {
180
+ return value
181
+ }
182
+
183
+ const result = validate(value)
184
+ if (result === true) {
185
+ return value
186
+ }
187
+
188
+ console.log(dim(typeof result === "string" ? result : "Invalid value"))
189
+ }
190
+ }
191
+
192
+ export async function promptConfirm(rl, options) {
193
+ const { label, defaultValue = true } = options
194
+ const hint = defaultValue ? "[Y/n]" : "[y/N]"
195
+
196
+ while (true) {
197
+ const answer = (await rl.question(`${label} ${dim(hint)}: `)).trim().toLowerCase()
198
+
199
+ if (answer === "") {
200
+ return defaultValue
201
+ }
202
+
203
+ if (answer === "y" || answer === "yes") {
204
+ return true
205
+ }
206
+
207
+ if (answer === "n" || answer === "no") {
208
+ return false
209
+ }
210
+ }
211
+ }
212
+
213
+ export async function promptSelect(rl, options) {
214
+ const { label, choices, defaultValue } = options
215
+
216
+ printSection(label)
217
+ for (let index = 0; index < choices.length; index += 1) {
218
+ const choice = choices[index]
219
+ const marker = `${index + 1}.`
220
+ const recommended = choice.value === defaultValue ? ` ${dim("(recommended)")}` : ""
221
+
222
+ console.log(` ${marker} ${choice.label}${recommended}`)
223
+ if (choice.description) {
224
+ console.log(` ${dim(choice.description)}`)
225
+ }
226
+ }
227
+
228
+ const fallbackIndex = Math.max(
229
+ 0,
230
+ choices.findIndex((choice) => choice.value === defaultValue)
231
+ )
232
+
233
+ while (true) {
234
+ const answer = (await rl.question(`Choose ${dim(`[${fallbackIndex + 1}]`)}: `)).trim()
235
+ const selectedIndex = answer === "" ? fallbackIndex : Number.parseInt(answer, 10) - 1
236
+
237
+ if (choices[selectedIndex]) {
238
+ console.log("")
239
+ return choices[selectedIndex].value
240
+ }
241
+
242
+ console.log(dim("Select one of the listed options."))
243
+ }
244
+ }
@@ -0,0 +1,15 @@
1
+ # Reset App
2
+
3
+ Generated with `reset-framework-cli`.
4
+
5
+ ## Commands
6
+
7
+ - `reset-framework-cli dev`
8
+ - `reset-framework-cli build`
9
+ - `reset-framework-cli package`
10
+
11
+ ## Project files
12
+
13
+ - `reset.config.json`: desktop app metadata and runtime config
14
+ - `frontend/` or project root: web frontend source, depending on the selected layout
15
+ - `.reset/`: generated build output and native runtime cache