reset-framework-cli 1.2.2 → 1.2.4

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