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
@@ -24,6 +24,36 @@ class TestCliProvider extends CliProvider {
24
24
  public testFindCommand = this.findCommand.bind(this);
25
25
  public testFindPreHooks = this.findPreHooks.bind(this);
26
26
  public testFindPostHooks = this.findPostHooks.bind(this);
27
+ public testGetEnumValues = this.getEnumValues.bind(this);
28
+ public testFormatFlagDescription = this.formatFlagDescription.bind(this);
29
+
30
+ /**
31
+ * Extract flag definitions from a command's flags schema (for testing printHelp logic).
32
+ */
33
+ public testExtractFlagDefs(flagsSchema: any) {
34
+ return Object.entries(flagsSchema.properties).map(([key, value]) => ({
35
+ key,
36
+ schema: value,
37
+ aliases: [
38
+ key,
39
+ ...((value as any).aliases ??
40
+ ((value as any).alias ? [(value as any).alias] : [])),
41
+ ],
42
+ description: (value as any).description,
43
+ }));
44
+ }
45
+
46
+ /**
47
+ * Format aliases array into flag string (e.g., "-t, --target").
48
+ * Sorts by length (shorter first).
49
+ */
50
+ public testFormatFlagStr(aliases: string[]): string {
51
+ return aliases
52
+ .slice()
53
+ .sort((a, b) => a.length - b.length)
54
+ .map((a) => (a.length === 1 ? `-${a}` : `--${a}`))
55
+ .join(", ");
56
+ }
27
57
  }
28
58
 
