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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +31 -0
- package/src/commands/build.js +115 -0
- package/src/commands/dev.js +160 -0
- package/src/commands/doctor.js +89 -0
- package/src/commands/init.js +629 -0
- package/src/commands/package.js +63 -0
- package/src/index.js +214 -0
- package/src/lib/context.js +66 -0
- package/src/lib/framework.js +55 -0
- package/src/lib/logger.js +11 -0
- package/src/lib/output.js +65 -0
- package/src/lib/process.js +165 -0
- package/src/lib/project.js +357 -0
- package/src/lib/toolchain.js +62 -0
- package/src/lib/ui.js +244 -0
- package/templates/basic/README.md +15 -0
- package/templates/basic/frontend/README.md +73 -0
- package/templates/basic/frontend/eslint.config.js +23 -0
- package/templates/basic/frontend/index.html +13 -0
- package/templates/basic/frontend/package.json +31 -0
- package/templates/basic/frontend/public/favicon.svg +1 -0
- package/templates/basic/frontend/public/icons.svg +24 -0
- package/templates/basic/frontend/src/App.css +138 -0
- package/templates/basic/frontend/src/App.tsx +72 -0
- package/templates/basic/frontend/src/assets/hero.png +0 -0
- package/templates/basic/frontend/src/assets/react.svg +1 -0
- package/templates/basic/frontend/src/assets/vite.svg +1 -0
- package/templates/basic/frontend/src/index.css +111 -0
- package/templates/basic/frontend/src/lib/reset.ts +16 -0
- package/templates/basic/frontend/src/main.tsx +10 -0
- package/templates/basic/frontend/tsconfig.app.json +24 -0
- package/templates/basic/frontend/tsconfig.json +7 -0
- package/templates/basic/frontend/tsconfig.node.json +26 -0
- package/templates/basic/frontend/vite.config.ts +6 -0
- 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
|