alepha 0.20.2 → 0.20.3

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 (208) hide show
  1. package/README.md +0 -1
  2. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  3. package/assets/swagger-ui/swagger-ui.css +1 -1
  4. package/dist/api/audits/index.browser.js +49 -0
  5. package/dist/api/audits/index.browser.js.map +1 -1
  6. package/dist/api/audits/index.d.ts.map +1 -1
  7. package/dist/api/audits/index.js +49 -0
  8. package/dist/api/audits/index.js.map +1 -1
  9. package/dist/api/files/index.d.ts.map +1 -1
  10. package/dist/api/files/index.js.map +1 -1
  11. package/dist/api/jobs/index.d.ts +16 -75
  12. package/dist/api/jobs/index.d.ts.map +1 -1
  13. package/dist/api/jobs/index.js.map +1 -1
  14. package/dist/api/keys/index.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +1 -10
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/organizations/index.d.ts.map +1 -1
  18. package/dist/api/parameters/index.browser.js +37 -0
  19. package/dist/api/parameters/index.browser.js.map +1 -1
  20. package/dist/api/parameters/index.d.ts +4 -65
  21. package/dist/api/parameters/index.d.ts.map +1 -1
  22. package/dist/api/parameters/index.js +37 -0
  23. package/dist/api/parameters/index.js.map +1 -1
  24. package/dist/api/payments/index.d.ts.map +1 -1
  25. package/dist/api/payments/index.js.map +1 -1
  26. package/dist/api/users/index.d.ts +207 -5184
  27. package/dist/api/users/index.d.ts.map +1 -1
  28. package/dist/api/users/index.js +2 -4
  29. package/dist/api/users/index.js.map +1 -1
  30. package/dist/api/verifications/index.d.ts.map +1 -1
  31. package/dist/api/verifications/index.js +2 -1
  32. package/dist/api/verifications/index.js.map +1 -1
  33. package/dist/bucket/index.js +5 -1
  34. package/dist/bucket/index.js.map +1 -1
  35. package/dist/bucket/index.workerd.js +5 -1
  36. package/dist/bucket/index.workerd.js.map +1 -1
  37. package/dist/cache/core/index.js.map +1 -1
  38. package/dist/cache/core/index.workerd.js.map +1 -1
  39. package/dist/captcha/index.js.map +1 -1
  40. package/dist/cli/core/index.d.ts +217 -11647
  41. package/dist/cli/core/index.d.ts.map +1 -1
  42. package/dist/cli/core/index.js +706 -42
  43. package/dist/cli/core/index.js.map +1 -1
  44. package/dist/cli/devtools/index.js +7 -1
  45. package/dist/cli/devtools/index.js.map +1 -1
  46. package/dist/cli/platform/index.d.ts +41 -64
  47. package/dist/cli/platform/index.d.ts.map +1 -1
  48. package/dist/cli/platform/index.js +47 -0
  49. package/dist/cli/platform/index.js.map +1 -1
  50. package/dist/cli/vendor/index.js +15 -0
  51. package/dist/cli/vendor/index.js.map +1 -1
  52. package/dist/command/index.js +1 -1
  53. package/dist/command/index.js.map +1 -1
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +2 -8
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js.map +1 -1
  58. package/dist/core/index.native.js.map +1 -1
  59. package/dist/core/index.workerd.js.map +1 -1
  60. package/dist/crypto/index.js.map +1 -1
  61. package/dist/datetime/index.js.map +1 -1
  62. package/dist/email/core/index.js.map +1 -1
  63. package/dist/email/smtp/index.js +2 -10522
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/fake/index.d.ts +4 -8085
  66. package/dist/fake/index.d.ts.map +1 -1
  67. package/dist/fake/index.js +3 -33554
  68. package/dist/fake/index.js.map +1 -1
  69. package/dist/lock/core/index.js.map +1 -1
  70. package/dist/lock/redis/index.js.map +1 -1
  71. package/dist/logger/index.js +32 -1
  72. package/dist/logger/index.js.map +1 -1
  73. package/dist/mcp/index.js +5 -1
  74. package/dist/mcp/index.js.map +1 -1
  75. package/dist/orm/core/index.browser.js +1 -361
  76. package/dist/orm/core/index.browser.js.map +1 -1
  77. package/dist/orm/core/index.bun.js +14 -406
  78. package/dist/orm/core/index.bun.js.map +1 -1
  79. package/dist/orm/core/index.d.ts +96 -5117
  80. package/dist/orm/core/index.d.ts.map +1 -1
  81. package/dist/orm/core/index.js +23 -419
  82. package/dist/orm/core/index.js.map +1 -1
  83. package/dist/orm/postgres/index.bun.js +17 -20
  84. package/dist/orm/postgres/index.bun.js.map +1 -1
  85. package/dist/orm/postgres/index.d.ts +2 -613
  86. package/dist/orm/postgres/index.d.ts.map +1 -1
  87. package/dist/orm/postgres/index.js +17 -20
  88. package/dist/orm/postgres/index.js.map +1 -1
  89. package/dist/react/core/index.js.map +1 -1
  90. package/dist/react/i18n/index.js.map +1 -1
  91. package/dist/react/intro/index.js +22 -17
  92. package/dist/react/intro/index.js.map +1 -1
  93. package/dist/react/router/index.browser.js +78 -2
  94. package/dist/react/router/index.browser.js.map +1 -1
  95. package/dist/react/router/index.d.ts +22 -1
  96. package/dist/react/router/index.d.ts.map +1 -1
  97. package/dist/react/router/index.js +102 -4
  98. package/dist/react/router/index.js.map +1 -1
  99. package/dist/react/testing/index.d.ts +1 -411
  100. package/dist/react/testing/index.d.ts.map +1 -1
  101. package/dist/react/testing/index.js +13 -12293
  102. package/dist/react/testing/index.js.map +1 -1
  103. package/dist/react/ui/index.js +3 -0
  104. package/dist/react/ui/index.js.map +1 -1
  105. package/dist/react/websocket/index.js.map +1 -1
  106. package/dist/redis/index.js.map +1 -1
  107. package/dist/scheduler/index.d.ts +1 -83
  108. package/dist/scheduler/index.d.ts.map +1 -1
  109. package/dist/scheduler/index.js +2 -391
  110. package/dist/scheduler/index.js.map +1 -1
  111. package/dist/scheduler/index.workerd.js +2 -391
  112. package/dist/scheduler/index.workerd.js.map +1 -1
  113. package/dist/security/index.browser.js.map +1 -1
  114. package/dist/security/index.d.ts +2 -325
  115. package/dist/security/index.d.ts.map +1 -1
  116. package/dist/security/index.js +3 -1362
  117. package/dist/security/index.js.map +1 -1
  118. package/dist/server/auth/index.d.ts +1 -1054
  119. package/dist/server/auth/index.d.ts.map +1 -1
  120. package/dist/server/auth/index.js +16 -1224
  121. package/dist/server/auth/index.js.map +1 -1
  122. package/dist/server/cookies/index.js.map +1 -1
  123. package/dist/server/core/index.browser.js.map +1 -1
  124. package/dist/server/core/index.d.ts +1 -4
  125. package/dist/server/core/index.d.ts.map +1 -1
  126. package/dist/server/core/index.js +19 -4
  127. package/dist/server/core/index.js.map +1 -1
  128. package/dist/server/links/index.browser.js.map +1 -1
  129. package/dist/server/links/index.js.map +1 -1
  130. package/dist/server/metrics/index.d.ts +1 -514
  131. package/dist/server/metrics/index.d.ts.map +1 -1
  132. package/dist/server/metrics/index.js +4 -4356
  133. package/dist/server/metrics/index.js.map +1 -1
  134. package/dist/server/rate-limit/index.js.map +1 -1
  135. package/dist/server/static/index.js.map +1 -1
  136. package/dist/server/swagger/index.js +1 -1
  137. package/dist/server/swagger/index.js.map +1 -1
  138. package/dist/sms/index.js.map +1 -1
  139. package/dist/system/index.browser.js.map +1 -1
  140. package/dist/system/index.js.map +1 -1
  141. package/dist/system/index.workerd.js.map +1 -1
  142. package/dist/topic/core/index.js.map +1 -1
  143. package/dist/websocket/index.browser.js +21 -0
  144. package/dist/websocket/index.browser.js.map +1 -1
  145. package/dist/websocket/index.js +21 -0
  146. package/dist/websocket/index.js.map +1 -1
  147. package/package.json +18 -15
  148. package/src/api/files/__tests__/FileController.spec.ts +1 -1
  149. package/src/api/jobs/__tests__/$job.spec.ts +5 -1
  150. package/src/api/users/schemas/userQuerySchema.ts +0 -1
  151. package/src/api/users/services/UserService.ts +1 -5
  152. package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
  153. package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
  154. package/src/api/verifications/services/VerificationService.ts +1 -0
  155. package/src/cli/core/__tests__/init.spec.ts +208 -0
  156. package/src/cli/core/commands/init.ts +12 -0
  157. package/src/cli/core/services/PackageManagerUtils.ts +23 -6
  158. package/src/cli/core/services/ProjectScaffolder.ts +298 -20
  159. package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
  160. package/src/cli/core/tasks/BuildServerTask.ts +8 -0
  161. package/src/cli/core/templates/apiIndexTs.ts +23 -1
  162. package/src/cli/core/templates/componentsJsonTs.ts +39 -0
  163. package/src/cli/core/templates/mainCss.ts +1 -0
  164. package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
  165. package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
  166. package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
  167. package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
  168. package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
  169. package/src/cli/core/templates/webAppRouterTs.ts +104 -1
  170. package/src/cli/core/templates/webIndexTs.ts +23 -1
  171. package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
  172. package/src/command/providers/CliProvider.ts +1 -1
  173. package/src/core/interfaces/Service.ts +3 -1
  174. package/src/core/providers/TypeProvider.ts +1 -1
  175. package/src/logger/services/Logger.ts +1 -1
  176. package/src/mcp/__tests__/$resource.spec.ts +1 -1
  177. package/src/mcp/__tests__/$tool.spec.ts +1 -1
  178. package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
  179. package/src/orm/__tests__/$repository-tests.ts +1 -0
  180. package/src/orm/__tests__/orm-next-tests.ts +2 -67
  181. package/src/orm/__tests__/orm-next.spec.ts +0 -21
  182. package/src/orm/core/index.shared.ts +0 -2
  183. package/src/orm/core/index.ts +1 -2
  184. package/src/orm/core/primitives/$repository.ts +3 -6
  185. package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
  186. package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
  187. package/src/orm/core/services/ModelBuilder.ts +1 -13
  188. package/src/orm/core/services/Repository.ts +1 -42
  189. package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
  190. package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
  191. package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
  192. package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
  193. package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
  194. package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
  195. package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
  196. package/src/react/router/providers/ReactServerProvider.ts +1 -0
  197. package/src/scheduler/providers/CronProvider.ts +1 -1
  198. package/src/security/primitives/$basicAuth.ts +1 -1
  199. package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
  200. package/src/server/core/interfaces/ServerRequest.ts +1 -0
  201. package/src/server/core/providers/ServerProvider.ts +1 -1
  202. package/src/server/core/providers/ServerRouterProvider.ts +2 -2
  203. package/src/server/core/services/HttpClient.ts +1 -1
  204. package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
  205. package/dist/react/testing/chunk-DBEY4PJZ.js +0 -16
  206. package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
  207. package/src/orm/core/helpers/parseQueryString.ts +0 -502
  208. package/src/orm/core/primitives/$view.ts +0 -88
