alepha 0.15.2 → 0.15.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 (132) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts +332 -332
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/files/index.d.ts +170 -170
  5. package/dist/api/files/index.d.ts.map +1 -1
  6. package/dist/api/jobs/index.d.ts +151 -151
  7. package/dist/api/keys/index.d.ts +195 -195
  8. package/dist/api/keys/index.d.ts.map +1 -1
  9. package/dist/api/parameters/index.d.ts +260 -260
  10. package/dist/api/users/index.d.ts +22 -11
  11. package/dist/api/users/index.d.ts.map +1 -1
  12. package/dist/api/users/index.js +7 -2
  13. package/dist/api/users/index.js.map +1 -1
  14. package/dist/api/verifications/index.d.ts +128 -128
  15. package/dist/api/verifications/index.d.ts.map +1 -1
  16. package/dist/bucket/index.d.ts +8 -0
  17. package/dist/bucket/index.d.ts.map +1 -1
  18. package/dist/bucket/index.js +7 -2
  19. package/dist/bucket/index.js.map +1 -1
  20. package/dist/cli/index.d.ts +191 -74
  21. package/dist/cli/index.d.ts.map +1 -1
  22. package/dist/cli/index.js +215 -48
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/command/index.d.ts +10 -0
  25. package/dist/command/index.d.ts.map +1 -1
  26. package/dist/command/index.js +67 -13
  27. package/dist/command/index.js.map +1 -1
  28. package/dist/core/index.browser.js +28 -21
  29. package/dist/core/index.browser.js.map +1 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +28 -21
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/index.native.js +28 -21
  34. package/dist/core/index.native.js.map +1 -1
  35. package/dist/email/index.d.ts +8 -0
  36. package/dist/email/index.d.ts.map +1 -1
  37. package/dist/email/index.js +7 -2
  38. package/dist/email/index.js.map +1 -1
  39. package/dist/mcp/index.d.ts +5 -5
  40. package/dist/orm/index.bun.js +32 -16
  41. package/dist/orm/index.bun.js.map +1 -1
  42. package/dist/orm/index.d.ts +4 -1
  43. package/dist/orm/index.d.ts.map +1 -1
  44. package/dist/orm/index.js +34 -22
  45. package/dist/orm/index.js.map +1 -1
  46. package/dist/react/router/index.browser.js +9 -15
  47. package/dist/react/router/index.browser.js.map +1 -1
  48. package/dist/react/router/index.d.ts +295 -407
  49. package/dist/react/router/index.d.ts.map +1 -1
  50. package/dist/react/router/index.js +566 -776
  51. package/dist/react/router/index.js.map +1 -1
  52. package/dist/redis/index.d.ts +19 -19
  53. package/dist/security/index.d.ts +42 -42
  54. package/dist/security/index.d.ts.map +1 -1
  55. package/dist/security/index.js +8 -7
  56. package/dist/security/index.js.map +1 -1
  57. package/dist/server/auth/index.d.ts +167 -167
  58. package/dist/server/core/index.d.ts +9 -9
  59. package/dist/server/health/index.d.ts +17 -17
  60. package/dist/server/links/index.d.ts +39 -39
  61. package/dist/server/static/index.js +7 -2
  62. package/dist/server/static/index.js.map +1 -1
  63. package/dist/server/swagger/index.d.ts +8 -0
  64. package/dist/server/swagger/index.d.ts.map +1 -1
  65. package/dist/server/swagger/index.js +7 -2
  66. package/dist/server/swagger/index.js.map +1 -1
  67. package/dist/sms/index.d.ts +8 -0
  68. package/dist/sms/index.d.ts.map +1 -1
  69. package/dist/sms/index.js +7 -2
  70. package/dist/sms/index.js.map +1 -1
  71. package/dist/system/index.browser.js +734 -12
  72. package/dist/system/index.browser.js.map +1 -1
  73. package/dist/system/index.d.ts +8 -0
  74. package/dist/system/index.d.ts.map +1 -1
  75. package/dist/system/index.js +7 -2
  76. package/dist/system/index.js.map +1 -1
  77. package/dist/vite/index.d.ts +1 -1
  78. package/dist/vite/index.js +15 -7
  79. package/dist/vite/index.js.map +1 -1
  80. package/package.json +4 -2
  81. package/src/api/logs/TODO.md +13 -10
  82. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  83. package/src/cli/atoms/buildOptions.ts +99 -9
  84. package/src/cli/commands/build.ts +149 -32
  85. package/src/cli/commands/db.ts +5 -7
  86. package/src/cli/commands/init.spec.ts +50 -6
  87. package/src/cli/commands/init.ts +28 -5
  88. package/src/cli/providers/ViteDevServerProvider.ts +1 -10
  89. package/src/cli/services/AlephaCliUtils.ts +16 -0
  90. package/src/cli/services/PackageManagerUtils.ts +2 -0
  91. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  92. package/src/cli/services/ProjectScaffolder.ts +28 -6
  93. package/src/cli/templates/agentMd.ts +6 -1
  94. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  95. package/src/cli/templates/apiIndexTs.ts +18 -4
  96. package/src/cli/templates/webAppRouterTs.ts +25 -1
  97. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  98. package/src/command/helpers/Runner.spec.ts +135 -0
  99. package/src/command/helpers/Runner.ts +4 -1
  100. package/src/command/providers/CliProvider.spec.ts +325 -0
  101. package/src/command/providers/CliProvider.ts +117 -7
  102. package/src/core/Alepha.ts +32 -25
  103. package/src/orm/index.bun.ts +1 -1
  104. package/src/orm/index.ts +2 -6
  105. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  106. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  107. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  108. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  109. package/src/react/router/hooks/useActive.ts +1 -1
  110. package/src/react/router/hooks/useRouter.ts +1 -1
  111. package/src/react/router/index.ts +4 -0
  112. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  113. package/src/react/router/primitives/$page.spec.tsx +0 -32
  114. package/src/react/router/primitives/$page.ts +6 -14
  115. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  116. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  117. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  118. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  119. package/src/react/router/providers/ReactServerProvider.ts +7 -78
  120. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  121. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  122. package/src/react/router/services/ReactRouter.ts +13 -13
  123. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  124. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  125. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  126. package/src/system/index.browser.ts +25 -0
  127. package/src/system/index.workerd.ts +1 -0
  128. package/src/system/providers/FileSystemProvider.ts +8 -0
  129. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  130. package/src/vite/tasks/buildServer.ts +2 -12
  131. package/src/vite/tasks/generateCloudflare.ts +10 -7
  132. 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,7 +262,7 @@ 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: () =>
