alepha 0.15.2 → 0.15.4

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 (180) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/audits/index.js +8 -0
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +170 -170
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js +1 -0
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +3 -0
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/notifications/index.browser.js +1 -0
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.js +1 -0
  15. package/dist/api/notifications/index.js.map +1 -1
  16. package/dist/api/parameters/index.d.ts +260 -260
  17. package/dist/api/parameters/index.d.ts.map +1 -1
  18. package/dist/api/parameters/index.js +10 -0
  19. package/dist/api/parameters/index.js.map +1 -1
  20. package/dist/api/users/index.d.ts +12 -1
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +18 -2
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/batch/index.d.ts +4 -4
  25. package/dist/bucket/index.d.ts +8 -0
  26. package/dist/bucket/index.d.ts.map +1 -1
  27. package/dist/bucket/index.js +7 -2
  28. package/dist/bucket/index.js.map +1 -1
  29. package/dist/cli/index.d.ts +196 -74
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +234 -50
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/command/index.d.ts +10 -0
  34. package/dist/command/index.d.ts.map +1 -1
  35. package/dist/command/index.js +67 -13
  36. package/dist/command/index.js.map +1 -1
  37. package/dist/core/index.browser.js +28 -21
  38. package/dist/core/index.browser.js.map +1 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +28 -21
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/index.native.js +28 -21
  43. package/dist/core/index.native.js.map +1 -1
  44. package/dist/email/index.d.ts +21 -13
  45. package/dist/email/index.d.ts.map +1 -1
  46. package/dist/email/index.js +10561 -4
  47. package/dist/email/index.js.map +1 -1
  48. package/dist/lock/core/index.d.ts +6 -1
  49. package/dist/lock/core/index.d.ts.map +1 -1
  50. package/dist/lock/core/index.js +9 -1
  51. package/dist/lock/core/index.js.map +1 -1
  52. package/dist/mcp/index.d.ts +5 -5
  53. package/dist/orm/index.bun.js +32 -16
  54. package/dist/orm/index.bun.js.map +1 -1
  55. package/dist/orm/index.d.ts +4 -1
  56. package/dist/orm/index.d.ts.map +1 -1
  57. package/dist/orm/index.js +34 -22
  58. package/dist/orm/index.js.map +1 -1
  59. package/dist/react/auth/index.browser.js +2 -1
  60. package/dist/react/auth/index.browser.js.map +1 -1
  61. package/dist/react/auth/index.js +2 -1
  62. package/dist/react/auth/index.js.map +1 -1
  63. package/dist/react/core/index.d.ts +3 -3
  64. package/dist/react/router/index.browser.js +9 -15
  65. package/dist/react/router/index.browser.js.map +1 -1
  66. package/dist/react/router/index.d.ts +305 -407
  67. package/dist/react/router/index.d.ts.map +1 -1
  68. package/dist/react/router/index.js +581 -781
  69. package/dist/react/router/index.js.map +1 -1
  70. package/dist/scheduler/index.d.ts +13 -1
  71. package/dist/scheduler/index.d.ts.map +1 -1
  72. package/dist/scheduler/index.js +42 -4
  73. package/dist/scheduler/index.js.map +1 -1
  74. package/dist/security/index.d.ts +42 -42
  75. package/dist/security/index.d.ts.map +1 -1
  76. package/dist/security/index.js +8 -7
  77. package/dist/security/index.js.map +1 -1
  78. package/dist/server/auth/index.d.ts +167 -167
  79. package/dist/server/compress/index.d.ts.map +1 -1
  80. package/dist/server/compress/index.js +1 -0
  81. package/dist/server/compress/index.js.map +1 -1
  82. package/dist/server/health/index.d.ts +17 -17
  83. package/dist/server/links/index.d.ts +39 -39
  84. package/dist/server/links/index.js +1 -1
  85. package/dist/server/links/index.js.map +1 -1
  86. package/dist/server/static/index.js +7 -2
  87. package/dist/server/static/index.js.map +1 -1
  88. package/dist/server/swagger/index.d.ts +8 -0
  89. package/dist/server/swagger/index.d.ts.map +1 -1
  90. package/dist/server/swagger/index.js +7 -2
  91. package/dist/server/swagger/index.js.map +1 -1
  92. package/dist/sms/index.d.ts +8 -0
  93. package/dist/sms/index.d.ts.map +1 -1
  94. package/dist/sms/index.js +7 -2
  95. package/dist/sms/index.js.map +1 -1
  96. package/dist/system/index.browser.js +734 -12
  97. package/dist/system/index.browser.js.map +1 -1
  98. package/dist/system/index.d.ts +8 -0
  99. package/dist/system/index.d.ts.map +1 -1
  100. package/dist/system/index.js +7 -2
  101. package/dist/system/index.js.map +1 -1
  102. package/dist/vite/index.d.ts +3 -2
  103. package/dist/vite/index.d.ts.map +1 -1
  104. package/dist/vite/index.js +42 -8
  105. package/dist/vite/index.js.map +1 -1
  106. package/dist/websocket/index.d.ts +34 -34
  107. package/dist/websocket/index.d.ts.map +1 -1
  108. package/package.json +9 -4
  109. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  110. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  111. package/src/api/jobs/controllers/AdminJobController.ts +3 -0
  112. package/src/api/logs/TODO.md +13 -10
  113. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  114. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  115. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  116. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  117. package/src/api/users/controllers/AdminUserController.ts +5 -0
  118. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  119. package/src/cli/atoms/buildOptions.ts +99 -9
  120. package/src/cli/commands/build.ts +150 -32
  121. package/src/cli/commands/db.ts +5 -7
  122. package/src/cli/commands/init.spec.ts +50 -6
  123. package/src/cli/commands/init.ts +28 -5
  124. package/src/cli/providers/ViteDevServerProvider.ts +31 -9
  125. package/src/cli/services/AlephaCliUtils.ts +16 -0
  126. package/src/cli/services/PackageManagerUtils.ts +2 -0
  127. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  128. package/src/cli/services/ProjectScaffolder.ts +28 -6
  129. package/src/cli/templates/agentMd.ts +6 -1
  130. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  131. package/src/cli/templates/apiIndexTs.ts +18 -4
  132. package/src/cli/templates/webAppRouterTs.ts +25 -1
  133. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  134. package/src/command/helpers/Runner.spec.ts +135 -0
  135. package/src/command/helpers/Runner.ts +4 -1
  136. package/src/command/providers/CliProvider.spec.ts +325 -0
  137. package/src/command/providers/CliProvider.ts +117 -7
  138. package/src/core/Alepha.ts +32 -25
  139. package/src/email/index.workerd.ts +36 -0
  140. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  141. package/src/lock/core/primitives/$lock.ts +13 -1
  142. package/src/orm/index.bun.ts +1 -1
  143. package/src/orm/index.ts +2 -6
  144. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  145. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  146. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  147. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  148. package/src/react/auth/services/ReactAuth.ts +3 -1
  149. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  150. package/src/react/router/hooks/useActive.ts +1 -1
  151. package/src/react/router/hooks/useRouter.ts +1 -1
  152. package/src/react/router/index.ts +4 -0
  153. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  154. package/src/react/router/primitives/$page.spec.tsx +0 -32
  155. package/src/react/router/primitives/$page.ts +6 -14
  156. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  157. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  158. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  159. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  160. package/src/react/router/providers/ReactServerProvider.ts +21 -82
  161. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  162. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  163. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  164. package/src/react/router/services/ReactRouter.ts +13 -13
  165. package/src/scheduler/index.workerd.ts +43 -0
  166. package/src/scheduler/providers/CronProvider.ts +53 -6
  167. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  168. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  169. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  170. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  171. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  172. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  173. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  174. package/src/system/index.browser.ts +25 -0
  175. package/src/system/index.workerd.ts +1 -0
  176. package/src/system/providers/FileSystemProvider.ts +8 -0
  177. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  178. package/src/vite/tasks/buildServer.ts +2 -12
  179. package/src/vite/tasks/generateCloudflare.ts +47 -8
  180. package/src/vite/tasks/generateDocker.ts +4 -0
