reset-framework-cli 1.1.1 → 1.1.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 (34) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +47 -47
  3. package/package.json +4 -4
  4. package/src/commands/build.js +114 -113
  5. package/src/commands/dev.js +157 -153
  6. package/src/commands/doctor.js +89 -89
  7. package/src/commands/init.js +925 -920
  8. package/src/commands/package.js +49 -44
  9. package/src/index.js +213 -213
  10. package/src/lib/context.js +66 -66
  11. package/src/lib/framework.js +150 -28
  12. package/src/lib/logger.js +11 -11
  13. package/src/lib/output.js +214 -106
  14. package/src/lib/process.js +188 -181
  15. package/src/lib/project.js +559 -475
  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 -138
  25. package/templates/basic/frontend/src/App.tsx +77 -76
  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,931 +1,936 @@
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
- const rootFrontendRunner = `import { readFileSync } from 'node:fs'
180
- import path from 'node:path'
181
- import { spawn } from 'node:child_process'
182
-
183
- const script = process.argv[2]
184
- if (!script) {
185
- throw new Error('Missing frontend script name.')
186
- }
187
-
188
- const appRoot = process.cwd()
189
- const config = JSON.parse(readFileSync(path.join(appRoot, 'reset.config.json'), 'utf8'))
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
+ const rootFrontendRunner = `import { readFileSync } from 'node:fs'
180
+ import path from 'node:path'
181
+ import { spawn } from 'node:child_process'
182
+
183
+ const script = process.argv[2]
184
+ if (!script) {
185
+ throw new Error('Missing frontend script name.')
186
+ }
187
+
188
+ const appRoot = process.cwd()
189
+ const config = JSON.parse(readFileSync(path.join(appRoot, 'reset.config.json'), 'utf8'))
190
190
  const frontendDir =
191
191
  typeof config?.project?.frontendDir === 'string' && config.project.frontendDir.trim() !== ''
192
192
  ? path.resolve(appRoot, config.project.frontendDir)
193
193
  : path.join(appRoot, 'frontend')
194
194
 
