reset-framework-cli 1.0.1 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reset-framework-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Command-line tooling for Reset Framework.",
6
6
  "license": "MIT",
@@ -11,9 +11,9 @@
11
11
  "node": ">=20.19.0"
12
12
  },
13
13
  "dependencies": {
14
- "@reset-framework/native": "1.0.1",
15
- "@reset-framework/schema": "1.0.1",
16
- "@reset-framework/sdk": "1.0.1"
14
+ "@reset-framework/native": "1.1.0",
15
+ "@reset-framework/schema": "1.1.0",
16
+ "@reset-framework/sdk": "1.1.0"
17
17
  },
18
18
  "bin": {
19
19
  "reset-framework-cli": "src/index.js"
@@ -1,12 +1,13 @@
1
1
  import { buildFrameworkRuntime } from "../lib/framework.js"
2
2
  import { stageMacOSAppBundle } from "../lib/output.js"
3
- import { resolveNpmCommand, runCommand } from "../lib/process.js"
3
+ import { resolvePackageManagerCommand, runCommand } from "../lib/process.js"
4
4
  import {
5
5
  assertAppProject,
6
6
  assertFrameworkInstall,
7
7
  loadResetConfig,
8
8
  resolveAppOutputPaths,
9
9
  resolveAppPaths,
10
+ resolveAppPackageManager,
10
11
  resolveConfigPath,
11
12
  resolveFrontendDir,
12
13
  resolveFrameworkBuildPaths,
@@ -34,6 +35,7 @@ export async function run(context) {
34
35
  assertFrameworkInstall(frameworkPaths)
35
36
  const config = loadResetConfig(appPaths)
36
37
  assertAppProject(appPaths, config)
38
+ const packageManager = resolveAppPackageManager(appPaths, config)
37
39
  const outputPaths = resolveAppOutputPaths(appPaths, config)
38
40
  const configPath = resolveConfigPath(appPaths)
39
41
  const frontendDir = resolveFrontendDir(appPaths, config)
@@ -49,6 +51,7 @@ export async function run(context) {
49
51
  printKeyValueTable([
50
52
  ["Config", configPath],
51
53
  ["Frontend", frontendDir],
54
+ ["Package manager", packageManager],
52
55
  ["Output", outputPaths.appBundlePath]
53
56
  ])
54
57
  console.log("")
@@ -82,7 +85,7 @@ export async function run(context) {
82
85
  const progress = createProgress(steps.length, "Build")
83
86
 
84
87
  if (!skipFrontend) {
85
- await runCommand(resolveNpmCommand(), ["run", "build"], {
88
+ await runCommand(resolvePackageManagerCommand(packageManager), ["run", "build"], {
86
89
  cwd: frontendDir,
87
90
  dryRun
88
91
  })
@@ -1,7 +1,7 @@
1
1
  import { buildFrameworkRuntime } from "../lib/framework.js"
2
2
  import {
3
3
  registerChildCleanup,
4
- resolveNpmCommand,
4
+ resolvePackageManagerCommand,
5
5
  spawnCommand,
6
6
  waitForProcessExit,
7
7
  waitForUrl
@@ -11,6 +11,7 @@ import {
11
11
  assertFrameworkInstall,
12
12
  loadResetConfig,
13
13
  resolveAppPaths,
14
+ resolveAppPackageManager,
14
15
  resolveConfigPath,
15
16
  resolveDevServerOptions,
16
17
  resolveFrontendDir,
@@ -39,6 +40,7 @@ export async function run(context) {
39
40
  assertFrameworkInstall(frameworkPaths)
40
41
  const config = loadResetConfig(appPaths)
41
42
  assertAppProject(appPaths, config)
43
+ const packageManager = resolveAppPackageManager(appPaths, config)
42
44
  const devServer = resolveDevServerOptions(config)
43
45
  const configPath = resolveConfigPath(appPaths)
44
46
  const frontendDir = resolveFrontendDir(appPaths, config)
@@ -54,6 +56,7 @@ export async function run(context) {
54
56
  printKeyValueTable([
55
57
  ["Config", configPath],
56
58
  ["Frontend", frontendDir],
59
+ ["Package manager", packageManager],
57
60
  ["Dev URL", config.frontend.devUrl]
58
61
  ])
59
62
  console.log("")
@@ -97,7 +100,7 @@ export async function run(context) {
97
100
  const children = []
98
101
 
99
102
  if (!skipFrontend) {
100
- const frontendProcess = spawnCommand(resolveNpmCommand(), [
103
+ const frontendProcess = spawnCommand(resolvePackageManagerCommand(packageManager), [
101
104
  "run",
102
105
  "dev",
103
106
  "--",
@@ -3,12 +3,14 @@ import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises"
3
3
  import path from "node:path"
4
4
 
5
5
  import {
6
+ captureCommandOutput,
6
7
  getInstallCommandArgs,
7
8
  resolvePackageManagerCommand,
8
9
  runCommand
9
10
  } from "../lib/process.js"
10
11
  import {
11
12
  makeAppMetadata,
13
+ resolveCliDependencySpec,
12
14
  resolveFrameworkPaths,
13
15
  resolveSdkDependencySpec
14
16
  } from "../lib/project.js"
@@ -172,6 +174,84 @@ function App() {
172
174
  export default App
173
175
  `
174
176
 
177
+ const rootFrontendRunner = `import { readFileSync } from 'node:fs'
178
+ import path from 'node:path'
179
+ import { spawn } from 'node:child_process'
180
+
181
+ const script = process.argv[2]
182
+ if (!script) {
183
+ throw new Error('Missing frontend script name.')
184
+ }
185
+
186
+ const appRoot = process.cwd()
187
+ const config = JSON.parse(readFileSync(path.join(appRoot, 'reset.config.json'), 'utf8'))
188
+ const frontendDir =
189
+ typeof config?.project?.frontendDir === 'string' && config.project.frontendDir.trim() !== ''
190
+ ? path.resolve(appRoot, config.project.frontendDir)
191
+ : path.join(appRoot, 'frontend')
192
+
193
+ function resolvePackageManagerCommand() {
194
+ const packageJsonPath = path.join(appRoot, 'package.json')
195
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
196
+ const packageManager = typeof manifest.packageManager === 'string' ? manifest.packageManager.trim() : ''
197
+ const name = packageManager.split('@')[0]
198
+
199
+ if (name === 'bun') {
200
+ return process.platform === 'win32' ? 'bun.exe' : 'bun'
201
+ }
202
+
203
+ if (name === 'pnpm') {
204
+ return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
205
+ }
206
+
207
+ if (name === 'yarn') {
208
+ return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
209
+ }
210
+
211
+ if (name === 'npm') {
212
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
213
+ }
214
+
215
+ const candidates = [
216
+ ['bun.lock', process.platform === 'win32' ? 'bun.exe' : 'bun'],
217
+ ['bun.lockb', process.platform === 'win32' ? 'bun.exe' : 'bun'],
218
+ ['pnpm-lock.yaml', process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'],
219
+ ['yarn.lock', process.platform === 'win32' ? 'yarn.cmd' : 'yarn'],
220
+ ['package-lock.json', process.platform === 'win32' ? 'npm.cmd' : 'npm']
221
+ ]
222
+
223
+ for (const [fileName, command] of candidates) {
224
+ try {
225
+ readFileSync(path.join(appRoot, fileName), 'utf8')
226
+ return command
227
+ } catch {}
228
+ }
229
+
230
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm'
231
+ }
232
+
233
+ const command = resolvePackageManagerCommand()
234
+ const child = spawn(command, ['run', script, '--', ...process.argv.slice(3)], {
235
+ cwd: frontendDir,
236
+ stdio: 'inherit',
237
+ env: process.env
238
+ })
239
+
240
+ child.once('error', (error) => {
241
+ console.error(error)
242
+ process.exit(1)
243
+ })
244
+
245
+ child.once('exit', (code, signal) => {
246
+ if (signal) {
247
+ process.kill(process.pid, signal)
248
+ return
249
+ }
250
+
251
+ process.exit(code ?? 1)
252
+ })
253
+ `
254
+
175
255
  export const description = "Scaffold a starter app with an interactive project setup"
176
256
 
177
257
  function listTemplates(templatesDir) {
@@ -183,15 +263,84 @@ function listTemplates(templatesDir) {
183
263
 
184
264
  function getRootGitignore(frontendDir) {
185
265
  if (frontendDir === ".") {
186
- return `.reset
187
- node_modules
188
- dist
266
+ return `.DS_Store
267
+ Thumbs.db
268
+ .idea/
269
+ .vs/
270
+ .vscode/
271
+ .cache/
272
+ .reset/
273
+ .turbo/
274
+ build/
275
+ coverage/
276
+ dist/
277
+ node_modules/
278
+ npm-debug.log*
279
+ yarn-debug.log*
280
+ yarn-error.log*
281
+ .pnpm-debug.log*
282
+ *.log
283
+ *.zip
284
+ *.tgz
285
+ *.tsbuildinfo
286
+ *.dSYM/
287
+ .env
288
+ .env.*
289
+ .env.local
290
+ .env.*.local
291
+ .eslintcache
292
+ bun.lock
293
+ bun.lockb
294
+ compile_commands.json
295
+ CMakeUserPresets.json
296
+ CMakeCache.txt
297
+ CMakeFiles/
298
+ cmake_install.cmake
299
+ install_manifest.txt
300
+ cmake-build-*/
301
+ Testing/
302
+ scripts/local/
189
303
  `
190
304
  }
191
305
 
192
- return `.reset
193
- ${frontendDir}/node_modules
194
- ${frontendDir}/dist
306
+ return `.DS_Store
307
+ Thumbs.db
308
+ .idea/
309
+ .vs/
310
+ .vscode/
311
+ .cache/
312
+ .reset/
313
+ .turbo/
314
+ build/
315
+ coverage/
316
+ node_modules/
317
+ ${frontendDir}/node_modules/
318
+ ${frontendDir}/dist/
319
+ npm-debug.log*
320
+ yarn-debug.log*
321
+ yarn-error.log*
322
+ .pnpm-debug.log*
323
+ *.log
324
+ *.zip
325
+ *.tgz
326
+ *.tsbuildinfo
327
+ *.dSYM/
328
+ .env
329
+ .env.*
330
+ .env.local
331
+ .env.*.local
332
+ .eslintcache
333
+ bun.lock
334
+ bun.lockb
335
+ compile_commands.json
336
+ CMakeUserPresets.json
337
+ CMakeCache.txt
338
+ CMakeFiles/
339
+ cmake_install.cmake
340
+ install_manifest.txt
341
+ cmake-build-*/
342
+ Testing/
343
+ scripts/local/
195
344
  `
196
345
  }
197
346
 
@@ -310,6 +459,91 @@ async function applyFrontendOverrides(frontendDir, options) {
310
459
  await writeFile(path.join(frontendDir, "vite.config.ts"), standaloneViteConfig, "utf8")
311
460
  }
312
461
 
462
+ function createRootPackageJson(options) {
463
+ const packageManagerVersion = options.packageManagerVersion
464
+ ? `${options.packageManager}@${options.packageManagerVersion}`
465
+ : undefined
466
+
467
+ const base = {
468
+ name: options.appName,
469
+ private: true,
470
+ version: "0.1.0",
471
+ description: `${options.productName} desktop app`,
472
+ scripts: {
473
+ dev: "reset-framework-cli dev",
474
+ build: "reset-framework-cli build",
475
+ package: "reset-framework-cli package",
476
+ doctor: "reset-framework-cli doctor"
477
+ },
478
+ devDependencies: {
479
+ "reset-framework-cli": options.cliDependencySpec
480
+ }
481
+ }
482
+
483
+ if (packageManagerVersion) {
484
+ base.packageManager = packageManagerVersion
485
+ }
486
+
487
+ if (options.frontendDir !== ".") {
488
+ base.workspaces = [options.frontendDir]
489
+ base.scripts["dev:web"] = "node ./scripts/run-frontend.mjs dev"
490
+ base.scripts["build:web"] = "node ./scripts/run-frontend.mjs build"
491
+ base.scripts["lint:web"] = "node ./scripts/run-frontend.mjs lint"
492
+ base.scripts["preview:web"] = "node ./scripts/run-frontend.mjs preview"
493
+ }
494
+
495
+ return base
496
+ }
497
+
498
+ function createRootReadme(options) {
499
+ const run = `${options.packageManager} run`
500
+ const webPath = options.frontendDir === "." ? "project root" : `${options.frontendDir}/`
501
+
502
+ return `# ${options.productName}
503
+
504
+ Generated with \`reset-framework-cli\`.
505
+
506
+ ## Commands
507
+
508
+ - \`${run} dev\`: start the frontend dev server and native desktop runtime
509
+ - \`${run} build\`: build the frontend and stage the desktop app bundle
510
+ - \`${run} package\`: archive the built desktop app
511
+ - \`${run} doctor\`: inspect the project and framework installation
512
+
513
+ ## Web-only commands
514
+
515
+ ${options.frontendDir === "." ? `- \`${run} dev:web\`: run the Vite frontend only
516
+ - \`${run} build:web\`: build the frontend only` : `- \`${run} dev:web\`: run the frontend workspace only
517
+ - \`${run} build:web\`: build the frontend workspace only`}
518
+
519
+ ## Project files
520
+
521
+ - \`reset.config.json\`: desktop app metadata and runtime configuration
522
+ - \`${webPath}\`: web frontend source
523
+ - \`.reset/\`: generated build output and native runtime cache
524
+ `
525
+ }
526
+
527
+ async function writeRootPackageFiles(options) {
528
+ await writeFile(
529
+ path.join(options.targetDir, "README.md"),
530
+ createRootReadme(options),
531
+ "utf8"
532
+ )
533
+
534
+ if (options.frontendDir !== ".") {
535
+ await writeFile(
536
+ path.join(options.targetDir, "package.json"),
537
+ JSON.stringify(createRootPackageJson(options), null, 2) + "\n",
538
+ "utf8"
539
+ )
540
+
541
+ const scriptsDir = path.join(options.targetDir, "scripts")
542
+ await mkdir(scriptsDir, { recursive: true })
543
+ await writeFile(path.join(scriptsDir, "run-frontend.mjs"), rootFrontendRunner, "utf8")
544
+ }
545
+ }
546
+
313
547
  async function writeProjectFiles(options) {
314
548
  const templateConfig = JSON.parse(await readFile(options.templateConfigPath, "utf8"))
315
549
 
@@ -343,6 +577,20 @@ async function writeProjectFiles(options) {
343
577
  )
344
578
  }
345
579
 
580
+ async function resolvePackageManagerVersion(packageManager) {
581
+ if (packageManager !== "npm" && packageManager !== "pnpm" && packageManager !== "yarn" && packageManager !== "bun") {
582
+ return null
583
+ }
584
+
585
+ try {
586
+ const command = resolvePackageManagerCommand(packageManager)
587
+ const result = await captureCommandOutput(command, ["--version"])
588
+ return typeof result === "string" && result.trim() !== "" ? result.trim() : null
589
+ } catch {
590
+ return null
591
+ }
592
+ }
593
+
346
594
  async function collectCreateAppOptions(context, templates) {
347
595
  const initialTarget = context.args[0] ?? "my-app"
348
596
  const defaultTargetDir = path.resolve(context.cwd, initialTarget)
@@ -377,8 +625,10 @@ async function collectCreateAppOptions(context, templates) {
377
625
  templateDir: path.join(frameworkPaths.templatesDir, defaults.templateName),
378
626
  templateConfigPath: path.join(frameworkPaths.templatesDir, defaults.templateName, "reset.config.json"),
379
627
  sourceFrontendDir: path.join(frameworkPaths.templatesDir, defaults.templateName, "frontend"),
628
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
380
629
  sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
381
630
  packageManager: resolvePackageManagerOption(context),
631
+ packageManagerVersion: await resolvePackageManagerVersion(resolvePackageManagerOption(context)),
382
632
  installDependencies: !context.flags["no-install"],
383
633
  force: Boolean(context.flags.force)
384
634
  }
@@ -464,6 +714,33 @@ async function collectCreateAppOptions(context, templates) {
464
714
  defaultValue: true
465
715
  })
466
716
 
717
+ const packageManager = await promptSelect(rl, {
718
+ label: "Package manager",
719
+ defaultValue: resolvePackageManagerOption(context),
720
+ choices: [
721
+ {
722
+ value: "npm",
723
+ label: "npm",
724
+ description: "Wide compatibility and predictable installs."
725
+ },
726
+ {
727
+ value: "bun",
728
+ label: "bun",
729
+ description: "Fast installs and lets bun run the root desktop scripts."
730
+ },
731
+ {
732
+ value: "pnpm",
733
+ label: "pnpm",
734
+ description: "Strict dependency graph with workspace support."
735
+ },
736
+ {
737
+ value: "yarn",
738
+ label: "yarn",
739
+ description: "Classic workspace-based JavaScript package workflow."
740
+ }
741
+ ]
742
+ })
743
+
467
744
  printSection("Summary")
468
745
  printKeyValueTable([
469
746
  ["Directory", targetDir],
@@ -472,6 +749,7 @@ async function collectCreateAppOptions(context, templates) {
472
749
  ["App ID", appId],
473
750
  ["Frontend", frontendDir === "." ? "project root" : `${frontendDir}/`],
474
751
  ["Styling", styling],
752
+ ["Package manager", packageManager],
475
753
  ["Install", installDependencies ? "yes" : "no"]
476
754
  ])
477
755
  console.log("")
@@ -494,7 +772,7 @@ async function collectCreateAppOptions(context, templates) {
494
772
  frontendDir,
495
773
  styling,
496
774
  installDependencies,
497
- packageManager: resolvePackageManagerOption(context)
775
+ packageManager
498
776
  }
499
777
  })
500
778
 
@@ -503,7 +781,9 @@ async function collectCreateAppOptions(context, templates) {
503
781
  templateDir: path.join(frameworkPaths.templatesDir, selected.templateName),
504
782
  templateConfigPath: path.join(frameworkPaths.templatesDir, selected.templateName, "reset.config.json"),
505
783
  sourceFrontendDir: path.join(frameworkPaths.templatesDir, selected.templateName, "frontend"),
784
+ cliDependencySpec: resolveCliDependencySpec(frameworkPaths),
506
785
  sdkDependencySpec: resolveSdkDependencySpec(frameworkPaths),
786
+ packageManagerVersion: await resolvePackageManagerVersion(selected.packageManager),
507
787
  force: Boolean(context.flags.force)
508
788
  }
509
789
  }
@@ -542,6 +822,7 @@ export async function run(context) {
542
822
  ["App ID", options.appId],
543
823
  ["Frontend", options.frontendDir === "." ? "project root" : `${options.frontendDir}/`],
544
824
  ["Styling", options.styling],
825
+ ["Package manager", options.packageManager],
545
826
  ["Install", options.installDependencies ? describeInstallCommand(options.packageManager) : "skipped"]
546
827
  ])
547
828
 
@@ -553,10 +834,16 @@ export async function run(context) {
553
834
  ["plan", "Write README", path.join(options.targetDir, "README.md")],
554
835
  ["plan", "Write config", path.join(options.targetDir, "reset.config.json")],
555
836
  ["plan", "Write ignore file", path.join(options.targetDir, ".gitignore")],
837
+ ["plan", "Write app package", path.join(options.targetDir, "package.json")],
556
838
  ["plan", "Write SDK bridge", path.join(targetFrontendDir, "src", "lib", "reset.ts")],
557
839
  ["plan", "Write Vite config", path.join(targetFrontendDir, "vite.config.ts")],
558
840
  ["plan", "Write TS config", path.join(targetFrontendDir, "tsconfig.app.json")]
559
841
  ])
842
+ if (options.frontendDir !== ".") {
843
+ printStatusTable([
844
+ ["plan", "Write frontend runner", path.join(options.targetDir, "scripts", "run-frontend.mjs")]
845
+ ])
846
+ }
560
847
  if (options.styling === "tailwindcss") {
561
848
  printStatusTable([
562
849
  ["plan", "Patch package.json", path.join(targetFrontendDir, "package.json")],
@@ -566,14 +853,14 @@ export async function run(context) {
566
853
  }
567
854
  if (options.installDependencies) {
568
855
  printStatusTable([
569
- ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${targetFrontendDir}`]
856
+ ["plan", "Install dependencies", `${describeInstallCommand(options.packageManager)} in ${options.targetDir}`]
570
857
  ])
571
858
  }
572
859
  return
573
860
  }
574
861
 
575
862
  console.log("")
576
- const progress = createProgress(options.installDependencies ? 7 : 6, "Scaffold")
863
+ const progress = createProgress(options.installDependencies ? 6 : 5, "Scaffold")
577
864
 
578
865
  await mkdir(options.targetDir, { recursive: true })
579
866
  progress.tick("Prepared project directory")
@@ -581,20 +868,44 @@ export async function run(context) {
581
868
  await copyFrontendTemplate(options.sourceFrontendDir, targetFrontendDir, options)
582
869
  progress.tick("Copied frontend starter")
583
870
 
584
- await cp(path.join(options.templateDir, "README.md"), path.join(options.targetDir, "README.md"), {
585
- force: options.force
586
- })
587
- progress.tick("Wrote project README")
588
-
589
871
  await writeProjectFiles(options)
590
- progress.tick("Wrote reset.config.json and root files")
872
+ await writeRootPackageFiles(options)
873
+ progress.tick("Wrote project files")
591
874
 
592
875
  await applyFrontendOverrides(targetFrontendDir, options)
593
876
  progress.tick("Applied frontend package wiring")
594
877
 
595
878
  const packageJsonPath = path.join(targetFrontendDir, "package.json")
596
879
  const frontendPackage = JSON.parse(await readFile(packageJsonPath, "utf8"))
597
- frontendPackage.name = options.appName
880
+
881
+ if (options.frontendDir === ".") {
882
+ const currentScripts = { ...(frontendPackage.scripts ?? {}) }
883
+ frontendPackage.name = options.appName
884
+ frontendPackage.private = true
885
+ frontendPackage.version = "0.1.0"
886
+ frontendPackage.description = `${options.productName} desktop app`
887
+ frontendPackage.devDependencies = {
888
+ ...frontendPackage.devDependencies,
889
+ "reset-framework-cli": options.cliDependencySpec
890
+ }
891
+ frontendPackage.scripts = {
892
+ dev: "reset-framework-cli dev",
893
+ "dev:web": currentScripts.dev ?? "vite",
894
+ build: "reset-framework-cli build",
895
+ "build:web": currentScripts.build ?? "tsc -b && vite build",
896
+ package: "reset-framework-cli package",
897
+ doctor: "reset-framework-cli doctor",
898
+ lint: currentScripts.lint ?? "eslint .",
899
+ preview: currentScripts.preview ?? "vite preview"
900
+ }
901
+
902
+ if (options.packageManagerVersion) {
903
+ frontendPackage.packageManager = `${options.packageManager}@${options.packageManagerVersion}`
904
+ }
905
+ } else {
906
+ frontendPackage.name = `${options.appName}-frontend`
907
+ }
908
+
598
909
  await writeFile(packageJsonPath, JSON.stringify(frontendPackage, null, 2) + "\n", "utf8")
599
910
  progress.tick("Finalized starter metadata")
600
911
 
@@ -603,7 +914,7 @@ export async function run(context) {
603
914
  resolvePackageManagerCommand(options.packageManager),
604
915
  getInstallCommandArgs(options.packageManager),
605
916
  {
606
- cwd: targetFrontendDir
917
+ cwd: options.targetDir
607
918
  }
608
919
  )
609
920
  progress.tick("Installed frontend dependencies")
@@ -613,6 +924,6 @@ export async function run(context) {
613
924
  printSection("Result")
614
925
  printStatusTable([
615
926
  ["done", "Project", options.targetDir],
616
- ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && reset-framework-cli dev`]
927
+ ["done", "Next step", `cd ${formatTargetForShell(options.targetDir)} && ${options.packageManager} run dev`]
617
928
  ])
618
929
  }
package/src/lib/output.js CHANGED
@@ -1,9 +1,99 @@
1
- import { cp, mkdir, rm } from "node:fs/promises"
1
+ import path from "node:path"
2
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises"
2
3
  import { existsSync } from "node:fs"
3
4
 
4
5
  import { logger } from "./logger.js"
5
6
  import { runCommand } from "./process.js"
6
7
 
8
+ function escapePlistString(value) {
9
+ return value
10
+ .replaceAll("&", "&")
11
+ .replaceAll("<", "&lt;")
12
+ .replaceAll(">", "&gt;")
13
+ }
14
+
15
+ function serializePlistValue(value, indentLevel = 1) {
16
+ const indent = " ".repeat(indentLevel)
17
+
18
+ if (Array.isArray(value)) {
19
+ if (value.length === 0) {
20
+ return `${indent}<array/>\n`
21
+ }
22
+
23
+ return (
24
+ `${indent}<array>\n` +
25
+ value.map((entry) => serializePlistValue(entry, indentLevel + 1)).join("") +
26
+ `${indent}</array>\n`
27
+ )
28
+ }
29
+
30
+ if (typeof value === "object" && value !== null) {
31
+ const entries = Object.entries(value)
32
+ if (entries.length === 0) {
33
+ return `${indent}<dict/>\n`
34
+ }
35
+
36
+ return (
37
+ `${indent}<dict>\n` +
38
+ entries
39
+ .map(
40
+ ([key, entryValue]) =>
41
+ `${" ".repeat(indentLevel + 1)}<key>${escapePlistString(key)}</key>\n${serializePlistValue(entryValue, indentLevel + 1)}`
42
+ )
43
+ .join("") +
44
+ `${indent}</dict>\n`
45
+ )
46
+ }
47
+
48
+ if (typeof value === "boolean") {
49
+ return `${indent}<${value ? "true" : "false"}/>\n`
50
+ }
51
+
52
+ if (typeof value === "number") {
53
+ return `${indent}<integer>${value}</integer>\n`
54
+ }
55
+
56
+ return `${indent}<string>${escapePlistString(String(value))}</string>\n`
57
+ }
58
+
59
+ function buildMacOSInfoPlist(config) {
60
+ const urlTypes = Array.isArray(config.protocols?.schemes)
61
+ ? config.protocols.schemes.map((schemeConfig) => ({
62
+ CFBundleTypeRole: schemeConfig.role ?? "Viewer",
63
+ CFBundleURLName:
64
+ typeof schemeConfig.name === "string" && schemeConfig.name.trim() !== ""
65
+ ? schemeConfig.name
66
+ : `${config.appId}.${schemeConfig.scheme}`,
67
+ CFBundleURLSchemes: [schemeConfig.scheme]
68
+ }))
69
+ : []
70
+
71
+ const plist = {
72
+ CFBundleDevelopmentRegion: "English",
73
+ CFBundleDisplayName: config.productName,
74
+ CFBundleExecutable: "reset-framework",
75
+ CFBundleIdentifier: config.appId,
76
+ CFBundleInfoDictionaryVersion: "6.0",
77
+ CFBundleName: config.productName,
78
+ CFBundlePackageType: "APPL",
79
+ CFBundleShortVersionString: config.version,
80
+ CFBundleVersion: config.version,
81
+ CSResourcesFileMapped: true
82
+ }
83
+
84
+ if (urlTypes.length > 0) {
85
+ plist.CFBundleURLTypes = urlTypes
86
+ }
87
+
88
+ return (
89
+ `<?xml version="1.0" encoding="UTF-8"?>\n` +
90
+ `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n` +
91
+ `<plist version="1.0">\n` +
92
+ serializePlistValue(plist, 0) +
93
+ `</plist>\n`
94
+ )
95
+ }
96
+
7
97
  export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
8
98
  const { dryRun = false, outputPaths, configPath } = options
9
99
 
@@ -40,6 +130,11 @@ export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config,
40
130
  await cp(configPath, outputPaths.bundledConfigPath, { force: true })
41
131
  await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
42
132
  await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
133
+ await writeFile(
134
+ path.join(outputPaths.appBundlePath, "Contents", "Info.plist"),
135
+ buildMacOSInfoPlist(config),
136
+ "utf8"
137
+ )
43
138
 
44
139
  return outputPaths
45
140
  }
@@ -82,6 +82,47 @@ export async function runCommand(command, args = [], options = {}) {
82
82
  })
83
83
  }
84
84
 
85
+ export async function captureCommandOutput(command, args = [], options = {}) {
86
+ const { cwd, env } = options
87
+
88
+ await Promise.resolve()
89
+
90
+ return await new Promise((resolve, reject) => {
91
+ const child = spawn(command, args, {
92
+ cwd,
93
+ env: env ? { ...process.env, ...env } : process.env,
94
+ stdio: ["ignore", "pipe", "pipe"]
95
+ })
96
+
97
+ let stdout = ""
98
+ let stderr = ""
99
+
100
+ child.stdout?.on("data", (chunk) => {
101
+ stdout += String(chunk)
102
+ })
103
+
104
+ child.stderr?.on("data", (chunk) => {
105
+ stderr += String(chunk)
106
+ })
107
+
108
+ child.once("error", reject)
109
+ child.once("exit", (code, signal) => {
110
+ if (code === 0) {
111
+ resolve(stdout.trim())
112
+ return
113
+ }
114
+
115
+ reject(
116
+ new Error(
117
+ signal
118
+ ? `Command terminated by signal: ${signal}`
119
+ : (stderr.trim() || stdout.trim() || `Command exited with code ${code}`)
120
+ )
121
+ )
122
+ })
123
+ })
124
+ }
125
+
85
126
  export function spawnCommand(command, args = [], options = {}) {
86
127
  const { cwd, dryRun = false, env } = options
87
128
 
@@ -76,6 +76,29 @@ function normalizeStyling(value) {
76
76
  throw new Error("project.styling must be either 'css' or 'tailwindcss'")
77
77
  }
78
78
 
79
+ function normalizeProtocolScheme(value) {
80
+ if (typeof value !== "string" || value.trim() === "") {
81
+ throw new Error("protocols.schemes[].scheme must be a non-empty string")
82
+ }
83
+
84
+ const normalized = value.trim().toLowerCase()
85
+ if (!/^[a-z][a-z0-9+.-]*$/.test(normalized)) {
86
+ throw new Error(
87
+ "protocols.schemes[].scheme must start with a letter and only contain letters, numbers, '+', '-', and '.'"
88
+ )
89
+ }
90
+
91
+ return normalized
92
+ }
93
+
94
+ function normalizeProtocolRole(value) {
95
+ if (value === "Editor" || value === "Viewer" || value === "Shell" || value === "None") {
96
+ return value
97
+ }
98
+
99
+ throw new Error("protocols.schemes[].role must be one of Editor, Viewer, Shell, None")
100
+ }
101
+
79
102
  function toTitleCase(value) {
80
103
  return value
81
104
  .split(/[^a-zA-Z0-9]+/)
@@ -88,6 +111,10 @@ function readJsonFile(filePath) {
88
111
  return JSON.parse(readFileSync(filePath, "utf8"))
89
112
  }
90
113
 
114
+ function fileExists(filePath) {
115
+ return existsSync(filePath)
116
+ }
117
+
91
118
  function isPathInside(parentDir, candidatePath) {
92
119
  const relative = path.relative(parentDir, candidatePath)
93
120
  return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
@@ -142,6 +169,8 @@ export function resolveFrameworkPaths() {
142
169
  const moduleDir = path.dirname(fileURLToPath(import.meta.url))
143
170
  const cliDir = path.resolve(moduleDir, "../..")
144
171
  const packagesDir = path.resolve(cliDir, "..")
172
+ const cliPackageJsonPath = path.join(cliDir, "package.json")
173
+ const cliManifest = readJsonFile(cliPackageJsonPath)
145
174
  const nativePackage = resolveWorkspacePackageInfo(
146
175
  "@reset-framework/native",
147
176
  path.join(packagesDir, "native"),
@@ -163,6 +192,13 @@ export function resolveFrameworkPaths() {
163
192
 
164
193
  return {
165
194
  cliDir,
195
+ cliPackage: {
196
+ packageName: cliManifest.name,
197
+ packageRoot: cliDir,
198
+ packageJsonPath: cliPackageJsonPath,
199
+ version: cliManifest.version ?? "0.0.0",
200
+ localFallback: true
201
+ },
166
202
  packagesDir,
167
203
  frameworkRoot: nativePackage.packageRoot,
168
204
  frameworkPackage: nativePackage,
@@ -207,6 +243,7 @@ export function resolveFrameworkBuildPaths(appPaths) {
207
243
  export function resolveAppPaths(appRoot) {
208
244
  return {
209
245
  appRoot,
246
+ appPackageJsonPath: path.join(appRoot, "package.json"),
210
247
  frontendDir: path.join(appRoot, "frontend"),
211
248
  resetConfigPath: path.join(appRoot, "reset.config.json"),
212
249
  legacyResetConfigPath: path.join(appRoot, "frontend", "reset.config.json")
@@ -276,7 +313,10 @@ export function validateResetConfig(rawConfig) {
276
313
  const build = optionalObject(rawConfig, "build")
277
314
  const project = optionalObject(rawConfig, "project")
278
315
  const security = optionalObject(rawConfig, "security")
316
+ const protocols = optionalObject(rawConfig, "protocols")
279
317
  const windowConfig = optionalObject(rawConfig, "window")
318
+ const rawProtocolSchemes = Array.isArray(protocols.schemes) ? protocols.schemes : []
319
+ const seenProtocolSchemes = new Set()
280
320
 
281
321
  return {
282
322
  ...rawConfig,
@@ -309,6 +349,32 @@ export function validateResetConfig(rawConfig) {
309
349
  return value
310
350
  })
311
351
  : []
352
+ },
353
+ protocols: {
354
+ schemes: rawProtocolSchemes.map((entry) => {
355
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
356
+ throw new Error("protocols.schemes entries must be objects")
357
+ }
358
+
359
+ const scheme = normalizeProtocolScheme(entry.scheme)
360
+ if (seenProtocolSchemes.has(scheme)) {
361
+ throw new Error(`Duplicate protocol scheme '${scheme}'`)
362
+ }
363
+
364
+ seenProtocolSchemes.add(scheme)
365
+
366
+ return {
367
+ scheme,
368
+ name:
369
+ typeof entry.name === "string" && entry.name.trim() !== ""
370
+ ? entry.name
371
+ : scheme,
372
+ role:
373
+ entry.role === undefined
374
+ ? "Viewer"
375
+ : normalizeProtocolRole(entry.role)
376
+ }
377
+ })
312
378
  }
313
379
  }
314
380
  }
@@ -388,3 +454,67 @@ export function resolveSdkDependencySpec(frameworkPaths) {
388
454
 
389
455
  return `^${frameworkPaths.sdkPackage.version}`
390
456
  }
457
+
458
+ export function resolveCliDependencySpec(frameworkPaths) {
459
+ if (frameworkPaths.isWorkspaceLayout || frameworkPaths.cliPackage.localFallback) {
460
+ return `file:${frameworkPaths.cliPackage.packageRoot}`
461
+ }
462
+
463
+ return `^${frameworkPaths.cliPackage.version}`
464
+ }
465
+
466
+ function detectPackageManagerFromManifest(packageJsonPath) {
467
+ if (!fileExists(packageJsonPath)) {
468
+ return null
469
+ }
470
+
471
+ const manifest = readJsonFile(packageJsonPath)
472
+ const raw = typeof manifest.packageManager === "string" ? manifest.packageManager.trim() : ""
473
+ if (raw === "") {
474
+ return null
475
+ }
476
+
477
+ const [name] = raw.split("@")
478
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") {
479
+ return name
480
+ }
481
+
482
+ return null
483
+ }
484
+
485
+ function detectPackageManagerFromLocks(directory) {
486
+ if (!fileExists(directory)) {
487
+ return null
488
+ }
489
+
490
+ if (fileExists(path.join(directory, "bun.lock")) || fileExists(path.join(directory, "bun.lockb"))) {
491
+ return "bun"
492
+ }
493
+
494
+ if (fileExists(path.join(directory, "pnpm-lock.yaml"))) {
495
+ return "pnpm"
496
+ }
497
+
498
+ if (fileExists(path.join(directory, "yarn.lock"))) {
499
+ return "yarn"
500
+ }
501
+
502
+ if (fileExists(path.join(directory, "package-lock.json"))) {
503
+ return "npm"
504
+ }
505
+
506
+ return null
507
+ }
508
+
509
+ export function resolveAppPackageManager(appPaths, config) {
510
+ const frontendDir = resolveFrontendDir(appPaths, config)
511
+ const frontendPackageJsonPath = path.join(frontendDir, "package.json")
512
+
513
+ return (
514
+ detectPackageManagerFromManifest(appPaths.appPackageJsonPath) ??
515
+ detectPackageManagerFromManifest(frontendPackageJsonPath) ??
516
+ detectPackageManagerFromLocks(appPaths.appRoot) ??
517
+ detectPackageManagerFromLocks(frontendDir) ??
518
+ "npm"
519
+ )
520
+ }
@@ -51,5 +51,8 @@
51
51
  "updater.*",
52
52
  "net.*"
53
53
  ]
54
+ },
55
+ "protocols": {
56
+ "schemes": []
54
57
  }
55
58
  }