alepha 0.14.3 → 0.15.0

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 (317) hide show
  1. package/README.md +2 -5
  2. package/dist/api/audits/index.d.ts +620 -811
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/files/index.d.ts +185 -377
  5. package/dist/api/files/index.d.ts.map +1 -1
  6. package/dist/api/files/index.js +0 -1
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +245 -435
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/notifications/index.d.ts +238 -429
  11. package/dist/api/notifications/index.d.ts.map +1 -1
  12. package/dist/api/parameters/index.d.ts +236 -427
  13. package/dist/api/parameters/index.d.ts.map +1 -1
  14. package/dist/api/users/index.browser.js +1 -2
  15. package/dist/api/users/index.browser.js.map +1 -1
  16. package/dist/api/users/index.d.ts +1010 -1196
  17. package/dist/api/users/index.d.ts.map +1 -1
  18. package/dist/api/users/index.js +178 -151
  19. package/dist/api/users/index.js.map +1 -1
  20. package/dist/api/verifications/index.d.ts +17 -17
  21. package/dist/api/verifications/index.d.ts.map +1 -1
  22. package/dist/batch/index.d.ts +122 -122
  23. package/dist/batch/index.d.ts.map +1 -1
  24. package/dist/batch/index.js +1 -2
  25. package/dist/batch/index.js.map +1 -1
  26. package/dist/bucket/index.d.ts +163 -163
  27. package/dist/bucket/index.d.ts.map +1 -1
  28. package/dist/cache/core/index.d.ts +46 -46
  29. package/dist/cache/core/index.d.ts.map +1 -1
  30. package/dist/cache/redis/index.d.ts.map +1 -1
  31. package/dist/cli/index.d.ts +384 -285
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/index.js +1113 -623
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/command/index.d.ts +299 -300
  36. package/dist/command/index.d.ts.map +1 -1
  37. package/dist/command/index.js +13 -9
  38. package/dist/command/index.js.map +1 -1
  39. package/dist/core/index.browser.js +445 -103
  40. package/dist/core/index.browser.js.map +1 -1
  41. package/dist/core/index.d.ts +733 -625
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +446 -103
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/index.native.js +445 -103
  46. package/dist/core/index.native.js.map +1 -1
  47. package/dist/datetime/index.d.ts +44 -44
  48. package/dist/datetime/index.d.ts.map +1 -1
  49. package/dist/datetime/index.js +4 -4
  50. package/dist/datetime/index.js.map +1 -1
  51. package/dist/email/index.d.ts +97 -50
  52. package/dist/email/index.d.ts.map +1 -1
  53. package/dist/email/index.js +129 -33
  54. package/dist/email/index.js.map +1 -1
  55. package/dist/fake/index.d.ts +7981 -14
  56. package/dist/fake/index.d.ts.map +1 -1
  57. package/dist/file/index.d.ts +523 -390
  58. package/dist/file/index.d.ts.map +1 -1
  59. package/dist/file/index.js +253 -1
  60. package/dist/file/index.js.map +1 -1
  61. package/dist/lock/core/index.d.ts +208 -208
  62. package/dist/lock/core/index.d.ts.map +1 -1
  63. package/dist/lock/redis/index.d.ts.map +1 -1
  64. package/dist/logger/index.d.ts +25 -26
  65. package/dist/logger/index.d.ts.map +1 -1
  66. package/dist/logger/index.js +12 -2
  67. package/dist/logger/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts +197 -197
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +1 -1
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/orm/chunk-DtkW-qnP.js +38 -0
  73. package/dist/orm/index.browser.js.map +1 -1
  74. package/dist/orm/index.bun.js +2814 -0
  75. package/dist/orm/index.bun.js.map +1 -0
  76. package/dist/orm/index.d.ts +1228 -1216
  77. package/dist/orm/index.d.ts.map +1 -1
  78. package/dist/orm/index.js +2041 -1967
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +248 -248
  81. package/dist/queue/core/index.d.ts.map +1 -1
  82. package/dist/queue/redis/index.d.ts.map +1 -1
  83. package/dist/redis/index.bun.js +285 -0
  84. package/dist/redis/index.bun.js.map +1 -0
  85. package/dist/redis/index.d.ts +118 -136
  86. package/dist/redis/index.d.ts.map +1 -1
  87. package/dist/redis/index.js +18 -38
  88. package/dist/redis/index.js.map +1 -1
  89. package/dist/retry/index.d.ts +69 -69
  90. package/dist/retry/index.d.ts.map +1 -1
  91. package/dist/router/index.d.ts +6 -6
  92. package/dist/router/index.d.ts.map +1 -1
  93. package/dist/scheduler/index.d.ts +25 -25
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/security/index.browser.js +5 -1
  96. package/dist/security/index.browser.js.map +1 -1
  97. package/dist/security/index.d.ts +417 -254
  98. package/dist/security/index.d.ts.map +1 -1
  99. package/dist/security/index.js +386 -86
  100. package/dist/security/index.js.map +1 -1
  101. package/dist/server/auth/index.d.ts +110 -110
  102. package/dist/server/auth/index.d.ts.map +1 -1
  103. package/dist/server/auth/index.js +20 -20
  104. package/dist/server/auth/index.js.map +1 -1
  105. package/dist/server/cache/index.d.ts +62 -47
  106. package/dist/server/cache/index.d.ts.map +1 -1
  107. package/dist/server/cache/index.js +56 -3
  108. package/dist/server/cache/index.js.map +1 -1
  109. package/dist/server/compress/index.d.ts +6 -0
  110. package/dist/server/compress/index.d.ts.map +1 -1
  111. package/dist/server/compress/index.js +36 -1
  112. package/dist/server/compress/index.js.map +1 -1
  113. package/dist/server/cookies/index.d.ts +6 -6
  114. package/dist/server/cookies/index.d.ts.map +1 -1
  115. package/dist/server/cookies/index.js +3 -3
  116. package/dist/server/cookies/index.js.map +1 -1
  117. package/dist/server/core/index.browser.js +2 -2
  118. package/dist/server/core/index.browser.js.map +1 -1
  119. package/dist/server/core/index.d.ts +242 -150
  120. package/dist/server/core/index.d.ts.map +1 -1
  121. package/dist/server/core/index.js +294 -125
  122. package/dist/server/core/index.js.map +1 -1
  123. package/dist/server/cors/index.d.ts +11 -12
  124. package/dist/server/cors/index.d.ts.map +1 -1
  125. package/dist/server/health/index.d.ts +0 -1
  126. package/dist/server/health/index.d.ts.map +1 -1
  127. package/dist/server/helmet/index.d.ts +2 -2
  128. package/dist/server/helmet/index.d.ts.map +1 -1
  129. package/dist/server/links/index.browser.js.map +1 -1
  130. package/dist/server/links/index.d.ts +123 -124
  131. package/dist/server/links/index.d.ts.map +1 -1
  132. package/dist/server/links/index.js +1 -2
  133. package/dist/server/links/index.js.map +1 -1
  134. package/dist/server/metrics/index.d.ts.map +1 -1
  135. package/dist/server/multipart/index.d.ts +6 -6
  136. package/dist/server/multipart/index.d.ts.map +1 -1
  137. package/dist/server/proxy/index.d.ts +102 -103
  138. package/dist/server/proxy/index.d.ts.map +1 -1
  139. package/dist/server/rate-limit/index.d.ts +16 -16
  140. package/dist/server/rate-limit/index.d.ts.map +1 -1
  141. package/dist/server/static/index.d.ts +44 -44
  142. package/dist/server/static/index.d.ts.map +1 -1
  143. package/dist/server/static/index.js +4 -0
  144. package/dist/server/static/index.js.map +1 -1
  145. package/dist/server/swagger/index.d.ts +48 -49
  146. package/dist/server/swagger/index.d.ts.map +1 -1
  147. package/dist/server/swagger/index.js +3 -5
  148. package/dist/server/swagger/index.js.map +1 -1
  149. package/dist/sms/index.d.ts +13 -11
  150. package/dist/sms/index.d.ts.map +1 -1
  151. package/dist/sms/index.js +7 -7
  152. package/dist/sms/index.js.map +1 -1
  153. package/dist/thread/index.d.ts +71 -72
  154. package/dist/thread/index.d.ts.map +1 -1
  155. package/dist/topic/core/index.d.ts +318 -318
  156. package/dist/topic/core/index.d.ts.map +1 -1
  157. package/dist/topic/redis/index.d.ts +6 -6
  158. package/dist/topic/redis/index.d.ts.map +1 -1
  159. package/dist/vite/index.d.ts +5805 -249
  160. package/dist/vite/index.d.ts.map +1 -1
  161. package/dist/vite/index.js +599 -513
  162. package/dist/vite/index.js.map +1 -1
  163. package/dist/websocket/index.browser.js +6 -6
  164. package/dist/websocket/index.browser.js.map +1 -1
  165. package/dist/websocket/index.d.ts +247 -247
  166. package/dist/websocket/index.d.ts.map +1 -1
  167. package/dist/websocket/index.js +6 -6
  168. package/dist/websocket/index.js.map +1 -1
  169. package/package.json +9 -14
  170. package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
  171. package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
  172. package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
  173. package/src/api/users/entities/users.ts +1 -1
  174. package/src/api/users/index.ts +8 -8
  175. package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
  176. package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
  177. package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
  178. package/src/api/users/services/CredentialService.ts +7 -7
  179. package/src/api/users/services/IdentityService.ts +4 -4
  180. package/src/api/users/services/RegistrationService.spec.ts +25 -27
  181. package/src/api/users/services/RegistrationService.ts +38 -27
  182. package/src/api/users/services/SessionCrudService.ts +3 -3
  183. package/src/api/users/services/SessionService.spec.ts +3 -3
  184. package/src/api/users/services/SessionService.ts +28 -9
  185. package/src/api/users/services/UserService.ts +7 -7
  186. package/src/batch/providers/BatchProvider.ts +1 -2
  187. package/src/cli/apps/AlephaCli.ts +0 -2
  188. package/src/cli/apps/AlephaPackageBuilderCli.ts +38 -19
  189. package/src/cli/assets/apiHelloControllerTs.ts +18 -0
  190. package/src/cli/assets/apiIndexTs.ts +16 -0
  191. package/src/cli/assets/claudeMd.ts +303 -0
  192. package/src/cli/assets/mainBrowserTs.ts +2 -2
  193. package/src/cli/assets/mainServerTs.ts +24 -0
  194. package/src/cli/assets/webAppRouterTs.ts +15 -0
  195. package/src/cli/assets/webHelloComponentTsx.ts +16 -0
  196. package/src/cli/assets/webIndexTs.ts +16 -0
  197. package/src/cli/atoms/buildOptions.ts +88 -0
  198. package/src/cli/commands/build.ts +70 -87
  199. package/src/cli/commands/db.ts +21 -22
  200. package/src/cli/commands/deploy.ts +17 -5
  201. package/src/cli/commands/dev.ts +22 -14
  202. package/src/cli/commands/format.ts +8 -2
  203. package/src/cli/commands/gen/env.ts +53 -0
  204. package/src/cli/commands/gen/openapi.ts +1 -1
  205. package/src/cli/commands/gen/resource.ts +15 -0
  206. package/src/cli/commands/gen.ts +7 -1
  207. package/src/cli/commands/init.ts +74 -30
  208. package/src/cli/commands/lint.ts +8 -2
  209. package/src/cli/commands/test.ts +8 -3
  210. package/src/cli/commands/typecheck.ts +5 -1
  211. package/src/cli/commands/verify.ts +5 -3
  212. package/src/cli/defineConfig.ts +49 -7
  213. package/src/cli/index.ts +0 -1
  214. package/src/cli/services/AlephaCliUtils.ts +39 -589
  215. package/src/cli/services/PackageManagerUtils.ts +301 -0
  216. package/src/cli/services/ProjectScaffolder.ts +306 -0
  217. package/src/command/helpers/Runner.spec.ts +2 -2
  218. package/src/command/helpers/Runner.ts +16 -4
  219. package/src/command/primitives/$command.ts +0 -6
  220. package/src/command/providers/CliProvider.ts +1 -3
  221. package/src/core/Alepha.ts +42 -0
  222. package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
  223. package/src/core/index.shared.ts +1 -0
  224. package/src/core/index.ts +2 -0
  225. package/src/core/primitives/$hook.ts +6 -2
  226. package/src/core/primitives/$module.spec.ts +4 -0
  227. package/src/core/providers/AlsProvider.ts +1 -1
  228. package/src/core/providers/CodecManager.spec.ts +12 -6
  229. package/src/core/providers/CodecManager.ts +26 -6
  230. package/src/core/providers/EventManager.ts +169 -13
  231. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +621 -0
  232. package/src/core/providers/KeylessJsonSchemaCodec.ts +407 -0
  233. package/src/core/providers/StateManager.spec.ts +27 -16
  234. package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
  235. package/src/email/providers/LocalEmailProvider.ts +52 -15
  236. package/src/email/providers/NodemailerEmailProvider.ts +167 -56
  237. package/src/file/errors/FileError.ts +7 -0
  238. package/src/file/index.ts +9 -1
  239. package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
  240. package/src/logger/index.ts +15 -3
  241. package/src/mcp/transports/StdioMcpTransport.ts +1 -1
  242. package/src/orm/index.browser.ts +1 -19
  243. package/src/orm/index.bun.ts +77 -0
  244. package/src/orm/index.shared-server.ts +22 -0
  245. package/src/orm/index.shared.ts +15 -0
  246. package/src/orm/index.ts +13 -39
  247. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
  248. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  249. package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
  250. package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
  251. package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
  252. package/src/orm/services/Repository.ts +8 -0
  253. package/src/queue/core/providers/WorkerProvider.spec.ts +48 -32
  254. package/src/redis/index.bun.ts +35 -0
  255. package/src/redis/providers/BunRedisProvider.ts +12 -43
  256. package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
  257. package/src/redis/providers/NodeRedisProvider.ts +16 -34
  258. package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
  259. package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
  260. package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
  261. package/src/security/index.browser.ts +5 -0
  262. package/src/security/index.ts +90 -7
  263. package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
  264. package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
  265. package/src/security/primitives/$role.ts +5 -5
  266. package/src/security/primitives/$serviceAccount.spec.ts +5 -5
  267. package/src/security/primitives/$serviceAccount.ts +3 -3
  268. package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
  269. package/src/server/auth/primitives/$auth.ts +10 -10
  270. package/src/server/auth/primitives/$authCredentials.ts +3 -3
  271. package/src/server/auth/primitives/$authGithub.ts +3 -3
  272. package/src/server/auth/primitives/$authGoogle.ts +3 -3
  273. package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
  274. package/src/server/cache/providers/ServerCacheProvider.spec.ts +183 -0
  275. package/src/server/cache/providers/ServerCacheProvider.ts +95 -10
  276. package/src/server/compress/providers/ServerCompressProvider.ts +61 -2
  277. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
  278. package/src/server/core/helpers/ServerReply.ts +2 -2
  279. package/src/server/core/providers/NodeHttpServerProvider.ts +25 -6
  280. package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
  281. package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
  282. package/src/server/core/providers/ServerProvider.ts +155 -22
  283. package/src/server/core/providers/ServerRouterProvider.ts +259 -115
  284. package/src/server/core/providers/ServerTimingProvider.ts +2 -2
  285. package/src/server/links/index.ts +1 -1
  286. package/src/server/links/providers/LinkProvider.ts +1 -1
  287. package/src/server/static/providers/ServerStaticProvider.ts +10 -0
  288. package/src/server/swagger/index.ts +1 -1
  289. package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -8
  290. package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
  291. package/src/sms/providers/LocalSmsProvider.ts +8 -7
  292. package/src/vite/helpers/boot.ts +28 -17
  293. package/src/vite/helpers/importViteReact.ts +13 -0
  294. package/src/vite/index.ts +1 -21
  295. package/src/vite/plugins/viteAlephaDev.ts +16 -1
  296. package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
  297. package/src/vite/tasks/buildClient.ts +11 -0
  298. package/src/vite/tasks/buildServer.ts +59 -4
  299. package/src/vite/tasks/devServer.ts +71 -0
  300. package/src/vite/tasks/generateCloudflare.ts +7 -0
  301. package/src/vite/tasks/index.ts +2 -1
  302. package/dist/server/security/index.browser.js +0 -13
  303. package/dist/server/security/index.browser.js.map +0 -1
  304. package/dist/server/security/index.d.ts +0 -173
  305. package/dist/server/security/index.d.ts.map +0 -1
  306. package/dist/server/security/index.js +0 -311
  307. package/dist/server/security/index.js.map +0 -1
  308. package/src/cli/assets/appRouterTs.ts +0 -9
  309. package/src/cli/assets/mainTs.ts +0 -13
  310. package/src/cli/assets/viteConfigTs.ts +0 -14
  311. package/src/cli/commands/run.ts +0 -24
  312. package/src/server/security/index.browser.ts +0 -10
  313. package/src/server/security/index.ts +0 -94
  314. package/src/vite/plugins/viteAlepha.ts +0 -37
  315. package/src/vite/plugins/viteAlephaBuild.ts +0 -281
  316. /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
  317. /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
