reset-framework-cli 1.2.0 → 1.2.1

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 (34) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +19 -19
  3. package/package.json +6 -6
  4. package/src/commands/build.js +71 -71
  5. package/src/commands/dev.js +121 -121
  6. package/src/commands/doctor.js +54 -54
  7. package/src/commands/init.js +866 -866
  8. package/src/commands/package.js +68 -68
  9. package/src/index.js +195 -195
  10. package/src/lib/context.js +66 -66
  11. package/src/lib/framework.js +57 -57
  12. package/src/lib/logger.js +11 -11
  13. package/src/lib/output.js +234 -234
  14. package/src/lib/process.js +303 -303
  15. package/src/lib/project.js +493 -493
  16. package/src/lib/toolchain.js +62 -62
  17. package/src/lib/ui.js +244 -244
  18. package/templates/basic/README.md +15 -15
  19. package/templates/basic/frontend/README.md +73 -73
  20. package/templates/basic/frontend/eslint.config.js +23 -23
  21. package/templates/basic/frontend/index.html +13 -13
  22. package/templates/basic/frontend/package.json +31 -31
  23. package/templates/basic/frontend/public/icons.svg +24 -24
  24. package/templates/basic/frontend/src/App.css +216 -216
  25. package/templates/basic/frontend/src/App.tsx +77 -77
  26. package/templates/basic/frontend/src/assets/vite.svg +1 -1
  27. package/templates/basic/frontend/src/index.css +111 -111
  28. package/templates/basic/frontend/src/lib/reset.ts +1 -1
  29. package/templates/basic/frontend/src/main.tsx +10 -10
  30. package/templates/basic/frontend/tsconfig.app.json +28 -28
  31. package/templates/basic/frontend/tsconfig.json +7 -7
  32. package/templates/basic/frontend/tsconfig.node.json +26 -26
  33. package/templates/basic/frontend/vite.config.ts +16 -16
  34. package/templates/basic/reset.config.json +58 -58