29
59
  describe("CliProvider", () => {
@@ -130,6 +160,128 @@ describe("CliProvider", () => {
130
160
  cli.testParseFlags(["--config", "{invalid}"], flagDefs),
131
161
  ).toThrow("Invalid JSON");
132
162
  });
163
+
164
+ it("should parse union(boolean, text) flag without value as true", () => {
165
+ const cli = createTestCli();
166
+ const flagDefs = [
167
+ {
168
+ key: "image",
169
+ aliases: ["i", "image"],
170
+ schema: t.union([t.boolean(), t.text()]),
171
+ },
172
+ ];
173
+
174
+ const result = cli.testParseFlags(["-i"], flagDefs);
175
+ expect(result.image).toBe(true);
176
+ });
177
+
178
+ it("should parse union(boolean, text) flag with = value as string", () => {
179
+ const cli = createTestCli();
180
+ const flagDefs = [
181
+ {
182
+ key: "image",
183
+ aliases: ["i", "image"],
184
+ schema: t.union([t.boolean(), t.text()]),
185
+ },
186
+ ];
187
+
188
+ const result = cli.testParseFlags(["-i=1.3.4"], flagDefs);
189
+ expect(result.image).toBe("1.3.4");
190
+ });
191
+
192
+ it("should parse union(boolean, text) flag with space value as string", () => {
193
+ const cli = createTestCli();
194
+ const flagDefs = [
195
+ {
196
+ key: "image",
197
+ aliases: ["i", "image"],
198
+ schema: t.union([t.boolean(), t.text()]),
199
+ },
200
+ ];
201
+
202
+ const result = cli.testParseFlags(["-i", "1.3.4"], flagDefs);
203
+ expect(result.image).toBe("1.3.4");
204
+ });
205
+
206
+ it("should parse union(boolean, text) flag without value when followed by another flag", () => {
207
+ const cli = createTestCli();
208
+ const flagDefs = [
209
+ {
210
+ key: "image",
211
+ aliases: ["i", "image"],
212
+ schema: t.union([t.boolean(), t.text()]),
213
+ },
214
+ { key: "verbose", aliases: ["v", "verbose"], schema: t.boolean() },
215
+ ];
216
+
217
+ const result = cli.testParseFlags(["-i", "-v"], flagDefs);
218
+ expect(result.image).toBe(true);
219
+ expect(result.verbose).toBe(true);
220
+ });
221
+
222
+ it("should parse union(boolean, text) flag with long form without value", () => {
223
+ const cli = createTestCli();
224
+ const flagDefs = [
225
+ {
226
+ key: "image",
227
+ aliases: ["i", "image"],
228
+ schema: t.union([t.boolean(), t.text()]),
229
+ },
230
+ ];
231
+
232
+ const result = cli.testParseFlags(["--image"], flagDefs);
233
+ expect(result.image).toBe(true);
234
+ });
235
+
236
+ it("should parse union(boolean, text) flag at end of argv without value", () => {
237
+ const cli = createTestCli();
238
+ const flagDefs = [
239
+ {
240
+ key: "image",
241
+ aliases: ["i", "image"],
242
+ schema: t.union([t.boolean(), t.text()]),
243
+ },
244
+ { key: "verbose", aliases: ["v", "verbose"], schema: t.boolean() },
245
+ ];
246
+
247
+ const result = cli.testParseFlags(["-v", "-i"], flagDefs);
248
+ expect(result.verbose).toBe(true);
249
+ expect(result.image).toBe(true);
250
+ });
251
+
252
+ it("should parse union(boolean, text) with empty = value as empty string", () => {
253
+ const cli = createTestCli();
254
+ const flagDefs = [
255
+ {
256
+ key: "image",
257
+ aliases: ["i", "image"],
258
+ schema: t.union([t.boolean(), t.text()]),
259
+ },
260
+ ];
261
+
262
+ // Note: --image= results in empty string value
263
+ const result = cli.testParseFlags(["--image="], flagDefs);
264
+ // Empty string is falsy, so it goes through the "no value" path → true
265
+ // This is expected behavior - use --image="" if you need empty string
266
+ expect(result.image).toBe(true);
267
+ });
268
+
269
+ it("should parse union(boolean, text) with value containing special chars", () => {
270
+ const cli = createTestCli();
271
+ const flagDefs = [
272
+ {
273
+ key: "image",
274
+ aliases: ["i", "image"],
275
+ schema: t.union([t.boolean(), t.text()]),
276
+ },
277
+ ];
278
+
279
+ const result = cli.testParseFlags(
280
+ ["--image=my-org/my-app:v1.2.3-beta"],
281
+ flagDefs,
282
+ );
283
+ expect(result.image).toBe("my-org/my-app:v1.2.3-beta");
284
+ });
133
285
  });
134
286
 
135
287
  // ─────────────────────────────────────────────────────────────────────────────
@@ -439,6 +591,56 @@ describe("CliProvider", () => {
439
591
  expect(result.has(0)).toBe(true); // --name=value
440
592
  expect(result.has(1)).toBe(false); // arg
441
593
  });
594
+
595
+ it("should consume value for union(boolean, text) when value is provided", () => {
596
+ const cli = createTestCli();
597
+ const flagDefs = [
598
+ {
599
+ key: "image",
600
+ aliases: ["i", "image"],
601
+ schema: t.union([t.boolean(), t.text()]),
602
+ },
603
+ ];
604
+
605
+ const result = cli.testGetFlagConsumedIndices(
606
+ ["-i", "1.3.4", "arg"],
607
+ flagDefs,
608
+ );
609
+ expect(result.has(0)).toBe(true); // -i
610
+ expect(result.has(1)).toBe(true); // 1.3.4 (consumed as value)
611
+ expect(result.has(2)).toBe(false); // arg
612
+ });
613
+
614
+ it("should not consume next arg for union(boolean, text) when next is a flag", () => {
615
+ const cli = createTestCli();
616
+ const flagDefs = [
617
+ {
618
+ key: "image",
619
+ aliases: ["i", "image"],
620
+ schema: t.union([t.boolean(), t.text()]),
621
+ },
622
+ { key: "verbose", aliases: ["v", "verbose"], schema: t.boolean() },
623
+ ];
624
+
625
+ const result = cli.testGetFlagConsumedIndices(["-i", "-v"], flagDefs);
626
+ expect(result.has(0)).toBe(true); // -i
627
+ expect(result.has(1)).toBe(true); // -v (not consumed by -i, but by itself)
628
+ });
629
+
630
+ it("should not consume next arg for union(boolean, text) at end of argv", () => {
631
+ const cli = createTestCli();
632
+ const flagDefs = [
633
+ {
634
+ key: "image",
635
+ aliases: ["i", "image"],
636
+ schema: t.union([t.boolean(), t.text()]),
637
+ },
638
+ ];
639
+
640
+ const result = cli.testGetFlagConsumedIndices(["-i"], flagDefs);
641
+ expect(result.has(0)).toBe(true); // -i
642
+ expect(result.size).toBe(1);
643
+ });
442
644
  });
