reset-framework-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +31 -0
  4. package/src/commands/build.js +115 -0
  5. package/src/commands/dev.js +160 -0
  6. package/src/commands/doctor.js +89 -0
  7. package/src/commands/init.js +629 -0
  8. package/src/commands/package.js +63 -0
  9. package/src/index.js +214 -0
  10. package/src/lib/context.js +66 -0
  11. package/src/lib/framework.js +55 -0
  12. package/src/lib/logger.js +11 -0
  13. package/src/lib/output.js +65 -0
  14. package/src/lib/process.js +165 -0
  15. package/src/lib/project.js +357 -0
  16. package/src/lib/toolchain.js +62 -0
  17. package/src/lib/ui.js +244 -0
  18. package/templates/basic/README.md +15 -0
  19. package/templates/basic/frontend/README.md +73 -0
  20. package/templates/basic/frontend/eslint.config.js +23 -0
  21. package/templates/basic/frontend/index.html +13 -0
  22. package/templates/basic/frontend/package.json +31 -0
  23. package/templates/basic/frontend/public/favicon.svg +1 -0
  24. package/templates/basic/frontend/public/icons.svg +24 -0
  25. package/templates/basic/frontend/src/App.css +138 -0
  26. package/templates/basic/frontend/src/App.tsx +72 -0
  27. package/templates/basic/frontend/src/assets/hero.png +0 -0
  28. package/templates/basic/frontend/src/assets/react.svg +1 -0
  29. package/templates/basic/frontend/src/assets/vite.svg +1 -0
  30. package/templates/basic/frontend/src/index.css +111 -0
  31. package/templates/basic/frontend/src/lib/reset.ts +16 -0
  32. package/templates/basic/frontend/src/main.tsx +10 -0
  33. package/templates/basic/frontend/tsconfig.app.json +24 -0
  34. package/templates/basic/frontend/tsconfig.json +7 -0
  35. package/templates/basic/frontend/tsconfig.node.json +26 -0
  36. package/templates/basic/frontend/vite.config.ts +6 -0
  37. package/templates/basic/reset.config.json +29 -0