@@ -0,0 +1,301 @@
1
+ import { $inject, Alepha } from "alepha";
2
+ import type { RunnerMethod } from "alepha/command";
3
+ import { FileSystemProvider } from "alepha/file";
4
+ import { $logger } from "alepha/logger";
5
+ import { version } from "../version.ts";
6
+
7
+ /**
8
+ * Utility service for package manager operations.
9
+ *
10
+ * Handles detection, installation, and cleanup for:
11
+ * - Yarn
12
+ * - npm
13
+ * - pnpm
14
+ * - Bun
15
+ */
16
+ export class PackageManagerUtils {
17
+ protected readonly log = $logger();
18
+ protected readonly fs = $inject(FileSystemProvider);
19
+ protected readonly alepha = $inject(Alepha);
20
+
21
+ /**
22
+ * Detect the package manager used in the project.
23
+ */
24
+ public async getPackageManager(
25
+ root: string,
26
+ flags?: { yarn?: boolean; pnpm?: boolean; npm?: boolean; bun?: boolean },
27
+ ): Promise<"yarn" | "pnpm" | "npm" | "bun"> {
28
+ if (flags?.yarn) return "yarn";
29
+ if (flags?.pnpm) return "pnpm";
30
+ if (flags?.npm) return "npm";
31
+ if (flags?.bun) return "bun";
32
+ if (this.alepha.isBun()) return "bun";
33
+ if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
34
+ if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
35
+ if (await this.fs.exists(this.fs.join(root, "pnpm-lock.yaml")))
36
+ return "pnpm";
37
+ return "npm";
38
+ }
39
+
40
+ /**
41
+ * Get the install command for a package.
42
+ */
43
+ public async getInstallCommand(
44
+ root: string,
45
+ packageName: string,
46
+ dev = true,
47
+ ): Promise<string> {
48
+ const pm = await this.getPackageManager(root);
49
+ let cmd: string;
50
+
51
+ switch (pm) {
52
+ case "yarn":
53
+ cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`;
54
+ break;
55
+ case "pnpm":
56
+ cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`;
57
+ break;
58
+ case "bun":
59
+ cmd = `bun add ${dev ? "-d" : ""} ${packageName}`;
60
+ break;
61
+ default:
62
+ cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`;
63
+ }
64
+
65
+ return cmd.replace(/\s+/g, " ").trim();
66
+ }
67
+
68
+ /**
69
+ * Check if a dependency is installed in the project.
70
+ */
71
+ public async hasDependency(
72
+ root: string,
73
+ packageName: string,
74
+ ): Promise<boolean> {
75
+ try {
76
+ const pkg = await this.readPackageJson(root);
77
+ return !!(
78
+ pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]
79
+ );
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if Expo is present in the project.
87
+ */
88
+ public async hasExpo(root: string): Promise<boolean> {
89
+ return this.hasDependency(root, "expo");
90
+ }
91
+
92
+ /**
93
+ * Install a dependency if it's missing from the project.
94
+ */
95
+ public async ensureDependency(
96
+ root: string,
97
+ packageName: string,
98
+ options: {
99
+ dev?: boolean;
100
+ run?: RunnerMethod;
101
+ exec?: (
102
+ cmd: string,
103
+ opts?: { global?: boolean; root?: string },
104
+ ) => Promise<void>;
105
+ } = {},
106
+ ): Promise<void> {
107
+ const { dev = true } = options;
108
+
109
+ if (await this.hasDependency(root, packageName)) {
110
+ this.log.debug(`Dependency '${packageName}' is already installed`);
111
+ return;
112
+ }
113
+
114
+ const cmd = await this.getInstallCommand(root, packageName, dev);
115
+
116
+ if (options.run) {
117
+ await options.run(cmd, { alias: `installing ${packageName}`, root });
118
+ } else if (options.exec) {
119
+ this.log.debug(`Installing ${packageName}`);
120
+ await options.exec(cmd, { global: true, root });
121
+ }
122
+ }
123
+
124
+ // ===========================================
125
+ // Package Manager Setup & Cleanup
126
+ // ===========================================
127
+
128
+ public async ensureYarn(root: string): Promise<void> {
129
+ const yarnrcPath = this.fs.join(root, ".yarnrc.yml");
130
+ if (!(await this.fs.exists(yarnrcPath))) {
131
+ await this.fs.writeFile(yarnrcPath, "nodeLinker: node-modules");
132
+ }
133
+ await this.removeAllPmFilesExcept(root, "yarn");
134
+ }
135
+
136
+ public async ensureBun(root: string): Promise<void> {
137
+ await this.removeAllPmFilesExcept(root, "bun");
138
+ }
139
+
140
+ public async ensurePnpm(root: string): Promise<void> {
141
+ await this.removeAllPmFilesExcept(root, "pnpm");
142
+ }
143
+
144
+ public async ensureNpm(root: string): Promise<void> {
145
+ await this.removeAllPmFilesExcept(root, "npm");
146
+ }
147
+
148
+ public async removeAllPmFilesExcept(
149
+ root: string,
150
+ except: string,
151
+ ): Promise<void> {
152
+ if (except !== "yarn") await this.removeYarn(root);
153
+ if (except !== "pnpm") await this.removePnpm(root);
154
+ if (except !== "npm") await this.removeNpm(root);
155
+ if (except !== "bun") await this.removeBun(root);
156
+ }
157
+
158
+ public async removeYarn(root: string): Promise<void> {
159
+ await this.removeFiles(root, [".yarn", ".yarnrc.yml", "yarn.lock"]);
160
+ await this.editPackageJson(root, (pkg) => {
161
+ delete pkg.packageManager;
162
+ return pkg;
163
+ });
164
+ }
165
+
166
+ public async removePnpm(root: string): Promise<void> {
167
+ await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
168
+ await this.editPackageJson(root, (pkg) => {
169
+ delete pkg.packageManager;
170
+ return pkg;
171
+ });
172
+ }
173
+
174
+ public async removeNpm(root: string): Promise<void> {
175
+ await this.removeFiles(root, ["package-lock.json"]);
176
+ }
177
+
178
+ public async removeBun(root: string): Promise<void> {
179
+ await this.removeFiles(root, ["bun.lockb", "bun.lock"]);
180
+ }
181
+
182
+ // ===========================================
183
+ // Package.json utilities
184
+ // ===========================================
185
+
186
+ public async readPackageJson(root: string): Promise<Record<string, any>> {
187
+ const content = await this.fs
188
+ .createFile({ path: this.fs.join(root, "package.json") })
189
+ .text();
190
+ return JSON.parse(content);
191
+ }
192
+
193
+ public async writePackageJson(
194
+ root: string,
195
+ content: Record<string, any>,
196
+ ): Promise<void> {
197
+ await this.fs.writeFile(
198
+ this.fs.join(root, "package.json"),
199
+ JSON.stringify(content, null, 2),
200
+ );
201
+ }
202
+
203
+ public async editPackageJson(
204
+ root: string,
205
+ editFn: (pkg: Record<string, any>) => Record<string, any>,
206
+ ): Promise<void> {
207
+ try {
208
+ const pkg = await this.readPackageJson(root);
209
+ const updated = editFn(pkg);
210
+ await this.writePackageJson(root, updated);
211
+ } catch {
212
+ // package.json doesn't exist, skip
213
+ }
214
+ }
215
+
216
+ public async ensurePackageJson(
217
+ root: string,
218
+ modes: DependencyModes,
219
+ ): Promise<Record<string, any>> {
220
+ const packageJsonPath = this.fs.join(root, "package.json");
221
+
222
+ if (!(await this.fs.exists(packageJsonPath))) {
223
+ const content = this.generatePackageJsonContent(modes);
224
+ await this.writePackageJson(root, content);
225
+ return content;
226
+ }
227
+
228
+ const packageJson = await this.readPackageJson(root);
229
+ const newContent = this.generatePackageJsonContent(modes);
230
+
231
+ packageJson.type = "module";
232
+ packageJson.dependencies ??= {};
233
+ packageJson.devDependencies ??= {};
234
+ packageJson.scripts ??= {};
235
+
236
+ Object.assign(packageJson.dependencies, newContent.dependencies);
237
+ Object.assign(packageJson.devDependencies, newContent.devDependencies);
238
+ Object.assign(packageJson.scripts, newContent.scripts);
239
+
240
+ await this.writePackageJson(root, packageJson);
241
+ return packageJson;
242
+ }
243
+
244
+ public generatePackageJsonContent(modes: DependencyModes): {
245
+ dependencies: Record<string, string>;
246
+ devDependencies: Record<string, string>;
247
+ scripts: Record<string, string>;
248
+ type: "module";
249
+ } {
250
+ const dependencies: Record<string, string> = {
251
+ alepha: `^${version}`,
252
+ };
253
+
254
+ const devDependencies: Record<string, string> = {};
255
+
256
+ const scripts: Record<string, string> = {
257
+ dev: "alepha dev",
258
+ build: "alepha build",
259
+ lint: "alepha lint",
260
+ typecheck: "alepha typecheck",
261
+ verify: "alepha verify",
262
+ };
263
+
264
+ if (modes.admin) {
265
+ dependencies["@alepha/ui"] = `^${version}`;
266
+ modes.web = true;
267
+ }
268
+
269
+ if (modes.web) {
270
+ dependencies["@alepha/react"] = `^${version}`;
271
+ dependencies.react = "^19.2.0";
272
+ dependencies["react-dom"] = "^19.2.0";
273
+ devDependencies["@types/react"] = "^19.2.0";
274
+ }
275
+
276
+ return {
277
+ type: "module",
278
+ dependencies,
279
+ devDependencies,
280
+ scripts,
281
+ };
282
+ }
283
+
284
+ // ===========================================
285
+ // Helper methods
286
+ // ===========================================
287
+
288
+ protected async removeFiles(root: string, files: string[]): Promise<void> {
289
+ await Promise.all(
290
+ files.map((file) =>
291
+ this.fs.rm(this.fs.join(root, file), { force: true, recursive: true }),
292
+ ),
293
+ );
294
+ }
295
+ }
296
+
297
+ export interface DependencyModes {
298
+ web?: boolean;
299
+ admin?: boolean;
300
+ expo?: boolean;
301
+ }
@@ -0,0 +1,306 @@
1
+ import { basename, dirname } from "node:path";
2
+ import { $inject } from "alepha";
3
+ import { FileSystemProvider } from "alepha/file";
4
+ import { $logger } from "alepha/logger";
5
+ import { apiHelloControllerTs } from "../assets/apiHelloControllerTs.ts";
6
+ import { apiIndexTs } from "../assets/apiIndexTs.ts";
7
+ import { biomeJson } from "../assets/biomeJson.ts";
8
+ import { type ClaudeMdOptions, claudeMd } from "../assets/claudeMd.ts";
9
+ import { dummySpecTs } from "../assets/dummySpecTs.ts";
10
+ import { editorconfig } from "../assets/editorconfig.ts";
11
+ import { indexHtml } from "../assets/indexHtml.ts";
12
+ import { mainBrowserTs } from "../assets/mainBrowserTs.ts";
13
+ import { mainServerTs } from "../assets/mainServerTs.ts";
14
+ import { tsconfigJson } from "../assets/tsconfigJson.ts";
15
+ import { webAppRouterTs } from "../assets/webAppRouterTs.ts";
16
+ import { webHelloComponentTsx } from "../assets/webHelloComponentTsx.ts";
17
+ import { webIndexTs } from "../assets/webIndexTs.ts";
18
+ import {
19
+ type DependencyModes,
20
+ PackageManagerUtils,
21
+ } from "./PackageManagerUtils.ts";
22
+
23
+ /**
24
+ * Service for scaffolding new Alepha projects.
25
+ *
26
+ * Handles creation of:
27
+ * - Project structure (src/api, src/web)
28
+ * - Configuration files (tsconfig, biome, editorconfig)
29
+ * - Entry points (main.server.ts, main.browser.ts)
30
+ * - Example code (HelloController, Hello component)
31
+ */
32
+ export class ProjectScaffolder {
33
+ protected readonly log = $logger();
34
+ protected readonly fs = $inject(FileSystemProvider);
35
+ protected readonly pm = $inject(PackageManagerUtils);
36
+
37
+ /**
38
+ * Get the app name from the directory name.
39
+ *
40
+ * Converts the directory name to a valid module name:
41
+ * - Converts to lowercase
42
+ * - Replaces spaces, dashes, underscores with nothing
43
+ * - Falls back to "app" if empty
44
+ */
45
+ public getAppName(root: string): string {
46
+ const dirName = basename(root);
47
+ const appName = dirName.toLowerCase().replace(/[\s\-_]/g, "");
48
+ return appName || "app";
49
+ }
50
+
51
+ /**
52
+ * Ensure all configuration files exist.
53
+ */
54
+ public async ensureConfig(
55
+ root: string,
56
+ opts: {
57
+ packageJson?: boolean | DependencyModes;
58
+ tsconfigJson?: boolean;
59
+ indexHtml?: boolean;
60
+ biomeJson?: boolean;
61
+ editorconfig?: boolean;
62
+ claudeMd?: boolean | ClaudeMdOptions;
63
+ },
64
+ ): Promise<void> {
65
+ const tasks: Promise<void>[] = [];
66
+
67
+ if (opts.packageJson) {
68
+ tasks.push(
69
+ this.pm
70
+ .ensurePackageJson(
71
+ root,
72
+ typeof opts.packageJson === "boolean" ? {} : opts.packageJson,
73
+ )
74
+ .then(() => {}),
75
+ );
76
+ }
77
+ if (opts.tsconfigJson) {
78
+ tasks.push(this.ensureTsConfig(root));
79
+ }
80
+ if (opts.indexHtml) {
81
+ tasks.push(this.ensureReactProject(root));
82
+ }
83
+ if (opts.biomeJson) {
84
+ tasks.push(this.ensureBiomeConfig(root));
85
+ }
86
+ if (opts.editorconfig) {
87
+ tasks.push(this.ensureEditorConfig(root));
88
+ }
89
+ if (opts.claudeMd) {
90
+ tasks.push(
91
+ this.ensureClaudeMd(
92
+ root,
93
+ typeof opts.claudeMd === "boolean" ? {} : opts.claudeMd,
94
+ ),
95
+ );
96
+ }
97
+
98
+ await Promise.all(tasks);
99
+ }
100
+
101
+ // ===========================================
102
+ // Config Files
103
+ // ===========================================
104
+
105
+ public async ensureTsConfig(root: string): Promise<void> {
106
+ // Check if tsconfig.json exists in current or parent directories
107
+ if (await this.existsInParents(root, "tsconfig.json")) {
108
+ return;
109
+ }
110
+ await this.fs.writeFile(this.fs.join(root, "tsconfig.json"), tsconfigJson);
111
+ }
112
+
113
+ public async ensureBiomeConfig(root: string): Promise<void> {
114
+ await this.ensureFileIfNotExists(root, "biome.json", biomeJson);
115
+ }
116
+
117
+ public async ensureEditorConfig(root: string): Promise<void> {
118
+ await this.ensureFileIfNotExists(root, ".editorconfig", editorconfig);
119
+ }
120
+
121
+ public async ensureClaudeMd(
122
+ root: string,
123
+ options: ClaudeMdOptions = {},
124
+ ): Promise<void> {
125
+ const path = this.fs.join(root, "CLAUDE.md");
126
+ if (!(await this.fs.exists(path))) {
127
+ await this.fs.writeFile(path, claudeMd(options));
128
+ }
129
+ }
130
+
131
+ // ===========================================
132
+ // API Project Structure
133
+ // ===========================================
134
+
135
+ /**
136
+ * Ensure src/main.server.ts exists with full API structure.
137
+ *
138
+ * Creates:
139
+ * - src/main.server.ts (entry point)
140
+ * - src/api/index.ts (API module)
141
+ * - src/api/controllers/HelloController.ts (example controller)
142
+ */
143
+ public async ensureApiProject(root: string): Promise<void> {
144
+ const srcDir = this.fs.join(root, "src");
145
+
146
+ // Don't overwrite existing content
147
+ if (await this.fs.exists(srcDir)) {
148
+ const files = await this.fs.ls(srcDir);
149
+ if (files.length > 0) return;
150
+ }
151
+
152
+ const appName = this.getAppName(root);
153
+
154
+ // Create directories
155
+ await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), {
156
+ recursive: true,
157
+ });
158
+
159
+ // Create files
160
+ await this.fs.writeFile(
161
+ this.fs.join(srcDir, "main.server.ts"),
162
+ mainServerTs(),
163
+ );
164
+ await this.fs.writeFile(
165
+ this.fs.join(srcDir, "api/index.ts"),
166
+ apiIndexTs({ appName }),
167
+ );
168
+ await this.fs.writeFile(
169
+ this.fs.join(srcDir, "api/controllers/HelloController.ts"),
170
+ apiHelloControllerTs(),
171
+ );
172
+ }
173
+
174
+ // ===========================================
175
+ // React Project Structure
176
+ // ===========================================
177
+
178
+ /**
179
+ * Ensure full React project structure exists.
180
+ *
181
+ * Creates:
182
+ * - index.html
183
+ * - src/main.server.ts, src/main.browser.ts
184
+ * - src/api/index.ts, src/api/controllers/HelloController.ts
185
+ * - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
186
+ */
187
+ public async ensureReactProject(root: string): Promise<void> {
188
+ if (await this.fs.exists(this.fs.join(root, "index.html"))) {
189
+ return;
190
+ }
191
+
192
+ const appName = this.getAppName(root);
193
+
194
+ // Create directories
195
+ await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), {
196
+ recursive: true,
197
+ });
198
+ await this.fs.mkdir(this.fs.join(root, "src/web/components"), {
199
+ recursive: true,
200
+ });
201
+
202
+ // index.html
203
+ await this.fs.writeFile(
204
+ this.fs.join(root, "index.html"),
205
+ indexHtml("src/main.browser.ts"),
206
+ );
207
+
208
+ // API structure
209
+ await this.ensureFileIfNotExists(
210
+ root,
211
+ "src/api/index.ts",
212
+ apiIndexTs({ appName }),
213
+ );
214
+ await this.ensureFileIfNotExists(
215
+ root,
216
+ "src/api/controllers/HelloController.ts",
217
+ apiHelloControllerTs(),
218
+ );
219
+ await this.ensureFileIfNotExists(
220
+ root,
221
+ "src/main.server.ts",
222
+ mainServerTs({ react: true }),
223
+ );
224
+
225
+ // Web structure
226
+ await this.ensureFileIfNotExists(
227
+ root,
228
+ "src/web/index.ts",
229
+ webIndexTs({ appName }),
230
+ );
231
+ await this.ensureFileIfNotExists(
232
+ root,
233
+ "src/web/AppRouter.ts",
234
+ webAppRouterTs(),
235
+ );
236
+ await this.ensureFileIfNotExists(
237
+ root,
238
+ "src/web/components/Hello.tsx",
239
+ webHelloComponentTsx(),
240
+ );
241
+ await this.ensureFileIfNotExists(
242
+ root,
243
+ "src/main.browser.ts",
244
+ mainBrowserTs(),
245
+ );
246
+ }
247
+
248
+ // ===========================================
249
+ // Test Directory
250
+ // ===========================================
251
+
252
+ /**
253
+ * Ensure test directory exists with a dummy test file.
254
+ */
255
+ public async ensureTestDir(root: string): Promise<void> {
256
+ const testDir = this.fs.join(root, "test");
257
+ const dummyPath = this.fs.join(testDir, "dummy.spec.ts");
258
+
259
+ if (!(await this.fs.exists(testDir))) {
260
+ await this.fs.mkdir(testDir, { recursive: true });
261
+ await this.fs.writeFile(dummyPath, dummySpecTs());
262
+ return;
263
+ }
264
+
265
+ const files = await this.fs.ls(testDir);
266
+ if (files.length === 0) {
267
+ await this.fs.writeFile(dummyPath, dummySpecTs());
268
+ }
269
+ }
270
+
271
+ // ===========================================
272
+ // Helpers
273
+ // ===========================================
274
+
275
+ protected async ensureFileIfNotExists(
276
+ root: string,
277
+ relativePath: string,
278
+ content: string,
279
+ ): Promise<void> {
280
+ const fullPath = this.fs.join(root, relativePath);
281
+ if (!(await this.fs.exists(fullPath))) {
282
+ await this.fs.writeFile(fullPath, content);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Check if a file exists in the given directory or any parent directory.
288
+ */
289
+ protected async existsInParents(
290
+ root: string,
291
+ filename: string,
292
+ ): Promise<boolean> {
293
+ let current = root;
294
+ while (true) {
295
+ if (await this.fs.exists(this.fs.join(current, filename))) {
296
+ return true;
297
+ }
298
+ const parent = dirname(current);
299
+ if (parent === current) {
300
+ // Reached filesystem root
301
+ return false;
302
+ }
303
+ current = parent;
304
+ }
305
+ }
306
+ }
@@ -109,7 +109,7 @@ describe("Runner", () => {
109
109
  await runner.run(`echo "Task 1"`);
110
110
  await runner.run("A slightly longer task name", () => {});
111
111
 
112
- runner.summary();
112
+ runner.end();
113
113
 
114
114
  const logs = mockLogger.logs
115
115
  .slice(4)
@@ -118,7 +118,7 @@ describe("Runner", () => {
118
118
  });
119
119
 
120
120
  test("summary() should not print a table if no tasks were run", () => {
121
- runner.summary();
121
+ runner.end();
122
122
 
123
123
  const logs = mockLogger.logs.map((l) => l.message);
124
124
  expect(logs.length).toBe(0);
@@ -20,6 +20,11 @@ export interface RunOptions {
20
20
  * Rename the command for logging purposes.
21
21
  */
22
22
  alias?: string;
23
+
24
+ /**
25
+ * Root directory to execute the command in.
26
+ */
27
+ root?: string;
23
28
  }
24
29
 
25
30
  export interface RunnerMethod {
@@ -73,11 +78,14 @@ export class Runner {
73
78
 
74
79
  this.firstTaskStarted = true;
75
80
 
81
+ const root =
82
+ typeof options === "object" && options.root ? options.root : undefined;
83
+
76
84
  if (Array.isArray(cmd)) {
77
85
  return await this.execute(
78
86
  cmd.map((it) =>
79
87
  typeof it === "string"
80
- ? { name: it, handler: () => this.exec(it) }
88
+ ? { name: it, handler: () => this.exec(it, { root }) }
81
89
  : it,
82
90
  ),
83
91
  );
@@ -89,7 +97,7 @@ export class Runner {
89
97
  typeof options === "function"
90
98
  ? options
91
99
  : typeof cmd === "string"
92
- ? () => this.exec(cmd)
100
+ ? () => this.exec(cmd, { root })
93
101
  : cmd.handler;
94
102
 
95
103
  return await this.execute({
@@ -140,11 +148,15 @@ export class Runner {
140
148
  return runFn;
141
149
  }
142
150
 
143
- protected async exec(cmd: string): Promise<string> {
151
+ protected async exec(
152
+ cmd: string,
153
+ opts: { root?: string } = {},
154
+ ): Promise<string> {
144
155
  return await new Promise<string>((resolve, reject) => {
145
156
  exec(
146
157
  cmd,
147
158
  {
159
+ cwd: opts.root,
148
160
  env: {
149
161
  ...process.env,
150
162
  LOG_FORMAT: "pretty",
@@ -179,7 +191,7 @@ export class Runner {
179
191
  /**
180
192
  * Prints a summary of all executed tasks and their durations.
181
193
  */
182
- public summary(): void {
194
+ public end(): void {
183
195
  if (this.useDynamicLogger && this.firstTaskStarted) {
184
196
  this.prettyPrint.endCommand();
185
197
  return;
@@ -105,12 +105,6 @@ export interface CommandPrimitiveOptions<
105
105
  */
106
106
  args?: A;
107
107
 
108
- /**
109
- * If false, skip summary message at the end of the command execution.
110
- * Summary will display only if ({ run }) method calls were made.
111
- */
112
- summary?: boolean;
113
-
114
108
  /**
115
109
  * Marks this command as the root command.
116
110
  * Equivalent to setting name to an empty string "".
@@ -237,9 +237,7 @@ export class CliProvider {
237
237
  await hook.options.handler(args as CommandHandlerArgs<TObject>);
238
238
  }
239
239
 
240
- if (command.options.summary !== false) {
241
- runner.summary();
242
- }
240
+ runner.end();
243
241
 
244
242
  this.log.debug(`Command '${command.name}' executed successfully.`);
245
243
  });