443
645
 
444
646
  // ─────────────────────────────────────────────────────────────────────────────
@@ -932,6 +1134,129 @@ describe("CliProvider", () => {
932
1134
  // Just verify it doesn't throw - hidden commands filtered in getTopLevelCommands
933
1135
  expect(() => cli.printHelp()).not.toThrow();
934
1136
  });
1137
+
1138
+ it("should include flag aliases in help output", () => {
1139
+ const cli = createTestCli();
1140
+
1141
+ const flagsSchema = t.object({
1142
+ target: t.optional(
1143
+ t.enum(["bare", "docker", "vercel"], {
1144
+ aliases: ["t"],
1145
+ description: "Deployment target",
1146
+ }),
1147
+ ),
1148
+ runtime: t.optional(
1149
+ t.enum(["node", "bun"], {
1150
+ alias: "r", // singular alias
1151
+ description: "JavaScript runtime",
1152
+ }),
1153
+ ),
1154
+ verbose: t.optional(
1155
+ t.boolean({
1156
+ description: "Verbose output",
1157
+ }),
1158
+ ),
1159
+ });
1160
+
1161
+ const flagDefs = cli.testExtractFlagDefs(flagsSchema);
1162
+
1163
+ // target should have key + aliases array
1164
+ const targetFlag = flagDefs.find((f) => f.key === "target");
1165
+ expect(targetFlag?.aliases).toContain("target");
1166
+ expect(targetFlag?.aliases).toContain("t");
1167
+ expect(cli.testFormatFlagStr(targetFlag!.aliases)).toBe("-t, --target");
1168
+
1169
+ // runtime should have key + alias (singular)
1170
+ const runtimeFlag = flagDefs.find((f) => f.key === "runtime");
1171
+ expect(runtimeFlag?.aliases).toContain("runtime");
1172
+ expect(runtimeFlag?.aliases).toContain("r");
1173
+ expect(cli.testFormatFlagStr(runtimeFlag!.aliases)).toBe("-r, --runtime");
1174
+
1175
+ // verbose should only have key (no aliases defined)
1176
+ const verboseFlag = flagDefs.find((f) => f.key === "verbose");
1177
+ expect(verboseFlag?.aliases).toEqual(["verbose"]);
1178
+ expect(cli.testFormatFlagStr(verboseFlag!.aliases)).toBe("--verbose");
1179
+ });
1180
+ });
1181
+
1182
+ // ─────────────────────────────────────────────────────────────────────────────
1183
+ // getEnumValues
1184
+ // ─────────────────────────────────────────────────────────────────────────────
1185
+
1186
+ describe("getEnumValues", () => {
1187
+ it("should extract values from t.enum schema", () => {
1188
+ const cli = createTestCli();
1189
+ const schema = t.enum(["yarn", "npm", "pnpm", "bun"]);
1190
+
1191
+ const values = cli.testGetEnumValues(schema);
1192
+
1193
+ expect(values).toEqual(["yarn", "npm", "pnpm", "bun"]);
1194
+ });
1195
+
1196
+ it("should return undefined for non-enum schemas", () => {
1197
+ const cli = createTestCli();
1198
+
1199
+ expect(cli.testGetEnumValues(t.string())).toBeUndefined();
1200
+ expect(cli.testGetEnumValues(t.boolean())).toBeUndefined();
1201
+ expect(cli.testGetEnumValues(t.number())).toBeUndefined();
1202
+ expect(cli.testGetEnumValues(t.object({}))).toBeUndefined();
1203
+ });
1204
+
1205
+ it("should return undefined for empty or undefined schema", () => {
1206
+ const cli = createTestCli();
1207
+
1208
+ expect(cli.testGetEnumValues(undefined as any)).toBeUndefined();
1209
+ });
1210
+ });
1211
+
1212
+ // ─────────────────────────────────────────────────────────────────────────────
1213
+ // formatFlagDescription
1214
+ // ─────────────────────────────────────────────────────────────────────────────
1215
+
1216
+ describe("formatFlagDescription", () => {
1217
+ it("should append enum values to description", () => {
1218
+ const cli = createTestCli();
1219
+ const schema = t.enum(["yarn", "npm", "pnpm", "bun"]);
1220
+
1221
+ const result = cli.testFormatFlagDescription(
1222
+ "Package manager to use",
1223
+ schema,
1224
+ );
1225
+
1226
+ expect(result).toContain("Package manager to use");
1227
+ expect(result).toContain("yarn");
1228
+ expect(result).toContain("npm");
1229
+ expect(result).toContain("pnpm");
1230
+ expect(result).toContain("bun");
1231
+ });
1232
+
1233
+ it("should return only enum values when description is empty", () => {
1234
+ const cli = createTestCli();
1235
+ const schema = t.enum(["a", "b", "c"]);
1236
+
1237
+ const result = cli.testFormatFlagDescription(undefined, schema);
1238
+
1239
+ expect(result).toContain("a");
1240
+ expect(result).toContain("b");
1241
+ expect(result).toContain("c");
1242
+ });
1243
+
1244
+ it("should return original description for non-enum schemas", () => {
1245
+ const cli = createTestCli();
1246
+
1247
+ expect(cli.testFormatFlagDescription("A string flag", t.string())).toBe(
1248
+ "A string flag",
1249
+ );
1250
+ expect(cli.testFormatFlagDescription("A boolean flag", t.boolean())).toBe(
1251
+ "A boolean flag",
1252
+ );
1253
+ });
1254
+
1255
+ it("should return empty string when no description and no enum", () => {
1256
+ const cli = createTestCli();
1257
+
1258
+ expect(cli.testFormatFlagDescription(undefined, t.string())).toBe("");
1259
+ });
935
1260
  });