@@ -0,0 +1,629 @@
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
+ getInstallCommandArgs,
7
+ resolvePackageManagerCommand,
8
+ runCommand
9
+ } from "../lib/process.js"
10
+ import {
11
+ makeAppMetadata,
12
+ resolveFrameworkPaths,
13
+ resolveSdkDependencySpec
14
+ } from "../lib/project.js"
15
+ import {
16
+ createProgress,
17
+ isInteractiveSession,
18
+ printBanner,
19
+ printKeyValueTable,
20
+ printSection,
21
+ printStatusTable,
22
+ promptConfirm,
23
+ promptSelect,
24
+ promptText,
25
+ withPromptSession
26
+ } from "../lib/ui.js"
27
+
28
+ const sdkRuntimeReexport = `export {
29
+ createResetClient,
30
+ getResetRuntime,
31
+ invoke,
32
+ invokeRaw,
33
+ isResetRuntimeAvailable,
34
+ ResetRuntimeUnavailableError,
35
+ } from '@reset-framework/sdk'
36
+
37
+ export type {
38
+ ResetInvokeError,
39
+ ResetInvokeResponse,
40
+ ResetInvokeSuccess,
41
+ ResetRuntime,
42
+ ResetRuntimeWindow,
43
+ } from '@reset-framework/sdk'
44
+ `
45
+
46
+ const standaloneViteConfig = `import { defineConfig } from 'vite'
47
+ import react from '@vitejs/plugin-react'
48
+
49
+ export default defineConfig({
50
+ plugins: [react()],
51
+ })
52
+ `
53
+
54
+ const tailwindViteConfig = `import { defineConfig } from 'vite'
55
+ import react from '@vitejs/plugin-react'
56
+ import tailwindcss from '@tailwindcss/vite'
57
+
58
+ export default defineConfig({
59
+ plugins: [react(), tailwindcss()],
60
+ })
61
+ `
62
+
63
+ const standaloneTsconfigApp = `{
64
+ "compilerOptions": {
65
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
66
+ "target": "ES2023",
67
+ "useDefineForClassFields": true,
68
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
69
+ "module": "ESNext",
70
+ "types": ["vite/client"],
71
+ "skipLibCheck": true,
72
+ "moduleResolution": "bundler",
73
+ "allowImportingTsExtensions": true,
74
+ "verbatimModuleSyntax": true,
75
+ "moduleDetection": "force",
76
+ "noEmit": true,
77
+ "jsx": "react-jsx",
78
+ "strict": true,
79
+ "noUnusedLocals": true,
80
+ "noUnusedParameters": true,
81
+ "erasableSyntaxOnly": true,
82
+ "noFallthroughCasesInSwitch": true,
83
+ "noUncheckedSideEffectImports": true
84
+ },
85
+ "include": ["src"]
86
+ }
87
+ `
88
+
89
+ const tailwindIndexCss = `@import "tailwindcss";
90
+
91
+ :root {
92
+ color-scheme: dark;
93
+ }
94
+
95
+ body {
96
+ margin: 0;
97
+ font-family: "Avenir Next", "Segoe UI", sans-serif;
98
+ background: #111111;
99
+ }
100
+ `
101
+
102
+ const tailwindAppTsx = `import { useEffect, useState } from 'react'
103
+ import { invoke } from './lib/reset'
104
+
105
+ function App() {
106
+ const [count, setCount] = useState(0)
107
+ const [appName, setAppName] = useState('Reset App')
108
+ const [platform, setPlatform] = useState('unknown')
109
+
110
+ useEffect(() => {
111
+ let cancelled = false
112
+
113
+ async function loadRuntimeInfo() {
114
+ try {
115
+ const [{ name }, { platform }] = await Promise.all([
116
+ invoke<{ name: string }>('app.getName'),
117
+ invoke<{ platform: string }>('runtime.platform'),
118
+ ])
119
+
120
+ if (cancelled) {
121
+ return
122
+ }
123
+
124
+ setAppName(name)
125
+ setPlatform(platform)
126
+ } catch (error) {
127
+ console.error('Failed to load runtime info', error)
128
+ }
129
+ }
130
+
131
+ loadRuntimeInfo()
132
+
133
+ return () => {
134
+ cancelled = true
135
+ }
136
+ }, [])
137
+
138
+ return (
139
+ <main className="min-h-screen bg-stone-950 text-stone-100">
140
+ <div className="mx-auto flex min-h-screen max-w-6xl flex-col px-6 py-12 lg:px-10">
141
+ <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">
142
+ {appName} on {platform}
143
+ </div>
144
+
145
+ <div className="mt-12 grid gap-12 lg:grid-cols-[minmax(0,1.4fr)_minmax(20rem,0.8fr)] lg:items-end">
146
+ <section>
147
+ <p className="text-sm font-medium uppercase tracking-[0.32em] text-amber-300">
148
+ Reset Framework
149
+ </p>
150
+ <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">
151
+ Tailwind-ready desktop apps with a native runtime underneath.
152
+ </h1>
153
+ <p className="mt-6 max-w-2xl text-lg leading-8 text-stone-300">
154
+ Edit the frontend, call native commands through the runtime bridge, and keep the
155
+ host layer out of the way while you build the actual product.
156
+ </p>
157
+ </section>
158
+
159
+ <section className="rounded-[2rem] border border-white/10 bg-white/5 p-6 shadow-2xl shadow-black/20 backdrop-blur">
160
+ <div className="flex items-center justify-between text-sm text-stone-400">
161
+ <span>Bridge</span>
162
+ <span>app.getName / runtime.platform</span>
163
+ </div>
164
+ <div className="mt-6 rounded-2xl border border-white/10 bg-black/30 p-5">
165
+ <div className="text-xs uppercase tracking-[0.24em] text-stone-500">
166
+ Counter
167
+ </div>
168
+ <div className="mt-4 text-4xl font-semibold text-white">{count}</div>
169
+ <button
170
+ 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"
171
+ onClick={() => setCount((value) => value + 1)}
172
+ >
173
+ Increment
174
+ </button>
175
+ </div>
176
+ </section>
177
+ </div>
178
+ </div>
179
+ </main>
180
+ )
181
+ }
182
+
183
+ export default App
184
+ `
185
+
186
+ export const description = "Scaffold a starter app with an interactive project setup"
187
+
188
+ function listTemplates(templatesDir) {
189
+ return readdirSync(templatesDir, { withFileTypes: true })
190
+ .filter((entry) => entry.isDirectory())
191
+ .map((entry) => entry.name)
192
+ .sort()
193
+ }
194
+
195
+ function getRootGitignore(frontendDir) {
196
+ if (frontendDir === ".") {
197
+ return `.reset
198
+ node_modules
199
+ dist
200
+ `
201
+ }
202
+
203
+ return `.reset
204
+ ${frontendDir}/node_modules
205
+ ${frontendDir}/dist
206
+ `
207
+ }
208
+
209
+ function resolveDefaultFrontendDir(context) {
210
+ if (typeof context.flags["frontend-dir"] === "string") {
211
+ return context.flags["frontend-dir"]
212
+ }
213
+
214
+ if (context.flags.flat) {
215
+ return "."
216
+ }
217
+
218
+ return "frontend"
219
+ }
220
+
221
+ function resolveDefaultStyling(context) {
222
+ if (context.flags.tailwind) {
223
+ return "tailwindcss"
224
+ }
225
+
226
+ if (context.flags["no-tailwind"]) {
227
+ return "css"
228
+ }
229
+
230
+ return "css"
231
+ }
232
+
233
+ function resolvePackageManagerOption(context) {
234
+ if (typeof context.flags["package-manager"] === "string") {
235
+ return context.flags["package-manager"]
236
+ }
237
+
238
+ return "npm"
239
+ }
240
+
241
+ function describeInstallCommand(packageManager) {
242
+ return [packageManager, ...getInstallCommandArgs(packageManager)].join(" ")
243
+ }
244
+
245
+ async function ensureWritableTarget(targetDir, force) {
246
+ if (!existsSync(targetDir)) {
247
+ return
248
+ }
249
+
250
+ const entries = await readdir(targetDir)
251
+
252
+ if (entries.length > 0 && !force) {
253
+ throw new Error(`Target directory is not empty: ${targetDir}. Use --force to continue.`)
254
+ }
255
+ }
256
+
257
+ async function copyFrontendTemplate(sourceFrontendDir, targetFrontendDir, options) {
258
+ const { force, frontendDir } = options
259
+ const flatLayout = frontendDir === "."
260
+
261
+ if (!flatLayout) {
262
+ await cp(sourceFrontendDir, targetFrontendDir, {
263
+ recursive: true,
264
+ force,
265
+ filter(source) {
266
+ const baseName = path.basename(source)
267
+ return (
268
+ baseName !== "node_modules" &&
269
+ baseName !== "dist" &&
270
+ baseName !== "bun.lockb" &&
271
+ baseName !== "reset.config.json"
272
+ )
273
+ }
274
+ })
275
+
276
+ return
277
+ }
278
+
279
+ await mkdir(targetFrontendDir, { recursive: true })
280
+
281
+ const entries = await readdir(sourceFrontendDir, { withFileTypes: true })
282
+ for (const entry of entries) {
283
+ if (["node_modules", "dist", "bun.lockb", "reset.config.json", ".gitignore", "README.md"].includes(entry.name)) {
284
+ continue
285
+ }
286
+
287
+ await cp(
288
+ path.join(sourceFrontendDir, entry.name),
289
+ path.join(targetFrontendDir, entry.name),
290
+ { recursive: true, force }
291
+ )
292
+ }
293
+ }
294
+
295
+ async function applyFrontendOverrides(frontendDir, options) {
296
+ await writeFile(path.join(frontendDir, "src", "lib", "reset.ts"), sdkRuntimeReexport, "utf8")
297
+ await writeFile(path.join(frontendDir, "tsconfig.app.json"), standaloneTsconfigApp, "utf8")
298
+
299
+ const packageJsonPath = path.join(frontendDir, "package.json")
300
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
301
+ frontendPackage.dependencies = {
302
+ ...frontendPackage.dependencies,
303
+ "@reset-framework/sdk": options.sdkDependencySpec
304
+ }
305
+
306
+ if (options.styling === "tailwindcss") {
307
+ frontendPackage.devDependencies = {
308
+ ...frontendPackage.devDependencies,
309
+ "@tailwindcss/vite": "latest",
310
+ tailwindcss: "latest"
311
+ }
312
+
313
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
314
+ await writeFile(path.join(frontendDir, "vite.config.ts"), tailwindViteConfig, "utf8")
315
+ await writeFile(path.join(frontendDir, "src", "index.css"), tailwindIndexCss, "utf8")
316
+ await writeFile(path.join(frontendDir, "src", "App.tsx"), tailwindAppTsx, "utf8")
317
+ return
318
+ }
319
+
320
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
321
+ await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
322
+ }
323
+
324
+ async function writeProjectFiles(options) {
325
+ const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
326
+
327
+ await writeFile(
328
+ path.join(options.targetDir, "reset.config.json"),
329
+ JSON.stringify(
330
+ {
331
+ ...templateConfig,
332
+ name: options.appName,
333
+ productName: options.productName,
334
+ appId: options.appId,
335
+ project: {
336
+ frontendDir: options.frontendDir,
337
+ styling: options.styling
338
+ },
339
+ window: {
340
+ ...templateConfig.window,
341
+ title: options.productName
342
+ }
343
+ },
344
+ null,
345
+ 2
346
+ ) + "\n",
347
+ "utf8"
348
+ )
349
+
350
+ await writeFile(
351
+ path.join(options.targetDir, ".gitignore"),
352
+ getRootGitignore(options.frontendDir),
353
+ "utf8"
354
+ )
355
+ }
356
+
357
+ async function collectCreateAppOptions(context, templates) {
358
+ const initialTarget = context.args[0] ?? "my-app"
359
+ const defaultTargetDir = path.resolve(context.cwd, initialTarget)
360
+ const defaultMetadata = makeAppMetadata(path.basename(defaultTargetDir))
361
+ const frameworkPaths = resolveFrameworkPaths()
362
+
363
+ const defaults = {
364
+ targetDir: defaultTargetDir,
365
+ templateName: typeof context.flags.template === "string" ? context.flags.template : "basic",
366
+ appName: defaultMetadata.name,
367
+ productName:
368
+ typeof context.flags["product-name"] === "string"
369
+ ? context.flags["product-name"]
370
+ : defaultMetadata.productName,
371
+ appId:
372
+ typeof context.flags["app-id"] === "string"
373
+ ? context.flags["app-id"]
374
+ : defaultMetadata.appId,
375
+ frontendDir: resolveDefaultFrontendDir(context),
376
+ styling: resolveDefaultStyling(context)
377
+ }
378
+
379
+ const interactive = isInteractiveSession() && !context.flags.yes && !context.flags["no-prompt"]
380
+
381
+ if (!interactive) {
382
+ if (!context.args[0]) {
383
+ throw new Error("Missing target directory. Use: reset-framework-cli create-app <directory>")
384
+ }
385
+
386
+ return {
387
+ ...defaults,
388
+ templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
389
+ templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
390
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
391
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
392
+ packageManager: resolvePackageManagerOption(context),
393
+ installDependencies: !context.flags["no-install"],
394
+ force: Boolean(context.flags.force)
395
+ }
396
+ }
397
+
398
+ const selected = await withPromptSession(async (rl) => {
399
+ const targetInput = await promptText(rl, {
400
+ label: "Project directory",
401
+ defaultValue: path.basename(defaults.targetDir),
402
+ validate(value) {
403
+ return value.trim() !== "" || "Enter a directory name."
404
+ }
405
+ })
406
+
407
+ const targetDir = path.resolve(context.cwd, targetInput)
408
+ const metadata = makeAppMetadata(path.basename(targetDir))
409
+
410
+ const templateName =
411
+ templates.length === 1
412
+ ? templates[0]
413
+ : await promptSelect(rl, {
414
+ label: "Template",
415
+ defaultValue: defaults.templateName,
416
+ choices: templates.map((template) => ({
417
+ value: template,
418
+ label: template,
419
+ description: "Starter project scaffold"
420
+ }))
421
+ })
422
+
423
+ const productName = await promptText(rl, {
424
+ label: "Product name",
425
+ defaultValue: defaults.productName || metadata.productName,
426
+ validate(value) {
427
+ return value.trim() !== "" || "Enter a product name."
428
+ }
429
+ })
430
+
431
+ const appId = await promptText(rl, {
432
+ label: "App ID",
433
+ defaultValue: defaults.appId || metadata.appId,
434
+ validate(value) {
435
+ return value.includes(".") || "Use a reverse-domain identifier such as com.example.my-app."
436
+ }
437
+ })
438
+
439
+ const frontendDir = await promptSelect(rl, {
440
+ label: "Frontend layout",
441
+ defaultValue: defaults.frontendDir === "." ? "." : "frontend",
442
+ choices: [
443
+ {
444
+ value: "frontend",
445
+ label: "Use a frontend/ subdirectory",
446
+ description: "Keeps the web app separate from config, output, and packaging files."
447
+ },
448
+ {
449
+ value: ".",
450
+ label: "Keep the frontend at project root",
451
+ description: "Flatter project layout with package.json and src/ directly in the app root."
452
+ }
453
+ ]
454
+ })
455
+
456
+ const styling = await promptSelect(rl, {
457
+ label: "Styling setup",
458
+ defaultValue: defaults.styling,
459
+ choices: [
460
+ {
461
+ value: "css",
462
+ label: "Vanilla CSS starter",
463
+ description: "Use the existing CSS-based starter with no extra styling dependency."
464
+ },
465
+ {
466
+ value: "tailwindcss",
467
+ label: "Tailwind CSS starter",
468
+ description: "Configure Tailwind CSS and swap the starter screen to utility classes."
469
+ }
470
+ ]
471
+ })
472
+
473
+ const installDependencies = await promptConfirm(rl, {
474
+ label: "Install frontend dependencies after scaffolding",
475
+ defaultValue: true
476
+ })
477
+
478
+ printSection("Summary")
479
+ printKeyValueTable([
480
+ ["Directory", targetDir],
481
+ ["Template", templateName],
482
+ ["Product", productName],
483
+ ["App ID", appId],
484
+ ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
485
+ ["Styling", styling],
486
+ ["Install", installDependencies ? "yes" : "no"]
487
+ ])
488
+ console.log("")
489
+
490
+ const confirmed = await promptConfirm(rl, {
491
+ label: "Create this project",
492
+ defaultValue: true
493
+ })
494
+
495
+ if (!confirmed) {
496
+ throw new Error("Create app cancelled.")
497
+ }
498
+
499
+ return {
500
+ targetDir,
501
+ templateName,
502
+ appName: metadata.name,
503
+ productName,
504
+ appId,
505
+ frontendDir,
506
+ styling,
507
+ installDependencies,
508
+ packageManager: resolvePackageManagerOption(context)
509
+ }
510
+ })
511
+
512
+ return {
513
+ ...selected,
514
+ templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
515
+ templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
516
+ sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
517
+ sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
518
+ force: Boolean(context.flags.force)
519
+ }
520
+ }
521
+
522
+ export async function run(context) {
523
+ const frameworkPaths = resolveFrameworkPaths()
524
+ const templates = listTemplates(frameworkPaths.templatesDir)
525
+ const formatTargetForShell = (targetDir) => {
526
+ const relative = path.relative(context.cwd, targetDir)
527
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
528
+ return relative || "."
529
+ }
530
+
531
+ return targetDir
532
+ }
533
+
534
+ printBanner("reset-framework-cli create-app", description)
535
+ const options = await collectCreateAppOptions(context, templates)
536
+
537
+ if (!templates.includes(options.templateName)) {
538
+ throw new Error(`Unknown template: ${options.templateName}`)
539
+ }
540
+
541
+ const targetFrontendDir =
542
+ options.frontendDir === "."
543
+ ? options.targetDir
544
+ : path.join(options.targetDir, options.frontendDir)
545
+
546
+ await ensureWritableTarget(options.targetDir, options.force)
547
+
548
+ printSection("Project")
549
+ printKeyValueTable([
550
+ ["Directory", options.targetDir],
551
+ ["Template", options.templateName],
552
+ ["Product", options.productName],
553
+ ["App ID", options.appId],
554
+ ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
555
+ ["Styling", options.styling],
556
+ ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
557
+ ])
558
+
559
+ if (Boolean(context.flags["dry-run"])) {
560
+ console.log("")
561
+ printSection("Dry run")
562
+ printStatusTable([
563
+ ["plan", "Copy starter", `${options.sourceFrontendDir} -> ${targetFrontendDir}`],
564
+ ["plan", "Write README", path.join(options.targetDir, "README.md")],
565
+ ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
566
+ ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
567
+ ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
568
+ ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
569
+ ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
570
+ ])
571
+ if (options.styling === "tailwindcss") {
572
+ printStatusTable([
573
+ ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
574
+ ["plan", "Write styles", path.join(targetFrontendDir, "src", "index.css")],
575
+ ["plan", "Write starter screen", path.join(targetFrontendDir, "src", "App.tsx")]
576
+ ])
577
+ }
578
+ if (options.installDependencies) {
579
+ printStatusTable([
580
+ ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${targetFrontendDir}`]
581
+ ])
582
+ }
583
+ return
584
+ }
585
+
586
+ console.log("")
587
+ const progress = createProgress(options.installDependencies ? 7 : 6, "Scaffold")
588
+
589
+ await mkdir(options.targetDir, { recursive: true })
590
+ progress.tick("Prepared project directory")
591
+
592
+ await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
593
+ progress.tick("Copied frontend starter")
594
+
595
+ await cp(path.join(options.templateDir, "README.md"), path.join(options.targetDir, "README.md"), {
596
+ force: options.force
597
+ })
598
+ progress.tick("Wrote project README")
599
+
600
+ await writeProjectFiles(options)
601
+ progress.tick("Wrote reset.config.json and root files")
602
+
603
+ await applyFrontendOverrides(targetFrontendDir, options)
604
+ progress.tick("Applied frontend package wiring")
605
+
606
+ const packageJsonPath = path.join(targetFrontendDir, "package.json")
607
+ const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
608
+ frontendPackage.name = options.appName
609
+ await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
610
+ progress.tick("Finalized starter metadata")
611
+
612
+ if (options.installDependencies) {
613
+ await runCommand(
614
+ resolvePackageManagerCommand(options.packageManager),
615
+ getInstallCommandArgs(options.packageManager),
616
+ {
617
+ cwd: targetFrontendDir
618
+ }
619
+ )
620
+ progress.tick("Installed frontend dependencies")
621
+ }
622
+
623
+ console.log("")
624
+ printSection("Result")
625
+ printStatusTable([
626
+ ["done", "Project", options.targetDir],
627
+ ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && reset-framework-cli dev`]
628
+ ])
629
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync } from "node:fs"
2
+
3
+ import { createMacOSZipArchive } from "../lib/output.js"
4
+ import {
5
+ assertAppProject,
6
+ loadResetConfig,
7
+ resolveAppOutputPaths,
8
+ resolveAppPaths
9
+ } from "../lib/project.js"
10
+ import {
11
+ createProgress,
12
+ printBanner,
13
+ printKeyValueTable,
14
+ printSection,
15
+ printStatusTable
16
+ } from "../lib/ui.js"
17
+
18
+ export const description = "Package the built desktop app into a distributable archive"
19
+
20
+ export async function run(context) {
21
+ const dryRun = Boolean(context.flags["dry-run"])
22
+ const appPaths = resolveAppPaths(context.projectRoot)
23
+ assertAppProject(appPaths)
24
+
25
+ const config = loadResetConfig(appPaths)
26
+ assertAppProject(appPaths, config)
27
+ const outputPaths = resolveAppOutputPaths(appPaths, config)
28
+
29
+ printBanner("reset-framework-cli package", description)
30
+ printSection("Project")
31
+ printKeyValueTable([
32
+ ["Desktop app", outputPaths.appBundlePath],
33
+ ["Archive", outputPaths.zipPath]
34
+ ])
35
+ console.log("")
36
+
37
+ printSection("Plan")
38
+ printStatusTable([
39
+ ["ready", "Archive", "Create a macOS zip package from the built app bundle"]
40
+ ])
41
+
42
+ if (!existsSync(outputPaths.appBundlePath) && !dryRun) {
43
+ throw new Error(
44
+ `Missing built app bundle at ${outputPaths.appBundlePath}. Run 'reset-framework-cli build' first.`
45
+ )
46
+ }
47
+
48
+ if (dryRun) {
49
+ console.log("")
50
+ printSection("Dry run")
51
+ printStatusTable([["plan", "Package", "No archive will be written"]])
52
+ return
53
+ }
54
+
55
+ console.log("")
56
+ const progress = createProgress(1, "Package")
57
+ await createMacOSZipArchive(outputPaths, { dryRun })
58
+ progress.tick("Archive created")
59
+
60
+ console.log("")
61
+ printSection("Result")
62
+ printStatusTable([["done", "Package", outputPaths.zipPath]])
63
+ }