@@ -37,7 +37,20 @@ const buildOptions = $atom({
37
37
  name: "alepha.cli.build.options",
38
38
  description: "Build configuration options",
39
39
  schema: t.object({
40
+ /**
41
+ * Generate build stats report.
42
+ *
43
+ * - `true` - Generate a static HTML report
44
+ * - `"json"` - Generate a JSON report
45
+ */
40
46
  stats: t.optional(t.union([t.boolean(), t.enum(["json"])])),
47
+ /**
48
+ * Deployment target for the build output.
49
+ *
50
+ * - `docker` - Generate Dockerfile for containerized deployment
51
+ * - `vercel` - Generate Vercel deployment configuration (forces node runtime)
52
+ * - `cloudflare` - Generate Cloudflare Workers configuration (forces workerd runtime)
53
+ */
41
54
  target: t.optional(t.enum([
42
55
  "bare",
43
56
  "docker",
@@ -45,15 +58,45 @@ const buildOptions = $atom({
45
58
  "cloudflare",
46
59
  "static"
47
60
  ])),
61
+ /**
62
+ * JavaScript runtime for the build output.
63
+ *
64
+ * - `node` - Node.js runtime (default)
65
+ * - `bun` - Bun runtime (uses bun export conditions)
66
+ * - `workerd` - Cloudflare Workers runtime (auto-set with cloudflare target)
67
+ *
68
+ * Note: Some targets force a specific runtime:
69
+ * - `cloudflare` always uses `workerd`
70
+ * - `vercel` always uses `node`
71
+ */
48
72
  runtime: t.optional(t.enum([
49
73
  "node",
50
74
  "bun",
51
75
  "workerd"
52
76
  ])),
77
+ /**
78
+ * Output directory configuration.
79
+ */
53
80
  output: t.optional(t.object({
81
+ /**
82
+ * Root dist directory.
83
+ *
84
+ * @default "dist"
85
+ */
54
86
  dist: t.optional(t.string({ default: "dist" })),
87
+ /**
88
+ * Public/client subdirectory.
89
+ *
90
+ * @default "public"
91
+ */
55
92
  public: t.optional(t.string({ default: "public" }))
56
93
  })),
94
+ /**
95
+ * Vercel-specific deployment configuration.
96
+ *
97
+ * Note: Set `target: "vercel"` to enable Vercel deployment.
98
+ * This object is only for additional configuration.
99
+ */
57
100
  vercel: t.optional(t.object({
58
101
  projectName: t.optional(t.string()),
59
102
  orgId: t.optional(t.string()),
@@ -63,31 +106,143 @@ const buildOptions = $atom({
63
106
  schedule: t.string()
64
107
  }))) }))
65
108
  })),
109
+ /**
110
+ * Cloudflare-specific deployment configuration.
111
+ *
112
+ * Note: Set `target: "cloudflare"` to enable Cloudflare deployment.
113
+ * This object is only for additional configuration.
114
+ */
66
115
  cloudflare: t.optional(t.object({ config: t.optional(t.json()) })),
116
+ /**
117
+ * Docker-specific deployment configuration.
118
+ *
119
+ * Note: Set `target: "docker"` to enable Docker deployment.
120
+ * This object is only for additional configuration.
121
+ */
67
122
  docker: t.optional(t.object({
123
+ /**
124
+ * Base image for the Dockerfile (FROM instruction).
125
+ *
126
+ * @default "node:24-alpine" for node runtime
127
+ * @default "oven/bun:alpine" for bun runtime
128
+ */
68
129
  from: t.optional(t.string()),
130
+ /**
131
+ * Command to run in the Docker container.
132
+ *
133
+ * @default "node" for node runtime
134
+ * @default "bun" for bun runtime
135
+ */
69
136
  command: t.optional(t.string()),
137
+ /**
138
+ * Docker build options (used when --image flag is passed).
139
+ */
70
140
  image: t.optional(t.object({
141
+ /**
142
+ * Default image tag (name without version).
143
+ *
144
+ * Used when --image is provided without a full override:
145
+ * - `--image` → `tag:latest`
146
+ * - `--image=1.3.4` → `tag:1.3.4`
147
+ * - `--image=other/img:v1` → `other/img:v1` (full override)
148
+ *
149
+ * @example "myproject/myapp"
150
+ * @example "ghcr.io/myorg/myapp"
151
+ */
71
152
  tag: t.string(),
153
+ /**
154
+ * Additional arguments to pass to `docker build`.
155
+ *
156
+ * @example '--platform linux/amd64 --no-cache'
157
+ */
72
158
  args: t.optional(t.string()),
159
+ /**
160
+ * Auto-add OCI standard labels (revision, created, version).
161
+ *
162
+ * Adds:
163
+ * - org.opencontainers.image.revision (git commit SHA)
164
+ * - org.opencontainers.image.created (build timestamp)
165
+ * - org.opencontainers.image.version (from image tag)
166
+ */
73
167
  oci: t.optional(t.boolean())
74
168
  }))
75
169
  })),
76
- static: t.optional(t.object({ domain: t.optional(t.string()) })),
170
+ /**
171
+ * Static site deployment configuration.
172
+ *
173
+ * Note: Set `target: "static"` to enable static site generation.
174
+ */
175
+ static: t.optional(t.object({
176
+ /**
177
+ * Surge domain for deployment.
178
+ *
179
+ * If set, a CNAME file is written to dist/public/.
180
+ * If not set, a domain is auto-generated from package.json name.
181
+ *
182
+ * @example "my-app.surge.sh"
183
+ * @example "my-custom-domain.com"
184
+ */
185
+ domain: t.optional(t.string()) })),
186
+ /**
187
+ * PWA (Progressive Web App) configuration.
188
+ *
189
+ * Generates a web app manifest and enables installability.
190
+ * Requires a client-side bundle (React).
191
+ */
77
192
  pwa: t.optional(t.object({
193
+ /**
194
+ * Full application name displayed on the splash screen
195
+ * and in the OS app switcher.
196
+ */
78
197
  name: t.string(),
198
+ /**
199
+ * Short name displayed on the home screen icon.
200
+ * Falls back to `name` if omitted.
201
+ */
79
202
  shortName: t.optional(t.string()),
203
+ /**
204
+ * Theme color used for the browser toolbar and OS chrome.
205
+ *
206
+ * @default "#ffffff"
207
+ */
80
208
  themeColor: t.optional(t.string()),
209
+ /**
210
+ * Background color for the splash screen.
211
+ *
212
+ * @default "#ffffff"
213
+ */
81
214
  backgroundColor: t.optional(t.string()),
215
+ /**
216
+ * Display mode for the installed PWA.
217
+ *
218
+ * - `standalone` - Looks like a native app (default)
219
+ * - `fullscreen` - Uses entire screen (games, immersive)
220
+ * - `minimal-ui` - Like standalone with minimal browser UI
221
+ * - `browser` - Standard browser tab
222
+ *
223
+ * @default "standalone"
224
+ */
82
225
  display: t.optional(t.enum([
83
226
  "standalone",
84
227
  "fullscreen",
85
228
  "minimal-ui",
86
229
  "browser"
87
230
  ])),
231
+ /**
232
+ * Enable offline support via service worker.
233
+ *
234
+ * TODO: Not yet implemented.
235
+ */
88
236
  offline: t.optional(t.boolean())
89
237
  })),
90
- sitemap: t.optional(t.object({ hostname: t.string() }))
238
+ /**
239
+ * Sitemap generation configuration.
240
+ */
241
+ sitemap: t.optional(t.object({
242
+ /**
243
+ * Base URL for sitemap entries.
244
+ */
245
+ hostname: t.string() }))
91
246
  }),