936
1261
 
937
1262
  // ─────────────────────────────────────────────────────────────────────────────
@@ -10,6 +10,7 @@ import {
10
10
  type Static,
11
11
  type TObject,
12
12
  type TSchema,
13
+ type TUnion,
13
14
  TypeBoxError,
14
15
  t,
15
16
  } from "alepha";
@@ -658,8 +659,24 @@ export class CliProvider {
658
659
  continue;
659
660
  }
660
661
 
662
+ // Check if schema is a union containing boolean (allows flag without value)
663
+ const isUnionWithBoolean =
664
+ t.schema.isUnion(def.schema) &&
665
+ (def.schema as TUnion).anyOf.some((s) => t.schema.isBoolean(s));
666
+
661
667
  if (t.schema.isBoolean(def.schema)) {
662
668
  result[def.key] = true;
669
+ } else if (isUnionWithBoolean && !value) {
670
+ // Union with boolean: --flag without value → true
671
+ const nextArg = argv[i + 1];
672
+ if (nextArg && !nextArg.startsWith("-")) {
673
+ // Has a value after space: --flag value
674
+ result[def.key] = nextArg;
675
+ i++; // consume next arg
676
+ } else {
677
+ // No value: --flag → true
678
+ result[def.key] = true;
679
+ }
663
680
  } else if (value) {
664
681
  // Value provided via --flag=value syntax
665
682
  try {
@@ -713,8 +730,24 @@ export class CliProvider {
713
730
  const def = flagDefs.find((d) => d.aliases.includes(rawKey));
714
731
  if (!def) continue;
715
732
 
733
+ // Check if schema is a union containing boolean
734
+ const isUnionWithBoolean =
735
+ t.schema.isUnion(def.schema) &&
736
+ (def.schema as TUnion).anyOf.some((s) => t.schema.isBoolean(s));
737
+
716
738
  // If not a boolean flag and no = value, the next arg is consumed as the value
717
- if (!t.schema.isBoolean(def.schema) && !hasEqualValue) {
739
+ // Exception: union with boolean can work without a value
740
+ if (
741
+ !t.schema.isBoolean(def.schema) &&
742
+ !isUnionWithBoolean &&
743
+ !hasEqualValue
744
+ ) {
745
+ const nextArg = argv[i + 1];
746
+ if (nextArg && !nextArg.startsWith("-")) {
747
+ consumed.add(i + 1);
748
+ }
749
+ } else if (isUnionWithBoolean && !hasEqualValue) {
750
+ // Union with boolean: check if next arg looks like a value (not a flag)
718
751
  const nextArg = argv[i + 1];
719
752
  if (nextArg && !nextArg.startsWith("-")) {
720
753
  consumed.add(i + 1);
@@ -928,7 +961,11 @@ export class CliProvider {
928
961
  ...Object.entries(command.flags.properties).map(([key, value]) => ({
929
962
  key,
930
963
  schema: value,
931
- aliases: (value as any).alias ?? [key],
964
+ aliases: [
965
+ key,
966
+ ...((value as any).aliases ??
967
+ ((value as any).alias ? [(value as any).alias] : [])),
968
+ ],
932
969
  description: (value as any).description,
933
970
  })),
934
971
  // Add --mode flag if command has mode option enabled
@@ -941,6 +978,7 @@ export class CliProvider {
941
978
  typeof command.options.mode === "string"
942
979
  ? `Environment mode - loads .env.{mode} (default: ${command.options.mode})`
943
980
  : "Environment mode (e.g., production, staging) - loads .env.{mode}",
981
+ schema: t.string() as TSchema,
944
982
  },
945
983
  ]
946
984
  : []),
@@ -951,13 +989,20 @@ export class CliProvider {
951
989
  ];
952
990
 
953
991
  const maxFlagLength = this.getMaxFlagLength(flags);
954
- for (const { aliases, description } of flags) {
955
- const flagStr = (Array.isArray(aliases) ? aliases : [aliases])
992
+ for (const flag of flags) {
993
+ const { aliases, description } = flag;
994
+ const schema = "schema" in flag ? (flag.schema as TSchema) : undefined;
995
+ // Sort aliases by length (shorter first: -t before --target)
996
+ const sortedAliases = (Array.isArray(aliases) ? aliases : [aliases])
997
+ .slice()
998
+ .sort((a, b) => a.length - b.length);
999
+ const flagStr = sortedAliases
956
1000
  .map((a: string) => (a.length === 1 ? `-${a}` : `--${a}`))
957
1001
  .join(", ");
958
1002
  const coloredFlag = c.set("GREY_LIGHT", flagStr);
959
1003
  const padding = " ".repeat(Math.max(0, maxFlagLength - flagStr.length));
960
- this.log.info(` ${coloredFlag}${padding} ${description ?? ""}`);
1004
+ const formattedDesc = this.formatFlagDescription(description, schema);
1005
+ this.log.info(` ${coloredFlag}${padding} ${formattedDesc}`);
961
1006
  }
962
1007
 
963
1008
  // Show environment variables if defined
@@ -1024,6 +1069,7 @@ export class CliProvider {
1024
1069
  []),
1025
1070
  ],
1026
1071
  description: (value as any).description,
1072
+ schema: value as TSchema,
1027
1073
  }))
1028
1074
  : [];
1029
1075
 
@@ -1032,13 +1078,14 @@ export class CliProvider {
1032
1078
  ...Object.values(this.getAllGlobalFlags()),
1033
1079
  ];
1034
1080
  const maxFlagLength = this.getMaxFlagLength(globalFlags);
1035
- for (const { aliases, description } of globalFlags) {
1081
+ for (const { aliases, description, schema } of globalFlags) {
1036
1082
  const flagStr = aliases
1037
1083
  .map((a) => (a.length === 1 ? `-${a}` : `--${a}`))
1038
1084
  .join(", ");
1039
1085
  const coloredFlag = c.set("GREY_LIGHT", flagStr);
1040
1086
  const padding = " ".repeat(Math.max(0, maxFlagLength - flagStr.length));
1041
- this.log.info(` ${coloredFlag}${padding} ${description ?? ""}`);
1087
+ const formattedDesc = this.formatFlagDescription(description, schema);
1088
+ this.log.info(` ${coloredFlag}${padding} ${formattedDesc}`);
1042
1089
  }
1043
1090
  }
1044
1091
  this.log.info(""); // Newline
@@ -1159,4 +1206,67 @@ export class CliProvider {
1159
1206
  }),
1160
1207
  );
