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
@@ -2,13 +2,14 @@ import { basename, dirname } from "node:path";
2
2
  import { $inject, AlephaError } from "alepha";
3
3
  import type { RunnerMethod } from "alepha/command";
4
4
  import { $logger, ConsoleColorProvider } from "alepha/logger";
5
- import { FileSystemProvider } from "alepha/system";
5
+ import { FileSystemProvider, ShellProvider } from "alepha/system";
6
6
  import { type AgentMdOptions, agentMd } from "../templates/agentMd.ts";
7
7
  import { alephaConfigTs } from "../templates/alephaConfigTs.ts";
8
8
  import { apiHelloControllerTs } from "../templates/apiHelloControllerTs.ts";
9
9
  import { apiHelloResponseSchemaTs } from "../templates/apiHelloResponseSchemaTs.ts";
10
10
  import { apiIndexTs } from "../templates/apiIndexTs.ts";
11
11
  import { biomeJson } from "../templates/biomeJson.ts";
12
+ import { componentsJsonTs } from "../templates/componentsJsonTs.ts";
12
13
  import { dummySpecTs } from "../templates/dummySpecTs.ts";
13
14
  import { editorconfig } from "../templates/editorconfig.ts";
14
15
  import { gitignore } from "../templates/gitignore.ts";
@@ -16,6 +17,19 @@ import { logoSvg } from "../templates/logoSvg.ts";
16
17
  import { mainBrowserTs } from "../templates/mainBrowserTs.ts";
17
18
  import { mainCss } from "../templates/mainCss.ts";
18
19
  import { mainServerTs } from "../templates/mainServerTs.ts";
20
+ import { saasAdminLayoutTsx } from "../templates/saasAdminLayoutTsx.ts";
21
+ import {
22
+ saasAdminSessionsTsx,
23
+ saasAdminUsersTsx,
24
+ } from "../templates/saasAdminPagesTsx.ts";
25
+ import { saasAuthLayoutTsx } from "../templates/saasAuthLayoutTsx.ts";
26
+ import {
27
+ saasAuthLoginTsx,
28
+ saasAuthRegisterTsx,
29
+ saasAuthResetPasswordTsx,
30
+ saasAuthVerifyEmailTsx,
31
+ } from "../templates/saasAuthPagesTsx.ts";
32
+ import { saasRealmProviderTs } from "../templates/saasRealmProviderTs.ts";
19
33
  import { tsconfigJson } from "../templates/tsconfigJson.ts";
20
34
  import { viteConfigTs } from "../templates/viteConfigTs.ts";
21
35
  import { vitestConfigTs } from "../templates/vitestConfigTs.ts";