@@ -12,7 +12,11 @@ import {
12
12
  generateVercel,
13
13
  prerenderPages,
14
14
  } from "alepha/vite";
15
- import { buildOptions } from "../atoms/buildOptions.ts";
15
+ import {
16
+ type BuildRuntime,
17
+ type BuildTarget,
18
+ buildOptions,
19
+ } from "../atoms/buildOptions.ts";
16
20
  import { AppEntryProvider } from "../providers/AppEntryProvider.ts";
17
21
  import { ViteBuildProvider } from "../providers/ViteBuildProvider.ts";
18
22
  import { AlephaCliUtils } from "../services/AlephaCliUtils.ts";
@@ -29,6 +33,41 @@ export class BuildCommand {
29
33
  protected readonly viteBuildProvider = $inject(ViteBuildProvider);
30
34
  protected readonly options = $use(buildOptions);
31
35
 
36
+ /**
37
+ * Resolve the effective runtime based on target and explicit runtime flag.
38
+ *
39
+ * Some targets force a specific runtime:
40
+ * - `cloudflare` always uses `workerd`
41
+ * - `vercel` always uses `node`
42
+ * - `docker` and bare deployments respect the runtime flag
43
+ *
44
+ * @throws {AlephaError} If an incompatible runtime is specified for a target
45
+ */
46
+ protected resolveRuntime(
47
+ target: BuildTarget | undefined,
48
+ runtime: BuildRuntime | undefined,
49
+ ): BuildRuntime {
50
+ if (target === "cloudflare") {
51
+ if (runtime && runtime !== "workerd") {
52
+ throw new AlephaError(
53
+ `Target 'cloudflare' requires 'workerd' runtime, got '${runtime}'`,
54
+ );
55
+ }
56
+ return "workerd";
57
+ }
58
+
59
+ if (target === "vercel") {
60
+ if (runtime && runtime !== "node") {
61
+ throw new AlephaError(
62
+ `Target 'vercel' requires 'node' runtime, got '${runtime}'`,
63
+ );
64
+ }
65
+ return "node";
66
+ }
67
+
68
+ return runtime ?? "node";
69
+ }
70
+
32
71
  public readonly build = $command({
33
72
  name: "build",
34
73
  mode: "production",
@@ -39,19 +78,23 @@ export class BuildCommand {
39
78
  description: "Generate build stats report",
40
79
  }),
41
80
  ),
42
- vercel: t.optional(
43
- t.boolean({
44
- description: "Generate Vercel deployment configuration",
81
+ target: t.optional(
82
+ t.enum(["bare", "docker", "vercel", "cloudflare"], {
83
+ aliases: ["t"],
84
+ description: "Deployment target",
45
85
  }),
46
86
  ),
47
- cloudflare: t.optional(
48
- t.boolean({
49
- description: "Generate Cloudflare Workers configuration",
87
+ runtime: t.optional(
88
+ t.enum(["node", "bun", "workerd"], {
89
+ aliases: ["r"],
90
+ description: "JavaScript runtime",
50
91
  }),
51
92
  ),
52
- docker: t.optional(
53
- t.boolean({
54
- description: "Generate Docker configuration",
93
+ image: t.optional(
94
+ t.union([t.boolean(), t.text()], {
95
+ aliases: ["i"],
96
+ description:
97
+ "Build Docker image. Use -i for latest, -i=<version> for specific version",
55
98
  }),
56
99
  ),
57
100
  sitemap: t.optional(
@@ -59,11 +102,6 @@ export class BuildCommand {
59
102
  description: "Generate sitemap.xml with base URL",
60
103
  }),
61
104
  ),
62
- bun: t.optional(
63
- t.boolean({
64
- description: "Prioritize .bun.ts entry files for Bun runtime",
65
- }),
66
- ),
67
105
  }),
68
106
  handler: async ({ flags, run, root }) => {
69
107
  process.env.NODE_ENV = "production";
@@ -88,6 +126,22 @@ export class BuildCommand {
88
126
  const options = this.options;
89
127
  await this.utils.loadEnv(root, [".env", ".env.production"]);
90
128
 
129
+ // Resolve target and runtime
130
+ const target = flags.target ?? options.target;
131
+ const runtime = this.resolveRuntime(
132
+ target,
133
+ flags.runtime ?? options.runtime,
134
+ );
135
+
136
+ // Validate --image requires --target=docker
137
+ if (flags.image && target !== "docker") {
138
+ throw new AlephaError(
139
+ `Flag '--image' requires '--target=docker', got '${target ?? "bare"}'`,
140
+ );
141
+ }
142
+
143
+ this.log.trace("Build configuration", { target, runtime });
144
+
91
145
  const stats = flags.stats ?? options.stats ?? false;
92
146
  let template = "";
93
147
  let hasClient = false;
@@ -140,20 +194,11 @@ export class BuildCommand {
140
194
  const clientIndexPath = `${distDir}/${publicDir}/index.html`;
141
195
  const clientBuilt = await this.fs.exists(clientIndexPath);
142
196
 
197
+ // Set export conditions based on runtime
143
198
  const conditions: string[] = [];
144
-
145
- // bun:
146
- // - alepha
147
- // - react-dom
148
-
149
- if (flags.bun) {
199
+ if (runtime === "bun") {
150
200
  conditions.push("bun");
151
- }
152
-
153
- // workerd:
154
- // - react-dom
155
- // - postgres
156
- if (options.cloudflare) {
201
+ } else if (runtime === "workerd") {
157
202
  conditions.push("workerd");
158
203
  }
159
204
 
@@ -204,8 +249,8 @@ export class BuildCommand {
204
249
  });
205
250
  }
206
251
 
207
- // Generate deployment configurations
208
- if (flags.vercel || options.vercel) {
252
+ // Generate deployment configuration based on target
253
+ if (target === "vercel") {
209
254
  await run({
210
255
  name: "add Vercel config",
211
256
  handler: () =>
@@ -217,26 +262,99 @@ export class BuildCommand {
217
262
  });
218
263
  }
219
264
 
220
- if (flags.cloudflare || options.cloudflare) {
265
+ if (target === "cloudflare") {
221
266
  await run({
222
267
  name: "add Cloudflare config",
223
268
  handler: () =>
224
269
  generateCloudflare({
225
270
  distDir,
226
271
  config: options.cloudflare?.config,
272
+ alepha: alepha!,
227
273
  }),
228
274
  });
229
275
  }
230
276
 
231
- if (flags.docker || options.docker) {
277
+ if (target === "docker") {
278
+ // Auto-configure Docker based on runtime
279
+ const dockerFrom =
280
+ options.docker?.from ??
281
+ (runtime === "bun" ? "oven/bun:alpine" : "node:24-alpine");
282
+ const dockerCommand =
283
+ options.docker?.command ?? (runtime === "bun" ? "bun" : "node");
284
+
232
285
  await run({
233
286
  name: "add Docker config",
234
287
  handler: () =>
235
288
  generateDocker({
236
289
  distDir,
237
- ...options.docker,
290
+ image: dockerFrom,
291
+ command: dockerCommand,
238
292
  }),
239
293
  });
294
+
295
+ // Build Docker image if --image flag is provided
296
+ if (flags.image) {
297
+ const imageConfig = options.docker?.image;
298
+ const flagValue =
299
+ typeof flags.image === "string" ? flags.image : null;
300
+
301
+ let imageTag: string;
302
+ let version: string;
303
+
304
+ if (!flagValue) {
305
+ // -i (no value) → use config tag:latest
306
+ if (!imageConfig?.tag) {
307
+ throw new AlephaError(
308
+ "Flag '--image' requires 'build.docker.image.tag' in config",
309
+ );
310
+ }
311
+ version = "latest";
312
+ imageTag = `${imageConfig.tag}:${version}`;
313
+ } else if (flagValue.startsWith(":")) {
314
+ // -i=:1.3.4 → version only, prepend config tag
315
+ if (!imageConfig?.tag) {
316
+ throw new AlephaError(
317
+ "Flag '--image=:version' requires 'build.docker.image.tag' in config",
318
+ );
319
+ }
320
+ version = flagValue.slice(1); // remove leading ":"
321
+ imageTag = `${imageConfig.tag}:${version}`;
322
+ } else if (flagValue.includes(":")) {
323
+ // -i=toto:1.3.4 → full image with version
324
+ imageTag = flagValue;
325
+ version = flagValue.split(":")[1];
326
+ } else {
327
+ // -i=toto → image name without version → add :latest
328
+ imageTag = `${flagValue}:latest`;
329
+ version = "latest";
330
+ }
331
+
332
+ const args: string[] = [];
333
+
334
+ // Add custom args
335
+ if (imageConfig?.args) {
336
+ args.push(imageConfig.args);
337
+ }
338
+
339
+ // Add OCI labels if enabled
340
+ if (imageConfig?.oci) {
341
+ const revision = await this.utils.getGitRevision();
342
+ const created = new Date().toISOString();
343
+
344
+ args.push(
345
+ `--label "org.opencontainers.image.revision=${revision}"`,
346
+ );
347
+ args.push(`--label "org.opencontainers.image.created=${created}"`);
348
+ args.push(`--label "org.opencontainers.image.version=${version}"`);
349
+ }
350
+
351
+ const argsStr = args.length > 0 ? `${args.join(" ")} ` : "";
352
+ const dockerCmd = `docker build ${argsStr}-t ${imageTag} ${distDir}`;
353
+
354
+ await run(dockerCmd, {
355
+ alias: `docker build ${imageTag}`,
356
+ });
357
+ }
240
358
  }
241
359
  },
242
360
  });
@@ -405,20 +405,18 @@ export class DbCommand {
405
405
  }
406
406
 
407
407
  const url = options.providerUrl;
408
- if (!url.startsWith("cloudflare-d1://")) {
409
- throw new AlephaError(
410
- "D1 provider URL must start with 'cloudflare-d1://'.",
411
- );
408
+ if (!url.startsWith("d1://")) {
409
+ throw new AlephaError("D1 provider URL must start with 'd1://'.");
412
410
  }
413
411
 
414
412
  const [, databaseId] = url
415
- .replace("cloudflare-d1://", "")
416
- .replace("cloudflare-d1:", "")
413
+ .replace("d1://", "")
414
+ .replace("d1:", "")
417
415
  .split(":");
418
416
 
419
417
  if (!databaseId) {
420
418
  throw new AlephaError(
421
- "Database ID is missing in the D1 provider URL. Cloudflare D1 URL format: cloudflare-d1://<database_name>:<database_id>",
419
+ "Database ID is missing in the D1 provider URL. Cloudflare D1 URL format: d1://<database_name>:<database_id>",
422
420
  );
423
421
  }
424
422
 
@@ -76,16 +76,16 @@ describe("alepha init", () => {
76
76
  });
77
77
 
78
78
  // ─────────────────────────────────────────────────────────────────────────────
79
- // Agent Files (--agent flag)
79
+ // AI Agent Files (--ai flag)
80
80
  // ─────────────────────────────────────────────────────────────────────────────
81
81
 
82
- describe("--agent flag", () => {
82
+ describe("--ai flag", () => {
83
83
  it("should create CLAUDE.md when claude CLI is installed", async () => {
84
84
  const { fs, shell, cli, cmd, json } = createTestEnv();
85
85
  await setupProject(fs, json);
86
86
  shell.installedCommands.add("claude");
87
87
 
88
- await cli.run(cmd.init, { argv: "--agent", root: "/project" });
88
+ await cli.run(cmd.init, { argv: "--ai", root: "/project" });
89
89
 
90
90
  expect(fs.wasWritten("/project/CLAUDE.md")).toBe(true);
91
91
  expect(fs.wasWritten("/project/AGENTS.md")).toBe(false);
@@ -95,7 +95,7 @@ describe("alepha init", () => {
95
95
  const { fs, cli, cmd, json } = createTestEnv();
96
96
  await setupProject(fs, json);
97
97
 
98
- await cli.run(cmd.init, { argv: "--agent", root: "/project" });
98
+ await cli.run(cmd.init, { argv: "--ai", root: "/project" });
99
99
 
100
100
  expect(fs.wasWritten("/project/AGENTS.md")).toBe(true);
101
101
  expect(fs.wasWritten("/project/CLAUDE.md")).toBe(false);
@@ -105,7 +105,7 @@ describe("alepha init", () => {
105
105
  const { fs, cli, cmd, json } = createTestEnv();
106
106
  await setupProject(fs, json);
107
107
 
108
- await cli.run(cmd.init, { argv: "--agent", root: "/project" });
108
+ await cli.run(cmd.init, { argv: "--ai", root: "/project" });
109
109
 
110
110
  expect(fs.wasWrittenMatching("/project/AGENTS.md", /Alepha/)).toBe(true);
111
111
  expect(fs.wasWrittenMatching("/project/AGENTS.md", /alepha lint/)).toBe(
@@ -113,7 +113,7 @@ describe("alepha init", () => {
113
113
  );
114
114
  });
115
115
 
116
- it("should not create agent files without --agent flag", async () => {
116
+ it("should not create agent files without --ai flag", async () => {
117
117
  const { fs, cli, cmd, json } = createTestEnv();
118
118
  await setupProject(fs, json);
119
119
 
@@ -180,6 +180,50 @@ describe("alepha init", () => {
180
180
  expect(shell.wasCalled("npm install")).toBe(true);
181
181
  expect(shell.wasCalled("yarn install")).toBe(false);
182
182
  });
183
+
184
+ it("should accept --pm=yarn", async () => {
185
+ const { fs, shell, cli, cmd, json } = createTestEnv();
186
+ await setupProject(fs, json);
187
+
188
+ await cli.run(cmd.init, { argv: "--pm=yarn", root: "/project" });
189
+
190
+ expect(shell.wasCalled("yarn install")).toBe(true);
191
+ });
192
+
193
+ it("should accept --pm=pnpm", async () => {
194
+ const { fs, shell, cli, cmd, json } = createTestEnv();
195
+ await setupProject(fs, json);
196
+
197
+ await cli.run(cmd.init, { argv: "--pm=pnpm", root: "/project" });
198
+
199
+ expect(shell.wasCalled("pnpm install")).toBe(true);
200
+ });
201
+
202
+ it("should accept --pm=bun", async () => {
203
+ const { fs, shell, cli, cmd, json } = createTestEnv();
204
+ await setupProject(fs, json);
205
+
206
+ await cli.run(cmd.init, { argv: "--pm=bun", root: "/project" });
207
+
208
+ expect(shell.wasCalled("bun install")).toBe(true);
209
+ });
210
+
211
+ it("should reject invalid --pm value", async () => {
212
+ const { fs, cli, cmd, json } = createTestEnv();
213
+ await setupProject(fs, json);
214
+
215
+ await expect(
216
+ cli.run(cmd.init, { argv: "--pm=invalid", root: "/project" }),
217
+ ).rejects.toThrowError(/Invalid flag/);
218
+ });
219
+
220
+ it("should show enum values in help for --pm flag", async () => {
221
+ const { alepha, cli, cmd } = createTestEnv();
222
+ await alepha.start();
223
+
224
+ // Verify printHelp works with the init command (which has an enum flag)
225
+ expect(() => cli.printHelp(cmd.init)).not.toThrow();
226
+ });
183
227
  });
184
228
 
185
229
  // ─────────────────────────────────────────────────────────────────────────────
@@ -26,9 +26,8 @@ export class InitCommand {
26
26
  }),
27
27
  ),
28
28
  flags: t.object({
29
- agent: t.optional(
29
+ ai: t.optional(
30
30
  t.boolean({
31
- aliases: ["a"],
32
31
  description:
33
32
  "Add AI agent instructions (CLAUDE.md if claude CLI installed, else AGENTS.md)",
34
33
  }),
@@ -56,6 +55,17 @@ export class InitCommand {
56
55
  "Include @alepha/ui (components, auth portal, admin portal)",
57
56
  }),
58
57
  ),
58
+ auth: t.optional(
59
+ t.boolean({
60
+ description:
61
+ "Include authentication (AppSecurity, $uiAuth). Implies --api --ui --react",
62
+ }),
63
+ ),
64
+ admin: t.optional(
65
+ t.boolean({
66
+ description: "Include admin portal ($uiAdmin). Implies --auth",
67
+ }),
68
+ ),
59
69
  test: t.optional(
60
70
  t.boolean({ description: "Include Vitest and create test directory" }),
61
71
  ),
@@ -69,9 +79,17 @@ export class InitCommand {
69
79
  handler: async ({ run, flags, root, args }) => {
70
80
  if (args) {
71
81
  root = this.fs.join(root, args);
72
- await this.fs.mkdir(root);
82
+ await this.fs.mkdir(root, { force: true });
73
83
  }
74
84
 
85
+ // Flag cascading: --admin → --auth → --ui → --react, --api
86
+ if (flags.admin) {
87
+ flags.auth = true;
88
+ }
89
+ if (flags.auth) {
90
+ flags.api = true;
91
+ flags.ui = true;
92
+ }
75
93
  if (flags.ui) {
76
94
  flags.react = true;
77
95
  }
@@ -81,7 +99,7 @@ export class InitCommand {
81
99
 
82
100
  // Detect agent type: claude CLI → CLAUDE.md, else → AGENTS.md
83
101
  let agentType: "claude" | "agents" | false = false;
84
- if (flags.agent) {
102
+ if (flags.ai) {
85
103
  const hasClaudeCli = await this.utils.isInstalledAsync("claude");
86
104
  agentType = hasClaudeCli ? "claude" : "agents";
87
105
  }
@@ -112,12 +130,17 @@ export class InitCommand {
112
130
  force,
113
131
  });
114
132
  if (flags.api) {
115
- await this.scaffolder.ensureApiProject(root, { force });
133
+ await this.scaffolder.ensureApiProject(root, {
134
+ auth: !!flags.auth,
135
+ force,
136
+ });
116
137
  }
117
138
  if (flags.react && !isExpo) {
118
139
  await this.scaffolder.ensureWebProject(root, {
119
140
  api: !!flags.api,
120
141
  ui: !!flags.ui,
142
+ auth: !!flags.auth,
143
+ admin: !!flags.admin,
121
144
  force,
122
145
  });
123
146
  }
@@ -3,7 +3,6 @@ import { $logger } from "alepha/logger";
3
3
  import { FileSystemProvider } from "alepha/system";
4
4
  import { importVite, importViteReact, viteAlephaSsrPreload } from "alepha/vite";
5
5
  import type { InlineConfig, Plugin, ViteDevServer } from "vite";
6
- import { ViteUtils } from "../services/ViteUtils.ts";
7
6
  import type { AppEntry } from "./AppEntryProvider.ts";
8
7
 
9
8
  export interface ViteDevServerOptions {
@@ -49,7 +48,6 @@ export interface ViteDevServerOptions {
49
48
  export class ViteDevServerProvider {
50
49
  protected readonly log = $logger();
51
50
  protected readonly fs = $inject(FileSystemProvider);
52
- protected readonly templateProvider = $inject(ViteUtils);
53
51
  protected server!: ViteDevServer;
54
52
  protected options!: ViteDevServerOptions;
55
53
  protected alepha: Alepha | null = null;
@@ -240,19 +238,17 @@ export class ViteDevServerProvider {
240
238
  }
241
239
 
242
240
  /**
243
- * Setup Alepha instance with Vite middleware and template.
241
+ * Setup Alepha instance with Vite middleware.
244
242
  */
245
243
  protected async setupAlepha(): Promise<void> {
246
244
  if (!this.alepha || !this.hasReact()) {
247
245
  return;
248
246
  }
249
247
 
250
- const template = await this.server.transformIndexHtml(
251
- "/",
252
- this.templateProvider.generateIndexHtml(this.options.entry),
253
- );
254
-
255
- this.alepha.store.set("alepha.react.server.template", template);
248
+ // Generate dev head content using Vite's transformIndexHtml
249
+ // This lets Vite and all plugins (React, etc.) inject their scripts
250
+ const devHead = await this.generateDevHead();
251
+ this.alepha.store.set("alepha.react.ssr.manifest" as any, { devHead });
256
252
 
257
253
  this.alepha.events.on("server:onRequest", {
258
254
  priority: "first",
@@ -273,6 +269,32 @@ export class ViteDevServerProvider {
273
269
  });
274
270
  }
275
271
 
272
+ /**
273
+ * Generate dev head content by transforming a minimal HTML through Vite.
274
+ * This lets Vite and all plugins inject their scripts (HMR client, React Fast Refresh, etc.).
275
+ */
276
+ protected async generateDevHead(): Promise<string> {
277
+ const { browser, style } = this.options.entry;
278
+
279
+ // Build minimal HTML with entry points
280
+ const scripts: string[] = [];
281
+ if (style) {
282
+ scripts.push(`<link rel="stylesheet" href="/${style}">`);
283
+ }
284
+ if (browser) {
285
+ scripts.push(`<script type="module" src="/${browser}"></script>`);
286
+ }
287
+
288
+ const minimalHtml = `<!DOCTYPE html><html><head>${scripts.join("\n")}</head><body></body></html>`;
289
+
290
+ // Transform through Vite to inject all plugin scripts
291
+ const transformed = await this.server.transformIndexHtml("/", minimalHtml);
292
+
293
+ // Extract head content
294
+ const headMatch = transformed.match(/<head>([\s\S]*?)<\/head>/i);
295
+ return headMatch?.[1]?.trim() ?? "";
296
+ }
297
+
276
298
  /**
277
299
  * Check if request is for an HTML page (not an asset).
278
300
  */
@@ -145,4 +145,20 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
145
145
  public isInstalledAsync(cmd: string): Promise<boolean> {
146
146
  return this.shell.isInstalled(cmd);
147
147
  }
148
+
149
+ /**
150
+ * Get the current git revision (commit SHA).
151
+ *
152
+ * @returns The short commit SHA or "unknown" if not in a git repo
153
+ */
154
+ public async getGitRevision(): Promise<string> {
155
+ try {
156
+ const result = await this.shell.run("git rev-parse --short HEAD", {
157
+ capture: true,
158
+ });
159
+ return result.trim();
160
+ } catch {
161
+ return "unknown";
162
+ }
163
+ }
148
164
  }
@@ -388,6 +388,8 @@ export class PackageManagerUtils {
388
388
  if (modes.react) {
389
389
  dependencies.react = alephaDeps.react;
390
390
  dependencies["react-dom"] = alephaDeps["react-dom"];
391
+ devDependencies["@vitejs/plugin-react"] =
392
+ alephaDeps["@vitejs/plugin-react"];
391
393
  devDependencies["@types/react"] = alephaDeps["@types/react"];
392
394
  }
393
395
 
@@ -0,0 +1,97 @@
1
+ import { Alepha } from "alepha";
2
+ import {
3
+ FileSystemProvider,
4
+ MemoryFileSystemProvider,
5
+ MemoryShellProvider,
6
+ ShellProvider,
7
+ } from "alepha/system";
8
+ import { describe, expect, it } from "vitest";
9
+ import { ProjectScaffolder } from "./ProjectScaffolder.ts";
10
+
11
+ describe("ProjectScaffolder", () => {
12
+ const createTestEnv = () => {
13
+ const alepha = Alepha.create()
14
+ .with({ provide: FileSystemProvider, use: MemoryFileSystemProvider })
15
+ .with({ provide: ShellProvider, use: MemoryShellProvider });
16
+
17
+ const scaffolder = alepha.inject(ProjectScaffolder);
18
+
19
+ return { alepha, scaffolder };
20
+ };
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // getAppName
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ describe("getAppName", () => {
27
+ it("should return lowercase directory name", () => {
28
+ const { scaffolder } = createTestEnv();
29
+
30
+ expect(scaffolder.getAppName("/project/MyApp")).toBe("myapp");
31
+ });
32
+
33
+ it("should remove dashes from directory name", () => {
34
+ const { scaffolder } = createTestEnv();
35
+
36
+ expect(scaffolder.getAppName("/project/my-cool-app")).toBe("mycoolapp");
37
+ });
38
+
39
+ it("should remove underscores from directory name", () => {
40
+ const { scaffolder } = createTestEnv();
41
+
42
+ expect(scaffolder.getAppName("/project/my_cool_app")).toBe("mycoolapp");
43
+ });
44
+
45
+ it("should remove spaces from directory name", () => {
46
+ const { scaffolder } = createTestEnv();
47
+
48
+ expect(scaffolder.getAppName("/project/my cool app")).toBe("mycoolapp");
49
+ });
50
+
51
+ it("should remove dots from directory name", () => {
52
+ const { scaffolder } = createTestEnv();
53
+
54
+ expect(scaffolder.getAppName("/project/my.cool.app")).toBe("mycoolapp");
55
+ });
56
+
57
+ it("should remove digits from directory name", () => {
58
+ const { scaffolder } = createTestEnv();
59
+
60
+ expect(scaffolder.getAppName("/project/app123")).toBe("app");
61
+ expect(scaffolder.getAppName("/project/my2app")).toBe("myapp");
62
+ expect(scaffolder.getAppName("/project/v2-app")).toBe("vapp");
63
+ });
64
+
65
+ it("should handle combination of special characters", () => {
66
+ const { scaffolder } = createTestEnv();
67
+
68
+ expect(scaffolder.getAppName("/project/my-cool_app.v2")).toBe(
69
+ "mycoolappv",
70
+ );
71
+ expect(scaffolder.getAppName("/project/test_app-2.0")).toBe("testapp");
72
+ });
73
+
74
+ it("should fallback to 'app' when all characters are removed", () => {
75
+ const { scaffolder } = createTestEnv();
76
+
77
+ expect(scaffolder.getAppName("/project/123")).toBe("app");
78
+ expect(scaffolder.getAppName("/project/---")).toBe("app");
79
+ expect(scaffolder.getAppName("/project/1.2.3")).toBe("app");
80
+ expect(scaffolder.getAppName("/project/_-._")).toBe("app");
81
+ });
82
+
83
+ it("should handle deeply nested paths", () => {
84
+ const { scaffolder } = createTestEnv();
85
+
86
+ expect(scaffolder.getAppName("/workspace/packages/apps/my-app")).toBe(
87
+ "myapp",
88
+ );
89
+ });
90
+
91
+ it("should handle root-level directories", () => {
92
+ const { scaffolder } = createTestEnv();
93
+
94
+ expect(scaffolder.getAppName("/myapp")).toBe("myapp");
95
+ });
96
+ });
97
+ });