1161
1208
  }
1209
+
1210
+ /**
1211
+ * Extract enum values from a schema if it represents an enum.
1212
+ * Returns undefined if the schema is not an enum.
1213
+ */
1214
+ protected getEnumValues(schema: TSchema): string[] | undefined {
1215
+ if (!schema) return undefined;
1216
+
1217
+ // Check if schema has an enum property (t.enum creates schemas with this)
1218
+ if (
1219
+ "enum" in schema &&
1220
+ Array.isArray(schema.enum) &&
1221
+ schema.enum.every((v) => typeof v === "string")
1222
+ ) {
1223
+ return schema.enum as string[];
1224
+ }
1225
+
1226
+ // Also check for union of string literals (alternative enum representation)
1227
+ if (t.schema.isUnion(schema)) {
1228
+ const union = schema as TUnion;
1229
+ const values: string[] = [];
1230
+
1231
+ for (const variant of union.anyOf) {
1232
+ // Check if the variant is a string literal (has const property)
1233
+ if (
1234
+ t.schema.isString(variant) &&
1235
+ "const" in variant &&
1236
+ typeof variant.const === "string"
1237
+ ) {
1238
+ values.push(variant.const);
1239
+ } else {
1240
+ // Not all variants are string literals, not a simple enum
1241
+ return undefined;
1242
+ }
1243
+ }
1244
+
1245
+ return values.length > 0 ? values : undefined;
1246
+ }
1247
+
1248
+ return undefined;
1249
+ }
1250
+
1251
+ /**
1252
+ * Format flag description with enum values if applicable.
1253
+ */
1254
+ protected formatFlagDescription(
1255
+ description: string | undefined,
1256
+ schema: TSchema | undefined,
1257
+ ): string {
1258
+ const baseDesc = description ?? "";
1259
+
1260
+ if (!schema) return baseDesc;
1261
+
1262
+ const enumValues = this.getEnumValues(schema);
1263
+ if (enumValues && enumValues.length > 0) {
1264
+ const valuesStr = enumValues.join(", ");
1265
+ const c = this.color;
1266
+ const enumHint = c.set("GREY_DARK", `[${valuesStr}]`);
1267
+ return baseDesc ? `${baseDesc} ${enumHint}` : enumHint;
1268
+ }
1269
+
1270
+ return baseDesc;
1271
+ }
1162
1272
  }