@@ -41,6 +55,7 @@ export class ProjectScaffolder {
41
55
  protected readonly log = $logger();
42
56
  protected readonly colors = $inject(ConsoleColorProvider);
43
57
  protected readonly fs = $inject(FileSystemProvider);
58
+ protected readonly shell = $inject(ShellProvider);
44
59
  protected readonly pm = $inject(PackageManagerUtils);
45
60
  protected readonly utils = $inject(AlephaCliUtils);
46
61
 
@@ -70,7 +85,13 @@ export class ProjectScaffolder {
70
85
  */
71
86
  checkWorkspace?: boolean;
72
87
  packageJson?: boolean | DependencyModes;
73
- tsconfigJson?: boolean;
88
+ /**
89
+ * `true` writes a tsconfig.json if one doesn't already exist (parent
90
+ * dirs included). `"local"` writes one when none exists *in this
91
+ * directory* — used by shadcn since the CLI reads the local
92
+ * tsconfig directly for import-alias detection.
93
+ */
94
+ tsconfigJson?: boolean | "local";
74
95
  biomeJson?: boolean;
75
96
  editorconfig?: boolean;
76
97
  agentMd?: false | AgentMdOptions;
@@ -91,7 +112,12 @@ export class ProjectScaffolder {
91
112
  );
92
113
  }
93
114
  if (opts.tsconfigJson) {
94
- tasks.push(this.ensureTsConfig(root, { force }));
115
+ tasks.push(
116
+ this.ensureTsConfig(root, {
117
+ force,
118
+ localOnly: opts.tsconfigJson === "local",
119
+ }),
120
+ );
95
121
  }
96
122
  if (opts.biomeJson) {
97
123
  tasks.push(this.ensureBiomeConfig(root, { force, checkWorkspace }));
@@ -112,10 +138,15 @@ export class ProjectScaffolder {
112
138
 
113
139
  public async ensureTsConfig(
114
140
  root: string,
115
- opts: { force?: boolean } = {},
141
+ opts: { force?: boolean; localOnly?: boolean } = {},
116
142
  ): Promise<void> {
117
- // Check if tsconfig.json exists in current or parent directories
118
- if (!opts.force && (await this.existsInParents(root, "tsconfig.json"))) {
143
+ // Check if tsconfig.json exists in current or parent directories.
144
+ // `localOnly: true` skips the parent walk — needed when a tool reads the
145
+ // local tsconfig directly (shadcn does this for import-alias detection).
146
+ const exists = opts.localOnly
147
+ ? await this.fs.exists(this.fs.join(root, "tsconfig.json"))
148
+ : await this.existsInParents(root, "tsconfig.json");
149
+ if (!opts.force && exists) {
119
150
  return;
120
151
  }
121
152
  await this.fs.writeFile(
@@ -240,7 +271,7 @@ export class ProjectScaffolder {
240
271
  */
241
272
  public async ensureApiProject(
242
273
  root: string,
243
- opts: { force?: boolean } = {},
274
+ opts: { saas?: boolean; force?: boolean } = {},
244
275
  ): Promise<void> {
245
276
  const appName = this.getAppName(root);
246
277
 
@@ -256,7 +287,7 @@ export class ProjectScaffolder {
256
287
  await this.ensureFile(
257
288
  root,
258
289
  "src/api/index.ts",
259
- apiIndexTs({ appName }),
290
+ apiIndexTs({ appName, saas: opts.saas }),
260
291
  opts.force,
261
292
  );
262
293
  await this.ensureFile(
@@ -271,6 +302,38 @@ export class ProjectScaffolder {
271
302
  apiHelloResponseSchemaTs(),
272
303
  opts.force,
273
304
  );
305
+
306
+ if (opts.saas) {
307
+ await this.fs.mkdir(this.fs.join(root, "src/api/providers"), {
308
+ recursive: true,
309
+ });
310
+ const adminEmail = await this.detectGitEmail();
311
+ await this.ensureFile(
312
+ root,
313
+ "src/api/providers/RealmProvider.ts",
314
+ saasRealmProviderTs({ adminEmail }),
315
+ opts.force,
316
+ );
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Best-effort lookup for the developer's git email (used as the seeded
322
+ * `adminEmails` entry in the SaaS realm). Returns undefined if git isn't
323
+ * available or if `user.email` isn't configured — the template falls back
324
+ * to `admin@example.com` in that case.
325
+ */
326
+ protected async detectGitEmail(): Promise<string | undefined> {
327
+ try {
328
+ const stdout = (await this.shell.run("git config --get user.email", {
329
+ capture: true,
330
+ })) as unknown as string;
331
+ const email = (stdout ?? "").trim();
332
+ if (!email || !email.includes("@")) return undefined;
333
+ return email;
334
+ } catch {
335
+ return undefined;
336
+ }
274
337
  }
275
338
 
276
339
  // ===========================================
@@ -290,6 +353,8 @@ export class ProjectScaffolder {
290
353
  opts: {
291
354
  api?: boolean;
292
355
  tailwind?: boolean;
356
+ shadcn?: boolean;
357
+ saas?: boolean;
293
358
  force?: boolean;
294
359
  } = {},
295
360
  ): Promise<void> {
@@ -300,6 +365,15 @@ export class ProjectScaffolder {
300
365
  recursive: true,
301
366
  });
302
367
 
368
+ if (opts.saas) {
369
+ await this.fs.mkdir(this.fs.join(root, "src/web/components/auth"), {
370
+ recursive: true,
371
+ });
372
+ await this.fs.mkdir(this.fs.join(root, "src/web/components/admin"), {
373
+ recursive: true,
374
+ });
375
+ }
376
+
303
377
  // public/favicon.svg
304
378
  await this.fs.mkdir(this.fs.join(root, "public"), { recursive: true });
305
379
  await this.ensureFile(root, "public/favicon.svg", logoSvg, opts.force);
@@ -317,17 +391,32 @@ export class ProjectScaffolder {
317
391
  await this.ensureFile(root, "vite.config.ts", viteConfigTs(), opts.force);
318
392
  }
319
393
 
394
+ // shadcn/ui: write components.json before running `shadcn init` — the
395
+ // CLI respects an existing config and skips its interactive prompts,
396
+ // which lets us pin our aliases (`@/web/*`) and the `@alepha` registry.
397
+ // The CLI itself writes the cn() helper, theme tokens, and installs
398
+ // runtime deps (clsx, tailwind-merge, class-variance-authority,
399
+ // lucide-react, tw-animate-css) — see runShadcnInit below.
400
+ if (opts.shadcn) {
401
+ await this.ensureFile(
402
+ root,
403
+ "components.json",
404
+ componentsJsonTs(),
405
+ opts.force,
406
+ );
407
+ }
408
+
320
409
  // Web structure
321
410
  await this.ensureFile(
322
411
  root,
323
412
  "src/web/index.ts",
324
- webIndexTs({ appName }),
413
+ webIndexTs({ appName, saas: opts.saas }),
325
414
  opts.force,
326
415
  );
327
416
  await this.ensureFile(
328
417
  root,
329
418
  "src/web/AppRouter.ts",
330
- webAppRouterTs({ api: opts.api }),
419
+ webAppRouterTs({ api: opts.api, saas: opts.saas }),
331
420
  opts.force,
332
421
  );
333
422
  await this.ensureFile(
@@ -342,6 +431,62 @@ export class ProjectScaffolder {
342
431
  mainBrowserTs(),
343
432
  opts.force,
344
433
  );
434
+
435
+ if (opts.saas) {
436
+ // Auth — layout + 4 pages, each a thin wrapper around the registry
437
+ // component that `shadcn add @alepha/auth-*` drops at
438
+ // src/web/components/auth-*.tsx.
439
+ await this.ensureFile(
440
+ root,
441
+ "src/web/components/auth/AuthLayout.tsx",
442
+ saasAuthLayoutTsx(),
443
+ opts.force,
444
+ );
445
+ await this.ensureFile(
446
+ root,
447
+ "src/web/components/auth/Login.tsx",
448
+ saasAuthLoginTsx(),
449
+ opts.force,
450
+ );
451
+ await this.ensureFile(
452
+ root,
453
+ "src/web/components/auth/Register.tsx",
454
+ saasAuthRegisterTsx(),
455
+ opts.force,
456
+ );
457
+ await this.ensureFile(
458
+ root,
459
+ "src/web/components/auth/ResetPassword.tsx",
460
+ saasAuthResetPasswordTsx(),
461
+ opts.force,
462
+ );
463
+ await this.ensureFile(
464
+ root,
465
+ "src/web/components/auth/VerifyEmail.tsx",
466
+ saasAuthVerifyEmailTsx(),
467
+ opts.force,
468
+ );
469
+
470
+ // Admin — AppShell layout + 5 admin-* pages
471
+ await this.ensureFile(
472
+ root,
473
+ "src/web/components/admin/AdminLayout.tsx",
474
+ saasAdminLayoutTsx(),
475
+ opts.force,
476
+ );
477
+ await this.ensureFile(
478
+ root,
479
+ "src/web/components/admin/Users.tsx",
480
+ saasAdminUsersTsx(),
481
+ opts.force,
482
+ );
483
+ await this.ensureFile(
484
+ root,
485
+ "src/web/components/admin/Sessions.tsx",
486
+ saasAdminSessionsTsx(),
487
+ opts.force,
488
+ );
489
+ }
345
490
  }
346
491
 
347
492
  // ===========================================
@@ -394,6 +539,10 @@ export class ProjectScaffolder {
394
539
  api?: boolean;
395
540
  react?: boolean;
396
541
  tailwind?: boolean;
542
+ /** boolean toggle, or a string preset id (default `b0` when bare). */
543
+ shadcn?: boolean | string;
544
+ /** boolean toggle, or a string preset id (default `b0` when bare). */
545
+ saas?: boolean | string;
397
546
  test?: boolean;
398
547
  force?: boolean;
399
548
  };
@@ -404,13 +553,41 @@ export class ProjectScaffolder {
404
553
  await this.fs.mkdir(root, { force: true });
405
554
  }
406
555
 
407
- // Flag cascading: --tailwind--react
556
+ // `--shadcn` / `--saas` are union flags: baretrue, string → preset.
557
+ // Capture the preset string (default `b0`), then normalize both flags
558
+ // to plain booleans so the rest of the pipeline keeps its boolean
559
+ // contract with PackageManagerUtils + ensureWebProject etc.
560
+ const shadcnPreset =
561
+ (typeof flags.saas === "string" && flags.saas) ||
562
+ (typeof flags.shadcn === "string" && flags.shadcn) ||
563
+ "b0";
564
+
565
+ // Cast to a narrower view so downstream sees pure booleans.
566
+ const f = flags as Omit<typeof flags, "shadcn" | "saas"> & {
567
+ shadcn?: boolean;
568
+ saas?: boolean;
569
+ };
570
+ f.shadcn = !!flags.shadcn;
571
+ f.saas = !!flags.saas;
572
+
573
+ // Flag cascading:
574
+ // --saas → --shadcn + --api
575
+ // --shadcn → --tailwind
576
+ // --tailwind→ --react
577
+ if (f.saas) {
578
+ f.shadcn = true;
579
+ f.api = true;
580
+ }
581
+ if (f.shadcn) {
582
+ f.tailwind = true;
583
+ }
408
584
  if (flags.tailwind) {
409
585
  flags.react = true;
410
586
  }
411
587
 
412
588
  // When codegen flags are set, target directory must be empty (unless --force)
413
- const hasCodegenFlags = flags.api || flags.react || flags.tailwind;
589
+ const hasCodegenFlags =
590
+ flags.api || flags.react || flags.tailwind || flags.shadcn || flags.saas;
414
591
  if (hasCodegenFlags && !flags.force) {
415
592
  const files = await this.fs.ls(root);
416
593
  // Allow a directory that only has package.json (common for monorepo packages)
@@ -441,9 +618,12 @@ export class ProjectScaffolder {
441
618
  handler: async () => {
442
619
  await this.ensureConfig(root, {
443
620
  force,
444
- packageJson: { ...flags, isPackage: workspace.isPackage },
445
- // Skip workspace-level configs if they exist at workspace root
446
- tsconfigJson: !workspace.config.tsconfigJson,
621
+ packageJson: { ...f, isPackage: workspace.isPackage },
622
+ // Skip workspace-level configs if they exist at workspace root
623
+ // unless --shadcn is set: the shadcn CLI reads the local
624
+ // tsconfig.json directly to detect import aliases (it doesn't
625
+ // follow `extends`), so we must ensure one exists in the package.
626
+ tsconfigJson: f.shadcn ? "local" : !workspace.config.tsconfigJson,
447
627
  biomeJson: true,
448
628
  editorconfig: !workspace.config.editorconfig,
449
629
  agentMd: agentType ? { type: agentType } : false,
@@ -459,12 +639,14 @@ export class ProjectScaffolder {
459
639
  force,
460
640
  });
461
641
  if (flags.api) {
462
- await this.ensureApiProject(root, { force });
642
+ await this.ensureApiProject(root, { saas: !!flags.saas, force });
463
643
  }
464
644
  if (flags.react && !isExpo) {
465
645
  await this.ensureWebProject(root, {
466
646
  api: !!flags.api,
467
647
  tailwind: !!flags.tailwind,
648
+ shadcn: !!flags.shadcn,
649
+ saas: !!flags.saas,
468
650
  force,
469
651
  });
470
652
  }
@@ -503,10 +685,76 @@ export class ProjectScaffolder {
503
685
  await this.ensureTestDir(root);
504
686
  }
505
687
 
506
- await run(`${pmName} run lint`, {
507
- alias: "running linter",
508
- root,
509
- });
688
+ // shadcn/ui: run `<pm> shadcn init` against the components.json we wrote
689
+ // earlier. shadcn detects the existing config, respects our aliases,
690
+ // injects theme tokens into src/main.css, writes src/web/lib/utils.ts,
691
+ // and installs runtime deps (clsx, tailwind-merge, etc.).
692
+ //
693
+ // Flags chosen to keep this fully non-interactive:
694
+ // --yes skip confirmation prompts (default in shadcn v4 but
695
+ // passed explicitly so older versions also behave)
696
+ // --no-monorepo skip the monorepo prompt — we ship a single-app
697
+ // layout; users opt into monorepo via `--monorepo`
698
+ // on the alepha side later
699
+ // --silent suppress shadcn's own progress output; alepha's
700
+ // runner already prints a status line
701
+ //
702
+ // We deliberately do NOT pass `--defaults` (would force Next.js +
703
+ // base-nova preset) or `--template` (only applies to scratch projects;
704
+ // ours already has main.server.ts / main.browser.ts).
705
+ // Each PM has a different way to exec a project-local binary.
706
+ const exec = pmExecPrefix(pmName);
707
+
708
+ if (flags.shadcn) {
709
+ // Fully non-interactive shadcn init. The `--preset` arg is what makes
710
+ // this work — without it shadcn falls back to interactive prompts even
711
+ // with --yes/--force. Defaults: vite template + radix base + reinstall
712
+ // (so the components.json we pre-wrote stays canonical).
713
+ await run(
714
+ `${exec} shadcn init --no-monorepo --base radix -t vite --yes --force --reinstall --preset ${escapeShellArg(shadcnPreset)}`,
715
+ { alias: `running shadcn init (preset ${shadcnPreset})`, root },
716
+ );
717
+ // Re-pin our aliases + alepha registry — `shadcn init --force`
718
+ // overwrites components.json with the template defaults.
719
+ await this.fs.writeFile(
720
+ this.fs.join(root, "components.json"),
721
+ componentsJsonTs(),
722
+ );
723
+ }
724
+
725
+ // SaaS preset: pull in the auth + admin registry components from the
726
+ // public alepha registry (already wired via components.json's
727
+ // `registries: { "@alepha": "https://alepha.dev/r/{name}.json" }`).
728
+ // Each `shadcn add` writes the component into src/web/components/* and
729
+ // pulls its peer primitives + dependencies (sonner, etc.).
730
+ if (flags.saas) {
731
+ // Pull the public SaaS bundle in one shot — it aggregates control,
732
+ // auto-form, alepha-table, use-confirm, app-shell, every auth-*, and
733
+ // every admin-* block. Definition lives at
734
+ // https://alepha.dev/r/saas.json (see @alepha/ui-registry).
735
+ // `--yes --overwrite` is the only combo that works non-interactively
736
+ // when registry items would replace files we pre-wrote (auth-login etc.
737
+ // overlap with shadcn primitives like button/input).
738
+ await run(`${exec} shadcn add @alepha/saas --yes --overwrite`, {
739
+ alias: "adding alepha saas registry bundle",
740
+ root,
741
+ });
742
+ }
743
+
744
+ // Best-effort lint: shadcn-imported registry components occasionally
745
+ // trip biome rules (e.g. noArrayIndexKey on a Fragment loop). The user
746
+ // can fix or silence these later — don't block the whole init.
747
+ try {
748
+ await run(`${pmName} run lint`, {
749
+ alias: "running linter",
750
+ root,
751
+ });
752
+ } catch (err) {
753
+ this.log.warn(
754
+ "Linter reported issues during init — continuing. Run `lint` again later to inspect.",
755
+ { error: err instanceof Error ? err.message : String(err) },
756
+ );
757
+ }
510
758
 
511
759
  // Initialize git repository if not in a workspace package
512
760
  if (!workspace.isPackage) {
@@ -587,3 +835,33 @@ export class ProjectScaffolder {
587
835
  }
588
836
  }
589
837
  }
838
+
839
+ /**
840
+ * Map a package manager name to the command that runs a project-local binary.
841
+ *
842
+ * - npm: `npx`
843
+ * - yarn: `yarn` (yarn auto-resolves binary names; `yarn shadcn ...` works)
844
+ * - pnpm: `pnpm exec`
845
+ * - bun: `bunx`
846
+ *
847
+ * Used to invoke `shadcn init` / `shadcn add` regardless of the user's PM —
848
+ * `npm shadcn ...` is invalid (it tries to run a script named `shadcn`).
849
+ */
850
+ /** Quote a value so it survives shell parsing. */
851
+ const escapeShellArg = (value: string): string => {
852
+ if (/^[A-Za-z0-9_./@:-]+$/.test(value)) return value;
853
+ return `'${value.replace(/'/g, "'\\''")}'`;
854
+ };
855
+
856
+ const pmExecPrefix = (pmName: string): string => {
857
+ switch (pmName) {
858
+ case "npm":
859
+ return "npx";
860
+ case "pnpm":
861
+ return "pnpm exec";
862
+ case "bun":
863
+ return "bunx";
864
+ default:
865
+ return "yarn";
866
+ }
867
+ };
@@ -8,7 +8,7 @@ import { BuildTask, type BuildTaskContext } from "./BuildTask.ts";
8
8
  *
9
9
  * Creates:
10
10
  * - Dockerfile with configurable base image
11
- * - Copies drizzle migrations if they exist
11
+ * - Copies migrations directory if it exists
12
12
  * - Builds Docker image when `--image` flag is provided
13
13
  */
14
14
  export class BuildDockerTask extends BuildTask {
@@ -32,7 +32,7 @@ export class BuildDockerTask extends BuildTask {
32
32
  await ctx.run({
33
33
  name: "generate deploy config (docker)",
34
34
  handler: async () => {
35
- await this.copyDrizzleMigrations(ctx.root, distDir);
35
+ await this.copyMigrations(ctx.root, distDir);
36
36
  await this.writeDockerfile(
37
37
  ctx.root,
38
38
  distDir,
@@ -47,14 +47,13 @@ export class BuildDockerTask extends BuildTask {
47
47
  }
48
48
  }
49
49
 
50
- protected async copyDrizzleMigrations(
51
- root: string,
52
- distDir: string,
53
- ): Promise<void> {
54
- const drizzleDir = this.fs.join(root, "drizzle");
55
- const hasMigrations = await this.fs.exists(drizzleDir);
56
- if (hasMigrations) {
57
- await this.fs.cp(drizzleDir, this.fs.join(root, distDir, "drizzle"));
50
+ protected async copyMigrations(root: string, distDir: string): Promise<void> {
51
+ const migrationsDir = this.fs.join(root, "migrations");
52
+ if (await this.fs.exists(migrationsDir)) {
53
+ await this.fs.cp(
54
+ migrationsDir,
55
+ this.fs.join(root, distDir, "migrations"),
56
+ );
58
57
  }
59
58
  }
60
59
 
@@ -135,6 +135,14 @@ export class BuildServerTask extends BuildTask {
135
135
  chunkFileNames: "[hash].js",
136
136
  assetFileNames: "[hash][extname]",
137
137
  format: "esm",
138
+ codeSplitting: {
139
+ groups: [
140
+ {
141
+ name: "react",
142
+ test: /node_modules\/react(\/|-dom\/)/,
143
+ },
144
+ ],
145
+ },
138
146
  // Rolldown/Oxc minifier: preserve class and function names
139
147
  minify: {
140
148
  mangle: { keepNames: true },
@@ -1,9 +1,31 @@
1
1
  export interface ApiIndexTsOptions {
2
2
  appName?: string;
3
+ /**
4
+ * Include `AlephaApiUsers` (realms, sessions, registration, identities,
5
+ * password reset, email verification, admin endpoints) plus the local
6
+ * `RealmProvider` that declares `$realm({ ... })`.
7
+ */
8
+ saas?: boolean;
3
9
  }
4
10
 
5
11
  export const apiIndexTs = (options: ApiIndexTsOptions = {}) => {
6
- const { appName = "app" } = options;
12
+ const { appName = "app", saas = false } = options;
13
+
14
+ if (saas) {
15
+ return `
16
+ import { $module } from "alepha";
17
+ import { AlephaApiUsers } from "alepha/api/users";
18
+ import { HelloController } from "./controllers/HelloController.ts";
19
+ import { RealmProvider } from "./providers/RealmProvider.ts";
20
+
21
+ export const ApiModule = $module({
22
+ name: "${appName}.api",
23
+ services: [HelloController, RealmProvider],
24
+ imports: [AlephaApiUsers],
25
+ });
26
+ `.trim();
27
+ }
28
+
7
29
  return `
8
30
  import { $module } from "alepha";
9
31
  import { HelloController } from "./controllers/HelloController.ts";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `components.json` is the shadcn CLI's project config — it tells
3
+ * `shadcn add` where to drop primitives, which tailwind tokens to use,
4
+ * which icon library to wire up, and which custom registries to resolve.
5
+ *
6
+ * Aliases follow shadcn's defaults (`@/components`, `@/lib/utils`) so the
7
+ * CLI honors them across `init` + `add` calls. Alepha app code lives at
8
+ * `src/web/` (Home, AppRouter, …) and the shadcn primitives live at
9
+ * `src/components/` — kept separate to make the registry components
10
+ * trivially upgradable via `shadcn add --overwrite`.
11
+ *
12
+ * The `registries` block pre-wires the public Alepha registry — consumers
13
+ * can immediately run e.g. `shadcn add @alepha/auth-login`.
14
+ */
15
+ export const componentsJsonTs = () =>
16
+ `{
17
+ "$schema": "https://ui.shadcn.com/schema.json",
18
+ "style": "new-york",
19
+ "rsc": false,
20
+ "tsx": true,
21
+ "tailwind": {
22
+ "config": "",
23
+ "css": "src/main.css",
24
+ "baseColor": "neutral",
25
+ "cssVariables": true
26
+ },
27
+ "aliases": {
28
+ "components": "@/components",
29
+ "utils": "@/lib/utils",
30
+ "ui": "@/components/ui",
31
+ "lib": "@/lib",
32
+ "hooks": "@/hooks"
33
+ },
34
+ "iconLibrary": "lucide",
35
+ "registries": {
36
+ "@alepha": "https://alepha.dev/r/{name}.json"
37
+ }
38
+ }
39
+ `;
@@ -11,6 +11,7 @@ export const mainCss = (opts: { tailwind?: boolean } = {}) => {
11
11
  *
12
12
  * Options:
13
13
  * - Tailwind CSS: Use \`alepha init --tailwind\` to add Tailwind CSS
14
+ * - shadcn/ui: Use \`alepha init --shadcn\` to add shadcn/ui setup
14
15
  * - Raw CSS: Write your own styles below
15
16
  */
16
17
 
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SaaS admin layout — full AppShell on /admin with a sidebar, breadcrumbs,
3
+ * a Sonner toaster, and a confirm provider. The page list grows with
4
+ * whatever `admin-*` registry components the user adds.
5
+ *
6
+ * All UI primitives come from `src/components/*` where `shadcn add` drops
7
+ * them; alepha app code lives in `src/web/*` and references them via the
8
+ * `@/components/*` alias.
9
+ */
10
+ export const saasAdminLayoutTsx = () =>
11
+ `import { AppShell } from "@/components/app-shell";
12
+ import { Toaster } from "@/components/ui/sonner";
13
+ import { TooltipProvider } from "@/components/ui/tooltip";
14
+ import { ConfirmProvider } from "@/components/use-confirm";
15
+ import { NestedView, useRouterState } from "alepha/react/router";
16
+ import { ShieldCheck, Users } from "lucide-react";
17
+
18
+ const NAV = [
19
+ {
20
+ label: "Identity",
21
+ items: [
22
+ { href: "/admin/users", label: "Users", icon: Users },
23
+ { href: "/admin/sessions", label: "Sessions", icon: ShieldCheck },
24
+ ],
25
+ },
26
+ ] as const;
27
+
28
+ const findCrumbs = (pathname: string): { label: string; href?: string }[] => {
29
+ for (const group of NAV) {
30
+ const match = group.items.find((it) => it.href === pathname);
31
+ if (match) return [{ label: group.label }, { label: match.label }];
32
+ }
33
+ return [];
34
+ };
35
+
36
+ const AdminLayout = () => {
37
+ const state = useRouterState();
38
+ const crumbs = findCrumbs(state.url.pathname);
39
+
40
+ return (
41
+ <TooltipProvider>
42
+ <ConfirmProvider>
43
+ <AppShell
44
+ brand={
45
+ <a
46
+ href="/admin"
47
+ className="flex items-center gap-2 px-2 py-2 font-semibold group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"
48
+ >
49
+ <span className="bg-primary text-primary-foreground flex size-7 shrink-0 items-center justify-center rounded">
50
+ α
51
+ </span>
52
+ <span className="truncate group-data-[collapsible=icon]:hidden">
53
+ Admin
54
+ </span>
55
+ </a>
56
+ }
57
+ nav={NAV.map((group) => ({
58
+ label: group.label,
59
+ items: group.items.map((it) => ({
60
+ href: it.href,
61
+ label: it.label,
62
+ icon: it.icon,
63
+ active: it.href === state.url.pathname,
64
+ })),
65
+ }))}
66
+ breadcrumbs={crumbs.length ? crumbs : undefined}
67
+ >
68
+ <NestedView />
69
+ </AppShell>
70
+ <Toaster />
71
+ </ConfirmProvider>
72
+ </TooltipProvider>
73
+ );
74
+ };
75
+
76
+ export default AdminLayout;
77
+ `;