@@ -1,866 +1,866 @@
1
- import { existsSync, readdirSync } from "node:fs"
2
- import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
3
- import path from "node:path"
4
-
5
- import {
6
- captureCommandOutput,
7
- getInstallCommandArgs,
8
- resolvePackageManagerCommand,
9
- runCommand
10
- } from "../lib/process.js"
11
- import {
12
- makeAppMetadata,
13
- resolveCliDependencySpec,
14
- resolveFrameworkPaths,
15
- resolveSdkDependencySpec
16
- } from "../lib/project.js"
17
- import {
18
- createProgress,
19
- isInteractiveSession,
20
- printBanner,
21
- printKeyValueTable,
22
- printSection,
23
- printStatusTable,
24
- promptConfirm,
25
- promptSelect,
26
- promptText,
27
- withPromptSession
28
- } from "../lib/ui.js"
29
-
30
- const sdkRuntimeReexport = `export * from '@reset-framework/sdk'
31
- `
32
-
33
- const standaloneViteConfig = `import { defineConfig } from 'vite'
34
- import react from '@vitejs/plugin-react'
35
-
36
- export default defineConfig({
37
- base: './',
38
- plugins: [react()],
39
- })
40
- `
41
-
42
- const tailwindViteConfig = `import { defineConfig } from 'vite'
43
- import react from '@vitejs/plugin-react'
44
- import tailwindcss from '@tailwindcss/vite'
45
-
46
- export default defineConfig({
47
- base: './',
48
- plugins: [react(), tailwindcss()],
49
- })
50
- `
51
-
52
- const standaloneTsconfigApp = `{
53
- "compilerOptions": {
54
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
55
- "target": "ES2023",
56
- "useDefineForClassFields": true,
57
- "lib": ["ES2023", "DOM", "DOM.Iterable"],
58
- "module": "ESNext",
59
- "types": ["vite/client"],
60
- "skipLibCheck": true,
61
- "moduleResolution": "bundler",
62
- "allowImportingTsExtensions": true,
63
- "verbatimModuleSyntax": true,
64
- "moduleDetection": "force",
65
- "noEmit": true,
66
- "jsx": "react-jsx",
67
- "strict": true,
68
- "noUnusedLocals": true,
69
- "noUnusedParameters": true,
70
- "erasableSyntaxOnly": true,
71
- "noFallthroughCasesInSwitch": true,
72
- "noUncheckedSideEffectImports": true
73
- },
74
- "include": ["src"]
75
- }
76
- `
77
-
78
- const tailwindIndexCss = `@import "tailwindcss";
79
-
80
- :root {
81
- color-scheme: dark;
82
- }
83
-
84
- body {
85
- margin: 0;
86
- font-family: "Avenir Next", "Segoe UI", sans-serif;
87
- background: #111111;
88
- }
89
- `
90
-
91
- const tailwindAppTsx = `import { useEffect, useState } from 'react'
92
- import { reset } from './lib/reset'
93
-
94
- function App() {
95
- const [count, setCount] = useState(0)
96
- const [appName, setAppName] = useState('Reset App')
97
- const [appVersion, setAppVersion] = useState('0.0.0')
98
- const [platform, setPlatform] = useState('unknown')
99
- const [bridgeVersion, setBridgeVersion] = useState('1')
100
-
101
- useEffect(() => {
102
- let cancelled = false
103
-
104
- async function loadRuntimeInfo() {
105
- try {
106
- const [appInfo, runtimeInfo] = await Promise.all([
107
- reset.app.getInfo(),
108
- reset.runtime.getInfo(),
109
- ])
110
-
111
- if (cancelled) {
112
- return
113
- }
114
-
115
- setAppName(appInfo.name)
116
- setAppVersion(appInfo.version)
117
- setPlatform(runtimeInfo.platform)
118
- setBridgeVersion(runtimeInfo.bridgeVersion)
119
- } catch (error) {
120
- console.error('Failed to load runtime info', error)
121
- }
122
- }
123
-
124
- loadRuntimeInfo()
125
-
126
- return () => {
127
- cancelled = true
128
- }
129
- }, [])
130
-
131
- return (
132
- <main className="min-h-screen bg-stone-950 text-stone-100">
133
- <div className="mx-auto flex min-h-screen max-w-6xl flex-col px-6 py-12 lg:px-10">
134
- <div className="inline-flex w-fit items-center rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm tracking-[0.18em] text-stone-300 uppercase">
135
- {appName} v{appVersion} on {platform}
136
- </div>
137
-
138
- <div className="mt-12 grid gap-12 lg:grid-cols-[minmax(0,1.4fr)_minmax(20rem,0.8fr)] lg:items-end">
139
- <section>
140
- <p className="text-sm font-medium uppercase tracking-[0.32em] text-amber-300">
141
- Reset Framework
142
- </p>
143
- <h1 className="mt-5 max-w-4xl text-5xl font-medium leading-[0.95] tracking-[-0.06em] text-white sm:text-6xl lg:text-7xl">
144
- Tailwind-ready desktop apps with a native runtime underneath.
145
- </h1>
146
- <p className="mt-6 max-w-2xl text-lg leading-8 text-stone-300">
147
- Edit the frontend, call native commands through the runtime bridge, and keep the
148
- host layer out of the way while you build the actual product.
149
- </p>
150
- </section>
151
-
152
- <section className="rounded-[2rem] border border-white/10 bg-white/5 p-6 shadow-2xl shadow-black/20 backdrop-blur">
153
- <div className="flex items-center justify-between text-sm text-stone-400">
154
- <span>Runtime bridge</span>
155
- <span>bridge v{bridgeVersion}</span>
156
- </div>
157
- <div className="mt-6 rounded-2xl border border-white/10 bg-black/30 p-5">
158
- <div className="text-xs uppercase tracking-[0.24em] text-stone-500">
159
- Counter
160
- </div>
161
- <div className="mt-4 text-4xl font-semibold text-white">{count}</div>
162
- <button
163
- className="mt-6 inline-flex items-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-stone-950 transition hover:bg-amber-200"
164
- onClick={() => setCount((value) => value + 1)}
165
- >
166
- Increment
167
- </button>
168
- </div>
169
- </section>
170
- </div>
171
- </div>
172
- </main>
173
- )
174
- }
175
-
176
- export default App
177
- `
178
-
179
- export const description = "Scaffold a starter app with an interactive project setup"
180
-
181
- function listTemplates(templatesDir) {
182
- return readdirSync(templatesDir, { withFileTypes: true })
183
- .filter((entry) => entry.isDirectory())
184
- .map((entry) => entry.name)
185
- .sort()
186
- }
187
-
188
- function getRootGitignore(frontendDir) {
189
- if (frontendDir === ".") {
190
- return `.DS_Store
191
- Thumbs.db
192
- .idea/
193
- .vs/
194
- .vscode/
195
- .cache/
196
- .reset/
197
- .turbo/
198
- build/
199
- coverage/
200
- dist/
201
- node_modules/
202
- npm-debug.log*
203
- yarn-debug.log*
204
- yarn-error.log*
205
- .pnpm-debug.log*
206
- *.log
207
- *.zip
208
- *.tgz
209
- *.tsbuildinfo
210
- *.dSYM/
211
- .env
212
- .env.*
213
- .env.local
214
- .env.*.local
215
- .eslintcache
216
- bun.lock
217
- bun.lockb
218
- compile_commands.json
219
- CMakeUserPresets.json
220
- CMakeCache.txt
221
- CMakeFiles/
222
- cmake_install.cmake
223
- install_manifest.txt
224
- cmake-build-*/
225
- Testing/
226
- scripts/local/
227
- `
228
- }
229
-
230
- return `.DS_Store
231
- Thumbs.db
232
- .idea/
233
- .vs/
234
- .vscode/
235
- .cache/
236
- .reset/
237
- .turbo/
238
- build/
239
- coverage/
240
- node_modules/
241
- ${frontendDir}/node_modules/
242
- ${frontendDir}/dist/
243
- npm-debug.log*
244
- yarn-debug.log*
245
- yarn-error.log*
246
- .pnpm-debug.log*
247
- *.log
248
- *.zip
249
- *.tgz
250
- *.tsbuildinfo
251
- *.dSYM/
252
- .env
253
- .env.*
254
- .env.local
255
- .env.*.local
256
- .eslintcache
257
- bun.lock
258
- bun.lockb
259
- compile_commands.json
260
- CMakeUserPresets.json
261
- CMakeCache.txt
262
- CMakeFiles/
263
- cmake_install.cmake
264
- install_manifest.txt
265
- cmake-build-*/
266
- Testing/
267
- scripts/local/
268
- `
269
- }
270
-
271
- function resolveDefaultFrontendDir(context) {
272
- if (typeof context.flags["frontend-dir"] === "string") {
273
- return context.flags["frontend-dir"]
274
- }
275
-
276
- if (context.flags.flat) {
277
- return "."
278
- }
279
-
280
- return "frontend"
281
- }
282
-
283
- function resolveDefaultStyling(context) {
284
- if (context.flags.tailwind) {
285
- return "tailwindcss"
286
- }
287
-
288
- if (context.flags["no-tailwind"]) {
289
- return "css"
290
- }
291
-
292
- return "css"
293
- }
294
-
295
- function resolvePackageManagerOption(context) {
296
- if (typeof context.flags["package-manager"] === "string") {
297
- return context.flags["package-manager"]
298
- }
299
-
300
- return "npm"
301
- }
302
-
303
- function describeInstallCommand(packageManager) {
304
- return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
305
- }
306
-
307
- function createFrontendScriptCommand(packageManager, frontendDir, scriptName) {
308
- const frontendPath = JSON.stringify(frontendDir.replace(/\\/g, "/"))
309
-
310
- if (packageManager === "npm") {
311
- return `npm --workspace ${frontendPath} run ${scriptName} --`
312
- }
313
-
314
- if (packageManager === "pnpm") {
315
- return `pnpm --dir ${frontendPath} run ${scriptName} --`
316
- }
317
-
318
- if (packageManager === "yarn") {
319
- return `yarn --cwd ${frontendPath} run ${scriptName}`
320
- }
321
-
322
- if (packageManager === "bun") {
323
- return `bun --cwd ${frontendPath} run ${scriptName}`
324
- }
325
-
326
- throw new Error(`Unsupported package manager: ${packageManager}`)
327
- }
328
-
329
- async function ensureWritableTarget(targetDir, force) {
330
- if (!existsSync(targetDir)) {
331
- return
332
- }
333
-
334
- const entries = await readdir(targetDir)
335
-
336
- if (entries.length > 0 && !force) {
337
- throw new Error(`Target directory is not empty: ${targetDir}. Use --force to continue.`)
338
- }
339
- }
340
-
341
- async function copyFrontendTemplate(sourceFrontendDir, targetFrontendDir, options) {
342
- const { force, frontendDir } = options
343
- const flatLayout = frontendDir === "."
344
-
345
- if (!flatLayout) {
346
- await cp(sourceFrontendDir, targetFrontendDir, {
347
- recursive: true,
348
- force,
349
- filter(source) {
350
- const baseName = path.basename(source)
351
- return (
352
- baseName !== "node_modules" &&
353
- baseName !== "dist" &&
354
- baseName !== "bun.lockb" &&
355
- baseName !== "reset.config.json"
356
- )
357
- }
358
- })
359
-
360
- return
361
- }
362
-
363
- await mkdir(targetFrontendDir, { recursive: true })
364
-
365
- const entries = await readdir(sourceFrontendDir, { withFileTypes: true })
366
- for (const entry of entries) {
367
- if (["node_modules", "dist", "bun.lockb", "reset.config.json", ".gitignore", "README.md"].includes(entry.name)) {
368
- continue
369
- }
370
-
371
- await cp(
372
- path.join(sourceFrontendDir, entry.name),
373
- path.join(targetFrontendDir, entry.name),
374
- { recursive: true, force }
375
- )
376
- }
377
- }
378
-
379
- async function applyFrontendOverrides(frontendDir, options) {
380
- await writeFile(path.join(frontendDir, "src", "lib", "reset.ts"), sdkRuntimeReexport, "utf8")
381
- await writeFile(path.join(frontendDir, "tsconfig.app.json"), standaloneTsconfigApp, "utf8")
382
-
383
- const packageJsonPath = path.join(frontendDir, "package.json")
384
- const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
385
- frontendPackage.dependencies = {
386
- ...frontendPackage.dependencies,
387
- "@reset-framework/sdk": options.sdkDependencySpec
388
- }
389
-
390
- if (options.styling === "tailwindcss") {
391
- frontendPackage.devDependencies = {
392
- ...frontendPackage.devDependencies,
393
- "@tailwindcss/vite": "latest",
394
- tailwindcss: "latest"
395
- }
396
-
397
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
398
- await writeFile(path.join(frontendDir, "vite.config.ts"), tailwindViteConfig, "utf8")
399
- await writeFile(path.join(frontendDir, "src", "index.css"), tailwindIndexCss, "utf8")
400
- await writeFile(path.join(frontendDir, "src", "App.tsx"), tailwindAppTsx, "utf8")
401
- return
402
- }
403
-
404
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
405
- await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
406
- }
407
-
408
- function createRootPackageJson(options) {
409
- const packageManagerVersion = options.packageManagerVersion
410
- ? `${options.packageManager}@${options.packageManagerVersion}`
411
- : undefined
412
-
413
- const base = {
414
- name: options.appName,
415
- private: true,
416
- version: "0.1.0",
417
- description: `${options.productName} desktop app`,
418
- scripts: {
419
- dev: "reset-framework-cli dev",
420
- build: "reset-framework-cli build",
421
- package: "reset-framework-cli package",
422
- doctor: "reset-framework-cli doctor"
423
- },
424
- devDependencies: {
425
- "reset-framework-cli": options.cliDependencySpec
426
- }
427
- }
428
-
429
- if (packageManagerVersion) {
430
- base.packageManager = packageManagerVersion
431
- }
432
-
433
- if (options.frontendDir !== ".") {
434
- base.workspaces = [options.frontendDir]
435
- base.scripts["dev:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "dev")
436
- base.scripts["build:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "build")
437
- base.scripts["lint:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "lint")
438
- base.scripts["preview:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "preview")
439
- }
440
-
441
- return base
442
- }
443
-
444
- function createRootReadme(options) {
445
- const run = `${options.packageManager} run`
446
- const webPath = options.frontendDir === "." ? "project root" : `${options.frontendDir}/`
447
-
448
- return `# ${options.productName}
449
-
450
- Generated with \`reset-framework-cli\`.
451
-
452
- ## Commands
453
-
454
- - \`${run} dev\`: start the frontend dev server and native desktop runtime
455
- - \`${run} build\`: build the frontend and stage the desktop app bundle
456
- - \`${run} package\`: archive the built desktop app
457
- - \`${run} doctor\`: inspect the project and framework installation
458
-
459
- ## Web-only commands
460
-
461
- ${options.frontendDir === "." ? `- \`${run} dev:web\`: run the Vite frontend only
462
- - \`${run} build:web\`: build the frontend only` : `- \`${run} dev:web\`: run the frontend workspace only
463
- - \`${run} build:web\`: build the frontend workspace only`}
464
-
465
- ## Project files
466
-
467
- - \`reset.config.json\`: desktop app metadata and runtime configuration
468
- - \`${webPath}\`: web frontend source
469
- - \`.reset/\`: generated build output and native runtime cache
470
- `
471
- }
472
-
473
- async function writeRootPackageFiles(options) {
474
- await writeFile(
475
- path.join(options.targetDir, "README.md"),
476
- createRootReadme(options),
477
- "utf8"
478
- )
479
-
480
- if (options.frontendDir !== ".") {
481
- await writeFile(
482
- path.join(options.targetDir, "package.json"),
483
- JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
484
- "utf8"
485
- )
486
- }
487
- }
488
-
489
- async function writeProjectFiles(options) {
490
- const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
491
-
492
- await writeFile(
493
- path.join(options.targetDir, "reset.config.json"),
494
- JSON.stringify(
495
- {
496
- ...templateConfig,
497
- name: options.appName,
498
- productName: options.productName,
499
- appId: options.appId,
500
- project: {
501
- frontendDir: options.frontendDir,
502
- styling: options.styling
503
- },
504
- window: {
505
- ...templateConfig.window,
506
- title: options.productName
507
- }
508
- },
509
- null,
510
- 2
511
- ) + "\n",
512
- "utf8"
513
- )
514
-
515
- await writeFile(
516
- path.join(options.targetDir, ".gitignore"),
517
- getRootGitignore(options.frontendDir),
518
- "utf8"
519
- )
520
- }
521
-
522
- async function resolvePackageManagerVersion(packageManager) {
523
- if (packageManager !== "npm" && packageManager !== "pnpm" && packageManager !== "yarn" && packageManager !== "bun") {
524
- return null
525
- }
526
-
527
- try {
528
- const command = resolvePackageManagerCommand(packageManager)
529
- const result = await captureCommandOutput(command, ["--version"])
530
- return typeof result === "string" && result.trim() !== "" ? result.trim() : null
531
- } catch {
532
- return null
533
- }
534
- }
535
-
536
- async function collectCreateAppOptions(context, templates) {
537
- const initialTarget = context.args[0] ?? "my-app"
538
- const defaultTargetDir = path.resolve(context.cwd, initialTarget)
539
- const defaultMetadata = makeAppMetadata(path.basename(defaultTargetDir))
540
- const frameworkPaths = resolveFrameworkPaths()
541
-
542
- const defaults = {
543
- targetDir: defaultTargetDir,
544
- templateName: typeof context.flags.template === "string" ? context.flags.template : "basic",
545
- appName: defaultMetadata.name,
546
- productName:
547
- typeof context.flags["product-name"] === "string"
548
- ? context.flags["product-name"]
549
- : defaultMetadata.productName,
550
- appId:
551
- typeof context.flags["app-id"] === "string"
552
- ? context.flags["app-id"]
553
- : defaultMetadata.appId,
554
- frontendDir: resolveDefaultFrontendDir(context),
555
- styling: resolveDefaultStyling(context)
556
- }
557
-
558
- const interactive = isInteractiveSession() && !context.flags.yes && !context.flags["no-prompt"]
559
-
560
- if (!interactive) {
561
- if (!context.args[0]) {
562
- throw new Error("Missing target directory. Use: reset-framework-cli create-app <directory>")
563
- }
564
-
565
- return {
566
- ...defaults,
567
- templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
568
- templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
569
- sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
570
- cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
571
- sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
572
- packageManager: resolvePackageManagerOption(context),
573
- packageManagerVersion: await resolvePackageManagerVersion(resolvePackageManagerOption(context)),
574
- installDependencies: !context.flags["no-install"],
575
- force: Boolean(context.flags.force)
576
- }
577
- }
578
-
579
- const selected = await withPromptSession(async (rl) => {
580
- const targetInput = await promptText(rl, {
581
- label: "Project directory",
582
- defaultValue: path.basename(defaults.targetDir),
583
- validate(value) {
584
- return value.trim() !== "" || "Enter a directory name."
585
- }
586
- })
587
-
588
- const targetDir = path.resolve(context.cwd, targetInput)
589
- const metadata = makeAppMetadata(path.basename(targetDir))
590
-
591
- const templateName =
592
- templates.length === 1
593
- ? templates[0]
594
- : await promptSelect(rl, {
595
- label: "Template",
596
- defaultValue: defaults.templateName,
597
- choices: templates.map((template) => ({
598
- value: template,
599
- label: template,
600
- description: "Starter project scaffold"
601
- }))
602
- })
603
-
604
- const productName = await promptText(rl, {
605
- label: "Product name",
606
- defaultValue: defaults.productName || metadata.productName,
607
- validate(value) {
608
- return value.trim() !== "" || "Enter a product name."
609
- }
610
- })
611
-
612
- const appId = await promptText(rl, {
613
- label: "App ID",
614
- defaultValue: defaults.appId || metadata.appId,
615
- validate(value) {
616
- return value.includes(".") || "Use a reverse-domain identifier such as com.example.my-app."
617
- }
618
- })
619
-
620
- const frontendDir = await promptSelect(rl, {
621
- label: "Frontend layout",
622
- defaultValue: defaults.frontendDir === "." ? "." : "frontend",
623
- choices: [
624
- {
625
- value: "frontend",
626
- label: "Use a frontend/ subdirectory",
627
- description: "Keeps the web app separate from config, output, and packaging files."
628
- },
629
- {
630
- value: ".",
631
- label: "Keep the frontend at project root",
632
- description: "Flatter project layout with package.json and src/ directly in the app root."
633
- }
634
- ]
635
- })
636
-
637
- const styling = await promptSelect(rl, {
638
- label: "Styling setup",
639
- defaultValue: defaults.styling,
640
- choices: [
641
- {
642
- value: "css",
643
- label: "Vanilla CSS starter",
644
- description: "Use the existing CSS-based starter with no extra styling dependency."
645
- },
646
- {
647
- value: "tailwindcss",
648
- label: "Tailwind CSS starter",
649
- description: "Configure Tailwind CSS and swap the starter screen to utility classes."
650
- }
651
- ]
652
- })
653
-
654
- const installDependencies = await promptConfirm(rl, {
655
- label: "Install frontend dependencies after scaffolding",
656
- defaultValue: true
657
- })
658
-
659
- const packageManager = await promptSelect(rl, {
660
- label: "Package manager",
661
- defaultValue: resolvePackageManagerOption(context),
662
- choices: [
663
- {
664
- value: "npm",
665
- label: "npm",
666
- description: "Wide compatibility and predictable installs."
667
- },
668
- {
669
- value: "bun",
670
- label: "bun",
671
- description: "Fast installs and lets bun run the root desktop scripts."
672
- },
673
- {
674
- value: "pnpm",
675
- label: "pnpm",
676
- description: "Strict dependency graph with workspace support."
677
- },
678
- {
679
- value: "yarn",
680
- label: "yarn",
681
- description: "Classic workspace-based JavaScript package workflow."
682
- }
683
- ]
684
- })
685
-
686
- printSection("Summary")
687
- printKeyValueTable([
688
- ["Directory", targetDir],
689
- ["Template", templateName],
690
- ["Product", productName],
691
- ["App ID", appId],
692
- ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
693
- ["Styling", styling],
694
- ["Package manager", packageManager],
695
- ["Install", installDependencies ? "yes" : "no"]
696
- ])
697
- console.log("")
698
-
699
- const confirmed = await promptConfirm(rl, {
700
- label: "Create this project",
701
- defaultValue: true
702
- })
703
-
704
- if (!confirmed) {
705
- throw new Error("Create app cancelled.")
706
- }
707
-
708
- return {
709
- targetDir,
710
- templateName,
711
- appName: metadata.name,
712
- productName,
713
- appId,
714
- frontendDir,
715
- styling,
716
- installDependencies,
717
- packageManager
718
- }
719
- })
720
-
721
- return {
722
- ...selected,
723
- templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
724
- templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
725
- sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
726
- cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
727
- sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
728
- packageManagerVersion: await resolvePackageManagerVersion(selected.packageManager),
729
- force: Boolean(context.flags.force)
730
- }
731
- }
732
-
733
- export async function run(context) {
734
- const frameworkPaths = resolveFrameworkPaths()
735
- const templates = listTemplates(frameworkPaths.templatesDir)
736
- const formatTargetForShell = (targetDir) => {
737
- const relative = path.relative(context.cwd, targetDir)
738
- if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
739
- return relative || "."
740
- }
741
-
742
- return targetDir
743
- }
744
-
745
- printBanner("reset-framework-cli create-app", description)
746
- const options = await collectCreateAppOptions(context, templates)
747
-
748
- if (!templates.includes(options.templateName)) {
749
- throw new Error(`Unknown template: ${options.templateName}`)
750
- }
751
-
752
- const targetFrontendDir =
753
- options.frontendDir === "."
754
- ? options.targetDir
755
- : path.join(options.targetDir, options.frontendDir)
756
-
757
- await ensureWritableTarget(options.targetDir, options.force)
758
-
759
- printSection("Project")
760
- printKeyValueTable([
761
- ["Directory", options.targetDir],
762
- ["Template", options.templateName],
763
- ["Product", options.productName],
764
- ["App ID", options.appId],
765
- ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
766
- ["Styling", options.styling],
767
- ["Package manager", options.packageManager],
768
- ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
769
- ])
770
-
771
- if (Boolean(context.flags["dry-run"])) {
772
- console.log("")
773
- printSection("Dry run")
774
- printStatusTable([
775
- ["plan", "Copy starter", `${options.sourceFrontendDir} -> ${targetFrontendDir}`],
776
- ["plan", "Write README", path.join(options.targetDir, "README.md")],
777
- ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
778
- ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
779
- ["plan", "Write app package", path.join(options.targetDir, "package.json")],
780
- ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
781
- ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
782
- ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
783
- ])
784
- if (options.styling === "tailwindcss") {
785
- printStatusTable([
786
- ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
787
- ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
788
- ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
789
- ])
790
- }
791
- if (options.installDependencies) {
792
- printStatusTable([
793
- ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${options.targetDir}`]
794
- ])
795
- }
796
- return
797
- }
798
-
799
- console.log("")
800
- const progress = createProgress(options.installDependencies ? 6 : 5, "Scaffold")
801
-
802
- await mkdir(options.targetDir, { recursive: true })
803
- progress.tick("Prepared project directory")
804
-
805
- await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
806
- progress.tick("Copied frontend starter")
807
-
808
- await writeProjectFiles(options)
809
- await writeRootPackageFiles(options)
810
- progress.tick("Wrote project files")
811
-
812
- await applyFrontendOverrides(targetFrontendDir, options)
813
- progress.tick("Applied frontend package wiring")
814
-
815
- const packageJsonPath = path.join(targetFrontendDir, "package.json")
816
- const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
817
-
818
- if (options.frontendDir === ".") {
819
- const currentScripts = { ...(frontendPackage.scripts ?? {}) }
820
- frontendPackage.name = options.appName
821
- frontendPackage.private = true
822
- frontendPackage.version = "0.1.0"
823
- frontendPackage.description = `${options.productName} desktop app`
824
- frontendPackage.devDependencies = {
825
- ...frontendPackage.devDependencies,
826
- "reset-framework-cli": options.cliDependencySpec
827
- }
828
- frontendPackage.scripts = {
829
- dev: "reset-framework-cli dev",
830
- "dev:web": currentScripts.dev ?? "vite",
831
- build: "reset-framework-cli build",
832
- "build:web": currentScripts.build ?? "tsc -b && vite build",
833
- package: "reset-framework-cli package",
834
- doctor: "reset-framework-cli doctor",
835
- lint: currentScripts.lint ?? "eslint .",
836
- preview: currentScripts.preview ?? "vite preview"
837
- }
838
-
839
- if (options.packageManagerVersion) {
840
- frontendPackage.packageManager = `${options.packageManager}@${options.packageManagerVersion}`
841
- }
842
- } else {
843
- frontendPackage.name = `${options.appName}-frontend`
844
- }
845
-
846
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
847
- progress.tick("Finalized starter metadata")
848
-
849
- if (options.installDependencies) {
850
- await runCommand(
851
- resolvePackageManagerCommand(options.packageManager),
852
- getInstallCommandArgs(options.packageManager),
853
- {
854
- cwd: options.targetDir
855
- }
856
- )
857
- progress.tick("Installed frontend dependencies")
858
- }
859
-
860
- console.log("")
861
- printSection("Result")
862
- printStatusTable([
863
- ["done", "Project", options.targetDir],
864
- ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && ${options.packageManager} run dev`]
865
- ])
866
- }
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+
5
+ import {
6
+ captureCommandOutput,
7
+ getInstallCommandArgs,
8
+ resolvePackageManagerCommand,
9
+ runCommand
10
+ } from "../lib/process.js"
11
+ import {
12
+ makeAppMetadata,
13
+ resolveCliDependencySpec,
14
+ resolveFrameworkPaths,
15
+ resolveSdkDependencySpec
16
+ } from "../lib/project.js"
17
+ import {
18
+ createProgress,
19
+ isInteractiveSession,
20
+ printBanner,
21
+ printKeyValueTable,
22
+ printSection,
23
+ printStatusTable,
24
+ promptConfirm,
25
+ promptSelect,
26
+ promptText,
27
+ withPromptSession
28
+ } from "../lib/ui.js"
29
+
30
+ const sdkRuntimeReexport = `export * from '@reset-framework/sdk'
31
+ `
32
+
33
+ const standaloneViteConfig = `import { defineConfig } from 'vite'
34
+ import react from '@vitejs/plugin-react'
35
+
36
+ export default defineConfig({
37
+ base: './',
38
+ plugins: [react()],
39
+ })
40
+ `
41
+
42
+ const tailwindViteConfig = `import { defineConfig } from 'vite'
43
+ import react from '@vitejs/plugin-react'
44
+ import tailwindcss from '@tailwindcss/vite'
45
+
46
+ export default defineConfig({
47
+ base: './',
48
+ plugins: [react(), tailwindcss()],
49
+ })
50
+ `
51
+
52
+ const standaloneTsconfigApp = `{
53
+ "compilerOptions": {
54
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
55
+ "target": "ES2023",
56
+ "useDefineForClassFields": true,
57
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
58
+ "module": "ESNext",
59
+ "types": ["vite/client"],
60
+ "skipLibCheck": true,
61
+ "moduleResolution": "bundler",
62
+ "allowImportingTsExtensions": true,
63
+ "verbatimModuleSyntax": true,
64
+ "moduleDetection": "force",
65
+ "noEmit": true,
66
+ "jsx": "react-jsx",
67
+ "strict": true,
68
+ "noUnusedLocals": true,
69
+ "noUnusedParameters": true,
70
+ "erasableSyntaxOnly": true,
71
+ "noFallthroughCasesInSwitch": true,
72
+ "noUncheckedSideEffectImports": true
73
+ },
74
+ "include": ["src"]
75
+ }
76
+ `
77
+
78
+ const tailwindIndexCss = `@import "tailwindcss";
79
+
80
+ :root {
81
+ color-scheme: dark;
82
+ }
83
+
84
+ body {
85
+ margin: 0;
86
+ font-family: "Avenir Next", "Segoe UI", sans-serif;
87
+ background: #111111;
88
+ }
89
+ `
90
+
91
+ const tailwindAppTsx = `import { useEffect, useState } from 'react'
92
+ import { reset } from './lib/reset'
93
+
94
+ function App() {
95
+ const [count, setCount] = useState(0)
96
+ const [appName, setAppName] = useState('Reset App')
97
+ const [appVersion, setAppVersion] = useState('0.0.0')
98
+ const [platform, setPlatform] = useState('unknown')
99
+ const [bridgeVersion, setBridgeVersion] = useState('1')
100
+
101
+ useEffect(() => {
102
+ let cancelled = false
103
+
104
+ async function loadRuntimeInfo() {
105
+ try {
106
+ const [appInfo, runtimeInfo] = await Promise.all([
107
+ reset.app.getInfo(),
108
+ reset.runtime.getInfo(),
109
+ ])
110
+
111
+ if (cancelled) {
112
+ return
113
+ }
114
+
115
+ setAppName(appInfo.name)
116
+ setAppVersion(appInfo.version)
117
+ setPlatform(runtimeInfo.platform)
118
+ setBridgeVersion(runtimeInfo.bridgeVersion)
119
+ } catch (error) {
120
+ console.error('Failed to load runtime info', error)
121
+ }
122
+ }
123
+
124
+ loadRuntimeInfo()
125
+
126
+ return () => {
127
+ cancelled = true
128
+ }
129
+ }, [])
130
+
131
+ return (
132
+ <main className="min-h-screen bg-stone-950 text-stone-100">
133
+ <div className="mx-auto flex min-h-screen max-w-6xl flex-col px-6 py-12 lg:px-10">
134
+ <div className="inline-flex w-fit items-center rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm tracking-[0.18em] text-stone-300 uppercase">
135
+ {appName} v{appVersion} on {platform}
136
+ </div>
137
+
138
+ <div className="mt-12 grid gap-12 lg:grid-cols-[minmax(0,1.4fr)_minmax(20rem,0.8fr)] lg:items-end">
139
+ <section>
140
+ <p className="text-sm font-medium uppercase tracking-[0.32em] text-amber-300">
141
+ Reset Framework
142
+ </p>
143
+ <h1 className="mt-5 max-w-4xl text-5xl font-medium leading-[0.95] tracking-[-0.06em] text-white sm:text-6xl lg:text-7xl">
144
+ Tailwind-ready desktop apps with a native runtime underneath.
145
+ </h1>
146
+ <p className="mt-6 max-w-2xl text-lg leading-8 text-stone-300">
147
+ Edit the frontend, call native commands through the runtime bridge, and keep the
148
+ host layer out of the way while you build the actual product.
149
+ </p>
150
+ </section>
151
+
152
+ <section className="rounded-[2rem] border border-white/10 bg-white/5 p-6 shadow-2xl shadow-black/20 backdrop-blur">
153
+ <div className="flex items-center justify-between text-sm text-stone-400">
154
+ <span>Runtime bridge</span>
155
+ <span>bridge v{bridgeVersion}</span>
156
+ </div>
157
+ <div className="mt-6 rounded-2xl border border-white/10 bg-black/30 p-5">
158
+ <div className="text-xs uppercase tracking-[0.24em] text-stone-500">
159
+ Counter
160
+ </div>
161
+ <div className="mt-4 text-4xl font-semibold text-white">{count}</div>
162
+ <button
163
+ className="mt-6 inline-flex items-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-stone-950 transition hover:bg-amber-200"
164
+ onClick={() => setCount((value) => value + 1)}
165
+ >
166
+ Increment
167
+ </button>
168
+ </div>
169
+ </section>
170
+ </div>
171
+ </div>
172
+ </main>
173
+ )
174
+ }
175
+
176
+ export default App
177
+ `
178
+
179
+ export const description = "Scaffold a starter app with an interactive project setup"
180
+
181
+ function listTemplates(templatesDir) {
182
+ return readdirSync(templatesDir, { withFileTypes: true })
183
+ .filter((entry) => entry.isDirectory())
184
+ .map((entry) => entry.name)
185
+ .sort()
186
+ }
187
+
188
+ function getRootGitignore(frontendDir) {
189
+ if (frontendDir === ".") {
190
+ return `.DS_Store
191
+ Thumbs.db
192
+ .idea/
193
+ .vs/
194
+ .vscode/
195
+ .cache/
196
+ .reset/
197
+ .turbo/
198
+ build/
199
+ coverage/
200
+ dist/
201
+ node_modules/
202
+ npm-debug.log*
203
+ yarn-debug.log*
204
+ yarn-error.log*
205
+ .pnpm-debug.log*
206
+ *.log
207
+ *.zip
208
+ *.tgz
209
+ *.tsbuildinfo
210
+ *.dSYM/
211
+ .env
212
+ .env.*
213
+ .env.local
214
+ .env.*.local
215
+ .eslintcache
216
+ bun.lock
217
+ bun.lockb
218
+ compile_commands.json
219
+ CMakeUserPresets.json
220
+ CMakeCache.txt
221
+ CMakeFiles/
222
+ cmake_install.cmake
223
+ install_manifest.txt
224
+ cmake-build-*/
225
+ Testing/
226
+ scripts/local/
227
+ `
228
+ }
229
+
230
+ return `.DS_Store
231
+ Thumbs.db
232
+ .idea/
233
+ .vs/
234
+ .vscode/
235
+ .cache/
236
+ .reset/
237
+ .turbo/
238
+ build/
239
+ coverage/
240
+ node_modules/
241
+ ${frontendDir}/node_modules/
242
+ ${frontendDir}/dist/
243
+ npm-debug.log*
244
+ yarn-debug.log*
245
+ yarn-error.log*
246
+ .pnpm-debug.log*
247
+ *.log
248
+ *.zip
249
+ *.tgz
250
+ *.tsbuildinfo
251
+ *.dSYM/
252
+ .env
253
+ .env.*
254
+ .env.local
255
+ .env.*.local
256
+ .eslintcache
257
+ bun.lock
258
+ bun.lockb
259
+ compile_commands.json
260
+ CMakeUserPresets.json
261
+ CMakeCache.txt
262
+ CMakeFiles/
263
+ cmake_install.cmake
264
+ install_manifest.txt
265
+ cmake-build-*/
266
+ Testing/
267
+ scripts/local/
268
+ `
269
+ }
270
+
271
+ function resolveDefaultFrontendDir(context) {
272
+ if (typeof context.flags["frontend-dir"] === "string") {
273
+ return context.flags["frontend-dir"]
274
+ }
275
+
276
+ if (context.flags.flat) {
277
+ return "."
278
+ }
279
+
280
+ return "frontend"
281
+ }
282
+
283
+ function resolveDefaultStyling(context) {
284
+ if (context.flags.tailwind) {
285
+ return "tailwindcss"
286
+ }
287
+
288
+ if (context.flags["no-tailwind"]) {
289
+ return "css"
290
+ }
291
+
292
+ return "css"
293
+ }
294
+
295
+ function resolvePackageManagerOption(context) {
296
+ if (typeof context.flags["package-manager"] === "string") {
297
+ return context.flags["package-manager"]
298
+ }
299
+
300
+ return "npm"
301
+ }
302
+
303
+ function describeInstallCommand(packageManager) {
304
+ return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
305
+ }
306
+
307
+ function createFrontendScriptCommand(packageManager, frontendDir, scriptName) {
308
+ const frontendPath = JSON.stringify(frontendDir.replace(/\\/g, "/"))
309
+
310
+ if (packageManager === "npm") {
311
+ return `npm --workspace ${frontendPath} run ${scriptName} --`
312
+ }
313
+
314
+ if (packageManager === "pnpm") {
315
+ return `pnpm --dir ${frontendPath} run ${scriptName} --`
316
+ }
317
+
318
+ if (packageManager === "yarn") {
319
+ return `yarn --cwd ${frontendPath} run ${scriptName}`
320
+ }
321
+
322
+ if (packageManager === "bun") {
323
+ return `bun --cwd ${frontendPath} run ${scriptName}`
324
+ }
325
+
326
+ throw new Error(`Unsupported package manager: ${packageManager}`)
327
+ }
328
+
329
+ async function ensureWritableTarget(targetDir, force) {
330
+ if (!existsSync(targetDir)) {
331
+ return
332
+ }
333
+
334
+ const entries = await readdir(targetDir)
335
+
336
+ if (entries.length > 0 && !force) {
337
+ throw new Error(`Target directory is not empty: ${targetDir}. Use --force to continue.`)
338
+ }
339
+ }
340
+
341
+ async function copyFrontendTemplate(sourceFrontendDir, targetFrontendDir, options) {
342
+ const { force, frontendDir } = options
343
+ const flatLayout = frontendDir === "."
344
+
345
+ if (!flatLayout) {
346
+ await cp(sourceFrontendDir, targetFrontendDir, {
347
+ recursive: true,
348
+ force,
349
+ filter(source) {
350
+ const baseName = path.basename(source)
351
+ return (
352
+ baseName !== "node_modules" &&
353
+ baseName !== "dist" &&
354
+ baseName !== "bun.lockb" &&
355
+ baseName !== "reset.config.json"
356
+ )
357
+ }
358
+ })
359
+
360
+ return
361
+ }
362
+
363
+ await mkdir(targetFrontendDir, { recursive: true })
364
+
365
+ const entries = await readdir(sourceFrontendDir, { withFileTypes: true })
366
+ for (const entry of entries) {
367
+ if (["node_modules", "dist", "bun.lockb", "reset.config.json", ".gitignore", "README.md"].includes(entry.name)) {
368
+ continue
369
+ }
370
+
371
+ await cp(
372
+ path.join(sourceFrontendDir, entry.name),
373
+ path.join(targetFrontendDir, entry.name),
374
+ { recursive: true, force }
375
+ )
376
+ }
377
+ }
378
+
379
+ async function applyFrontendOverrides(frontendDir, options) {
380
+ await writeFile(path.join(frontendDir, "src", "lib", "reset.ts"), sdkRuntimeReexport, "utf8")
381
+ await writeFile(path.join(frontendDir, "tsconfig.app.json"), standaloneTsconfigApp, "utf8")
382
+
383
+ const packageJsonPath = path.join(frontendDir, "package.json")
384
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
385
+ frontendPackage.dependencies = {
386
+ ...frontendPackage.dependencies,
387
+ "@reset-framework/sdk": options.sdkDependencySpec
388
+ }
389
+
390
+ if (options.styling === "tailwindcss") {
391
+ frontendPackage.devDependencies = {
392
+ ...frontendPackage.devDependencies,
393
+ "@tailwindcss/vite": "latest",
394
+ tailwindcss: "latest"
395
+ }
396
+
397
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
398
+ await writeFile(path.join(frontendDir, "vite.config.ts"), tailwindViteConfig, "utf8")
399
+ await writeFile(path.join(frontendDir, "src", "index.css"), tailwindIndexCss, "utf8")
400
+ await writeFile(path.join(frontendDir, "src", "App.tsx"), tailwindAppTsx, "utf8")
401
+ return
402
+ }
403
+
404
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
405
+ await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
406
+ }
407
+
408
+ function createRootPackageJson(options) {
409
+ const packageManagerVersion = options.packageManagerVersion
410
+ ? `${options.packageManager}@${options.packageManagerVersion}`
411
+ : undefined
412
+
413
+ const base = {
414
+ name: options.appName,
415
+ private: true,
416
+ version: "0.1.0",
417
+ description: `${options.productName} desktop app`,
418
+ scripts: {
419
+ dev: "reset-framework-cli dev",
420
+ build: "reset-framework-cli build",
421
+ package: "reset-framework-cli package",
422
+ doctor: "reset-framework-cli doctor"
423
+ },
424
+ devDependencies: {
425
+ "reset-framework-cli": options.cliDependencySpec
426
+ }
427
+ }
428
+
429
+ if (packageManagerVersion) {
430
+ base.packageManager = packageManagerVersion
431
+ }
432
+
433
+ if (options.frontendDir !== ".") {
434
+ base.workspaces = [options.frontendDir]
435
+ base.scripts["dev:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "dev")
436
+ base.scripts["build:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "build")
437
+ base.scripts["lint:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "lint")
438
+ base.scripts["preview:web"] = createFrontendScriptCommand(options.packageManager, options.frontendDir, "preview")
439
+ }
440
+
441
+ return base
442
+ }
443
+
444
+ function createRootReadme(options) {
445
+ const run = `${options.packageManager} run`
446
+ const webPath = options.frontendDir === "." ? "project root" : `${options.frontendDir}/`
447
+
448
+ return `# ${options.productName}
449
+
450
+ Generated with \`reset-framework-cli\`.
451
+
452
+ ## Commands
453
+
454
+ - \`${run} dev\`: start the frontend dev server and native desktop runtime
455
+ - \`${run} build\`: build the frontend and stage the desktop app bundle
456
+ - \`${run} package\`: archive the built desktop app
457
+ - \`${run} doctor\`: inspect the project and framework installation
458
+
459
+ ## Web-only commands
460
+
461
+ ${options.frontendDir === "." ? `- \`${run} dev:web\`: run the Vite frontend only
462
+ - \`${run} build:web\`: build the frontend only` : `- \`${run} dev:web\`: run the frontend workspace only
463
+ - \`${run} build:web\`: build the frontend workspace only`}
464
+
465
+ ## Project files
466
+
467
+ - \`reset.config.json\`: desktop app metadata and runtime configuration
468
+ - \`${webPath}\`: web frontend source
469
+ - \`.reset/\`: generated build output and native runtime cache
470
+ `
471
+ }
472
+
473
+ async function writeRootPackageFiles(options) {
474
+ await writeFile(
475
+ path.join(options.targetDir, "README.md"),
476
+ createRootReadme(options),
477
+ "utf8"
478
+ )
479
+
480
+ if (options.frontendDir !== ".") {
481
+ await writeFile(
482
+ path.join(options.targetDir, "package.json"),
483
+ JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
484
+ "utf8"
485
+ )
486
+ }
487
+ }
488
+
489
+ async function writeProjectFiles(options) {
490
+ const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
491
+
492
+ await writeFile(
493
+ path.join(options.targetDir, "reset.config.json"),
494
+ JSON.stringify(
495
+ {
496
+ ...templateConfig,
497
+ name: options.appName,
498
+ productName: options.productName,
499
+ appId: options.appId,
500
+ project: {
501
+ frontendDir: options.frontendDir,
502
+ styling: options.styling
503
+ },
504
+ window: {
505
+ ...templateConfig.window,
506
+ title: options.productName
507
+ }
508
+ },
509
+ null,
510
+ 2
511
+ ) + "\n",
512
+ "utf8"
513
+ )
514
+
515
+ await writeFile(
516
+ path.join(options.targetDir, ".gitignore"),
517
+ getRootGitignore(options.frontendDir),
518
+ "utf8"
519
+ )
520
+ }
521
+
522
+ async function resolvePackageManagerVersion(packageManager) {
523
+ if (packageManager !== "npm" && packageManager !== "pnpm" && packageManager !== "yarn" && packageManager !== "bun") {
524
+ return null
525
+ }
526
+
527
+ try {
528
+ const command = resolvePackageManagerCommand(packageManager)
529
+ const result = await captureCommandOutput(command, ["--version"])
530
+ return typeof result === "string" && result.trim() !== "" ? result.trim() : null
531
+ } catch {
532
+ return null
533
+ }
534
+ }
535
+
536
+ async function collectCreateAppOptions(context, templates) {
537
+ const initialTarget = context.args[0] ?? "my-app"
538
+ const defaultTargetDir = path.resolve(context.cwd, initialTarget)
539
+ const defaultMetadata = makeAppMetadata(path.basename(defaultTargetDir))
540
+ const frameworkPaths = resolveFrameworkPaths()
541
+
542
+ const defaults = {
543
+ targetDir: defaultTargetDir,
544
+ templateName: typeof context.flags.template === "string" ? context.flags.template : "basic",
545
+ appName: defaultMetadata.name,
546
+ productName:
547
+ typeof context.flags["product-name"] === "string"
548
+ ? context.flags["product-name"]
549
+ : defaultMetadata.productName,
550
+ appId:
551
+ typeof context.flags["app-id"] === "string"
552
+ ? context.flags["app-id"]
553
+ : defaultMetadata.appId,
554
+ frontendDir: resolveDefaultFrontendDir(context),
555
+ styling: resolveDefaultStyling(context)
556
+ }
557
+
558
+ const interactive = isInteractiveSession() && !context.flags.yes && !context.flags["no-prompt"]
559
+
560
+ if (!interactive) {
561
+ if (!context.args[0]) {
562
+ throw new Error("Missing target directory. Use: reset-framework-cli create-app <directory>")
563
+ }
564
+
565
+ return {
566
+ ...defaults,
567
+ templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
568
+ templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
569
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
570
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
571
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
572
+ packageManager: resolvePackageManagerOption(context),
573
+ packageManagerVersion: await resolvePackageManagerVersion(resolvePackageManagerOption(context)),
574
+ installDependencies: !context.flags["no-install"],
575
+ force: Boolean(context.flags.force)
576
+ }
577
+ }
578
+
579
+ const selected = await withPromptSession(async (rl) => {
580
+ const targetInput = await promptText(rl, {
581
+ label: "Project directory",
582
+ defaultValue: path.basename(defaults.targetDir),
583
+ validate(value) {
584
+ return value.trim() !== "" || "Enter a directory name."
585
+ }
586
+ })
587
+
588
+ const targetDir = path.resolve(context.cwd, targetInput)
589
+ const metadata = makeAppMetadata(path.basename(targetDir))
590
+
591
+ const templateName =
592
+ templates.length === 1
593
+ ? templates[0]
594
+ : await promptSelect(rl, {
595
+ label: "Template",
596
+ defaultValue: defaults.templateName,
597
+ choices: templates.map((template) => ({
598
+ value: template,
599
+ label: template,
600
+ description: "Starter project scaffold"
601
+ }))
602
+ })
603
+
604
+ const productName = await promptText(rl, {
605
+ label: "Product name",
606
+ defaultValue: defaults.productName || metadata.productName,
607
+ validate(value) {
608
+ return value.trim() !== "" || "Enter a product name."
609
+ }
610
+ })
611
+
612
+ const appId = await promptText(rl, {
613
+ label: "App ID",
614
+ defaultValue: defaults.appId || metadata.appId,
615
+ validate(value) {
616
+ return value.includes(".") || "Use a reverse-domain identifier such as com.example.my-app."
617
+ }
618
+ })
619
+
620
+ const frontendDir = await promptSelect(rl, {
621
+ label: "Frontend layout",
622
+ defaultValue: defaults.frontendDir === "." ? "." : "frontend",
623
+ choices: [
624
+ {
625
+ value: "frontend",
626
+ label: "Use a frontend/ subdirectory",
627
+ description: "Keeps the web app separate from config, output, and packaging files."
628
+ },
629
+ {
630
+ value: ".",
631
+ label: "Keep the frontend at project root",
632
+ description: "Flatter project layout with package.json and src/ directly in the app root."
633
+ }
634
+ ]
635
+ })
636
+
637
+ const styling = await promptSelect(rl, {
638
+ label: "Styling setup",
639
+ defaultValue: defaults.styling,
640
+ choices: [
641
+ {
642
+ value: "css",
643
+ label: "Vanilla CSS starter",
644
+ description: "Use the existing CSS-based starter with no extra styling dependency."
645
+ },
646
+ {
647
+ value: "tailwindcss",
648
+ label: "Tailwind CSS starter",
649
+ description: "Configure Tailwind CSS and swap the starter screen to utility classes."
650
+ }
651
+ ]
652
+ })
653
+
654
+ const installDependencies = await promptConfirm(rl, {
655
+ label: "Install frontend dependencies after scaffolding",
656
+ defaultValue: true
657
+ })
658
+
659
+ const packageManager = await promptSelect(rl, {
660
+ label: "Package manager",
661
+ defaultValue: resolvePackageManagerOption(context),
662
+ choices: [
663
+ {
664
+ value: "npm",
665
+ label: "npm",
666
+ description: "Wide compatibility and predictable installs."
667
+ },
668
+ {
669
+ value: "bun",
670
+ label: "bun",
671
+ description: "Fast installs and lets bun run the root desktop scripts."
672
+ },
673
+ {
674
+ value: "pnpm",
675
+ label: "pnpm",
676
+ description: "Strict dependency graph with workspace support."
677
+ },
678
+ {
679
+ value: "yarn",
680
+ label: "yarn",
681
+ description: "Classic workspace-based JavaScript package workflow."
682
+ }
683
+ ]
684
+ })
685
+
686
+ printSection("Summary")
687
+ printKeyValueTable([
688
+ ["Directory", targetDir],
689
+ ["Template", templateName],
690
+ ["Product", productName],
691
+ ["App ID", appId],
692
+ ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
693
+ ["Styling", styling],
694
+ ["Package manager", packageManager],
695
+ ["Install", installDependencies ? "yes" : "no"]
696
+ ])
697
+ console.log("")
698
+
699
+ const confirmed = await promptConfirm(rl, {
700
+ label: "Create this project",
701
+ defaultValue: true
702
+ })
703
+
704
+ if (!confirmed) {
705
+ throw new Error("Create app cancelled.")
706
+ }
707
+
708
+ return {
709
+ targetDir,
710
+ templateName,
711
+ appName: metadata.name,
712
+ productName,
713
+ appId,
714
+ frontendDir,
715
+ styling,
716
+ installDependencies,
717
+ packageManager
718
+ }
719
+ })
720
+
721
+ return {
722
+ ...selected,
723
+ templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
724
+ templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
725
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
726
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
727
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
728
+ packageManagerVersion: await resolvePackageManagerVersion(selected.packageManager),
729
+ force: Boolean(context.flags.force)
730
+ }
731
+ }
732
+
733
+ export async function run(context) {
734
+ const frameworkPaths = resolveFrameworkPaths()
735
+ const templates = listTemplates(frameworkPaths.templatesDir)
736
+ const formatTargetForShell = (targetDir) => {
737
+ const relative = path.relative(context.cwd, targetDir)
738
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
739
+ return relative || "."
740
+ }
741
+
742
+ return targetDir
743
+ }
744
+
745
+ printBanner("reset-framework-cli create-app", description)
746
+ const options = await collectCreateAppOptions(context, templates)
747
+
748
+ if (!templates.includes(options.templateName)) {
749
+ throw new Error(`Unknown template: ${options.templateName}`)
750
+ }
751
+
752
+ const targetFrontendDir =
753
+ options.frontendDir === "."
754
+ ? options.targetDir
755
+ : path.join(options.targetDir, options.frontendDir)
756
+
757
+ await ensureWritableTarget(options.targetDir, options.force)
758
+
759
+ printSection("Project")
760
+ printKeyValueTable([
761
+ ["Directory", options.targetDir],
762
+ ["Template", options.templateName],
763
+ ["Product", options.productName],
764
+ ["App ID", options.appId],
765
+ ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
766
+ ["Styling", options.styling],
767
+ ["Package manager", options.packageManager],
768
+ ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
769
+ ])
770
+
771
+ if (Boolean(context.flags["dry-run"])) {
772
+ console.log("")
773
+ printSection("Dry run")
774
+ printStatusTable([
775
+ ["plan", "Copy starter", `${options.sourceFrontendDir} -> ${targetFrontendDir}`],
776
+ ["plan", "Write README", path.join(options.targetDir, "README.md")],
777
+ ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
778
+ ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
779
+ ["plan", "Write app package", path.join(options.targetDir, "package.json")],
780
+ ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
781
+ ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
782
+ ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
783
+ ])
784
+ if (options.styling === "tailwindcss") {
785
+ printStatusTable([
786
+ ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
787
+ ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
788
+ ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
789
+ ])
790
+ }
791
+ if (options.installDependencies) {
792
+ printStatusTable([
793
+ ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${options.targetDir}`]
794
+ ])
795
+ }
796
+ return
797
+ }
798
+
799
+ console.log("")
800
+ const progress = createProgress(options.installDependencies ? 6 : 5, "Scaffold")
801
+
802
+ await mkdir(options.targetDir, { recursive: true })
803
+ progress.tick("Prepared project directory")
804
+
805
+ await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
806
+ progress.tick("Copied frontend starter")
807
+
808
+ await writeProjectFiles(options)
809
+ await writeRootPackageFiles(options)
810
+ progress.tick("Wrote project files")
811
+
812
+ await applyFrontendOverrides(targetFrontendDir, options)
813
+ progress.tick("Applied frontend package wiring")
814
+
815
+ const packageJsonPath = path.join(targetFrontendDir, "package.json")
816
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
817
+
818
+ if (options.frontendDir === ".") {
819
+ const currentScripts = { ...(frontendPackage.scripts ?? {}) }
820
+ frontendPackage.name = options.appName
821
+ frontendPackage.private = true
822
+ frontendPackage.version = "0.1.0"
823
+ frontendPackage.description = `${options.productName} desktop app`
824
+ frontendPackage.devDependencies = {
825
+ ...frontendPackage.devDependencies,
826
+ "reset-framework-cli": options.cliDependencySpec
827
+ }
828
+ frontendPackage.scripts = {
829
+ dev: "reset-framework-cli dev",
830
+ "dev:web": currentScripts.dev ?? "vite",
831
+ build: "reset-framework-cli build",
832
+ "build:web": currentScripts.build ?? "tsc -b && vite build",
833
+ package: "reset-framework-cli package",
834
+ doctor: "reset-framework-cli doctor",
835
+ lint: currentScripts.lint ?? "eslint .",
836
+ preview: currentScripts.preview ?? "vite preview"
837
+ }
838
+
839
+ if (options.packageManagerVersion) {
840
+ frontendPackage.packageManager = `${options.packageManager}@${options.packageManagerVersion}`
841
+ }
842
+ } else {
843
+ frontendPackage.name = `${options.appName}-frontend`
844
+ }
845
+
846
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
847
+ progress.tick("Finalized starter metadata")
848
+
849
+ if (options.installDependencies) {
850
+ await runCommand(
851
+ resolvePackageManagerCommand(options.packageManager),
852
+ getInstallCommandArgs(options.packageManager),
853
+ {
854
+ cwd: options.targetDir
855
+ }
856
+ )
857
+ progress.tick("Installed frontend dependencies")
858
+ }
859
+
860
+ console.log("")
861
+ printSection("Result")
862
+ printStatusTable([
863
+ ["done", "Project", options.targetDir],
864
+ ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && ${options.packageManager} run dev`]
865
+ ])
866
+ }