reset-framework-cli 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +19 -19
  3. package/package.json +8 -6
  4. package/src/commands/build.js +91 -73
  5. package/src/commands/dev.js +143 -122
  6. package/src/commands/doctor.js +61 -54
  7. package/src/commands/init.js +946 -866
  8. package/src/commands/package.js +68 -68
  9. package/src/index.js +195 -195
  10. package/src/lib/backend.js +123 -0
  11. package/src/lib/context.js +66 -66
  12. package/src/lib/framework.js +57 -57
  13. package/src/lib/logger.js +11 -11
  14. package/src/lib/output.js +251 -236
  15. package/src/lib/process.js +303 -303
  16. package/src/lib/project.js +531 -494
  17. package/src/lib/toolchain.js +62 -62
  18. package/src/lib/ui.js +244 -244
  19. package/templates/basic/README.md +15 -15
  20. package/templates/basic/frontend/README.md +73 -73
  21. package/templates/basic/frontend/eslint.config.js +23 -23
  22. package/templates/basic/frontend/index.html +13 -13
  23. package/templates/basic/frontend/package.json +31 -31
  24. package/templates/basic/frontend/public/icons.svg +24 -24
  25. package/templates/basic/frontend/src/App.css +216 -216
  26. package/templates/basic/frontend/src/App.tsx +77 -77
  27. package/templates/basic/frontend/src/assets/vite.svg +1 -1
  28. package/templates/basic/frontend/src/index.css +111 -111
  29. package/templates/basic/frontend/src/lib/reset.ts +1 -1
  30. package/templates/basic/frontend/src/main.tsx +10 -10
  31. package/templates/basic/frontend/tsconfig.app.json +28 -28
  32. package/templates/basic/frontend/tsconfig.json +7 -7
  33. package/templates/basic/frontend/tsconfig.node.json +26 -26
  34. package/templates/basic/frontend/vite.config.ts +16 -16
  35. package/templates/basic/reset.config.json +58 -58
@@ -1,8 +1,8 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { createRequire } from "node:module"
3
- import path from "node:path"
4
- import { fileURLToPath } from "node:url"
5
-
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { createRequire } from "node:module"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
6
  const require = createRequire(import.meta.url)
7
7
 
8
8
  const runtimePackageDescriptors = [
@@ -31,166 +31,166 @@ const runtimePackageDescriptors = [
31
31
  binarySegments: ["dist", "win32-x64", "reset-framework.exe"]
32
32
  }
33
33
  ]