@@ -472,44 +472,51 @@ export class Alepha {
472
472
 
473
473
  this.starting = Promise.withResolvers();
474
474
 
475
- const now = Date.now();
475
+ try {
476
+ const now = Date.now();
476
477
 
477
- this.log?.info("Starting App...");
478
+ this.log?.info("Starting App...");
478
479
 
479
- for (const [key] of this.substitutions.entries()) {
480
- this.inject(key);
481
- }
482
-
483
- const target = this.store.get("alepha.target");
484
- if (target) {
485
- this.modules = [];
486
- this.registry = new Map();
487
- this.primitiveRegistry = new Map();
488
- this.pendingInstantiations = [];
489
- this.substitutions = new Map();
490
- this.events.clear();
491
- delete (target as any)[MODULE];
492
- this.with(target);
493
480
  for (const [key] of this.substitutions.entries()) {
494
481
  this.inject(key);
495
482
  }
496
- }
497
483
 
498
- this.locked = true;
484
+ const target = this.store.get("alepha.target");
485
+ if (target) {
486
+ this.modules = [];
487
+ this.registry = new Map();
488
+ this.primitiveRegistry = new Map();
489
+ this.pendingInstantiations = [];
490
+ this.substitutions = new Map();
491
+ this.events.clear();
492
+ delete (target as any)[MODULE];
493
+ this.with(target);
494
+ for (const [key] of this.substitutions.entries()) {
495
+ this.inject(key);
496
+ }
497
+ }
499
498
 
500
- await this.events.emit("configure", this, { log: true });
499
+ this.locked = true;
501
500
 
502
- this.configured = true;
501
+ await this.events.emit("configure", this, { log: true });
503
502
 
504
- await this.events.emit("start", this, { log: true });
503
+ this.configured = true;
505
504
 
506
- this.started = true;
505
+ await this.events.emit("start", this, { log: true });
507
506
 
508
- await this.events.emit("ready", this, { log: true });
507
+ this.started = true;
509
508
 
510
- this.log?.info(`App is now ready [${Date.now() - now}ms]`);
509
+ await this.events.emit("ready", this, { log: true });
511
510
 
512
- this.ready = true;
511
+ this.log?.info(`App is now ready [${Date.now() - now}ms]`);
512
+
513
+ this.ready = true;
514
+ } catch (error) {
515
+ this.starting.reject(error);
516
+ const promise = this.starting.promise;
517
+ this.starting = undefined;
518
+ return promise;
519
+ }
513
520
 
514
521
  this.starting.resolve(this);
515
522
  this.starting = undefined;
@@ -0,0 +1,36 @@
1
+ import { $module } from "alepha";
2
+ import { $email } from "./primitives/$email.ts";
3
+ import { EmailProvider } from "./providers/EmailProvider.ts";
4
+ import { MemoryEmailProvider } from "./providers/MemoryEmailProvider.ts";
5
+ import { WorkermailerEmailProvider } from "./providers/WorkermailerEmailProvider.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ export * from "./errors/EmailError.ts";
10
+ export * from "./primitives/$email.ts";
11
+ export * from "./providers/EmailProvider.ts";
12
+ export * from "./providers/MemoryEmailProvider.ts";
13
+ export * from "./providers/WorkermailerEmailProvider.ts";
14
+
15
+ // ---------------------------------------------------------------------------------------------------------------------
16
+
17
+ export const AlephaEmail = $module({
18
+ name: "alepha.email",
19
+ primitives: [$email],
20
+ services: [EmailProvider, MemoryEmailProvider, WorkermailerEmailProvider],
21
+ register: (alepha) => {
22
+ if (alepha.env.EMAIL_HOST) {
23
+ alepha.with({
24
+ optional: true,
25
+ provide: EmailProvider,
26
+ use: WorkermailerEmailProvider,
27
+ });
28
+ } else {
29
+ alepha.with({
30
+ optional: true,
31
+ provide: EmailProvider,
32
+ use: MemoryEmailProvider,
33
+ });
34
+ }
35
+ },
36
+ });