reset-framework-cli 1.1.2 → 1.1.5

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