34
-
35
- function requireString(object, key, scope) {
36
- if (typeof object?.[key] !== "string" || object[key].trim() === "") {
37
- throw new Error(`Missing or invalid string field '${scope}.${key}'`)
38
- }
39
-
40
- return object[key]
41
- }
42
-
43
- function optionalString(object, key, fallback) {
44
- if (object == null || !(key in object)) {
45
- return fallback
46
- }
47
-
48
- if (typeof object[key] !== "string" || object[key].trim() === "") {
49
- throw new Error(`Missing or invalid string field '${key}'`)
50
- }
51
-
52
- return object[key]
53
- }
54
-
55
- function optionalObject(object, key) {
56
- if (object == null || !(key in object)) {
57
- return {}
58
- }
59
-
60
- if (typeof object[key] !== "object" || object[key] === null || Array.isArray(object[key])) {
61
- throw new Error(`Missing or invalid object field '${key}'`)
62
- }
63
-
64
- return object[key]
65
- }
66
-
67
- function sanitizeName(value) {
68
- return value
69
- .toLowerCase()
70
- .replace(/[^a-z0-9]+/g, "-")
71
- .replace(/^-+|-+$/g, "") || "reset-app"
72
- }
73
-
74
- function normalizeFrontendDir(value) {
75
- const trimmed = value.trim()
76
-
77
- if (trimmed === "." || trimmed === "./") {
78
- return "."
79
- }
80
-
81
- if (path.isAbsolute(trimmed)) {
82
- throw new Error("project.frontendDir must be a relative path")
83
- }
84
-
85
- const normalized = path.normalize(trimmed).replace(/\\/g, "/").replace(/\/+$/g, "")
86
-
87
- if (normalized === "" || normalized === ".") {
88
- return "."
89
- }
90
-
91
- if (normalized.startsWith("../")) {
92
- throw new Error("project.frontendDir cannot point outside the app root")
93
- }
94
-
95
- return normalized
96
- }
97
-
98
- function normalizeStyling(value) {
99
- if (value === "css" || value === "tailwindcss") {
100
- return value
101
- }
102
-
103
- throw new Error("project.styling must be either 'css' or 'tailwindcss'")
104
- }
105
-
106
- function normalizeProtocolScheme(value) {
107
- if (typeof value !== "string" || value.trim() === "") {
108
- throw new Error("protocols.schemes[].scheme must be a non-empty string")
109
- }
110
-
111
- const normalized = value.trim().toLowerCase()
112
- if (!/^[a-z][a-z0-9+.-]*$/.test(normalized)) {
113
- throw new Error(
114
- "protocols.schemes[].scheme must start with a letter and only contain letters, numbers, '+', '-', and '.'"
115
- )
116
- }
117
-
118
- return normalized
119
- }
120
-
121
- function normalizeProtocolRole(value) {
122
- if (value === "Editor" || value === "Viewer" || value === "Shell" || value === "None") {
123
- return value
124
- }
125
-
126
- throw new Error("protocols.schemes[].role must be one of Editor, Viewer, Shell, None")
127
- }
128
-
129
- function toTitleCase(value) {
130
- return value
131
- .split(/[^a-zA-Z0-9]+/)
132
- .filter(Boolean)
133
- .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
134
- .join(" ") || "Reset App"
135
- }
136
-
137
- function readJsonFile(filePath) {
138
- return JSON.parse(readFileSync(filePath, "utf8"))
139
- }
140
-
141
- function fileExists(filePath) {
142
- return existsSync(filePath)
143
- }
144
-
145
- function getFrameworkBuildType(mode) {
146
- return mode === "release" ? "Release" : "Debug"
147
- }
148
-
149
- function resolveExistingCandidate(candidates) {
150
- return candidates.find((candidate) => existsSync(candidate)) ?? null
151
- }
152
-
153
- function isPathInside(parentDir, candidatePath) {
154
- const relative = path.relative(parentDir, candidatePath)
155
- return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
156
- }
157
-
34
+
35
+ function requireString(object, key, scope) {
36
+ if (typeof object?.[key] !== "string" || object[key].trim() === "") {
37
+ throw new Error(`Missing or invalid string field '${scope}.${key}'`)
38
+ }
39
+
40
+ return object[key]
41
+ }
42
+
43
+ function optionalString(object, key, fallback) {
44
+ if (object == null || !(key in object)) {
45
+ return fallback
46
+ }
47
+
48
+ if (typeof object[key] !== "string" || object[key].trim() === "") {
49
+ throw new Error(`Missing or invalid string field '${key}'`)
50
+ }
51
+
52
+ return object[key]
53
+ }
54
+
55
+ function optionalObject(object, key) {
56
+ if (object == null || !(key in object)) {
57
+ return {}
58
+ }
59
+
60
+ if (typeof object[key] !== "object" || object[key] === null || Array.isArray(object[key])) {
61
+ throw new Error(`Missing or invalid object field '${key}'`)
62
+ }
63
+
64
+ return object[key]
65
+ }
66
+
67
+ function sanitizeName(value) {
68
+ return value
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .replace(/^-+|-+$/g, "") || "reset-app"
72
+ }
73
+
74
+ function normalizeFrontendDir(value) {
75
+ const trimmed = value.trim()
76
+
77
+ if (trimmed === "." || trimmed === "./") {
78
+ return "."
79
+ }
80
+
81
+ if (path.isAbsolute(trimmed)) {
82
+ throw new Error("project.frontendDir must be a relative path")
83
+ }
84
+
85
+ const normalized = path.normalize(trimmed).replace(/\\/g, "/").replace(/\/+$/g, "")
86
+
87
+ if (normalized === "" || normalized === ".") {
88
+ return "."
89
+ }
90
+
91
+ if (normalized.startsWith("../")) {
92
+ throw new Error("project.frontendDir cannot point outside the app root")
93
+ }
94
+
95
+ return normalized
96
+ }
97
+
98
+ function normalizeStyling(value) {
99
+ if (value === "css" || value === "tailwindcss") {
100
+ return value
101
+ }
102
+
103
+ throw new Error("project.styling must be either 'css' or 'tailwindcss'")
104
+ }
105
+
106
+ function normalizeProtocolScheme(value) {
107
+ if (typeof value !== "string" || value.trim() === "") {
108
+ throw new Error("protocols.schemes[].scheme must be a non-empty string")
109
+ }
110
+
111
+ const normalized = value.trim().toLowerCase()
112
+ if (!/^[a-z][a-z0-9+.-]*$/.test(normalized)) {
113
+ throw new Error(
114
+ "protocols.schemes[].scheme must start with a letter and only contain letters, numbers, '+', '-', and '.'"
115
+ )
116
+ }
117
+
118
+ return normalized
119
+ }
120
+
121
+ function normalizeProtocolRole(value) {
122
+ if (value === "Editor" || value === "Viewer" || value === "Shell" || value === "None") {
123
+ return value
124
+ }
125
+
126
+ throw new Error("protocols.schemes[].role must be one of Editor, Viewer, Shell, None")
127
+ }
128
+
129
+ function toTitleCase(value) {
130
+ return value
131
+ .split(/[^a-zA-Z0-9]+/)
132
+ .filter(Boolean)
133
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
134
+ .join(" ") || "Reset App"
135
+ }
136
+
137
+ function readJsonFile(filePath) {
138
+ return JSON.parse(readFileSync(filePath, "utf8"))
139
+ }
140
+
141
+ function fileExists(filePath) {
142
+ return existsSync(filePath)
143
+ }
144
+
145
+ function getFrameworkBuildType(mode) {
146
+ return mode === "release" ? "Release" : "Debug"
147
+ }
148
+
149
+ function resolveExistingCandidate(candidates) {
150
+ return candidates.find((candidate) => existsSync(candidate)) ?? null
151
+ }
152
+
153
+ function isPathInside(parentDir, candidatePath) {
154
+ const relative = path.relative(parentDir, candidatePath)
155
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
156
+ }
157
+
158
158
  function resolvePackageInfo(packageName, fallbackRoot) {
159
159
  try {
160
- const packageJsonPath = require.resolve(`${packageName}/package.json`)
161
- const packageRoot = path.dirname(packageJsonPath)
162
- const manifest = readJsonFile(packageJsonPath)
163
-
164
- return {
165
- packageName: manifest.name ?? packageName,
166
- packageRoot,
167
- packageJsonPath,
168
- version: manifest.version ?? "0.0.0",
169
- localFallback: false
170
- }
171
- } catch {
172
- const packageJsonPath = path.join(fallbackRoot, "package.json")
173
- const manifest = readJsonFile(packageJsonPath)
174
-
175
- return {
176
- packageName: manifest.name ?? packageName,
177
- packageRoot: fallbackRoot,
178
- packageJsonPath,
179
- version: manifest.version ?? "0.0.0",
180
- localFallback: true
181
- }
160
+ const packageJsonPath = require.resolve(`${packageName}/package.json`)
161
+ const packageRoot = path.dirname(packageJsonPath)
162
+ const manifest = readJsonFile(packageJsonPath)
163
+
164
+ return {
165
+ packageName: manifest.name ?? packageName,
166
+ packageRoot,
167
+ packageJsonPath,
168
+ version: manifest.version ?? "0.0.0",
169
+ localFallback: false
170
+ }
171
+ } catch {
172
+ const packageJsonPath = path.join(fallbackRoot, "package.json")
173
+ const manifest = readJsonFile(packageJsonPath)
174
+
175
+ return {
176
+ packageName: manifest.name ?? packageName,
177
+ packageRoot: fallbackRoot,
178
+ packageJsonPath,
179
+ version: manifest.version ?? "0.0.0",
180
+ localFallback: true
181
+ }
182
182
  }
183
183
  }