@@ -228,15 +273,87 @@ export class BuildCommand {
228
273
  });
229
274
  }
230
275
 
231
- if (flags.docker || options.docker) {
276
+ if (target === "docker") {
277
+ // Auto-configure Docker based on runtime
278
+ const dockerFrom =
279
+ options.docker?.from ??
280
+ (runtime === "bun" ? "oven/bun:alpine" : "node:24-alpine");
281
+ const dockerCommand =
282
+ options.docker?.command ?? (runtime === "bun" ? "bun" : "node");
283
+
232
284
  await run({
233
285
  name: "add Docker config",
234
286
  handler: () =>
235
287
  generateDocker({
236
288
  distDir,
237
- ...options.docker,
289
+ image: dockerFrom,
290
+ command: dockerCommand,
238
291
  }),
239
292
  });
293
+
294
+ // Build Docker image if --image flag is provided
295
+ if (flags.image) {
296
+ const imageConfig = options.docker?.image;
297
+ const flagValue =
298
+ typeof flags.image === "string" ? flags.image : null;
299
+
300
+ let imageTag: string;
301
+ let version: string;
302
+
303
+ if (!flagValue) {
304
+ // -i (no value) → use config tag:latest
305
+ if (!imageConfig?.tag) {
306
+ throw new AlephaError(
307
+ "Flag '--image' requires 'build.docker.image.tag' in config",
308
+ );
309
+ }
310
+ version = "latest";
311
+ imageTag = `${imageConfig.tag}:${version}`;
312
+ } else if (flagValue.startsWith(":")) {
313
+ // -i=:1.3.4 → version only, prepend config tag
314
+ if (!imageConfig?.tag) {
315
+ throw new AlephaError(
316
+ "Flag '--image=:version' requires 'build.docker.image.tag' in config",
317
+ );
318
+ }
319
+ version = flagValue.slice(1); // remove leading ":"
320
+ imageTag = `${imageConfig.tag}:${version}`;
321
+ } else if (flagValue.includes(":")) {
322
+ // -i=toto:1.3.4 → full image with version
323
+ imageTag = flagValue;
324
+ version = flagValue.split(":")[1];
325
+ } else {
326
+ // -i=toto → image name without version → add :latest
327
+ imageTag = `${flagValue}:latest`;
328
+ version = "latest";
329
+ }
330
+
331
+ const args: string[] = [];
332
+
333
+ // Add custom args
334
+ if (imageConfig?.args) {
335
+ args.push(imageConfig.args);
336
+ }
337
+
338
+ // Add OCI labels if enabled
339
+ if (imageConfig?.oci) {
340
+ const revision = await this.utils.getGitRevision();
341
+ const created = new Date().toISOString();
342
+
343
+ args.push(
344
+ `--label "org.opencontainers.image.revision=${revision}"`,
345
+ );
346
+ args.push(`--label "org.opencontainers.image.created=${created}"`);
347
+ args.push(`--label "org.opencontainers.image.version=${version}"`);
348
+ }
349
+
350
+ const argsStr = args.length > 0 ? `${args.join(" ")} ` : "";
351
+ const dockerCmd = `docker build ${argsStr}-t ${imageTag} ${distDir}`;
352
+
353
+ await run(dockerCmd, {
354
+ alias: `docker build ${imageTag}`,
355
+ });
356
+ }
240
357
  }
241
358
  },
242
359
  });
@@ -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,20 +238,13 @@ 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);
256
-
257
248
  this.alepha.events.on("server:onRequest", {
258
249
  priority: "first",
259
250
  callback: async ({ request }) => {
@@ -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
+ });