92
247
  default: {}
93
248
  });
@@ -102,7 +257,11 @@ const buildOptions = $atom({
102
257
  const devOptions = $atom({
103
258
  name: "alepha.cli.dev.options",
104
259
  description: "Dev configuration options",
105
- schema: t.object({ noViteReactPlugin: t.optional(t.boolean({ default: false })) }),
260
+ schema: t.object({
261
+ /**
262
+ * Disable Vite React plugin.
263
+ */
264
+ noViteReactPlugin: t.optional(t.boolean({ default: false })) }),
106
265
  default: {}
107
266
  });
108
267
  //#endregion
@@ -598,7 +757,7 @@ var AlephaCliUtils = class {
598
757
  };
599
758
  //#endregion
600
759
  //#region ../../package.json
601
- var version$1 = "0.20.2";
760
+ var version$1 = "0.20.3";
602
761
  //#endregion
603
762
  //#region ../../src/cli/core/alephaPackageJson.ts
604
763
  const alephaPackageJson = {
@@ -627,18 +786,19 @@ const alephaPackageJson = {
627
786
  "drizzle-orm": "^0.45.2",
628
787
  "postgres": "^3.4.9",
629
788
  "tsx": "^4.21.0",
630
- "typebox": "^1.1.23",
631
- "typescript": "^6.0.2",
632
- "vite-bundle-analyzer": "^1.3.7",
789
+ "typebox": "^1.1.34",
790
+ "typescript": "^6.0.3",
791
+ "vite-bundle-analyzer": "^1.3.8",
633
792
  "ws": "^8.20.0"
634
793
  },
635
794
  devDependencies: {
636
- "@biomejs/biome": "^2.4.12",
637
- "@electric-sql/pglite": "^0.4.4",
795
+ "@biomejs/biome": "^2.4.13",
796
+ "@electric-sql/pglite": "^0.4.5",
638
797
  "@faker-js/faker": "^10.4.0",
798
+ "@tailwindcss/vite": "^4.2.4",
639
799
  "@testing-library/dom": "^10.4.1",
640
800
  "@testing-library/react": "^16.3.2",
641
- "@types/bun": "^1.3.12",
801
+ "@types/bun": "^1.3.13",
642
802
  "@types/node": "^25.6.0",
643
803
  "@types/nodemailer": "^8.0.0",
644
804
  "@types/react": "^19.2.14",
@@ -646,17 +806,19 @@ const alephaPackageJson = {
646
806
  "@types/ws": "^8.18.1",
647
807
  "cron-schedule": "^6.0.0",
648
808
  "drizzle-kit": "^0.31.10",
649
- "jose": "^6.2.2",
650
- "jsdom": "^29.0.2",
651
- "nodemailer": "^8.0.5",
652
- "openid-client": "^6.8.3",
809
+ "jose": "^6.2.3",
810
+ "jsdom": "^29.1.0",
811
+ "nodemailer": "^8.0.7",
812
+ "openid-client": "^6.8.4",
653
813
  "prom-client": "^15.1.3",
654
814
  "react": "^19.2.5",
655
815
  "react-dom": "^19.2.5",
656
- "swagger-ui-dist": "^5.32.4",
657
- "tsdown": "^0.21.8",
658
- "vite": "^8.0.8",
659
- "vitest": "^4.1.4"
816
+ "shadcn": "^4.6.0",
817
+ "swagger-ui-dist": "^5.32.5",
818
+ "tailwindcss": "^4.2.4",
819
+ "tsdown": "^0.21.10",
820
+ "vite": "^8.0.10",
821
+ "vitest": "^4.1.5"
660
822
  },
661
823
  peerDependencies: {
662
824
  "react": "^19",
@@ -1285,14 +1447,14 @@ var PackageManagerUtils = class {
1285
1447
  "yarn.lock"
1286
1448
  ]);
1287
1449
  await this.editPackageJson(root, (pkg) => {
1288
- delete pkg.packageManager;
1450
+ pkg.packageManager = void 0;
1289
1451
  return pkg;
1290
1452
  });
1291
1453
  }
1292
1454
  async removePnpm(root) {
1293
1455
  await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
1294
1456
  await this.editPackageJson(root, (pkg) => {
1295
- delete pkg.packageManager;
1457
+ pkg.packageManager = void 0;
1296
1458
  return pkg;
1297
1459
  });
1298
1460
  }
@@ -1342,7 +1504,7 @@ var PackageManagerUtils = class {
1342
1504
  const alephaDeps = alephaPackageJson.devDependencies;
1343
1505
  const dependencies = { alepha: `^${version}` };
1344
1506
  const devDependencies = { vite: alephaDeps.vite };
1345
- if (!modes.react) devDependencies["drizzle-kit"] = alephaDeps["drizzle-kit"];
1507
+ if (!modes.react || modes.saas) devDependencies["drizzle-kit"] = alephaDeps["drizzle-kit"];
1346
1508
  if (!modes.isPackage) {
1347
1509
  devDependencies["@biomejs/biome"] = alephaDeps["@biomejs/biome"];
1348
1510
  if (modes.test) devDependencies.vitest = alephaDeps.vitest;
@@ -1356,9 +1518,10 @@ var PackageManagerUtils = class {
1356
1518
  };
1357
1519
  if (modes.test) scripts.test = "vitest run";
1358
1520
  if (modes.tailwind) {
1359
- devDependencies.tailwindcss = "^4.2.0";
1360
- devDependencies["@tailwindcss/vite"] = "^4.2.0";
1521
+ devDependencies.tailwindcss = alephaDeps.tailwindcss;
1522
+ devDependencies["@tailwindcss/vite"] = alephaDeps["@tailwindcss/vite"];
1361
1523
  }
1524
+ if (modes.shadcn) devDependencies.shadcn = alephaDeps.shadcn;
1362
1525
  if (modes.react) {
1363
1526
  dependencies.react = alephaDeps.react;
1364
1527
  dependencies["react-dom"] = alephaDeps["react-dom"];
@@ -1472,7 +1635,19 @@ export type HelloResponse = Static<typeof helloResponseSchema>;
1472
1635
  //#endregion
1473
1636
  //#region ../../src/cli/core/templates/apiIndexTs.ts
1474
1637
  const apiIndexTs = (options = {}) => {
1475
- const { appName = "app" } = options;
1638
+ const { appName = "app", saas = false } = options;
1639
+ if (saas) return `
1640
+ import { $module } from "alepha";
1641
+ import { AlephaApiUsers } from "alepha/api/users";
1642
+ import { HelloController } from "./controllers/HelloController.ts";
1643
+ import { RealmProvider } from "./providers/RealmProvider.ts";
1644
+
1645
+ export const ApiModule = $module({
1646
+ name: "${appName}.api",
1647
+ services: [HelloController, RealmProvider],
1648
+ imports: [AlephaApiUsers],
1649
+ });
1650
+ `.trim();
1476
1651
  return `
1477
1652
  import { $module } from "alepha";
1478
1653
  import { HelloController } from "./controllers/HelloController.ts";
@@ -1519,6 +1694,46 @@ const biomeJson = () => `
1519
1694
  }
1520
1695
  `.trim();
1521
1696
  //#endregion
1697
+ //#region ../../src/cli/core/templates/componentsJsonTs.ts
1698
+ /**
1699
+ * `components.json` is the shadcn CLI's project config — it tells
1700
+ * `shadcn add` where to drop primitives, which tailwind tokens to use,
1701
+ * which icon library to wire up, and which custom registries to resolve.
1702
+ *
1703
+ * Aliases follow shadcn's defaults (`@/components`, `@/lib/utils`) so the
1704
+ * CLI honors them across `init` + `add` calls. Alepha app code lives at
1705
+ * `src/web/` (Home, AppRouter, …) and the shadcn primitives live at
1706
+ * `src/components/` — kept separate to make the registry components
1707
+ * trivially upgradable via `shadcn add --overwrite`.
1708
+ *
1709
+ * The `registries` block pre-wires the public Alepha registry — consumers
1710
+ * can immediately run e.g. `shadcn add @alepha/auth-login`.
1711
+ */
1712
+ const componentsJsonTs = () => `{
1713
+ "$schema": "https://ui.shadcn.com/schema.json",
1714
+ "style": "new-york",
1715
+ "rsc": false,
1716
+ "tsx": true,
1717
+ "tailwind": {
1718
+ "config": "",
1719
+ "css": "src/main.css",
1720
+ "baseColor": "neutral",
1721
+ "cssVariables": true
1722
+ },
1723
+ "aliases": {
1724
+ "components": "@/components",
1725
+ "utils": "@/lib/utils",
1726
+ "ui": "@/components/ui",
1727
+ "lib": "@/lib",
1728
+ "hooks": "@/hooks"
1729
+ },
1730
+ "iconLibrary": "lucide",
1731
+ "registries": {
1732
+ "@alepha": "https://alepha.dev/r/{name}.json"
1733
+ }
1734
+ }
1735
+ `;
1736
+ //#endregion
1522
1737
  //#region ../../src/cli/core/templates/dummySpecTs.ts
1523
1738
  const dummySpecTs = () => `
1524
1739
  import { test, expect } from "vitest";
@@ -1660,6 +1875,7 @@ const mainCss = (opts = {}) => {
1660
1875
  *
1661
1876
  * Options:
1662
1877
  * - Tailwind CSS: Use \`alepha init --tailwind\` to add Tailwind CSS
1878
+ * - shadcn/ui: Use \`alepha init --shadcn\` to add shadcn/ui setup
1663
1879
  * - Raw CSS: Write your own styles below
1664
1880
  */
1665
1881
 
@@ -1690,6 +1906,225 @@ run(alepha);
1690
1906
  `.trim();
1691
1907
  };
1692
1908
  //#endregion
1909
+ //#region ../../src/cli/core/templates/saasAdminLayoutTsx.ts
1910
+ /**
1911
+ * SaaS admin layout — full AppShell on /admin with a sidebar, breadcrumbs,
1912
+ * a Sonner toaster, and a confirm provider. The page list grows with
1913
+ * whatever `admin-*` registry components the user adds.
1914
+ *
1915
+ * All UI primitives come from `src/components/*` where `shadcn add` drops
1916
+ * them; alepha app code lives in `src/web/*` and references them via the
1917
+ * `@/components/*` alias.
1918
+ */
1919
+ const saasAdminLayoutTsx = () => `import { AppShell } from "@/components/app-shell";
1920
+ import { Toaster } from "@/components/ui/sonner";
1921
+ import { TooltipProvider } from "@/components/ui/tooltip";
1922
+ import { ConfirmProvider } from "@/components/use-confirm";
1923
+ import { NestedView, useRouterState } from "alepha/react/router";
1924
+ import { ShieldCheck, Users } from "lucide-react";
1925
+
1926
+ const NAV = [
1927
+ {
1928
+ label: "Identity",
1929
+ items: [
1930
+ { href: "/admin/users", label: "Users", icon: Users },
1931
+ { href: "/admin/sessions", label: "Sessions", icon: ShieldCheck },
1932
+ ],
1933
+ },
1934
+ ] as const;
1935
+
1936
+ const findCrumbs = (pathname: string): { label: string; href?: string }[] => {
1937
+ for (const group of NAV) {
1938
+ const match = group.items.find((it) => it.href === pathname);
1939
+ if (match) return [{ label: group.label }, { label: match.label }];
1940
+ }
1941
+ return [];
1942
+ };
1943
+
1944
+ const AdminLayout = () => {
1945
+ const state = useRouterState();
1946
+ const crumbs = findCrumbs(state.url.pathname);
1947
+
1948
+ return (
1949
+ <TooltipProvider>
1950
+ <ConfirmProvider>
1951
+ <AppShell
1952
+ brand={
1953
+ <a
1954
+ href="/admin"
1955
+ className="flex items-center gap-2 px-2 py-2 font-semibold group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
1956
+ >
1957
+ <span className="bg-primary text-primary-foreground flex size-7 shrink-0 items-center justify-center rounded">
1958
+ α
1959
+ </span>
1960
+ <span className="truncate group-data-[collapsible=icon]:hidden">
1961
+ Admin
1962
+ </span>
1963
+ </a>
1964
+ }
1965
+ nav={NAV.map((group) => ({
1966
+ label: group.label,
1967
+ items: group.items.map((it) => ({
1968
+ href: it.href,
1969
+ label: it.label,
1970
+ icon: it.icon,
1971
+ active: it.href === state.url.pathname,
1972
+ })),
1973
+ }))}
1974
+ breadcrumbs={crumbs.length ? crumbs : undefined}
1975
+ >
1976
+ <NestedView />
1977
+ </AppShell>
1978
+ <Toaster />
1979
+ </ConfirmProvider>
1980
+ </TooltipProvider>
1981
+ );
1982
+ };
1983
+
1984
+ export default AdminLayout;
1985
+ `;
1986
+ //#endregion
1987
+ //#region ../../src/cli/core/templates/saasAdminPagesTsx.ts
1988
+ /**
1989
+ * Admin pages — each is a thin wrapper around the matching `admin-*`
1990
+ * registry component (placed at `src/components/admin/*`). The starter
1991
+ * ships with Users + Sessions; add more by `shadcn add @alepha/admin-…`
1992
+ * and a matching `$page(...)` in AppRouter.
1993
+ */
1994
+ const saasAdminUsersTsx = () => `import { AdminUsers } from "@/components/admin/admin-users";
1995
+
1996
+ const AdminUsersPage = () => {
1997
+ return <AdminUsers />;
1998
+ };
1999
+
2000
+ export default AdminUsersPage;
2001
+ `;
2002
+ const saasAdminSessionsTsx = () => `import { AdminSessions } from "@/components/admin/admin-sessions";
2003
+
2004
+ const AdminSessionsPage = () => {
2005
+ return <AdminSessions />;
2006
+ };
2007
+
2008
+ export default AdminSessionsPage;
2009
+ `;
2010
+ //#endregion
2011
+ //#region ../../src/cli/core/templates/saasAuthLayoutTsx.ts
2012
+ /**
2013
+ * SaaS auth layout — wraps every /auth/* page with a centered card.
2014
+ * Routes (login, register, reset-password, verify-email) are mounted as
2015
+ * children so they share this shell.
2016
+ */
2017
+ const saasAuthLayoutTsx = () => `import { NestedView } from "alepha/react/router";
2018
+
2019
+ const AuthLayout = () => {
2020
+ return (
2021
+ <div className="bg-background flex min-h-screen items-center justify-center p-4">
2022
+ <div className="w-full max-w-md">
2023
+ <NestedView />
2024
+ </div>
2025
+ </div>
2026
+ );
2027
+ };
2028
+
2029
+ export default AuthLayout;
2030
+ `;
2031
+ //#endregion
2032
+ //#region ../../src/cli/core/templates/saasAuthPagesTsx.ts
2033
+ /**
2034
+ * Per-page wrapper around the registry components. The registry component
2035
+ * receives the realm config from the page loader; the page itself stays a
2036
+ * thin shell so apps can layer their branding around it.
2037
+ *
2038
+ * Registry components land at `src/components/auth/*` (shadcn defaults).
2039
+ */
2040
+ const saasAuthLoginTsx = () => `import { AuthLogin } from "@/components/auth/auth-login";
2041
+ import type { RealmConfig } from "alepha/api/users";
2042
+
2043
+ export interface AuthLoginPageProps {
2044
+ realmConfig: RealmConfig;
2045
+ }
2046
+
2047
+ const AuthLoginPage = (props: AuthLoginPageProps) => {
2048
+ return <AuthLogin realmConfig={props.realmConfig} />;
2049
+ };
2050
+
2051
+ export default AuthLoginPage;
2052
+ `;
2053
+ const saasAuthRegisterTsx = () => `import { AuthRegister } from "@/components/auth/auth-register";
2054
+ import type { RealmConfig } from "alepha/api/users";
2055
+
2056
+ export interface AuthRegisterPageProps {
2057
+ realmConfig: RealmConfig;
2058
+ }
2059
+
2060
+ const AuthRegisterPage = (props: AuthRegisterPageProps) => {
2061
+ return <AuthRegister realmConfig={props.realmConfig} />;
2062
+ };
2063
+
2064
+ export default AuthRegisterPage;
2065
+ `;
2066
+ const saasAuthResetPasswordTsx = () => `import { AuthResetPassword } from "@/components/auth/auth-reset-password";
2067
+ import type { RealmConfig } from "alepha/api/users";
2068
+
2069
+ export interface AuthResetPasswordPageProps {
2070
+ realmConfig: RealmConfig;
2071
+ }
2072
+
2073
+ const AuthResetPasswordPage = (props: AuthResetPasswordPageProps) => {
2074
+ return <AuthResetPassword realmConfig={props.realmConfig} />;
2075
+ };
2076
+
2077
+ export default AuthResetPasswordPage;
2078
+ `;
2079
+ const saasAuthVerifyEmailTsx = () => `import { AuthVerifyEmail } from "@/components/auth/auth-verify-email";
2080
+
2081
+ const AuthVerifyEmailPage = () => {
2082
+ return <AuthVerifyEmail />;
2083
+ };
2084
+
2085
+ export default AuthVerifyEmailPage;
2086
+ `;
2087
+ //#endregion
2088
+ //#region ../../src/cli/core/templates/saasRealmProviderTs.ts
2089
+ /**
2090
+ * Realm provider scaffolded by `alepha init --saas`.
2091
+ *
2092
+ * Minimal hello-world setup: credentials login with email, one admin seeded
2093
+ * with the developer's git email at scaffold time, and an `admin:ui`
2094
+ * permission used by the AppRouter to gate `/admin/*`. The default `admin`
2095
+ * role grants `*` (so it inherits `admin:ui`); the default `user` role
2096
+ * excludes `admin:*` (so non-admins get a 403 before the shell renders).
2097
+ *
2098
+ * Add `$env`, more permissions, or stricter settings as the project grows.
2099
+ */
2100
+ const saasRealmProviderTs = (options = {}) => {
2101
+ const adminEmail = options.adminEmail ?? "admin@example.com";
2102
+ return `import { $realm } from "alepha/api/users";
2103
+ import { $permission } from "alepha/security";
2104
+
2105
+ export class RealmProvider {
2106
+ /**
2107
+ * Permission required to open the admin UI. Wired into AppRouter.adminLayout
2108
+ * via \`$secure({ permissions: ["admin:ui"] })\`.
2109
+ */
2110
+ adminUi = $permission({
2111
+ group: "admin",
2112
+ name: "ui",
2113
+ description: "Access to the admin UI shell",
2114
+ });
2115
+
2116
+ realm = $realm({
2117
+ settings: {
2118
+ adminEmails: [${JSON.stringify(adminEmail)}],
2119
+ },
2120
+ identities: {
2121
+ credentials: true,
2122
+ },
2123
+ });
2124
+ }
2125
+ `;
2126
+ };
2127
+ //#endregion
1693
2128
  //#region ../../src/cli/core/templates/tsconfigJson.ts
1694
2129
  const tsconfigJson = () => `
1695
2130
  {
@@ -1733,6 +2168,10 @@ export default defineConfig({
1733
2168
  //#endregion
1734
2169
  //#region ../../src/cli/core/templates/webAppRouterTs.ts
1735
2170
  const webAppRouterTs = (options) => {
2171
+ if (options.saas) return saasAppRouterTs();
2172
+ return basicAppRouterTs(options);
2173
+ };
2174
+ const basicAppRouterTs = (options) => {
1736
2175
  const imports = ["import { $page } from \"alepha/react/router\";"];
1737
2176
  const classMembers = [];
1738
2177
  if (options.api) {
@@ -1754,6 +2193,95 @@ export class AppRouter {
1754
2193
  ${classMembers.join("\n\n")}
1755
2194
  }`;
1756
2195
  };
2196
+ /**
2197
+ * SaaS router wires three trees onto the app:
2198
+ * / → Home
2199
+ * /auth/* → AuthLayout + login / register / reset / verify
2200
+ * /admin/* → AdminLayout + users / sessions / api-keys / parameters / audits
2201
+ *
2202
+ * Each auth page resolves the realm config from its loader, so the registry
2203
+ * components render with everything they need on first paint.
2204
+ */
2205
+ const saasAppRouterTs = () => `import type { RealmController } from "alepha/api/users";
2206
+ import { $page, NotFound } from "alepha/react/router";
2207
+ import { $secure } from "alepha/security";
2208
+ import { $client } from "alepha/server/links";
2209
+ import type { HelloController } from "../api/controllers/HelloController.ts";
2210
+
2211
+ export class AppRouter {
2212
+ protected readonly api = $client<HelloController>();
2213
+ protected readonly realmApi = $client<RealmController>();
2214
+
2215
+ home = $page({
2216
+ path: "/",
2217
+ lazy: () => import("./components/Home.tsx"),
2218
+ loader: () => this.api.hello(),
2219
+ });
2220
+
2221
+ // ── /auth — login, register, reset, verify ─────────────────────────────
2222
+ authLayout = $page({
2223
+ path: "/auth",
2224
+ lazy: () => import("./components/auth/AuthLayout.tsx"),
2225
+ });
2226
+
2227
+ login = $page({
2228
+ parent: this.authLayout,
2229
+ path: "/login",
2230
+ name: "login",
2231
+ head: { title: "Sign in" },
2232
+ lazy: () => import("./components/auth/Login.tsx"),
2233
+ loader: async () => ({ realmConfig: await this.realmApi.getRealmConfig() }),
2234
+ });
2235
+
2236
+ register = $page({
2237
+ parent: this.authLayout,
2238
+ path: "/register",
2239
+ name: "register",
2240
+ head: { title: "Sign up" },
2241
+ lazy: () => import("./components/auth/Register.tsx"),
2242
+ loader: async () => ({ realmConfig: await this.realmApi.getRealmConfig() }),
2243
+ });
2244
+
2245
+ resetPassword = $page({
2246
+ parent: this.authLayout,
2247
+ path: "/reset-password",
2248
+ head: { title: "Reset password" },
2249
+ lazy: () => import("./components/auth/ResetPassword.tsx"),
2250
+ loader: async () => ({ realmConfig: await this.realmApi.getRealmConfig() }),
2251
+ });
2252
+
2253
+ verifyEmail = $page({
2254
+ parent: this.authLayout,
2255
+ path: "/verify-email",
2256
+ head: { title: "Verify email" },
2257
+ lazy: () => import("./components/auth/VerifyEmail.tsx"),
2258
+ });
2259
+
2260
+ // ── /admin — gated by 'admin:ui' permission, declared in RealmProvider.
2261
+ // Children inherit the gate via the parent chain.
2262
+ adminLayout = $page({
2263
+ path: "/admin",
2264
+ use: [$secure({ permissions: ["admin:ui"] })],
2265
+ lazy: () => import("./components/admin/AdminLayout.tsx"),
2266
+ });
2267
+
2268
+ adminUsers = $page({
2269
+ parent: this.adminLayout,
2270
+ path: "/users",
2271
+ head: { title: "Users" },
2272
+ lazy: () => import("./components/admin/Users.tsx"),
2273
+ });
2274
+
2275
+ adminSessions = $page({
2276
+ parent: this.adminLayout,
2277
+ path: "/sessions",
2278
+ head: { title: "Sessions" },
2279
+ lazy: () => import("./components/admin/Sessions.tsx"),
2280
+ });
2281
+
2282
+ notFound = $page({ path: "/*", component: NotFound });
2283
+ }
2284
+ `;
1757
2285
  //#endregion
1758
2286
  //#region ../../src/cli/core/templates/webHomeComponentTsx.ts
1759
2287
  const webHomeComponentTsx = (options = {}) => {
@@ -1782,7 +2310,19 @@ export default Home;
1782
2310
  //#endregion
1783
2311
  //#region ../../src/cli/core/templates/webIndexTs.ts
1784
2312
  const webIndexTs = (options = {}) => {
1785
- const { appName = "app" } = options;
2313
+ const { appName = "app", saas = false } = options;
2314
+ if (saas) return `
2315
+ import { $module } from "alepha";
2316
+ import { AlephaReactAuth } from "alepha/react/auth";
2317
+ import { AlephaReactI18n } from "alepha/react/i18n";
2318
+ import { AppRouter } from "./AppRouter.ts";
2319
+
2320
+ export const WebModule = $module({
2321
+ name: "${appName}.web",
2322
+ services: [AppRouter],
2323
+ imports: [AlephaReactAuth, AlephaReactI18n],
2324
+ });
2325
+ `.trim();
1786
2326
  return `
1787
2327
  import { $module } from "alepha";
1788
2328
  import { AppRouter } from "./AppRouter.ts";
@@ -1808,6 +2348,7 @@ var ProjectScaffolder = class {
1808
2348
  log = $logger();
1809
2349
  colors = $inject(ConsoleColorProvider);
1810
2350
  fs = $inject(FileSystemProvider);
2351
+ shell = $inject(ShellProvider);
1811
2352
  pm = $inject(PackageManagerUtils);
1812
2353
  utils = $inject(AlephaCliUtils);
1813
2354
  /**
@@ -1829,7 +2370,10 @@ var ProjectScaffolder = class {
1829
2370
  const force = opts.force ?? false;
1830
2371
  const checkWorkspace = opts.checkWorkspace ?? false;
1831
2372
  if (opts.packageJson) tasks.push(this.pm.ensurePackageJson(root, typeof opts.packageJson === "boolean" ? {} : opts.packageJson).then(() => {}));
1832
- if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root, { force }));
2373
+ if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root, {
2374
+ force,
2375
+ localOnly: opts.tsconfigJson === "local"
2376
+ }));
1833
2377
  if (opts.biomeJson) tasks.push(this.ensureBiomeConfig(root, {
1834
2378
  force,
1835
2379
  checkWorkspace
@@ -1845,7 +2389,8 @@ var ProjectScaffolder = class {
1845
2389
  await Promise.all(tasks);
1846
2390
  }
1847
2391
  async ensureTsConfig(root, opts = {}) {
1848
- if (!opts.force && await this.existsInParents(root, "tsconfig.json")) return;
2392
+ const exists = opts.localOnly ? await this.fs.exists(this.fs.join(root, "tsconfig.json")) : await this.existsInParents(root, "tsconfig.json");
2393
+ if (!opts.force && exists) return;
1849
2394
  await this.fs.writeFile(this.fs.join(root, "tsconfig.json"), tsconfigJson());
1850
2395
  }
1851
2396
  async ensureBiomeConfig(root, opts = {}) {
@@ -1904,9 +2449,32 @@ var ProjectScaffolder = class {
1904
2449
  const appName = this.getAppName(root);
1905
2450
  await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
1906
2451
  await this.fs.mkdir(this.fs.join(root, "src/api/schemas"), { recursive: true });
1907
- await this.ensureFile(root, "src/api/index.ts", apiIndexTs({ appName }), opts.force);
2452
+ await this.ensureFile(root, "src/api/index.ts", apiIndexTs({
2453
+ appName,
2454
+ saas: opts.saas
2455
+ }), opts.force);
1908
2456
  await this.ensureFile(root, "src/api/controllers/HelloController.ts", apiHelloControllerTs({ appName }), opts.force);
1909
2457
  await this.ensureFile(root, "src/api/schemas/helloResponseSchema.ts", apiHelloResponseSchemaTs(), opts.force);
2458
+ if (opts.saas) {
2459
+ await this.fs.mkdir(this.fs.join(root, "src/api/providers"), { recursive: true });
2460
+ const adminEmail = await this.detectGitEmail();
2461
+ await this.ensureFile(root, "src/api/providers/RealmProvider.ts", saasRealmProviderTs({ adminEmail }), opts.force);
2462
+ }
2463
+ }
2464
+ /**
2465
+ * Best-effort lookup for the developer's git email (used as the seeded
2466
+ * `adminEmails` entry in the SaaS realm). Returns undefined if git isn't
2467
+ * available or if `user.email` isn't configured — the template falls back
2468
+ * to `admin@example.com` in that case.
2469
+ */
2470
+ async detectGitEmail() {
2471
+ try {
2472
+ const email = (await this.shell.run("git config --get user.email", { capture: true }) ?? "").trim();
2473
+ if (!email || !email.includes("@")) return void 0;
2474
+ return email;
2475
+ } catch {
2476
+ return;
2477
+ }
1910
2478
  }
1911
2479
  /**
1912
2480
  * Ensure web/React project structure exists.
@@ -1919,14 +2487,35 @@ var ProjectScaffolder = class {
1919
2487
  async ensureWebProject(root, opts = {}) {
1920
2488
  const appName = this.getAppName(root);
1921
2489
  await this.fs.mkdir(this.fs.join(root, "src/web/components"), { recursive: true });
2490
+ if (opts.saas) {
2491
+ await this.fs.mkdir(this.fs.join(root, "src/web/components/auth"), { recursive: true });
2492
+ await this.fs.mkdir(this.fs.join(root, "src/web/components/admin"), { recursive: true });
2493
+ }
1922
2494
  await this.fs.mkdir(this.fs.join(root, "public"), { recursive: true });
1923
2495
  await this.ensureFile(root, "public/favicon.svg", logoSvg, opts.force);
1924
2496
  await this.ensureFile(root, "src/main.css", mainCss({ tailwind: opts.tailwind }), opts.force);
1925
2497
  if (opts.tailwind) await this.ensureFile(root, "vite.config.ts", viteConfigTs(), opts.force);
1926
- await this.ensureFile(root, "src/web/index.ts", webIndexTs({ appName }), opts.force);
1927
- await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs({ api: opts.api }), opts.force);
2498
+ if (opts.shadcn) await this.ensureFile(root, "components.json", componentsJsonTs(), opts.force);
2499
+ await this.ensureFile(root, "src/web/index.ts", webIndexTs({
2500
+ appName,
2501
+ saas: opts.saas
2502
+ }), opts.force);
2503
+ await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs({
2504
+ api: opts.api,
2505
+ saas: opts.saas
2506
+ }), opts.force);
1928
2507
  await this.ensureFile(root, "src/web/components/Home.tsx", webHomeComponentTsx({ api: opts.api }), opts.force);
1929
2508
  await this.ensureFile(root, "src/main.browser.ts", mainBrowserTs(), opts.force);
2509
+ if (opts.saas) {
2510
+ await this.ensureFile(root, "src/web/components/auth/AuthLayout.tsx", saasAuthLayoutTsx(), opts.force);
2511
+ await this.ensureFile(root, "src/web/components/auth/Login.tsx", saasAuthLoginTsx(), opts.force);
2512
+ await this.ensureFile(root, "src/web/components/auth/Register.tsx", saasAuthRegisterTsx(), opts.force);
2513
+ await this.ensureFile(root, "src/web/components/auth/ResetPassword.tsx", saasAuthResetPasswordTsx(), opts.force);
2514
+ await this.ensureFile(root, "src/web/components/auth/VerifyEmail.tsx", saasAuthVerifyEmailTsx(), opts.force);
2515
+ await this.ensureFile(root, "src/web/components/admin/AdminLayout.tsx", saasAdminLayoutTsx(), opts.force);
2516
+ await this.ensureFile(root, "src/web/components/admin/Users.tsx", saasAdminUsersTsx(), opts.force);
2517
+ await this.ensureFile(root, "src/web/components/admin/Sessions.tsx", saasAdminSessionsTsx(), opts.force);
2518
+ }
1930
2519
  }
1931
2520
  /**
1932
2521
  * Ensure test directory exists with a dummy test file + a self-contained
@@ -1953,8 +2542,17 @@ var ProjectScaffolder = class {
1953
2542
  root = this.fs.join(root, args);
1954
2543
  await this.fs.mkdir(root, { force: true });
1955
2544
  }
2545
+ const shadcnPreset = typeof flags.saas === "string" && flags.saas || typeof flags.shadcn === "string" && flags.shadcn || "b0";
2546
+ const f = flags;
2547
+ f.shadcn = !!flags.shadcn;
2548
+ f.saas = !!flags.saas;
2549
+ if (f.saas) {
2550
+ f.shadcn = true;
2551
+ f.api = true;
2552
+ }
2553
+ if (f.shadcn) f.tailwind = true;
1956
2554
  if (flags.tailwind) flags.react = true;
1957
- if ((flags.api || flags.react || flags.tailwind) && !flags.force) {
2555
+ if ((flags.api || flags.react || flags.tailwind || flags.shadcn || flags.saas) && !flags.force) {
1958
2556
  if ((await this.fs.ls(root)).filter((f) => f !== "package.json").length > 0) throw new AlephaError(`Target directory is not empty (${root}). Use --force to overwrite existing files.`);
1959
2557
  }
1960
2558
  const workspace = await this.pm.getWorkspaceContext(root);
@@ -1968,10 +2566,10 @@ var ProjectScaffolder = class {
1968
2566
  await this.ensureConfig(root, {
1969
2567
  force,
1970
2568
  packageJson: {
1971
- ...flags,
2569
+ ...f,
1972
2570
  isPackage: workspace.isPackage
1973
2571
  },
1974
- tsconfigJson: !workspace.config.tsconfigJson,
2572
+ tsconfigJson: f.shadcn ? "local" : !workspace.config.tsconfigJson,
1975
2573
  biomeJson: true,
1976
2574
  editorconfig: !workspace.config.editorconfig,
1977
2575
  agentMd: agentType ? { type: agentType } : false
@@ -1982,10 +2580,15 @@ var ProjectScaffolder = class {
1982
2580
  react: !!flags.react && !isExpo,
1983
2581
  force
1984
2582
  });
1985
- if (flags.api) await this.ensureApiProject(root, { force });
2583
+ if (flags.api) await this.ensureApiProject(root, {
2584
+ saas: !!flags.saas,
2585
+ force
2586
+ });
1986
2587
  if (flags.react && !isExpo) await this.ensureWebProject(root, {
1987
2588
  api: !!flags.api,
1988
2589
  tailwind: !!flags.tailwind,
2590
+ shadcn: !!flags.shadcn,
2591
+ saas: !!flags.saas,
1989
2592
  force
1990
2593
  });
1991
2594
  }
@@ -2003,10 +2606,26 @@ var ProjectScaffolder = class {
2003
2606
  root: installRoot
2004
2607
  });
2005
2608
  if (flags.test) await this.ensureTestDir(root);
2006
- await run(`${pmName} run lint`, {
2007
- alias: "running linter",
2609
+ const exec = pmExecPrefix(pmName);
2610
+ if (flags.shadcn) {
2611
+ await run(`${exec} shadcn init --no-monorepo --base radix -t vite --yes --force --reinstall --preset ${escapeShellArg(shadcnPreset)}`, {
2612
+ alias: `running shadcn init (preset ${shadcnPreset})`,
2613
+ root
2614
+ });
2615
+ await this.fs.writeFile(this.fs.join(root, "components.json"), componentsJsonTs());
2616
+ }
2617
+ if (flags.saas) await run(`${exec} shadcn add @alepha/saas --yes --overwrite`, {
2618
+ alias: "adding alepha saas registry bundle",
2008
2619
  root
2009
2620
  });
2621
+ try {
2622
+ await run(`${pmName} run lint`, {
2623
+ alias: "running linter",
2624
+ root
2625
+ });
2626
+ } catch (err) {
2627
+ this.log.warn("Linter reported issues during init — continuing. Run `lint` again later to inspect.", { error: err instanceof Error ? err.message : String(err) });
2628
+ }
2010
2629
  if (!workspace.isPackage) {
2011
2630
  if (await this.ensureGitRepo(root, { force })) await run("git add .", {
2012
2631
  alias: "staging generated files",
@@ -2045,6 +2664,30 @@ var ProjectScaffolder = class {
2045
2664
  }
2046
2665
  }
2047
2666
  };
2667
+ /**
2668
+ * Map a package manager name to the command that runs a project-local binary.
2669
+ *
2670
+ * - npm: `npx`
2671
+ * - yarn: `yarn` (yarn auto-resolves binary names; `yarn shadcn ...` works)
2672
+ * - pnpm: `pnpm exec`
2673
+ * - bun: `bunx`
2674
+ *
2675
+ * Used to invoke `shadcn init` / `shadcn add` regardless of the user's PM —
2676
+ * `npm shadcn ...` is invalid (it tries to run a script named `shadcn`).
2677
+ */
2678
+ /** Quote a value so it survives shell parsing. */
2679
+ const escapeShellArg = (value) => {
2680
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
2681
+ return `'${value.replace(/'/g, "'\\''")}'`;
2682
+ };
2683
+ const pmExecPrefix = (pmName) => {
2684
+ switch (pmName) {
2685
+ case "npm": return "npx";
2686
+ case "pnpm": return "pnpm exec";
2687
+ case "bun": return "bunx";
2688
+ default: return "yarn";
2689
+ }
2690
+ };
2048
2691
  //#endregion
2049
2692
  //#region ../../src/cli/core/tasks/BuildTask.ts
2050
2693
  /**
@@ -2456,7 +3099,7 @@ var BuildCompressTask = class extends BuildTask {
2456
3099
  *
2457
3100
  * Creates:
2458
3101
  * - Dockerfile with configurable base image
2459
- * - Copies drizzle migrations if they exist
3102
+ * - Copies migrations directory if it exists
2460
3103
  * - Builds Docker image when `--image` flag is provided
2461
3104
  */
2462
3105
  var BuildDockerTask = class extends BuildTask {
@@ -2471,15 +3114,15 @@ var BuildDockerTask = class extends BuildTask {
2471
3114
  await ctx.run({
2472
3115
  name: "generate deploy config (docker)",
2473
3116
  handler: async () => {
2474
- await this.copyDrizzleMigrations(ctx.root, distDir);
3117
+ await this.copyMigrations(ctx.root, distDir);
2475
3118
  await this.writeDockerfile(ctx.root, distDir, dockerFrom, dockerCommand);
2476
3119
  }
2477
3120
  });
2478
3121
  if (ctx.flags?.image) await this.buildDockerImage(ctx, distDir);
2479
3122
  }
2480
- async copyDrizzleMigrations(root, distDir) {
2481
- const drizzleDir = this.fs.join(root, "drizzle");
2482
- if (await this.fs.exists(drizzleDir)) await this.fs.cp(drizzleDir, this.fs.join(root, distDir, "drizzle"));
3123
+ async copyMigrations(root, distDir) {
3124
+ const migrationsDir = this.fs.join(root, "migrations");
3125
+ if (await this.fs.exists(migrationsDir)) await this.fs.cp(migrationsDir, this.fs.join(root, distDir, "migrations"));
2483
3126
  }
2484
3127
  async writeDockerfile(root, distDir, image, command) {
2485
3128
  const dockerfile = `# This file was automatically generated. DO NOT MODIFY.
@@ -2740,6 +3383,10 @@ var BuildServerTask = class extends BuildTask {
2740
3383
  chunkFileNames: "[hash].js",
2741
3384
  assetFileNames: "[hash][extname]",
2742
3385
  format: "esm",
3386
+ codeSplitting: { groups: [{
3387
+ name: "react",
3388
+ test: /node_modules\/react(\/|-dom\/)/
3389
+ }] },
2743
3390
  minify: {
2744
3391
  mangle: { keepNames: true },
2745
3392
  compress: { keepNames: {
@@ -4280,7 +4927,12 @@ const DEFAULT_IGNORE = [
4280
4927
  */
4281
4928
  const changelogOptions = $atom({
4282
4929
  name: "alepha.cli.changelog.options",
4283
- schema: t.object({ ignore: t.optional(t.array(t.string())) }),
4930
+ schema: t.object({
4931
+ /**
4932
+ * Scopes to ignore (e.g., "project", "release", "chore").
4933
+ * Commits like `feat(chore): ...` will be excluded from changelog.
4934
+ */
4935
+ ignore: t.optional(t.array(t.string())) }),
4284
4936
  default: { ignore: DEFAULT_IGNORE }
4285
4937
  });
4286
4938
  //#endregion
@@ -4422,10 +5074,20 @@ var ChangelogCommand = class {
4422
5074
  name: "changelog",
4423
5075
  description: "Generate changelog from conventional commits (outputs to stdout)",
4424
5076
  flags: t.object({
5077
+ /**
5078
+ * Show changes from this ref (tag, commit, branch).
5079
+ * Defaults to the latest version tag.
5080
+ * Example: --from=1.0.0
5081
+ */
4425
5082
  from: t.optional(t.string({
4426
5083
  aliases: ["f"],
4427
5084
  description: "Starting ref (default: latest tag)"
4428
5085
  })),
5086
+ /**
5087
+ * Show changes up to this ref (tag, commit, branch).
5088
+ * Defaults to HEAD.
5089
+ * Example: --to=main
5090
+ */
4429
5091
  to: t.optional(t.string({
4430
5092
  aliases: ["t"],
4431
5093
  description: "Ending ref (default: HEAD)"
@@ -4588,6 +5250,8 @@ var InitCommand = class {
4588
5250
  description: "Include React dependencies and web module (src/web/)"
4589
5251
  })),
4590
5252
  tailwind: t.optional(t.boolean({ description: "Include Tailwind CSS with Vite plugin. Implies --react" })),
5253
+ shadcn: t.optional(t.union([t.boolean(), t.text()], { description: "Set up shadcn/ui (components.json, cn helper, theme tokens, alepha registry). Pass an optional preset id (default: b0). Implies --react and --tailwind" })),
5254
+ saas: t.optional(t.union([t.boolean(), t.text()], { description: "Scaffold a SaaS starter: auth (login/register/reset/verify) + admin panel (/admin AppShell with users/sessions/api-keys/parameters/audits). Pass an optional preset id (default: b0). Implies --shadcn and --api" })),
4591
5255
  test: t.optional(t.boolean({ description: "Include Vitest and create test directory" })),
4592
5256
  force: t.optional(t.boolean({
4593
5257
  aliases: ["f"],