184
184
 
185
185
  function resolveWorkspacePackageInfo(packageName, workspaceRoot, fallbackInfo) {
186
- const packageJsonPath = path.join(workspaceRoot, "package.json")
187
-
188
- if (!existsSync(packageJsonPath)) {
189
- return fallbackInfo
190
- }
191
-
192
- const manifest = readJsonFile(packageJsonPath)
193
-
186
+ const packageJsonPath = path.join(workspaceRoot, "package.json")
187
+
188
+ if (!existsSync(packageJsonPath)) {
189
+ return fallbackInfo
190
+ }
191
+
192
+ const manifest = readJsonFile(packageJsonPath)
193
+
194
194
  return {
195
195
  packageName: manifest.name ?? packageName,
196
196
  packageRoot: workspaceRoot,
@@ -325,7 +325,12 @@ export function resolveFrameworkPaths() {
325
325
  "@reset-framework/sdk",
326
326
  path.join(packagesDir, "sdk"),
327
327
  resolvePackageInfo("@reset-framework/sdk", path.join(packagesDir, "sdk"))
328
- )
328
+ )
329
+ const backendPackage = resolveWorkspacePackageInfo(
330
+ "@reset-framework/backend",
331
+ path.join(packagesDir, "backend"),
332
+ resolvePackageInfo("@reset-framework/backend", path.join(packagesDir, "backend"))
333
+ )
329
334
  const schemaPackage = resolveWorkspacePackageInfo(
330
335
  "@reset-framework/schema",
331
336
  path.join(packagesDir, "schema"),
@@ -334,7 +339,7 @@ export function resolveFrameworkPaths() {
334
339
  const runtimePackage = resolveRuntimePackage(packagesDir)
335
340
  const isWorkspaceLayout =
336
341
  isWorkspacePackagesDir &&
337
- [cliDir, sdkPackage.packageRoot, schemaPackage.packageRoot].every((packageRoot) =>
342
+ [cliDir, backendPackage.packageRoot, sdkPackage.packageRoot, schemaPackage.packageRoot].every((packageRoot) =>
338
343
  isPathInside(packagesDir, packageRoot)
339
344
  )
340
345
 
@@ -351,6 +356,7 @@ export function resolveFrameworkPaths() {
351
356
  frameworkRoot: nativePackage?.packageRoot ?? null,
352
357
  frameworkPackage: nativePackage,
353
358
  isWorkspaceLayout,
359
+ backendPackage,
354
360
  sdkPackage,
355
361
  schemaPackage,
356
362
  runtimePackage,
@@ -361,58 +367,58 @@ export function resolveFrameworkPaths() {
361
367
  templatesDir: path.join(cliDir, "templates")
362
368
  }
363
369
  }
364
-
370
+
365
371
  export function resolveFrameworkBuildPaths(appPaths) {
366
- const frameworkCacheRoot = path.join(appPaths.appRoot, ".reset", "framework")
367
- const isWindows = process.platform === "win32"
368
- const devRuntimeRoot = path.join(frameworkCacheRoot, "build", "dev", "runtime")
369
- const releaseRuntimeRoot = path.join(frameworkCacheRoot, "build", "release", "runtime")
370
- const devAppBinaryCandidates = isWindows
371
- ? [
372
- path.join(devRuntimeRoot, getFrameworkBuildType("dev"), "reset-framework.exe"),
373
- path.join(devRuntimeRoot, "reset-framework.exe")
374
- ]
375
- : [
376
- path.join(
377
- frameworkCacheRoot,
378
- "build",
379
- "dev",
380
- "runtime",
381
- "reset-framework.app",
382
- "Contents",
383
- "MacOS",
384
- "reset-framework"
385
- )
386
- ]
387
- const releaseAppBinaryCandidates = isWindows
388
- ? [
389
- path.join(releaseRuntimeRoot, getFrameworkBuildType("release"), "reset-framework.exe"),
390
- path.join(releaseRuntimeRoot, "reset-framework.exe")
391
- ]
392
- : [
393
- path.join(
394
- frameworkCacheRoot,
395
- "build",
396
- "release",
397
- "runtime",
398
- "reset-framework.app"
399
- )
400
- ]
401
-
402
- return {
403
- frameworkCacheRoot,
404
- devBuildDir: path.join(frameworkCacheRoot, "build", "dev"),
405
- releaseBuildDir: path.join(frameworkCacheRoot, "build", "release"),
406
- devRuntimeOutputCandidates: isWindows
407
- ? [path.join(devRuntimeRoot, getFrameworkBuildType("dev")), devRuntimeRoot]
408
- : [path.join(frameworkCacheRoot, "build", "dev", "runtime", "reset-framework.app")],
409
- releaseRuntimeOutputCandidates: isWindows
410
- ? [path.join(releaseRuntimeRoot, getFrameworkBuildType("release")), releaseRuntimeRoot]
411
- : [path.join(frameworkCacheRoot, "build", "release", "runtime", "reset-framework.app")],
412
- devAppBinaryCandidates,
413
- releaseAppBinaryCandidates,
414
- devAppBinary: devAppBinaryCandidates[0],
415
- releaseAppTemplate: releaseAppBinaryCandidates[0]
372
+ const frameworkCacheRoot = path.join(appPaths.appRoot, ".reset", "framework")
373
+ const isWindows = process.platform === "win32"
374
+ const devRuntimeRoot = path.join(frameworkCacheRoot, "build", "dev", "runtime")
375
+ const releaseRuntimeRoot = path.join(frameworkCacheRoot, "build", "release", "runtime")
376
+ const devAppBinaryCandidates = isWindows
377
+ ? [
378
+ path.join(devRuntimeRoot, getFrameworkBuildType("dev"), "reset-framework.exe"),
379
+ path.join(devRuntimeRoot, "reset-framework.exe")
380
+ ]
381
+ : [
382
+ path.join(
383
+ frameworkCacheRoot,
384
+ "build",
385
+ "dev",
386
+ "runtime",
387
+ "reset-framework.app",
388
+ "Contents",
389
+ "MacOS",
390
+ "reset-framework"
391
+ )
392
+ ]
393
+ const releaseAppBinaryCandidates = isWindows
394
+ ? [
395
+ path.join(releaseRuntimeRoot, getFrameworkBuildType("release"), "reset-framework.exe"),
396
+ path.join(releaseRuntimeRoot, "reset-framework.exe")
397
+ ]
398
+ : [
399
+ path.join(
400
+ frameworkCacheRoot,
401
+ "build",
402
+ "release",
403
+ "runtime",
404
+ "reset-framework.app"
405
+ )
406
+ ]
407
+
408
+ return {
409
+ frameworkCacheRoot,
410
+ devBuildDir: path.join(frameworkCacheRoot, "build", "dev"),
411
+ releaseBuildDir: path.join(frameworkCacheRoot, "build", "release"),
412
+ devRuntimeOutputCandidates: isWindows
413
+ ? [path.join(devRuntimeRoot, getFrameworkBuildType("dev")), devRuntimeRoot]
414
+ : [path.join(frameworkCacheRoot, "build", "dev", "runtime", "reset-framework.app")],
415
+ releaseRuntimeOutputCandidates: isWindows
416
+ ? [path.join(releaseRuntimeRoot, getFrameworkBuildType("release")), releaseRuntimeRoot]
417
+ : [path.join(frameworkCacheRoot, "build", "release", "runtime", "reset-framework.app")],
418
+ devAppBinaryCandidates,
419
+ releaseAppBinaryCandidates,
420
+ devAppBinary: devAppBinaryCandidates[0],
421
+ releaseAppTemplate: releaseAppBinaryCandidates[0]
416
422
  }
417
423
  }
418
424
 
@@ -497,15 +503,15 @@ export function resolveFrameworkRuntimeBinary(frameworkPaths, frameworkBuildPath
497
503
  mode === "release"
498
504
  ? (frameworkBuildPaths.releaseAppBinaryCandidates ?? [frameworkBuildPaths.releaseAppTemplate])
499
505
  : (frameworkBuildPaths.devAppBinaryCandidates ?? [frameworkBuildPaths.devAppBinary])
500
- const existing = resolveExistingCandidate(candidates)
501
-
502
- if (existing) {
503
- return existing
504
- }
505
-
506
- if (mustExist) {
507
- throw new Error(`Missing built runtime executable. Looked in: ${candidates.join(", ")}`)
508
- }
506
+ const existing = resolveExistingCandidate(candidates)
507
+
508
+ if (existing) {
509
+ return existing
510
+ }
511
+
512
+ if (mustExist) {
513
+ throw new Error(`Missing built runtime executable. Looked in: ${candidates.join(", ")}`)
514
+ }
509
515
 
510
516
  return candidates[0]
511
517
  }
@@ -530,47 +536,60 @@ export function resolveFrameworkRuntimeOutputDir(frameworkPaths, frameworkBuildP
530
536
  mode === "release"
531
537
  ? (frameworkBuildPaths.releaseRuntimeOutputCandidates ?? [])
532
538
  : (frameworkBuildPaths.devRuntimeOutputCandidates ?? [])
533
- const existing = resolveExistingCandidate(candidates)
534
-
535
- if (existing) {
536
- return existing
537
- }
538
-
539
- if (candidates.length > 0) {
540
- if (mustExist) {
541
- throw new Error(`Missing built runtime output directory. Looked in: ${candidates.join(", ")}`)
542
- }
543
-
544
- return candidates[0]
545
- }
546
-
539
+ const existing = resolveExistingCandidate(candidates)
540
+
541
+ if (existing) {
542
+ return existing
543
+ }
544
+
545
+ if (candidates.length > 0) {
546
+ if (mustExist) {
547
+ throw new Error(`Missing built runtime output directory. Looked in: ${candidates.join(", ")}`)
548
+ }
549
+
550
+ return candidates[0]
551
+ }
552
+
547
553
  return path.dirname(resolveFrameworkRuntimeBinary(frameworkPaths, frameworkBuildPaths, mode, options))
548
554
  }
549
-
550
- export function resolveAppPaths(appRoot) {
551
- return {
552
- appRoot,
553
- appPackageJsonPath: path.join(appRoot, "package.json"),
554
- frontendDir: path.join(appRoot, "frontend"),
555
- resetConfigPath: path.join(appRoot, "reset.config.json"),
556
- legacyResetConfigPath: path.join(appRoot, "frontend", "reset.config.json")
557
- }
558
- }
559
-
560
- export function resolveConfigPath(appPaths) {
561
- if (existsSync(appPaths.resetConfigPath)) {
562
- return appPaths.resetConfigPath
563
- }
564
-
565
- if (existsSync(appPaths.legacyResetConfigPath)) {
566
- return appPaths.legacyResetConfigPath
567
- }
568
-
569
- return appPaths.resetConfigPath
570
- }
571
-
555
+
556
+ export function resolveAppPaths(appRoot) {
557
+ return {
558
+ appRoot,
559
+ appPackageJsonPath: path.join(appRoot, "package.json"),
560
+ backendDir: path.join(appRoot, "backend"),
561
+ frontendDir: path.join(appRoot, "frontend"),
562
+ resetConfigPath: path.join(appRoot, "reset.config.json"),
563
+ legacyResetConfigPath: path.join(appRoot, "frontend", "reset.config.json")
564
+ }
565
+ }
566
+
567
+ export function resolveOptionalBackendEntry(appPaths) {
568
+ const candidates = [
569
+ path.join(appPaths.backendDir, "src", "index.ts"),
570
+ path.join(appPaths.backendDir, "src", "index.js"),
571
+ path.join(appPaths.backendDir, "index.ts"),
572
+ path.join(appPaths.backendDir, "index.js")
573
+ ]
574
+
575
+ return resolveExistingCandidate(candidates)
576
+ }
577
+
578
+ export function resolveConfigPath(appPaths) {
579
+ if (existsSync(appPaths.resetConfigPath)) {
580
+ return appPaths.resetConfigPath
581
+ }
582
+
583
+ if (existsSync(appPaths.legacyResetConfigPath)) {
584
+ return appPaths.legacyResetConfigPath
585
+ }
586
+
587
+ return appPaths.resetConfigPath
588
+ }
589
+
572
590
  export function getFrameworkChecks(frameworkPaths) {
573
591
  return [
592
+ ["backend", frameworkPaths.backendPackage.packageJsonPath],
574
593
  ["sdk", frameworkPaths.sdkPackage.packageJsonPath],
575
594
  ["schema", frameworkPaths.schemaPackage.packageJsonPath],
576
595
  ["templates", frameworkPaths.templatesDir]
@@ -638,6 +657,7 @@ export function assertFrameworkInstall(frameworkPaths) {
638
657
  export function assertFrameworkSourceInstall(frameworkPaths) {
639
658
  const missing = [
640
659
  ["native source", frameworkPaths.frameworkPackage?.packageJsonPath],
660
+ ["backend", frameworkPaths.backendPackage.packageJsonPath],
641
661
  ["sdk", frameworkPaths.sdkPackage.packageJsonPath],
642
662
  ["cmake", frameworkPaths.rootCMakePath],
643
663
  ["runtime", frameworkPaths.runtimeDir],
@@ -657,239 +677,256 @@ export function assertFrameworkSourceInstall(frameworkPaths) {
657
677
  export function getAppChecks(appPaths) {
658
678
  return [["config", resolveConfigPath(appPaths)]]
659
679
  }
660
-
661
- export function assertAppProject(appPaths, config) {
662
- const checks = [...getAppChecks(appPaths)]
663
-
664
- if (config) {
665
- checks.unshift(["frontend", resolveFrontendDir(appPaths, config)])
666
- }
667
-
668
- const missing = checks
669
- .filter(([, filePath]) => !existsSync(filePath))
670
- .map(([label]) => label)
671
-
672
- if (missing.length > 0) {
673
- throw new Error(
674
- `Current directory does not look like a Reset app. Missing: ${missing.join(", ")}`
675
- )
676
- }
677
- }
678
-
679
- export function validateResetConfig(rawConfig) {
680
- const frontend = optionalObject(rawConfig, "frontend")
681
- const build = optionalObject(rawConfig, "build")
682
- const project = optionalObject(rawConfig, "project")
683
- const security = optionalObject(rawConfig, "security")
684
- const protocols = optionalObject(rawConfig, "protocols")
685
- const windowConfig = optionalObject(rawConfig, "window")
686
- const rawProtocolSchemes = Array.isArray(protocols.schemes) ? protocols.schemes : []
687
- const seenProtocolSchemes = new Set()
688
-
689
- return {
690
- ...rawConfig,
691
- name: sanitizeName(requireString(rawConfig, "name", "config")),
692
- productName: requireString(rawConfig, "productName", "config"),
693
- appId: requireString(rawConfig, "appId", "config"),
694
- version: requireString(rawConfig, "version", "config"),
695
- window: {
696
- title: optionalString(windowConfig, "title", "Reset App")
697
- },
698
- frontend: {
699
- devUrl: requireString(frontend, "devUrl", "frontend"),
700
- distDir: optionalString(frontend, "distDir", "dist"),
701
- entryHtml: optionalString(frontend, "entryHtml", "index.html")
702
- },
703
- project: {
704
- frontendDir: normalizeFrontendDir(optionalString(project, "frontendDir", "frontend")),
705
- styling: normalizeStyling(optionalString(project, "styling", "css"))
706
- },
707
- build: {
708
- outputDir: optionalString(build, "outputDir", ".reset/build")
709
- },
710
- security: {
711
- permissions: Array.isArray(security.permissions)
712
- ? security.permissions.map((value) => {
713
- if (typeof value !== "string" || value.trim() === "") {
714
- throw new Error("Missing or invalid string field 'security.permissions'")
715
- }
716
-
717
- return value
718
- })
719
- : []
720
- },
721
- protocols: {
722
- schemes: rawProtocolSchemes.map((entry) => {
723
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
724
- throw new Error("protocols.schemes entries must be objects")
725
- }
726
-
727
- const scheme = normalizeProtocolScheme(entry.scheme)
728
- if (seenProtocolSchemes.has(scheme)) {
729
- throw new Error(`Duplicate protocol scheme '${scheme}'`)
730
- }
731
-
732
- seenProtocolSchemes.add(scheme)
733
-
734
- return {
735
- scheme,
736
- name:
737
- typeof entry.name === "string" && entry.name.trim() !== ""
738
- ? entry.name
739
- : scheme,
740
- role:
741
- entry.role === undefined
742
- ? "Viewer"
743
- : normalizeProtocolRole(entry.role)
744
- }
745
- })
746
- }
747
- }
748
- }
749
-
750
- export function loadResetConfig(appPaths) {
751
- const configPath = resolveConfigPath(appPaths)
752
- const raw = readFileSync(configPath, "utf8")
753
-
754
- try {
755
- return validateResetConfig(JSON.parse(raw))
756
- } catch (error) {
757
- if (error instanceof SyntaxError) {
758
- throw new Error(`Invalid JSON in ${configPath}: ${error.message}`)
759
- }
760
-
761
- throw error
762
- }
763
- }
764
-
765
- export function resolveAppOutputPaths(appPaths, config) {
766
- const outputRoot = path.resolve(appPaths.appRoot, config.build.outputDir)
767
- const frontendDir = resolveFrontendDir(appPaths, config)
768
- const isWindows = process.platform === "win32"
769
- const appBundleName = isWindows ? config.productName : `${config.productName}.app`
770
- const platformDir = path.join(outputRoot, isWindows ? "windows" : "macos")
771
- const appBundlePath = path.join(platformDir, appBundleName)
772
- const resourcesDir = isWindows
773
- ? path.join(appBundlePath, "resources")
774
- : path.join(appBundlePath, "Contents", "Resources")
775
- const packagesDir = path.join(outputRoot, "packages")
776
-
777
- return {
778
- outputRoot,
779
- macosDir: isWindows ? undefined : platformDir,
780
- windowsDir: isWindows ? platformDir : undefined,
781
- appBundlePath,
782
- appExecutablePath: isWindows
783
- ? path.join(appBundlePath, `${config.productName}.exe`)
784
- : undefined,
785
- resourcesDir,
786
- bundledConfigPath: path.join(resourcesDir, "reset.config.json"),
787
- bundledFrontendDir: path.join(resourcesDir, config.frontend.distDir),
788
- frontendDistDir: path.resolve(frontendDir, config.frontend.distDir),
789
- frontendEntryFile: path.resolve(
790
- frontendDir,
791
- config.frontend.distDir,
792
- config.frontend.entryHtml
793
- ),
794
- packagesDir,
795
- zipPath: path.join(packagesDir, `${config.name}-${isWindows ? "windows" : "macos"}.zip`)
796
- }
797
- }
798
-
799
- export function resolveFrontendDir(appPaths, config) {
800
- return path.resolve(appPaths.appRoot, config.project.frontendDir)
801
- }
802
-
803
- export function resolveDevServerOptions(config) {
804
- const url = new URL(config.frontend.devUrl)
805
-
806
- return {
807
- host: url.hostname,
808
- port: url.port || "5173",
809
- url: url.toString()
810
- }
811
- }
812
-
813
- export function makeAppMetadata(appName) {
814
- const name = sanitizeName(appName)
815
- const productName = toTitleCase(appName)
816
-
817
- return {
818
- name,
819
- productName,
820
- appId: `com.example.${name}`,
821
- title: productName
822
- }
823
- }
824
-
825
- export function resolveSdkDependencySpec(frameworkPaths) {
826
- if (frameworkPaths.isWorkspaceLayout || frameworkPaths.sdkPackage.localFallback) {
827
- return `file:${frameworkPaths.sdkPackage.packageRoot}`
828
- }
829
-
830
- return `^${frameworkPaths.sdkPackage.version}`
831
- }
832
-
833
- export function resolveCliDependencySpec(frameworkPaths) {
834
- if (frameworkPaths.isWorkspaceLayout || frameworkPaths.cliPackage.localFallback) {
835
- return `file:${frameworkPaths.cliPackage.packageRoot}`
836
- }
837
-
838
- return `^${frameworkPaths.cliPackage.version}`
839
- }
840
-
841
- function detectPackageManagerFromManifest(packageJsonPath) {
842
- if (!fileExists(packageJsonPath)) {
843
- return null
844
- }
845
-
846
- const manifest = readJsonFile(packageJsonPath)
847
- const raw = typeof manifest.packageManager === "string" ? manifest.packageManager.trim() : ""
848
- if (raw === "") {
849
- return null
850
- }
851
-
852
- const [name] = raw.split("@")
853
- if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") {
854
- return name
855
- }
856
-
857
- return null
858
- }
859
-
860
- function detectPackageManagerFromLocks(directory) {
861
- if (!fileExists(directory)) {
862
- return null
863
- }
864
-
865
- if (fileExists(path.join(directory, "bun.lock")) || fileExists(path.join(directory, "bun.lockb"))) {
866
- return "bun"
867
- }
868
-
869
- if (fileExists(path.join(directory, "pnpm-lock.yaml"))) {
870
- return "pnpm"
871
- }
872
-
873
- if (fileExists(path.join(directory, "yarn.lock"))) {
874
- return "yarn"
875
- }
876
-
877
- if (fileExists(path.join(directory, "package-lock.json"))) {
878
- return "npm"
879
- }
880
-
881
- return null
882
- }
883
-
884
- export function resolveAppPackageManager(appPaths, config) {
885
- const frontendDir = resolveFrontendDir(appPaths, config)
886
- const frontendPackageJsonPath = path.join(frontendDir, "package.json")
887
-
888
- return (
889
- detectPackageManagerFromManifest(appPaths.appPackageJsonPath) ??
890
- detectPackageManagerFromManifest(frontendPackageJsonPath) ??
891
- detectPackageManagerFromLocks(appPaths.appRoot) ??
892
- detectPackageManagerFromLocks(frontendDir) ??
893
- "npm"
894
- )
895
- }
680
+
681
+ export function assertAppProject(appPaths, config) {
682
+ const checks = [...getAppChecks(appPaths)]
683
+
684
+ if (config) {
685
+ checks.unshift(["frontend", resolveFrontendDir(appPaths, config)])
686
+ }
687
+
688
+ const missing = checks
689
+ .filter(([, filePath]) => !existsSync(filePath))
690
+ .map(([label]) => label)
691
+
692
+ if (missing.length > 0) {
693
+ throw new Error(
694
+ `Current directory does not look like a Reset app. Missing: ${missing.join(", ")}`
695
+ )
696
+ }
697
+ }
698
+
699
+ export function validateResetConfig(rawConfig) {
700
+ const frontend = optionalObject(rawConfig, "frontend")
701
+ const build = optionalObject(rawConfig, "build")
702
+ const project = optionalObject(rawConfig, "project")
703
+ const security = optionalObject(rawConfig, "security")
704
+ const protocols = optionalObject(rawConfig, "protocols")
705
+ const windowConfig = optionalObject(rawConfig, "window")
706
+ const rawProtocolSchemes = Array.isArray(protocols.schemes) ? protocols.schemes : []
707
+ const seenProtocolSchemes = new Set()
708
+
709
+ return {
710
+ ...rawConfig,
711
+ name: sanitizeName(requireString(rawConfig, "name", "config")),
712
+ productName: requireString(rawConfig, "productName", "config"),
713
+ appId: requireString(rawConfig, "appId", "config"),
714
+ version: requireString(rawConfig, "version", "config"),
715
+ window: {
716
+ title: optionalString(windowConfig, "title", "Reset App")
717
+ },
718
+ frontend: {
719
+ devUrl: requireString(frontend, "devUrl", "frontend"),
720
+ distDir: optionalString(frontend, "distDir", "dist"),
721
+ entryHtml: optionalString(frontend, "entryHtml", "index.html")
722
+ },
723
+ project: {
724
+ frontendDir: normalizeFrontendDir(optionalString(project, "frontendDir", "frontend")),
725
+ styling: normalizeStyling(optionalString(project, "styling", "css"))
726
+ },
727
+ build: {
728
+ outputDir: optionalString(build, "outputDir", ".reset/build")
729
+ },
730
+ security: {
731
+ permissions: Array.isArray(security.permissions)
732
+ ? security.permissions.map((value) => {
733
+ if (typeof value !== "string" || value.trim() === "") {
734
+ throw new Error("Missing or invalid string field 'security.permissions'")
735
+ }
736
+
737
+ return value
738
+ })
739
+ : []
740
+ },
741
+ protocols: {
742
+ schemes: rawProtocolSchemes.map((entry) => {
743
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
744
+ throw new Error("protocols.schemes entries must be objects")
745
+ }
746
+
747
+ const scheme = normalizeProtocolScheme(entry.scheme)
748
+ if (seenProtocolSchemes.has(scheme)) {
749
+ throw new Error(`Duplicate protocol scheme '${scheme}'`)
750
+ }
751
+
752
+ seenProtocolSchemes.add(scheme)
753
+
754
+ return {
755
+ scheme,
756
+ name:
757
+ typeof entry.name === "string" && entry.name.trim() !== ""
758
+ ? entry.name
759
+ : scheme,
760
+ role:
761
+ entry.role === undefined
762
+ ? "Viewer"
763
+ : normalizeProtocolRole(entry.role)
764
+ }
765
+ })
766
+ }
767
+ }
768
+ }
769
+
770
+ export function loadResetConfig(appPaths) {
771
+ const configPath = resolveConfigPath(appPaths)
772
+ const raw = readFileSync(configPath, "utf8")
773
+
774
+ try {
775
+ return validateResetConfig(JSON.parse(raw))
776
+ } catch (error) {
777
+ if (error instanceof SyntaxError) {
778
+ throw new Error(`Invalid JSON in ${configPath}: ${error.message}`)
779
+ }
780
+
781
+ throw error
782
+ }
783
+ }
784
+
785
+ export function resolveAppOutputPaths(appPaths, config) {
786
+ const outputRoot = path.resolve(appPaths.appRoot, config.build.outputDir)
787
+ const frontendDir = resolveFrontendDir(appPaths, config)
788
+ const isWindows = process.platform === "win32"
789
+ const appBundleName = isWindows ? config.productName : `${config.productName}.app`
790
+ const platformDir = path.join(outputRoot, isWindows ? "windows" : "macos")
791
+ const appBundlePath = path.join(platformDir, appBundleName)
792
+ const resourcesDir = isWindows
793
+ ? path.join(appBundlePath, "resources")
794
+ : path.join(appBundlePath, "Contents", "Resources")
795
+ const packagesDir = path.join(outputRoot, "packages")
796
+
797
+ return {
798
+ outputRoot,
799
+ macosDir: isWindows ? undefined : platformDir,
800
+ windowsDir: isWindows ? platformDir : undefined,
801
+ appBundlePath,
802
+ appExecutablePath: isWindows
803
+ ? path.join(appBundlePath, `${config.productName}.exe`)
804
+ : undefined,
805
+ resourcesDir,
806
+ bundledConfigPath: path.join(resourcesDir, "reset.config.json"),
807
+ bundledBackendDir: path.join(resourcesDir, "backend"),
808
+ bundledBackendEntryPath: path.join(resourcesDir, "backend", "app.mjs"),
809
+ bundledBackendRunnerPath: path.join(resourcesDir, "backend", "runner.mjs"),
810
+ bundledBackendSupportModulePath: path.join(resourcesDir, "backend", "index.js"),
811
+ bundledBackendRuntimePath: path.join(
812
+ resourcesDir,
813
+ "backend",
814
+ process.platform === "win32" ? "backend-runtime.exe" : "backend-runtime"
815
+ ),
816
+ bundledFrontendDir: path.join(resourcesDir, config.frontend.distDir),
817
+ frontendDistDir: path.resolve(frontendDir, config.frontend.distDir),
818
+ frontendEntryFile: path.resolve(
819
+ frontendDir,
820
+ config.frontend.distDir,
821
+ config.frontend.entryHtml
822
+ ),
823
+ packagesDir,
824
+ zipPath: path.join(packagesDir, `${config.name}-${isWindows ? "windows" : "macos"}.zip`)
825
+ }
826
+ }
827
+
828
+ export function resolveFrontendDir(appPaths, config) {
829
+ return path.resolve(appPaths.appRoot, config.project.frontendDir)
830
+ }
831
+
832
+ export function resolveDevServerOptions(config) {
833
+ const url = new URL(config.frontend.devUrl)
834
+
835
+ return {
836
+ host: url.hostname,
837
+ port: url.port || "5173",
838
+ url: url.toString()
839
+ }
840
+ }
841
+
842
+ export function makeAppMetadata(appName) {
843
+ const name = sanitizeName(appName)
844
+ const productName = toTitleCase(appName)
845
+
846
+ return {
847
+ name,
848
+ productName,
849
+ appId: `com.example.${name}`,
850
+ title: productName
851
+ }
852
+ }
853
+
854
+ export function resolveSdkDependencySpec(frameworkPaths) {
855
+ if (frameworkPaths.isWorkspaceLayout || frameworkPaths.sdkPackage.localFallback) {
856
+ return `file:${frameworkPaths.sdkPackage.packageRoot}`
857
+ }
858
+
859
+ return `^${frameworkPaths.sdkPackage.version}`
860
+ }
861
+
862
+ export function resolveBackendDependencySpec(frameworkPaths) {
863
+ if (frameworkPaths.isWorkspaceLayout || frameworkPaths.backendPackage.localFallback) {
864
+ return `file:${frameworkPaths.backendPackage.packageRoot}`
865
+ }
866
+
867
+ return `^${frameworkPaths.backendPackage.version}`
868
+ }
869
+
870
+ export function resolveCliDependencySpec(frameworkPaths) {
871
+ if (frameworkPaths.isWorkspaceLayout || frameworkPaths.cliPackage.localFallback) {
872
+ return `file:${frameworkPaths.cliPackage.packageRoot}`
873
+ }
874
+
875
+ return `^${frameworkPaths.cliPackage.version}`
876
+ }
877
+
878
+ function detectPackageManagerFromManifest(packageJsonPath) {
879
+ if (!fileExists(packageJsonPath)) {
880
+ return null
881
+ }
882
+
883
+ const manifest = readJsonFile(packageJsonPath)
884
+ const raw = typeof manifest.packageManager === "string" ? manifest.packageManager.trim() : ""
885
+ if (raw === "") {
886
+ return null
887
+ }
888
+
889
+ const [name] = raw.split("@")
890
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") {
891
+ return name
892
+ }
893
+
894
+ return null
895
+ }
896
+
897
+ function detectPackageManagerFromLocks(directory) {
898
+ if (!fileExists(directory)) {
899
+ return null
900
+ }
901
+
902
+ if (fileExists(path.join(directory, "bun.lock")) || fileExists(path.join(directory, "bun.lockb"))) {
903
+ return "bun"
904
+ }
905
+
906
+ if (fileExists(path.join(directory, "pnpm-lock.yaml"))) {
907
+ return "pnpm"
908
+ }
909
+
910
+ if (fileExists(path.join(directory, "yarn.lock"))) {
911
+ return "yarn"
912
+ }
913
+
914
+ if (fileExists(path.join(directory, "package-lock.json"))) {
915
+ return "npm"
916
+ }
917
+
918
+ return null
919
+ }
920
+
921
+ export function resolveAppPackageManager(appPaths, config) {
922
+ const frontendDir = resolveFrontendDir(appPaths, config)
923
+ const frontendPackageJsonPath = path.join(frontendDir, "package.json")
924
+
925
+ return (
926
+ detectPackageManagerFromManifest(appPaths.appPackageJsonPath) ??
927
+ detectPackageManagerFromManifest(frontendPackageJsonPath) ??
928
+ detectPackageManagerFromLocks(appPaths.appRoot) ??
929
+ detectPackageManagerFromLocks(frontendDir) ??
930
+ "npm"
931
+ )
932
+ }