195
- function resolvePackageManagerCommand() {
196
- const packageJsonPath = path.join(appRoot, 'package.json')
197
- const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
198
- const packageManager = typeof manifest.packageManager === 'string' ? manifest.packageManager.trim() : ''
199
- const name = packageManager.split('@')[0]
200
-
201
- if (name === 'bun') {
202
- return process.platform === 'win32' ? 'bun.exe' : 'bun'
203
- }
204
-
205
- if (name === 'pnpm') {
206
- return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
207
- }
208
-
209
- if (name === 'yarn') {
210
- return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
211
- }
212
-
213
- if (name === 'npm') {
214
- return process.platform === 'win32' ? 'npm.cmd' : 'npm'
215
- }
216
-
217
- const candidates = [
218
- ['bun.lock', process.platform === 'win32' ? 'bun.exe' : 'bun'],
219
- ['bun.lockb', process.platform === 'win32' ? 'bun.exe' : 'bun'],
220
- ['pnpm-lock.yaml', process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'],
221
- ['yarn.lock', process.platform === 'win32' ? 'yarn.cmd' : 'yarn'],
222
- ['package-lock.json', process.platform === 'win32' ? 'npm.cmd' : 'npm']
223
- ]
224
-
225
- for (const [fileName, command] of candidates) {
226
- try {
227
- readFileSync(path.join(appRoot, fileName), 'utf8')
228
- return command
229
- } catch {}
230
- }
231
-
232
- return process.platform === 'win32' ? 'npm.cmd' : 'npm'
195
+ function shouldUseShell(command) {
196
+ return process.platform === 'win32' && typeof command === 'string' && /\\.(cmd|bat)$/i.test(command)
233
197
  }
234
-
198
+
199
+ function resolvePackageManagerCommand() {
200
+ const packageJsonPath = path.join(appRoot, 'package.json')
201
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
202
+ const packageManager = typeof manifest.packageManager === 'string' ? manifest.packageManager.trim() : ''
203
+ const name = packageManager.split('@')[0]
204
+
205
+ if (name === 'bun') {
206
+ return process.platform === 'win32' ? 'bun.exe' : 'bun'
207
+ }
208
+
209
+ if (name === 'pnpm') {
210
+ return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
211
+ }
212
+
213
+ if (name === 'yarn') {
214
+ return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
215
+ }
216
+
217
+ if (name === 'npm') {
218
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
219
+ }
220
+
221
+ const candidates = [
222
+ ['bun.lock', process.platform === 'win32' ? 'bun.exe' : 'bun'],
223
+ ['bun.lockb', process.platform === 'win32' ? 'bun.exe' : 'bun'],
224
+ ['pnpm-lock.yaml', process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'],
225
+ ['yarn.lock', process.platform === 'win32' ? 'yarn.cmd' : 'yarn'],
226
+ ['package-lock.json', process.platform === 'win32' ? 'npm.cmd' : 'npm']
227
+ ]
228
+
229
+ for (const [fileName, command] of candidates) {
230
+ try {
231
+ readFileSync(path.join(appRoot, fileName), 'utf8')
232
+ return command
233
+ } catch {}
234
+ }
235
+
236
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
237
+ }
238
+
235
239
  const command = resolvePackageManagerCommand()
236
240
  const child = spawn(command, ['run', script, '--', ...process.argv.slice(3)], {
237
241
  cwd: frontendDir,
238
242
  stdio: 'inherit',
239
- env: process.env
240
- })
241
-
242
- child.once('error', (error) => {
243
- console.error(error)
244
- process.exit(1)
243
+ env: process.env,
244
+ shell: shouldUseShell(command)
245
245
  })
246
-
247
- child.once('exit', (code, signal) => {
248
- if (signal) {
249
- process.kill(process.pid, signal)
250
- return
251
- }
252
-
253
- process.exit(code ?? 1)
254
- })
255
- `
256
-
257
- export const description = "Scaffold a starter app with an interactive project setup"
258
-
259
- function listTemplates(templatesDir) {
260
- return readdirSync(templatesDir, { withFileTypes: true })
261
- .filter((entry) => entry.isDirectory())
262
- .map((entry) => entry.name)
263
- .sort()
264
- }
265
-
266
- function getRootGitignore(frontendDir) {
267
- if (frontendDir === ".") {
268
- return `.DS_Store
269
- Thumbs.db
270
- .idea/
271
- .vs/
272
- .vscode/
273
- .cache/
274
- .reset/
275
- .turbo/
276
- build/
277
- coverage/
278
- dist/
279
- node_modules/
280
- npm-debug.log*
281
- yarn-debug.log*
282
- yarn-error.log*
283
- .pnpm-debug.log*
284
- *.log
285
- *.zip
286
- *.tgz
287
- *.tsbuildinfo
288
- *.dSYM/
289
- .env
290
- .env.*
291
- .env.local
292
- .env.*.local
293
- .eslintcache
294
- bun.lock
295
- bun.lockb
296
- compile_commands.json
297
- CMakeUserPresets.json
298
- CMakeCache.txt
299
- CMakeFiles/
300
- cmake_install.cmake
301
- install_manifest.txt
302
- cmake-build-*/
303
- Testing/
304
- scripts/local/
305
- `
306
- }
307
-
308
- return `.DS_Store
309
- Thumbs.db
310
- .idea/
311
- .vs/
312
- .vscode/
313
- .cache/
314
- .reset/
315
- .turbo/
316
- build/
317
- coverage/
318
- node_modules/
319
- ${frontendDir}/node_modules/
320
- ${frontendDir}/dist/
321
- npm-debug.log*
322
- yarn-debug.log*
323
- yarn-error.log*
324
- .pnpm-debug.log*
325
- *.log
326
- *.zip
327
- *.tgz
328
- *.tsbuildinfo
329
- *.dSYM/
330
- .env
331
- .env.*
332
- .env.local
333
- .env.*.local
334
- .eslintcache
335
- bun.lock
336
- bun.lockb
337
- compile_commands.json
338
- CMakeUserPresets.json
339
- CMakeCache.txt
340
- CMakeFiles/
341
- cmake_install.cmake
342
- install_manifest.txt
343
- cmake-build-*/
344
- Testing/
345
- scripts/local/
346
- `
347
- }
348
-
349
- function resolveDefaultFrontendDir(context) {
350
- if (typeof context.flags["frontend-dir"] === "string") {
351
- return context.flags["frontend-dir"]
352
- }
353
-
354
- if (context.flags.flat) {
355
- return "."
356
- }
357
-
358
- return "frontend"
359
- }
360
-
361
- function resolveDefaultStyling(context) {
362
- if (context.flags.tailwind) {
363
- return "tailwindcss"
364
- }
365
-
366
- if (context.flags["no-tailwind"]) {
367
- return "css"
368
- }
369
-
370
- return "css"
371
- }
372
-
373
- function resolvePackageManagerOption(context) {
374
- if (typeof context.flags["package-manager"] === "string") {
375
- return context.flags["package-manager"]
376
- }
377
-
378
- return "npm"
379
- }
380
-
381
- function describeInstallCommand(packageManager) {
382
- return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
383
- }
384
-
385
- async function ensureWritableTarget(targetDir, force) {
386
- if (!existsSync(targetDir)) {
387
- return
388
- }
389
-
390
- const entries = await readdir(targetDir)
391
-
392
- if (entries.length > 0 && !force) {
393
- throw new Error(`Target directory is not empty: ${targetDir}. Use --force to continue.`)
394
- }
395
- }
396
-
397
- async function copyFrontendTemplate(sourceFrontendDir, targetFrontendDir, options) {
398
- const { force, frontendDir } = options
399
- const flatLayout = frontendDir === "."
400
-
401
- if (!flatLayout) {
402
- await cp(sourceFrontendDir, targetFrontendDir, {
403
- recursive: true,
404
- force,
405
- filter(source) {
406
- const baseName = path.basename(source)
407
- return (
408
- baseName !== "node_modules" &&
409
- baseName !== "dist" &&
410
- baseName !== "bun.lockb" &&
411
- baseName !== "reset.config.json"
412
- )
413
- }
414
- })
415
-
416
- return
417
- }
418
-
419
- await mkdir(targetFrontendDir, { recursive: true })
420
-
421
- const entries = await readdir(sourceFrontendDir, { withFileTypes: true })
422
- for (const entry of entries) {
423
- if (["node_modules", "dist", "bun.lockb", "reset.config.json", ".gitignore", "README.md"].includes(entry.name)) {
424
- continue
425
- }
426
-
427
- await cp(
428
- path.join(sourceFrontendDir, entry.name),
429
- path.join(targetFrontendDir, entry.name),
430
- { recursive: true, force }
431
- )
432
- }
433
- }
434
-
435
- async function applyFrontendOverrides(frontendDir, options) {
436
- await writeFile(path.join(frontendDir, "src", "lib", "reset.ts"), sdkRuntimeReexport, "utf8")
437
- await writeFile(path.join(frontendDir, "tsconfig.app.json"), standaloneTsconfigApp, "utf8")
438
-
439
- const packageJsonPath = path.join(frontendDir, "package.json")
440
- const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
441
- frontendPackage.dependencies = {
442
- ...frontendPackage.dependencies,
443
- "@reset-framework/sdk": options.sdkDependencySpec
444
- }
445
-
446
- if (options.styling === "tailwindcss") {
447
- frontendPackage.devDependencies = {
448
- ...frontendPackage.devDependencies,
449
- "@tailwindcss/vite": "latest",
450
- tailwindcss: "latest"
451
- }
452
-
453
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
454
- await writeFile(path.join(frontendDir, "vite.config.ts"), tailwindViteConfig, "utf8")
455
- await writeFile(path.join(frontendDir, "src", "index.css"), tailwindIndexCss, "utf8")
456
- await writeFile(path.join(frontendDir, "src", "App.tsx"), tailwindAppTsx, "utf8")
457
- return
458
- }
459
-
460
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
461
- await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
462
- }
463
-
464
- function createRootPackageJson(options) {
465
- const packageManagerVersion = options.packageManagerVersion
466
- ? `${options.packageManager}@${options.packageManagerVersion}`
467
- : undefined
468
-
469
- const base = {
470
- name: options.appName,
471
- private: true,
472
- version: "0.1.0",
473
- description: `${options.productName} desktop app`,
474
- scripts: {
475
- dev: "reset-framework-cli dev",
476
- build: "reset-framework-cli build",
477
- package: "reset-framework-cli package",
478
- doctor: "reset-framework-cli doctor"
479
- },
480
- devDependencies: {
481
- "reset-framework-cli": options.cliDependencySpec
482
- }
483
- }
484
-
485
- if (packageManagerVersion) {
486
- base.packageManager = packageManagerVersion
487
- }
488
-
489
- if (options.frontendDir !== ".") {
490
- base.workspaces = [options.frontendDir]
491
- base.scripts["dev:web"] = "node ./scripts/run-frontend.mjs dev"
492
- base.scripts["build:web"] = "node ./scripts/run-frontend.mjs build"
493
- base.scripts["lint:web"] = "node ./scripts/run-frontend.mjs lint"
494
- base.scripts["preview:web"] = "node ./scripts/run-frontend.mjs preview"
495
- }
496
-
497
- return base
498
- }
499
-
500
- function createRootReadme(options) {
501
- const run = `${options.packageManager} run`
502
- const webPath = options.frontendDir === "." ? "project root" : `${options.frontendDir}/`
503
-
504
- return `# ${options.productName}
505
-
506
- Generated with \`reset-framework-cli\`.
507
-
508
- ## Commands
509
-
510
- - \`${run} dev\`: start the frontend dev server and native desktop runtime
511
- - \`${run} build\`: build the frontend and stage the desktop app bundle
512
- - \`${run} package\`: archive the built desktop app
513
- - \`${run} doctor\`: inspect the project and framework installation
514
-
515
- ## Web-only commands
516
-
517
- ${options.frontendDir === "." ? `- \`${run} dev:web\`: run the Vite frontend only
518
- - \`${run} build:web\`: build the frontend only` : `- \`${run} dev:web\`: run the frontend workspace only
519
- - \`${run} build:web\`: build the frontend workspace only`}
520
-
521
- ## Project files
522
-
523
- - \`reset.config.json\`: desktop app metadata and runtime configuration
524
- - \`${webPath}\`: web frontend source
525
- - \`.reset/\`: generated build output and native runtime cache
526
- `
527
- }
528
-
529
- async function writeRootPackageFiles(options) {
530
- await writeFile(
531
- path.join(options.targetDir, "README.md"),
532
- createRootReadme(options),
533
- "utf8"
534
- )
535
-
536
- if (options.frontendDir !== ".") {
537
- await writeFile(
538
- path.join(options.targetDir, "package.json"),
539
- JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
540
- "utf8"
541
- )
542
-
543
- const scriptsDir = path.join(options.targetDir, "scripts")
544
- await mkdir(scriptsDir, { recursive: true })
545
- await writeFile(path.join(scriptsDir, "run-frontend.mjs"), rootFrontendRunner, "utf8")
546
- }
547
- }
548
-
549
- async function writeProjectFiles(options) {
550
- const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
551
-
552
- await writeFile(
553
- path.join(options.targetDir, "reset.config.json"),
554
- JSON.stringify(
555
- {
556
- ...templateConfig,
557
- name: options.appName,
558
- productName: options.productName,
559
- appId: options.appId,
560
- project: {
561
- frontendDir: options.frontendDir,
562
- styling: options.styling
563
- },
564
- window: {
565
- ...templateConfig.window,
566
- title: options.productName
567
- }
568
- },
569
- null,
570
- 2
571
- ) + "\n",
572
- "utf8"
573
- )
574
-
575
- await writeFile(
576
- path.join(options.targetDir, ".gitignore"),
577
- getRootGitignore(options.frontendDir),
578
- "utf8"
579
- )
580
- }
581
-
582
- async function resolvePackageManagerVersion(packageManager) {
583
- if (packageManager !== "npm" && packageManager !== "pnpm" && packageManager !== "yarn" && packageManager !== "bun") {
584
- return null
585
- }
586
-
587
- try {
588
- const command = resolvePackageManagerCommand(packageManager)
589
- const result = await captureCommandOutput(command, ["--version"])
590
- return typeof result === "string" && result.trim() !== "" ? result.trim() : null
591
- } catch {
592
- return null
593
- }
594
- }
595
-
596
- async function collectCreateAppOptions(context, templates) {
597
- const initialTarget = context.args[0] ?? "my-app"
598
- const defaultTargetDir = path.resolve(context.cwd, initialTarget)
599
- const defaultMetadata = makeAppMetadata(path.basename(defaultTargetDir))
600
- const frameworkPaths = resolveFrameworkPaths()
601
-
602
- const defaults = {
603
- targetDir: defaultTargetDir,
604
- templateName: typeof context.flags.template === "string" ? context.flags.template : "basic",
605
- appName: defaultMetadata.name,
606
- productName:
607
- typeof context.flags["product-name"] === "string"
608
- ? context.flags["product-name"]
609
- : defaultMetadata.productName,
610
- appId:
611
- typeof context.flags["app-id"] === "string"
612
- ? context.flags["app-id"]
613
- : defaultMetadata.appId,
614
- frontendDir: resolveDefaultFrontendDir(context),
615
- styling: resolveDefaultStyling(context)
616
- }
617
-
618
- const interactive = isInteractiveSession() && !context.flags.yes && !context.flags["no-prompt"]
619
-
620
- if (!interactive) {
621
- if (!context.args[0]) {
622
- throw new Error("Missing target directory. Use: reset-framework-cli create-app <directory>")
623
- }
624
-
625
- return {
626
- ...defaults,
627
- templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
628
- templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
629
- sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
630
- cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
631
- sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
632
- packageManager: resolvePackageManagerOption(context),
633
- packageManagerVersion: await resolvePackageManagerVersion(resolvePackageManagerOption(context)),
634
- installDependencies: !context.flags["no-install"],
635
- force: Boolean(context.flags.force)
636
- }
637
- }
638
-
639
- const selected = await withPromptSession(async (rl) => {
640
- const targetInput = await promptText(rl, {
641
- label: "Project directory",
642
- defaultValue: path.basename(defaults.targetDir),
643
- validate(value) {
644
- return value.trim() !== "" || "Enter a directory name."
645
- }
646
- })
647
-
648
- const targetDir = path.resolve(context.cwd, targetInput)
649
- const metadata = makeAppMetadata(path.basename(targetDir))
650
-
651
- const templateName =
652
- templates.length === 1
653
- ? templates[0]
654
- : await promptSelect(rl, {
655
- label: "Template",
656
- defaultValue: defaults.templateName,
657
- choices: templates.map((template) => ({
658
- value: template,
659
- label: template,
660
- description: "Starter project scaffold"
661
- }))
662
- })
663
-
664
- const productName = await promptText(rl, {
665
- label: "Product name",
666
- defaultValue: defaults.productName || metadata.productName,
667
- validate(value) {
668
- return value.trim() !== "" || "Enter a product name."
669
- }
670
- })
671
-
672
- const appId = await promptText(rl, {
673
- label: "App ID",
674
- defaultValue: defaults.appId || metadata.appId,
675
- validate(value) {
676
- return value.includes(".") || "Use a reverse-domain identifier such as com.example.my-app."
677
- }
678
- })
679
-
680
- const frontendDir = await promptSelect(rl, {
681
- label: "Frontend layout",
682
- defaultValue: defaults.frontendDir === "." ? "." : "frontend",
683
- choices: [
684
- {
685
- value: "frontend",
686
- label: "Use a frontend/ subdirectory",
687
- description: "Keeps the web app separate from config, output, and packaging files."
688
- },
689
- {
690
- value: ".",
691
- label: "Keep the frontend at project root",
692
- description: "Flatter project layout with package.json and src/ directly in the app root."
693
- }
694
- ]
695
- })
696
-
697
- const styling = await promptSelect(rl, {
698
- label: "Styling setup",
699
- defaultValue: defaults.styling,
700
- choices: [
701
- {
702
- value: "css",
703
- label: "Vanilla CSS starter",
704
- description: "Use the existing CSS-based starter with no extra styling dependency."
705
- },
706
- {
707
- value: "tailwindcss",
708
- label: "Tailwind CSS starter",
709
- description: "Configure Tailwind CSS and swap the starter screen to utility classes."
710
- }
711
- ]
712
- })
713
-
714
- const installDependencies = await promptConfirm(rl, {
715
- label: "Install frontend dependencies after scaffolding",
716
- defaultValue: true
717
- })
718
-
719
- const packageManager = await promptSelect(rl, {
720
- label: "Package manager",
721
- defaultValue: resolvePackageManagerOption(context),
722
- choices: [
723
- {
724
- value: "npm",
725
- label: "npm",
726
- description: "Wide compatibility and predictable installs."
727
- },
728
- {
729
- value: "bun",
730
- label: "bun",
731
- description: "Fast installs and lets bun run the root desktop scripts."
732
- },
733
- {
734
- value: "pnpm",
735
- label: "pnpm",
736
- description: "Strict dependency graph with workspace support."
737
- },
738
- {
739
- value: "yarn",
740
- label: "yarn",
741
- description: "Classic workspace-based JavaScript package workflow."
742
- }
743
- ]
744
- })
745
-
746
- printSection("Summary")
747
- printKeyValueTable([
748
- ["Directory", targetDir],
749
- ["Template", templateName],
750
- ["Product", productName],
751
- ["App ID", appId],
752
- ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
753
- ["Styling", styling],
754
- ["Package manager", packageManager],
755
- ["Install", installDependencies ? "yes" : "no"]
756
- ])
757
- console.log("")
758
-
759
- const confirmed = await promptConfirm(rl, {
760
- label: "Create this project",
761
- defaultValue: true
762
- })
763
-
764
- if (!confirmed) {
765
- throw new Error("Create app cancelled.")
766
- }
767
-
768
- return {
769
- targetDir,
770
- templateName,
771
- appName: metadata.name,
772
- productName,
773
- appId,
774
- frontendDir,
775
- styling,
776
- installDependencies,
777
- packageManager
778
- }
779
- })
780
-
781
- return {
782
- ...selected,
783
- templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
784
- templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
785
- sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
786
- cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
787
- sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
788
- packageManagerVersion: await resolvePackageManagerVersion(selected.packageManager),
789
- force: Boolean(context.flags.force)
790
- }
791
- }
792
-
793
- export async function run(context) {
794
- const frameworkPaths = resolveFrameworkPaths()
795
- const templates = listTemplates(frameworkPaths.templatesDir)
796
- const formatTargetForShell = (targetDir) => {
797
- const relative = path.relative(context.cwd, targetDir)
798
- if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
799
- return relative || "."
800
- }
801
-
802
- return targetDir
803
- }
804
-
805
- printBanner("reset-framework-cli create-app", description)
806
- const options = await collectCreateAppOptions(context, templates)
807
-
808
- if (!templates.includes(options.templateName)) {
809
- throw new Error(`Unknown template: ${options.templateName}`)
810
- }
811
-
812
- const targetFrontendDir =
813
- options.frontendDir === "."
814
- ? options.targetDir
815
- : path.join(options.targetDir, options.frontendDir)
816
-
817
- await ensureWritableTarget(options.targetDir, options.force)
818
-
819
- printSection("Project")
820
- printKeyValueTable([
821
- ["Directory", options.targetDir],
822
- ["Template", options.templateName],
823
- ["Product", options.productName],
824
- ["App ID", options.appId],
825
- ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
826
- ["Styling", options.styling],
827
- ["Package manager", options.packageManager],
828
- ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
829
- ])
830
-
831
- if (Boolean(context.flags["dry-run"])) {
832
- console.log("")
833
- printSection("Dry run")
834
- printStatusTable([
835
- ["plan", "Copy starter", `${options.sourceFrontendDir} -> ${targetFrontendDir}`],
836
- ["plan", "Write README", path.join(options.targetDir, "README.md")],
837
- ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
838
- ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
839
- ["plan", "Write app package", path.join(options.targetDir, "package.json")],
840
- ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
841
- ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
842
- ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
843
- ])
844
- if (options.frontendDir !== ".") {
845
- printStatusTable([
846
- ["plan", "Write frontend runner", path.join(options.targetDir, "scripts", "run-frontend.mjs")]
847
- ])
848
- }
849
- if (options.styling === "tailwindcss") {
850
- printStatusTable([
851
- ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
852
- ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
853
- ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
854
- ])
855
- }
856
- if (options.installDependencies) {
857
- printStatusTable([
858
- ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${options.targetDir}`]
859
- ])
860
- }
861
- return
862
- }
863
-
864
- console.log("")
865
- const progress = createProgress(options.installDependencies ? 6 : 5, "Scaffold")
866
-
867
- await mkdir(options.targetDir, { recursive: true })
868
- progress.tick("Prepared project directory")
869
-
870
- await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
871
- progress.tick("Copied frontend starter")
872
-
873
- await writeProjectFiles(options)
874
- await writeRootPackageFiles(options)
875
- progress.tick("Wrote project files")
876
-
877
- await applyFrontendOverrides(targetFrontendDir, options)
878
- progress.tick("Applied frontend package wiring")
879
-
880
- const packageJsonPath = path.join(targetFrontendDir, "package.json")
881
- const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
882
-
883
- if (options.frontendDir === ".") {
884
- const currentScripts = { ...(frontendPackage.scripts ?? {}) }
885
- frontendPackage.name = options.appName
886
- frontendPackage.private = true
887
- frontendPackage.version = "0.1.0"
888
- frontendPackage.description = `${options.productName} desktop app`
889
- frontendPackage.devDependencies = {
890
- ...frontendPackage.devDependencies,
891
- "reset-framework-cli": options.cliDependencySpec
892
- }
893
- frontendPackage.scripts = {
894
- dev: "reset-framework-cli dev",
895
- "dev:web": currentScripts.dev ?? "vite",
896
- build: "reset-framework-cli build",
897
- "build:web": currentScripts.build ?? "tsc -b && vite build",
898
- package: "reset-framework-cli package",
899
- doctor: "reset-framework-cli doctor",
900
- lint: currentScripts.lint ?? "eslint .",
901
- preview: currentScripts.preview ?? "vite preview"
902
- }
903
-
904
- if (options.packageManagerVersion) {
905
- frontendPackage.packageManager = `${options.packageManager}@${options.packageManagerVersion}`
906
- }
907
- } else {
908
- frontendPackage.name = `${options.appName}-frontend`
909
- }
910
-
911
- await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
912
- progress.tick("Finalized starter metadata")
913
-
914
- if (options.installDependencies) {
915
- await runCommand(
916
- resolvePackageManagerCommand(options.packageManager),
917
- getInstallCommandArgs(options.packageManager),
918
- {
919
- cwd: options.targetDir
920
- }
921
- )
922
- progress.tick("Installed frontend dependencies")
923
- }
924
-
925
- console.log("")
926
- printSection("Result")
927
- printStatusTable([
928
- ["done", "Project", options.targetDir],
929
- ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && ${options.packageManager} run dev`]
930
- ])
931
- }
246
+
247
+ child.once('error', (error) => {
248
+ console.error(error)
249
+ process.exit(1)
250
+ })
251
+
252
+ child.once('exit', (code, signal) => {
253
+ if (signal) {
254
+ process.kill(process.pid, signal)
255
+ return
256
+ }
257
+
258
+ process.exit(code ?? 1)
259
+ })
260
+ `
261
+
262
+ export const description = "Scaffold a starter app with an interactive project setup"
263
+
264
+ function listTemplates(templatesDir) {
265
+ return readdirSync(templatesDir, { withFileTypes: true })
266
+ .filter((entry) => entry.isDirectory())
267
+ .map((entry) => entry.name)
268
+ .sort()
269
+ }
270
+
271
+ function getRootGitignore(frontendDir) {
272
+ if (frontendDir === ".") {
273
+ return `.DS_Store
274
+ Thumbs.db
275
+ .idea/
276
+ .vs/
277
+ .vscode/
278
+ .cache/
279
+ .reset/
280
+ .turbo/
281
+ build/
282
+ coverage/
283
+ dist/
284
+ node_modules/
285
+ npm-debug.log*
286
+ yarn-debug.log*
287
+ yarn-error.log*
288
+ .pnpm-debug.log*
289
+ *.log
290
+ *.zip
291
+ *.tgz
292
+ *.tsbuildinfo
293
+ *.dSYM/
294
+ .env
295
+ .env.*
296
+ .env.local
297
+ .env.*.local
298
+ .eslintcache
299
+ bun.lock
300
+ bun.lockb
301
+ compile_commands.json
302
+ CMakeUserPresets.json
303
+ CMakeCache.txt
304
+ CMakeFiles/
305
+ cmake_install.cmake
306
+ install_manifest.txt
307
+ cmake-build-*/
308
+ Testing/
309
+ scripts/local/
310
+ `
311
+ }
312
+
313
+ return `.DS_Store
314
+ Thumbs.db
315
+ .idea/
316
+ .vs/
317
+ .vscode/
318
+ .cache/
319
+ .reset/
320
+ .turbo/
321
+ build/
322
+ coverage/
323
+ node_modules/
324
+ ${frontendDir}/node_modules/
325
+ ${frontendDir}/dist/
326
+ npm-debug.log*
327
+ yarn-debug.log*
328
+ yarn-error.log*
329
+ .pnpm-debug.log*
330
+ *.log
331
+ *.zip
332
+ *.tgz
333
+ *.tsbuildinfo
334
+ *.dSYM/
335
+ .env
336
+ .env.*
337
+ .env.local
338
+ .env.*.local
339
+ .eslintcache
340
+ bun.lock
341
+ bun.lockb
342
+ compile_commands.json
343
+ CMakeUserPresets.json
344
+ CMakeCache.txt
345
+ CMakeFiles/
346
+ cmake_install.cmake
347
+ install_manifest.txt
348
+ cmake-build-*/
349
+ Testing/
350
+ scripts/local/
351
+ `
352
+ }
353
+
354
+ function resolveDefaultFrontendDir(context) {
355
+ if (typeof context.flags["frontend-dir"] === "string") {
356
+ return context.flags["frontend-dir"]
357
+ }
358
+
359
+ if (context.flags.flat) {
360
+ return "."
361
+ }
362
+
363
+ return "frontend"
364
+ }
365
+
366
+ function resolveDefaultStyling(context) {
367
+ if (context.flags.tailwind) {
368
+ return "tailwindcss"
369
+ }
370
+
371
+ if (context.flags["no-tailwind"]) {
372
+ return "css"
373
+ }
374
+
375
+ return "css"
376
+ }
377
+
378
+ function resolvePackageManagerOption(context) {
379
+ if (typeof context.flags["package-manager"] === "string") {
380
+ return context.flags["package-manager"]
381
+ }
382
+
383
+ return "npm"
384
+ }
385
+
386
+ function describeInstallCommand(packageManager) {
387
+ return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
388
+ }
389
+
390
+ async function ensureWritableTarget(targetDir, force) {
391
+ if (!existsSync(targetDir)) {
392
+ return
393
+ }
394
+
395
+ const entries = await readdir(targetDir)
396
+
397
+ if (entries.length > 0 && !force) {
398
+ throw new Error(`Target directory is not empty: ${targetDir}. Use --force to continue.`)
399
+ }
400
+ }
401
+
402
+ async function copyFrontendTemplate(sourceFrontendDir, targetFrontendDir, options) {
403
+ const { force, frontendDir } = options
404
+ const flatLayout = frontendDir === "."
405
+
406
+ if (!flatLayout) {
407
+ await cp(sourceFrontendDir, targetFrontendDir, {
408
+ recursive: true,
409
+ force,
410
+ filter(source) {
411
+ const baseName = path.basename(source)
412
+ return (
413
+ baseName !== "node_modules" &&
414
+ baseName !== "dist" &&
415
+ baseName !== "bun.lockb" &&
416
+ baseName !== "reset.config.json"
417
+ )
418
+ }
419
+ })
420
+
421
+ return
422
+ }
423
+
424
+ await mkdir(targetFrontendDir, { recursive: true })
425
+
426
+ const entries = await readdir(sourceFrontendDir, { withFileTypes: true })
427
+ for (const entry of entries) {
428
+ if (["node_modules", "dist", "bun.lockb", "reset.config.json", ".gitignore", "README.md"].includes(entry.name)) {
429
+ continue
430
+ }
431
+
432
+ await cp(
433
+ path.join(sourceFrontendDir, entry.name),
434
+ path.join(targetFrontendDir, entry.name),
435
+ { recursive: true, force }
436
+ )
437
+ }
438
+ }
439
+
440
+ async function applyFrontendOverrides(frontendDir, options) {
441
+ await writeFile(path.join(frontendDir, "src", "lib", "reset.ts"), sdkRuntimeReexport, "utf8")
442
+ await writeFile(path.join(frontendDir, "tsconfig.app.json"), standaloneTsconfigApp, "utf8")
443
+
444
+ const packageJsonPath = path.join(frontendDir, "package.json")
445
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
446
+ frontendPackage.dependencies = {
447
+ ...frontendPackage.dependencies,
448
+ "@reset-framework/sdk": options.sdkDependencySpec
449
+ }
450
+
451
+ if (options.styling === "tailwindcss") {
452
+ frontendPackage.devDependencies = {
453
+ ...frontendPackage.devDependencies,
454
+ "@tailwindcss/vite": "latest",
455
+ tailwindcss: "latest"
456
+ }
457
+
458
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
459
+ await writeFile(path.join(frontendDir, "vite.config.ts"), tailwindViteConfig, "utf8")
460
+ await writeFile(path.join(frontendDir, "src", "index.css"), tailwindIndexCss, "utf8")
461
+ await writeFile(path.join(frontendDir, "src", "App.tsx"), tailwindAppTsx, "utf8")
462
+ return
463
+ }
464
+
465
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
466
+ await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
467
+ }
468
+
469
+ function createRootPackageJson(options) {
470
+ const packageManagerVersion = options.packageManagerVersion
471
+ ? `${options.packageManager}@${options.packageManagerVersion}`
472
+ : undefined
473
+
474
+ const base = {
475
+ name: options.appName,
476
+ private: true,
477
+ version: "0.1.0",
478
+ description: `${options.productName} desktop app`,
479
+ scripts: {
480
+ dev: "reset-framework-cli dev",
481
+ build: "reset-framework-cli build",
482
+ package: "reset-framework-cli package",
483
+ doctor: "reset-framework-cli doctor"
484
+ },
485
+ devDependencies: {
486
+ "reset-framework-cli": options.cliDependencySpec
487
+ }
488
+ }
489
+
490
+ if (packageManagerVersion) {
491
+ base.packageManager = packageManagerVersion
492
+ }
493
+
494
+ if (options.frontendDir !== ".") {
495
+ base.workspaces = [options.frontendDir]
496
+ base.scripts["dev:web"] = "node ./scripts/run-frontend.mjs dev"
497
+ base.scripts["build:web"] = "node ./scripts/run-frontend.mjs build"
498
+ base.scripts["lint:web"] = "node ./scripts/run-frontend.mjs lint"
499
+ base.scripts["preview:web"] = "node ./scripts/run-frontend.mjs preview"
500
+ }
501
+
502
+ return base
503
+ }
504
+
505
+ function createRootReadme(options) {
506
+ const run = `${options.packageManager} run`
507
+ const webPath = options.frontendDir === "." ? "project root" : `${options.frontendDir}/`
508
+
509
+ return `# ${options.productName}
510
+
511
+ Generated with \`reset-framework-cli\`.
512
+
513
+ ## Commands
514
+
515
+ - \`${run} dev\`: start the frontend dev server and native desktop runtime
516
+ - \`${run} build\`: build the frontend and stage the desktop app bundle
517
+ - \`${run} package\`: archive the built desktop app
518
+ - \`${run} doctor\`: inspect the project and framework installation
519
+
520
+ ## Web-only commands
521
+
522
+ ${options.frontendDir === "." ? `- \`${run} dev:web\`: run the Vite frontend only
523
+ - \`${run} build:web\`: build the frontend only` : `- \`${run} dev:web\`: run the frontend workspace only
524
+ - \`${run} build:web\`: build the frontend workspace only`}
525
+
526
+ ## Project files
527
+
528
+ - \`reset.config.json\`: desktop app metadata and runtime configuration
529
+ - \`${webPath}\`: web frontend source
530
+ - \`.reset/\`: generated build output and native runtime cache
531
+ `
532
+ }
533
+
534
+ async function writeRootPackageFiles(options) {
535
+ await writeFile(
536
+ path.join(options.targetDir, "README.md"),
537
+ createRootReadme(options),
538
+ "utf8"
539
+ )
540
+
541
+ if (options.frontendDir !== ".") {
542
+ await writeFile(
543
+ path.join(options.targetDir, "package.json"),
544
+ JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
545
+ "utf8"
546
+ )
547
+
548
+ const scriptsDir = path.join(options.targetDir, "scripts")
549
+ await mkdir(scriptsDir, { recursive: true })
550
+ await writeFile(path.join(scriptsDir, "run-frontend.mjs"), rootFrontendRunner, "utf8")
551
+ }
552
+ }
553
+
554
+ async function writeProjectFiles(options) {
555
+ const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
556
+
557
+ await writeFile(
558
+ path.join(options.targetDir, "reset.config.json"),
559
+ JSON.stringify(
560
+ {
561
+ ...templateConfig,
562
+ name: options.appName,
563
+ productName: options.productName,
564
+ appId: options.appId,
565
+ project: {
566
+ frontendDir: options.frontendDir,
567
+ styling: options.styling
568
+ },
569
+ window: {
570
+ ...templateConfig.window,
571
+ title: options.productName
572
+ }
573
+ },
574
+ null,
575
+ 2
576
+ ) + "\n",
577
+ "utf8"
578
+ )
579
+
580
+ await writeFile(
581
+ path.join(options.targetDir, ".gitignore"),
582
+ getRootGitignore(options.frontendDir),
583
+ "utf8"
584
+ )
585
+ }
586
+
587
+ async function resolvePackageManagerVersion(packageManager) {
588
+ if (packageManager !== "npm" && packageManager !== "pnpm" && packageManager !== "yarn" && packageManager !== "bun") {
589
+ return null
590
+ }
591
+
592
+ try {
593
+ const command = resolvePackageManagerCommand(packageManager)
594
+ const result = await captureCommandOutput(command, ["--version"])
595
+ return typeof result === "string" && result.trim() !== "" ? result.trim() : null
596
+ } catch {
597
+ return null
598
+ }
599
+ }
600
+
601
+ async function collectCreateAppOptions(context, templates) {
602
+ const initialTarget = context.args[0] ?? "my-app"
603
+ const defaultTargetDir = path.resolve(context.cwd, initialTarget)
604
+ const defaultMetadata = makeAppMetadata(path.basename(defaultTargetDir))
605
+ const frameworkPaths = resolveFrameworkPaths()
606
+
607
+ const defaults = {
608
+ targetDir: defaultTargetDir,
609
+ templateName: typeof context.flags.template === "string" ? context.flags.template : "basic",
610
+ appName: defaultMetadata.name,
611
+ productName:
612
+ typeof context.flags["product-name"] === "string"
613
+ ? context.flags["product-name"]
614
+ : defaultMetadata.productName,
615
+ appId:
616
+ typeof context.flags["app-id"] === "string"
617
+ ? context.flags["app-id"]
618
+ : defaultMetadata.appId,
619
+ frontendDir: resolveDefaultFrontendDir(context),
620
+ styling: resolveDefaultStyling(context)
621
+ }
622
+
623
+ const interactive = isInteractiveSession() && !context.flags.yes && !context.flags["no-prompt"]
624
+
625
+ if (!interactive) {
626
+ if (!context.args[0]) {
627
+ throw new Error("Missing target directory. Use: reset-framework-cli create-app <directory>")
628
+ }
629
+
630
+ return {
631
+ ...defaults,
632
+ templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
633
+ templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
634
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
635
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
636
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
637
+ packageManager: resolvePackageManagerOption(context),
638
+ packageManagerVersion: await resolvePackageManagerVersion(resolvePackageManagerOption(context)),
639
+ installDependencies: !context.flags["no-install"],
640
+ force: Boolean(context.flags.force)
641
+ }
642
+ }
643
+
644
+ const selected = await withPromptSession(async (rl) => {
645
+ const targetInput = await promptText(rl, {
646
+ label: "Project directory",
647
+ defaultValue: path.basename(defaults.targetDir),
648
+ validate(value) {
649
+ return value.trim() !== "" || "Enter a directory name."
650
+ }
651
+ })
652
+
653
+ const targetDir = path.resolve(context.cwd, targetInput)
654
+ const metadata = makeAppMetadata(path.basename(targetDir))
655
+
656
+ const templateName =
657
+ templates.length === 1
658
+ ? templates[0]
659
+ : await promptSelect(rl, {
660
+ label: "Template",
661
+ defaultValue: defaults.templateName,
662
+ choices: templates.map((template) => ({
663
+ value: template,
664
+ label: template,
665
+ description: "Starter project scaffold"
666
+ }))
667
+ })
668
+
669
+ const productName = await promptText(rl, {
670
+ label: "Product name",
671
+ defaultValue: defaults.productName || metadata.productName,
672
+ validate(value) {
673
+ return value.trim() !== "" || "Enter a product name."
674
+ }
675
+ })
676
+
677
+ const appId = await promptText(rl, {
678
+ label: "App ID",
679
+ defaultValue: defaults.appId || metadata.appId,
680
+ validate(value) {
681
+ return value.includes(".") || "Use a reverse-domain identifier such as com.example.my-app."
682
+ }
683
+ })
684
+
685
+ const frontendDir = await promptSelect(rl, {
686
+ label: "Frontend layout",
687
+ defaultValue: defaults.frontendDir === "." ? "." : "frontend",
688
+ choices: [
689
+ {
690
+ value: "frontend",
691
+ label: "Use a frontend/ subdirectory",
692
+ description: "Keeps the web app separate from config, output, and packaging files."
693
+ },
694
+ {
695
+ value: ".",
696
+ label: "Keep the frontend at project root",
697
+ description: "Flatter project layout with package.json and src/ directly in the app root."
698
+ }
699
+ ]
700
+ })
701
+
702
+ const styling = await promptSelect(rl, {
703
+ label: "Styling setup",
704
+ defaultValue: defaults.styling,
705
+ choices: [
706
+ {
707
+ value: "css",
708
+ label: "Vanilla CSS starter",
709
+ description: "Use the existing CSS-based starter with no extra styling dependency."
710
+ },
711
+ {
712
+ value: "tailwindcss",
713
+ label: "Tailwind CSS starter",
714
+ description: "Configure Tailwind CSS and swap the starter screen to utility classes."
715
+ }
716
+ ]
717
+ })
718
+
719
+ const installDependencies = await promptConfirm(rl, {
720
+ label: "Install frontend dependencies after scaffolding",
721
+ defaultValue: true
722
+ })
723
+
724
+ const packageManager = await promptSelect(rl, {
725
+ label: "Package manager",
726
+ defaultValue: resolvePackageManagerOption(context),
727
+ choices: [
728
+ {
729
+ value: "npm",
730
+ label: "npm",
731
+ description: "Wide compatibility and predictable installs."
732
+ },
733
+ {
734
+ value: "bun",
735
+ label: "bun",
736
+ description: "Fast installs and lets bun run the root desktop scripts."
737
+ },
738
+ {
739
+ value: "pnpm",
740
+ label: "pnpm",
741
+ description: "Strict dependency graph with workspace support."
742
+ },
743
+ {
744
+ value: "yarn",
745
+ label: "yarn",
746
+ description: "Classic workspace-based JavaScript package workflow."
747
+ }
748
+ ]
749
+ })
750
+
751
+ printSection("Summary")
752
+ printKeyValueTable([
753
+ ["Directory", targetDir],
754
+ ["Template", templateName],
755
+ ["Product", productName],
756
+ ["App ID", appId],
757
+ ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
758
+ ["Styling", styling],
759
+ ["Package manager", packageManager],
760
+ ["Install", installDependencies ? "yes" : "no"]
761
+ ])
762
+ console.log("")
763
+
764
+ const confirmed = await promptConfirm(rl, {
765
+ label: "Create this project",
766
+ defaultValue: true
767
+ })
768
+
769
+ if (!confirmed) {
770
+ throw new Error("Create app cancelled.")
771
+ }
772
+
773
+ return {
774
+ targetDir,
775
+ templateName,
776
+ appName: metadata.name,
777
+ productName,
778
+ appId,
779
+ frontendDir,
780
+ styling,
781
+ installDependencies,
782
+ packageManager
783
+ }
784
+ })
785
+
786
+ return {
787
+ ...selected,
788
+ templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
789
+ templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
790
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
791
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
792
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
793
+ packageManagerVersion: await resolvePackageManagerVersion(selected.packageManager),
794
+ force: Boolean(context.flags.force)
795
+ }
796
+ }
797
+
798
+ export async function run(context) {
799
+ const frameworkPaths = resolveFrameworkPaths()
800
+ const templates = listTemplates(frameworkPaths.templatesDir)
801
+ const formatTargetForShell = (targetDir) => {
802
+ const relative = path.relative(context.cwd, targetDir)
803
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
804
+ return relative || "."
805
+ }
806
+
807
+ return targetDir
808
+ }
809
+
810
+ printBanner("reset-framework-cli create-app", description)
811
+ const options = await collectCreateAppOptions(context, templates)
812
+
813
+ if (!templates.includes(options.templateName)) {
814
+ throw new Error(`Unknown template: ${options.templateName}`)
815
+ }
816
+
817
+ const targetFrontendDir =
818
+ options.frontendDir === "."
819
+ ? options.targetDir
820
+ : path.join(options.targetDir, options.frontendDir)
821
+
822
+ await ensureWritableTarget(options.targetDir, options.force)
823
+
824
+ printSection("Project")
825
+ printKeyValueTable([
826
+ ["Directory", options.targetDir],
827
+ ["Template", options.templateName],
828
+ ["Product", options.productName],
829
+ ["App ID", options.appId],
830
+ ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
831
+ ["Styling", options.styling],
832
+ ["Package manager", options.packageManager],
833
+ ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
834
+ ])
835
+
836
+ if (Boolean(context.flags["dry-run"])) {
837
+ console.log("")
838
+ printSection("Dry run")
839
+ printStatusTable([
840
+ ["plan", "Copy starter", `${options.sourceFrontendDir} -> ${targetFrontendDir}`],
841
+ ["plan", "Write README", path.join(options.targetDir, "README.md")],
842
+ ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
843
+ ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
844
+ ["plan", "Write app package", path.join(options.targetDir, "package.json")],
845
+ ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
846
+ ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
847
+ ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
848
+ ])
849
+ if (options.frontendDir !== ".") {
850
+ printStatusTable([
851
+ ["plan", "Write frontend runner", path.join(options.targetDir, "scripts", "run-frontend.mjs")]
852
+ ])
853
+ }
854
+ if (options.styling === "tailwindcss") {
855
+ printStatusTable([
856
+ ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
857
+ ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
858
+ ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
859
+ ])
860
+ }
861
+ if (options.installDependencies) {
862
+ printStatusTable([
863
+ ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${options.targetDir}`]
864
+ ])
865
+ }
866
+ return
867
+ }
868
+
869
+ console.log("")
870
+ const progress = createProgress(options.installDependencies ? 6 : 5, "Scaffold")
871
+
872
+ await mkdir(options.targetDir, { recursive: true })
873
+ progress.tick("Prepared project directory")
874
+
875
+ await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
876
+ progress.tick("Copied frontend starter")
877
+
878
+ await writeProjectFiles(options)
879
+ await writeRootPackageFiles(options)
880
+ progress.tick("Wrote project files")
881
+
882
+ await applyFrontendOverrides(targetFrontendDir, options)
883
+ progress.tick("Applied frontend package wiring")
884
+
885
+ const packageJsonPath = path.join(targetFrontendDir, "package.json")
886
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
887
+
888
+ if (options.frontendDir === ".") {
889
+ const currentScripts = { ...(frontendPackage.scripts ?? {}) }
890
+ frontendPackage.name = options.appName
891
+ frontendPackage.private = true
892
+ frontendPackage.version = "0.1.0"
893
+ frontendPackage.description = `${options.productName} desktop app`
894
+ frontendPackage.devDependencies = {
895
+ ...frontendPackage.devDependencies,
896
+ "reset-framework-cli": options.cliDependencySpec
897
+ }
898
+ frontendPackage.scripts = {
899
+ dev: "reset-framework-cli dev",
900
+ "dev:web": currentScripts.dev ?? "vite",
901
+ build: "reset-framework-cli build",
902
+ "build:web": currentScripts.build ?? "tsc -b && vite build",
903
+ package: "reset-framework-cli package",
904
+ doctor: "reset-framework-cli doctor",
905
+ lint: currentScripts.lint ?? "eslint .",
906
+ preview: currentScripts.preview ?? "vite preview"
907
+ }
908
+
909
+ if (options.packageManagerVersion) {
910
+ frontendPackage.packageManager = `${options.packageManager}@${options.packageManagerVersion}`
911
+ }
912
+ } else {
913
+ frontendPackage.name = `${options.appName}-frontend`
914
+ }
915
+
916
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
917
+ progress.tick("Finalized starter metadata")
918
+
919
+ if (options.installDependencies) {
920
+ await runCommand(
921
+ resolvePackageManagerCommand(options.packageManager),
922
+ getInstallCommandArgs(options.packageManager),
923
+ {
924
+ cwd: options.targetDir
925
+ }
926
+ )
927
+ progress.tick("Installed frontend dependencies")
928
+ }
929
+
930
+ console.log("")
931
+ printSection("Result")
932
+ printStatusTable([
933
+ ["done", "Project", options.targetDir],
934
+ ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && ${options.packageManager} run dev`]
935
+ ])
936
+ }