alepha 0.14.4 → 0.15.1

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 (322) hide show
  1. package/README.md +44 -102
  2. package/dist/api/audits/index.d.ts +331 -443
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +2 -2
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +0 -113
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -3
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +151 -262
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/notifications/index.browser.js +4 -4
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +164 -276
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/notifications/index.js +4 -4
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +265 -377
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.browser.js +1 -2
  21. package/dist/api/users/index.browser.js.map +1 -1
  22. package/dist/api/users/index.d.ts +195 -301
  23. package/dist/api/users/index.d.ts.map +1 -1
  24. package/dist/api/users/index.js +203 -184
  25. package/dist/api/users/index.js.map +1 -1
  26. package/dist/api/verifications/index.d.ts.map +1 -1
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/batch/index.js +1 -2
  29. package/dist/batch/index.js.map +1 -1
  30. package/dist/bucket/index.d.ts.map +1 -1
  31. package/dist/cache/core/index.d.ts.map +1 -1
  32. package/dist/cache/redis/index.d.ts.map +1 -1
  33. package/dist/cache/redis/index.js +2 -2
  34. package/dist/cache/redis/index.js.map +1 -1
  35. package/dist/cli/index.d.ts +5900 -165
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +1481 -639
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +8 -4
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +29 -25
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +563 -54
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +175 -8
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +564 -54
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +563 -54
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts.map +1 -1
  52. package/dist/datetime/index.js +4 -4
  53. package/dist/datetime/index.js.map +1 -1
  54. package/dist/email/index.d.ts +89 -42
  55. package/dist/email/index.d.ts.map +1 -1
  56. package/dist/email/index.js +129 -33
  57. package/dist/email/index.js.map +1 -1
  58. package/dist/fake/index.d.ts +7969 -2
  59. package/dist/fake/index.d.ts.map +1 -1
  60. package/dist/fake/index.js +22 -22
  61. package/dist/fake/index.js.map +1 -1
  62. package/dist/file/index.d.ts +134 -1
  63. package/dist/file/index.d.ts.map +1 -1
  64. package/dist/file/index.js +253 -1
  65. package/dist/file/index.js.map +1 -1
  66. package/dist/lock/core/index.d.ts.map +1 -1
  67. package/dist/lock/redis/index.d.ts.map +1 -1
  68. package/dist/logger/index.d.ts +1 -2
  69. package/dist/logger/index.d.ts.map +1 -1
  70. package/dist/logger/index.js +1 -5
  71. package/dist/logger/index.js.map +1 -1
  72. package/dist/mcp/index.d.ts +19 -1
  73. package/dist/mcp/index.d.ts.map +1 -1
  74. package/dist/mcp/index.js +28 -4
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/orm/chunk-DH6iiROE.js +38 -0
  77. package/dist/orm/index.browser.js +9 -9
  78. package/dist/orm/index.browser.js.map +1 -1
  79. package/dist/orm/index.bun.js +2821 -0
  80. package/dist/orm/index.bun.js.map +1 -0
  81. package/dist/orm/index.d.ts +318 -169
  82. package/dist/orm/index.d.ts.map +1 -1
  83. package/dist/orm/index.js +2086 -1776
  84. package/dist/orm/index.js.map +1 -1
  85. package/dist/queue/core/index.d.ts +4 -4
  86. package/dist/queue/core/index.d.ts.map +1 -1
  87. package/dist/queue/redis/index.d.ts.map +1 -1
  88. package/dist/redis/index.bun.js +285 -0
  89. package/dist/redis/index.bun.js.map +1 -0
  90. package/dist/redis/index.d.ts +13 -31
  91. package/dist/redis/index.d.ts.map +1 -1
  92. package/dist/redis/index.js +18 -38
  93. package/dist/redis/index.js.map +1 -1
  94. package/dist/retry/index.d.ts.map +1 -1
  95. package/dist/router/index.d.ts.map +1 -1
  96. package/dist/scheduler/index.d.ts +83 -1
  97. package/dist/scheduler/index.d.ts.map +1 -1
  98. package/dist/scheduler/index.js +393 -1
  99. package/dist/scheduler/index.js.map +1 -1
  100. package/dist/security/index.browser.js +5 -1
  101. package/dist/security/index.browser.js.map +1 -1
  102. package/dist/security/index.d.ts +598 -112
  103. package/dist/security/index.d.ts.map +1 -1
  104. package/dist/security/index.js +1808 -97
  105. package/dist/security/index.js.map +1 -1
  106. package/dist/server/auth/index.d.ts +1200 -175
  107. package/dist/server/auth/index.d.ts.map +1 -1
  108. package/dist/server/auth/index.js +1268 -37
  109. package/dist/server/auth/index.js.map +1 -1
  110. package/dist/server/cache/index.d.ts +6 -3
  111. package/dist/server/cache/index.d.ts.map +1 -1
  112. package/dist/server/cache/index.js +1 -1
  113. package/dist/server/cache/index.js.map +1 -1
  114. package/dist/server/compress/index.d.ts.map +1 -1
  115. package/dist/server/cookies/index.d.ts.map +1 -1
  116. package/dist/server/cookies/index.js +3 -3
  117. package/dist/server/cookies/index.js.map +1 -1
  118. package/dist/server/core/index.d.ts +115 -13
  119. package/dist/server/core/index.d.ts.map +1 -1
  120. package/dist/server/core/index.js +321 -139
  121. package/dist/server/core/index.js.map +1 -1
  122. package/dist/server/cors/index.d.ts +0 -1
  123. package/dist/server/cors/index.d.ts.map +1 -1
  124. package/dist/server/health/index.d.ts +0 -1
  125. package/dist/server/health/index.d.ts.map +1 -1
  126. package/dist/server/helmet/index.d.ts.map +1 -1
  127. package/dist/server/links/index.browser.js +9 -1
  128. package/dist/server/links/index.browser.js.map +1 -1
  129. package/dist/server/links/index.d.ts +1 -2
  130. package/dist/server/links/index.d.ts.map +1 -1
  131. package/dist/server/links/index.js +14 -7
  132. package/dist/server/links/index.js.map +1 -1
  133. package/dist/server/metrics/index.d.ts +514 -1
  134. package/dist/server/metrics/index.d.ts.map +1 -1
  135. package/dist/server/metrics/index.js +4462 -4
  136. package/dist/server/metrics/index.js.map +1 -1
  137. package/dist/server/multipart/index.d.ts.map +1 -1
  138. package/dist/server/proxy/index.d.ts +0 -1
  139. package/dist/server/proxy/index.d.ts.map +1 -1
  140. package/dist/server/rate-limit/index.d.ts.map +1 -1
  141. package/dist/server/static/index.d.ts.map +1 -1
  142. package/dist/server/swagger/index.d.ts +1 -2
  143. package/dist/server/swagger/index.d.ts.map +1 -1
  144. package/dist/server/swagger/index.js +1 -2
  145. package/dist/server/swagger/index.js.map +1 -1
  146. package/dist/sms/index.d.ts +3 -1
  147. package/dist/sms/index.d.ts.map +1 -1
  148. package/dist/sms/index.js +10 -10
  149. package/dist/sms/index.js.map +1 -1
  150. package/dist/thread/index.d.ts +0 -1
  151. package/dist/thread/index.d.ts.map +1 -1
  152. package/dist/thread/index.js +2 -2
  153. package/dist/thread/index.js.map +1 -1
  154. package/dist/topic/core/index.d.ts.map +1 -1
  155. package/dist/topic/redis/index.d.ts.map +1 -1
  156. package/dist/vite/index.d.ts +6315 -149
  157. package/dist/vite/index.d.ts.map +1 -1
  158. package/dist/vite/index.js +140 -469
  159. package/dist/vite/index.js.map +1 -1
  160. package/dist/websocket/index.browser.js +9 -9
  161. package/dist/websocket/index.browser.js.map +1 -1
  162. package/dist/websocket/index.d.ts +28 -28
  163. package/dist/websocket/index.d.ts.map +1 -1
  164. package/dist/websocket/index.js +9 -9
  165. package/dist/websocket/index.js.map +1 -1
  166. package/package.json +13 -18
  167. package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
  168. package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
  169. package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
  170. package/src/api/users/entities/users.ts +1 -1
  171. package/src/api/users/index.ts +8 -8
  172. package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
  173. package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
  174. package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
  175. package/src/api/users/services/CredentialService.ts +7 -7
  176. package/src/api/users/services/IdentityService.ts +4 -4
  177. package/src/api/users/services/RegistrationService.spec.ts +25 -27
  178. package/src/api/users/services/RegistrationService.ts +38 -27
  179. package/src/api/users/services/SessionCrudService.ts +3 -3
  180. package/src/api/users/services/SessionService.spec.ts +3 -3
  181. package/src/api/users/services/SessionService.ts +27 -18
  182. package/src/api/users/services/UserService.ts +7 -7
  183. package/src/batch/providers/BatchProvider.ts +1 -2
  184. package/src/cli/apps/AlephaCli.ts +2 -2
  185. package/src/cli/apps/AlephaPackageBuilderCli.ts +47 -20
  186. package/src/cli/assets/apiHelloControllerTs.ts +19 -0
  187. package/src/cli/assets/apiIndexTs.ts +16 -0
  188. package/src/cli/assets/biomeJson.ts +2 -1
  189. package/src/cli/assets/claudeMd.ts +308 -0
  190. package/src/cli/assets/dummySpecTs.ts +2 -1
  191. package/src/cli/assets/editorconfig.ts +2 -1
  192. package/src/cli/assets/mainBrowserTs.ts +4 -3
  193. package/src/cli/assets/mainCss.ts +24 -0
  194. package/src/cli/assets/mainServerTs.ts +24 -0
  195. package/src/cli/assets/tsconfigJson.ts +2 -1
  196. package/src/cli/assets/webAppRouterTs.ts +16 -0
  197. package/src/cli/assets/webHelloComponentTsx.ts +20 -0
  198. package/src/cli/assets/webIndexTs.ts +16 -0
  199. package/src/cli/atoms/appEntryOptions.ts +13 -0
  200. package/src/cli/atoms/buildOptions.ts +1 -1
  201. package/src/cli/atoms/changelogOptions.ts +1 -1
  202. package/src/cli/commands/build.ts +97 -61
  203. package/src/cli/commands/db.ts +21 -18
  204. package/src/cli/commands/deploy.ts +17 -5
  205. package/src/cli/commands/dev.ts +26 -47
  206. package/src/cli/commands/gen/env.ts +1 -1
  207. package/src/cli/commands/init.ts +79 -25
  208. package/src/cli/commands/lint.ts +9 -3
  209. package/src/cli/commands/test.ts +8 -2
  210. package/src/cli/commands/typecheck.ts +5 -1
  211. package/src/cli/commands/verify.ts +4 -2
  212. package/src/cli/defineConfig.ts +9 -0
  213. package/src/cli/index.ts +2 -1
  214. package/src/cli/providers/AppEntryProvider.ts +131 -0
  215. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  216. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  217. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  218. package/src/cli/services/AlephaCliUtils.ts +72 -602
  219. package/src/cli/services/PackageManagerUtils.ts +308 -0
  220. package/src/cli/services/ProjectScaffolder.ts +329 -0
  221. package/src/command/helpers/Runner.ts +15 -3
  222. package/src/core/Alepha.ts +2 -8
  223. package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
  224. package/src/core/index.shared.ts +1 -0
  225. package/src/core/index.ts +2 -0
  226. package/src/core/primitives/$hook.ts +6 -2
  227. package/src/core/primitives/$module.spec.ts +4 -0
  228. package/src/core/primitives/$module.ts +12 -0
  229. package/src/core/providers/AlsProvider.ts +1 -1
  230. package/src/core/providers/CodecManager.spec.ts +12 -6
  231. package/src/core/providers/CodecManager.ts +26 -6
  232. package/src/core/providers/EventManager.ts +169 -13
  233. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +878 -0
  234. package/src/core/providers/KeylessJsonSchemaCodec.ts +789 -0
  235. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  236. package/src/core/providers/StateManager.spec.ts +27 -16
  237. package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
  238. package/src/email/providers/LocalEmailProvider.ts +52 -15
  239. package/src/email/providers/NodemailerEmailProvider.ts +167 -56
  240. package/src/file/errors/FileError.ts +7 -0
  241. package/src/file/index.ts +9 -1
  242. package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
  243. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  244. package/src/mcp/errors/McpError.ts +30 -0
  245. package/src/mcp/index.ts +3 -0
  246. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  247. package/src/orm/index.browser.ts +1 -19
  248. package/src/orm/index.bun.ts +77 -0
  249. package/src/orm/index.shared-server.ts +22 -0
  250. package/src/orm/index.shared.ts +15 -0
  251. package/src/orm/index.ts +19 -39
  252. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  253. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
  254. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  255. package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
  256. package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
  257. package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
  258. package/src/orm/services/Repository.ts +19 -0
  259. package/src/redis/index.bun.ts +35 -0
  260. package/src/redis/providers/BunRedisProvider.ts +12 -43
  261. package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
  262. package/src/redis/providers/NodeRedisProvider.ts +16 -34
  263. package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
  264. package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
  265. package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
  266. package/src/security/index.browser.ts +5 -0
  267. package/src/security/index.ts +90 -7
  268. package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
  269. package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
  270. package/src/security/primitives/$role.ts +5 -5
  271. package/src/security/primitives/$serviceAccount.spec.ts +5 -5
  272. package/src/security/primitives/$serviceAccount.ts +3 -3
  273. package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
  274. package/src/server/auth/primitives/$auth.ts +10 -10
  275. package/src/server/auth/primitives/$authCredentials.ts +3 -3
  276. package/src/server/auth/primitives/$authGithub.ts +3 -3
  277. package/src/server/auth/primitives/$authGoogle.ts +3 -3
  278. package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
  279. package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
  280. package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
  281. package/src/server/core/index.ts +1 -1
  282. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  283. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  284. package/src/server/core/providers/NodeHttpServerProvider.ts +92 -24
  285. package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
  286. package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
  287. package/src/server/core/providers/ServerProvider.ts +144 -24
  288. package/src/server/core/providers/ServerRouterProvider.ts +259 -115
  289. package/src/server/core/providers/ServerTimingProvider.ts +2 -2
  290. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  291. package/src/server/links/index.browser.ts +2 -0
  292. package/src/server/links/index.ts +3 -1
  293. package/src/server/links/providers/LinkProvider.ts +1 -1
  294. package/src/server/swagger/index.ts +1 -1
  295. package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
  296. package/src/sms/providers/LocalSmsProvider.ts +8 -7
  297. package/src/vite/index.ts +3 -2
  298. package/src/vite/tasks/buildClient.ts +0 -1
  299. package/src/vite/tasks/buildServer.ts +80 -22
  300. package/src/vite/tasks/copyAssets.ts +5 -4
  301. package/src/vite/tasks/generateCloudflare.ts +7 -0
  302. package/src/vite/tasks/generateSitemap.ts +64 -23
  303. package/src/vite/tasks/index.ts +0 -2
  304. package/src/vite/tasks/prerenderPages.ts +49 -24
  305. package/dist/server/security/index.browser.js +0 -13
  306. package/dist/server/security/index.browser.js.map +0 -1
  307. package/dist/server/security/index.d.ts +0 -173
  308. package/dist/server/security/index.d.ts.map +0 -1
  309. package/dist/server/security/index.js +0 -311
  310. package/dist/server/security/index.js.map +0 -1
  311. package/src/cli/assets/appRouterTs.ts +0 -9
  312. package/src/cli/assets/indexHtml.ts +0 -15
  313. package/src/cli/assets/mainTs.ts +0 -13
  314. package/src/cli/commands/format.ts +0 -17
  315. package/src/server/security/index.browser.ts +0 -10
  316. package/src/server/security/index.ts +0 -94
  317. package/src/vite/helpers/boot.ts +0 -106
  318. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  319. package/src/vite/tasks/devServer.ts +0 -69
  320. package/src/vite/tasks/runAlepha.ts +0 -270
  321. /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
  322. /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
@@ -0,0 +1,308 @@
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
+ * Check if React is present in the project.
94
+ */
95
+ public async hasReact(root: string): Promise<boolean> {
96
+ return this.hasDependency(root, "react");
97
+ }
98
+
99
+ /**
100
+ * Install a dependency if it's missing from the project.
101
+ */
102
+ public async ensureDependency(
103
+ root: string,
104
+ packageName: string,
105
+ options: {
106
+ dev?: boolean;
107
+ run?: RunnerMethod;
108
+ exec?: (
109
+ cmd: string,
110
+ opts?: { global?: boolean; root?: string },
111
+ ) => Promise<void>;
112
+ } = {},
113
+ ): Promise<void> {
114
+ const { dev = true } = options;
115
+
116
+ if (await this.hasDependency(root, packageName)) {
117
+ this.log.debug(`Dependency '${packageName}' is already installed`);
118
+ return;
119
+ }
120
+
121
+ const cmd = await this.getInstallCommand(root, packageName, dev);
122
+
123
+ if (options.run) {
124
+ await options.run(cmd, { alias: `add ${packageName}`, root });
125
+ } else if (options.exec) {
126
+ this.log.debug(`Installing ${packageName}`);
127
+ await options.exec(cmd, { global: true, root });
128
+ }
129
+ }
130
+
131
+ // ===========================================
132
+ // Package Manager Setup & Cleanup
133
+ // ===========================================
134
+
135
+ public async ensureYarn(root: string): Promise<void> {
136
+ const yarnrcPath = this.fs.join(root, ".yarnrc.yml");
137
+ if (!(await this.fs.exists(yarnrcPath))) {
138
+ await this.fs.writeFile(yarnrcPath, "nodeLinker: node-modules");
139
+ }
140
+ await this.removeAllPmFilesExcept(root, "yarn");
141
+ }
142
+
143
+ public async ensureBun(root: string): Promise<void> {
144
+ await this.removeAllPmFilesExcept(root, "bun");
145
+ }
146
+
147
+ public async ensurePnpm(root: string): Promise<void> {
148
+ await this.removeAllPmFilesExcept(root, "pnpm");
149
+ }
150
+
151
+ public async ensureNpm(root: string): Promise<void> {
152
+ await this.removeAllPmFilesExcept(root, "npm");
153
+ }
154
+
155
+ public async removeAllPmFilesExcept(
156
+ root: string,
157
+ except: string,
158
+ ): Promise<void> {
159
+ if (except !== "yarn") await this.removeYarn(root);
160
+ if (except !== "pnpm") await this.removePnpm(root);
161
+ if (except !== "npm") await this.removeNpm(root);
162
+ if (except !== "bun") await this.removeBun(root);
163
+ }
164
+
165
+ public async removeYarn(root: string): Promise<void> {
166
+ await this.removeFiles(root, [".yarn", ".yarnrc.yml", "yarn.lock"]);
167
+ await this.editPackageJson(root, (pkg) => {
168
+ delete pkg.packageManager;
169
+ return pkg;
170
+ });
171
+ }
172
+
173
+ public async removePnpm(root: string): Promise<void> {
174
+ await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
175
+ await this.editPackageJson(root, (pkg) => {
176
+ delete pkg.packageManager;
177
+ return pkg;
178
+ });
179
+ }
180
+
181
+ public async removeNpm(root: string): Promise<void> {
182
+ await this.removeFiles(root, ["package-lock.json"]);
183
+ }
184
+
185
+ public async removeBun(root: string): Promise<void> {
186
+ await this.removeFiles(root, ["bun.lockb", "bun.lock"]);
187
+ }
188
+
189
+ // ===========================================
190
+ // Package.json utilities
191
+ // ===========================================
192
+
193
+ public async readPackageJson(root: string): Promise<Record<string, any>> {
194
+ const content = await this.fs
195
+ .createFile({ path: this.fs.join(root, "package.json") })
196
+ .text();
197
+ return JSON.parse(content);
198
+ }
199
+
200
+ public async writePackageJson(
201
+ root: string,
202
+ content: Record<string, any>,
203
+ ): Promise<void> {
204
+ await this.fs.writeFile(
205
+ this.fs.join(root, "package.json"),
206
+ JSON.stringify(content, null, 2),
207
+ );
208
+ }
209
+
210
+ public async editPackageJson(
211
+ root: string,
212
+ editFn: (pkg: Record<string, any>) => Record<string, any>,
213
+ ): Promise<void> {
214
+ try {
215
+ const pkg = await this.readPackageJson(root);
216
+ const updated = editFn(pkg);
217
+ await this.writePackageJson(root, updated);
218
+ } catch {
219
+ // package.json doesn't exist, skip
220
+ }
221
+ }
222
+
223
+ public async ensurePackageJson(
224
+ root: string,
225
+ modes: DependencyModes,
226
+ ): Promise<Record<string, any>> {
227
+ const packageJsonPath = this.fs.join(root, "package.json");
228
+
229
+ if (!(await this.fs.exists(packageJsonPath))) {
230
+ const content = this.generatePackageJsonContent(modes);
231
+ await this.writePackageJson(root, content);
232
+ return content;
233
+ }
234
+
235
+ const packageJson = await this.readPackageJson(root);
236
+ const newContent = this.generatePackageJsonContent(modes);
237
+
238
+ packageJson.type = "module";
239
+ packageJson.dependencies ??= {};
240
+ packageJson.devDependencies ??= {};
241
+ packageJson.scripts ??= {};
242
+
243
+ Object.assign(packageJson.dependencies, newContent.dependencies);
244
+ Object.assign(packageJson.devDependencies, newContent.devDependencies);
245
+ Object.assign(packageJson.scripts, newContent.scripts);
246
+
247
+ await this.writePackageJson(root, packageJson);
248
+ return packageJson;
249
+ }
250
+
251
+ public generatePackageJsonContent(modes: DependencyModes): {
252
+ dependencies: Record<string, string>;
253
+ devDependencies: Record<string, string>;
254
+ scripts: Record<string, string>;
255
+ type: "module";
256
+ } {
257
+ const dependencies: Record<string, string> = {
258
+ alepha: `^${version}`,
259
+ };
260
+
261
+ const devDependencies: Record<string, string> = {};
262
+
263
+ const scripts: Record<string, string> = {
264
+ dev: "alepha dev",
265
+ build: "alepha build",
266
+ lint: "alepha lint",
267
+ typecheck: "alepha typecheck",
268
+ verify: "alepha verify",
269
+ };
270
+
271
+ if (modes.ui) {
272
+ dependencies["@alepha/ui"] = `^${version}`;
273
+ modes.react = true;
274
+ }
275
+
276
+ if (modes.react) {
277
+ dependencies["@alepha/react"] = `^${version}`;
278
+ dependencies.react = "^19.2.0";
279
+ dependencies["react-dom"] = "^19.2.0";
280
+ devDependencies["@types/react"] = "^19.2.0";
281
+ }
282
+
283
+ return {
284
+ type: "module",
285
+ dependencies,
286
+ devDependencies,
287
+ scripts,
288
+ };
289
+ }
290
+
291
+ // ===========================================
292
+ // Helper methods
293
+ // ===========================================
294
+
295
+ protected async removeFiles(root: string, files: string[]): Promise<void> {
296
+ await Promise.all(
297
+ files.map((file) =>
298
+ this.fs.rm(this.fs.join(root, file), { force: true, recursive: true }),
299
+ ),
300
+ );
301
+ }
302
+ }
303
+
304
+ export interface DependencyModes {
305
+ react?: boolean;
306
+ ui?: boolean;
307
+ expo?: boolean;
308
+ }
@@ -0,0 +1,329 @@
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 { mainBrowserTs } from "../assets/mainBrowserTs.ts";
12
+ import { mainCss } from "../assets/mainCss.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
+ force?: boolean;
58
+ packageJson?: boolean | DependencyModes;
59
+ tsconfigJson?: boolean;
60
+ indexHtml?: boolean;
61
+ biomeJson?: boolean;
62
+ editorconfig?: boolean;
63
+ claudeMd?: boolean | ClaudeMdOptions;
64
+ },
65
+ ): Promise<void> {
66
+ const tasks: Promise<void>[] = [];
67
+ const force = opts.force ?? false;
68
+
69
+ if (opts.packageJson) {
70
+ tasks.push(
71
+ this.pm
72
+ .ensurePackageJson(
73
+ root,
74
+ typeof opts.packageJson === "boolean" ? {} : opts.packageJson,
75
+ )
76
+ .then(() => {}),
77
+ );
78
+ }
79
+ if (opts.tsconfigJson) {
80
+ tasks.push(this.ensureTsConfig(root, { force }));
81
+ }
82
+ if (opts.indexHtml) {
83
+ tasks.push(this.ensureReactProject(root, { force }));
84
+ }
85
+ if (opts.biomeJson) {
86
+ tasks.push(this.ensureBiomeConfig(root, { force }));
87
+ }
88
+ if (opts.editorconfig) {
89
+ tasks.push(this.ensureEditorConfig(root, { force }));
90
+ }
91
+ if (opts.claudeMd) {
92
+ tasks.push(
93
+ this.ensureClaudeMd(
94
+ root,
95
+ typeof opts.claudeMd === "boolean"
96
+ ? { force }
97
+ : { ...opts.claudeMd, force },
98
+ ),
99
+ );
100
+ }
101
+
102
+ await Promise.all(tasks);
103
+ }
104
+
105
+ // ===========================================
106
+ // Config Files
107
+ // ===========================================
108
+
109
+ public async ensureTsConfig(
110
+ root: string,
111
+ opts: { force?: boolean } = {},
112
+ ): Promise<void> {
113
+ // Check if tsconfig.json exists in current or parent directories
114
+ if (!opts.force && (await this.existsInParents(root, "tsconfig.json"))) {
115
+ return;
116
+ }
117
+ await this.fs.writeFile(
118
+ this.fs.join(root, "tsconfig.json"),
119
+ tsconfigJson(),
120
+ );
121
+ }
122
+
123
+ public async ensureBiomeConfig(
124
+ root: string,
125
+ opts: { force?: boolean } = {},
126
+ ): Promise<void> {
127
+ await this.ensureFile(root, "biome.json", biomeJson(), opts.force);
128
+ }
129
+
130
+ public async ensureEditorConfig(
131
+ root: string,
132
+ opts: { force?: boolean } = {},
133
+ ): Promise<void> {
134
+ await this.ensureFile(root, ".editorconfig", editorconfig(), opts.force);
135
+ }
136
+
137
+ public async ensureClaudeMd(
138
+ root: string,
139
+ options: ClaudeMdOptions & { force?: boolean } = {},
140
+ ): Promise<void> {
141
+ await this.ensureFile(root, "CLAUDE.md", claudeMd(options), options.force);
142
+ }
143
+
144
+ // ===========================================
145
+ // API Project Structure
146
+ // ===========================================
147
+
148
+ /**
149
+ * Ensure src/main.server.ts exists with full API structure.
150
+ *
151
+ * Creates:
152
+ * - src/main.server.ts (entry point)
153
+ * - src/api/index.ts (API module)
154
+ * - src/api/controllers/HelloController.ts (example controller)
155
+ */
156
+ public async ensureApiProject(
157
+ root: string,
158
+ opts: { force?: boolean } = {},
159
+ ): Promise<void> {
160
+ const srcDir = this.fs.join(root, "src");
161
+
162
+ // Don't overwrite existing content unless force is set
163
+ if (!opts.force && (await this.fs.exists(srcDir))) {
164
+ const files = await this.fs.ls(srcDir);
165
+ if (files.length > 0) return;
166
+ }
167
+
168
+ const appName = this.getAppName(root);
169
+
170
+ // Create directories
171
+ await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), {
172
+ recursive: true,
173
+ });
174
+
175
+ // Create files
176
+ await this.ensureFile(srcDir, "main.server.ts", mainServerTs(), opts.force);
177
+ await this.ensureFile(
178
+ srcDir,
179
+ "api/index.ts",
180
+ apiIndexTs({ appName }),
181
+ opts.force,
182
+ );
183
+ await this.ensureFile(
184
+ srcDir,
185
+ "api/controllers/HelloController.ts",
186
+ apiHelloControllerTs(),
187
+ opts.force,
188
+ );
189
+ }
190
+
191
+ // ===========================================
192
+ // React Project Structure
193
+ // ===========================================
194
+
195
+ /**
196
+ * Ensure full React project structure exists.
197
+ *
198
+ * Creates:
199
+ * - src/main.server.ts, src/main.browser.ts
200
+ * - src/api/index.ts, src/api/controllers/HelloController.ts
201
+ * - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
202
+ */
203
+ public async ensureReactProject(
204
+ root: string,
205
+ opts: { force?: boolean } = {},
206
+ ): Promise<void> {
207
+ const appName = this.getAppName(root);
208
+
209
+ // Create directories
210
+ await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), {
211
+ recursive: true,
212
+ });
213
+ await this.fs.mkdir(this.fs.join(root, "src/web/components"), {
214
+ recursive: true,
215
+ });
216
+
217
+ // src/main.css
218
+ await this.ensureFile(root, "src/main.css", mainCss(), opts.force);
219
+
220
+ // API structure
221
+ await this.ensureFile(
222
+ root,
223
+ "src/api/index.ts",
224
+ apiIndexTs({ appName }),
225
+ opts.force,
226
+ );
227
+ await this.ensureFile(
228
+ root,
229
+ "src/api/controllers/HelloController.ts",
230
+ apiHelloControllerTs(),
231
+ opts.force,
232
+ );
233
+ await this.ensureFile(
234
+ root,
235
+ "src/main.server.ts",
236
+ mainServerTs({ react: true }),
237
+ opts.force,
238
+ );
239
+
240
+ // Web structure
241
+ await this.ensureFile(
242
+ root,
243
+ "src/web/index.ts",
244
+ webIndexTs({ appName }),
245
+ opts.force,
246
+ );
247
+ await this.ensureFile(
248
+ root,
249
+ "src/web/AppRouter.ts",
250
+ webAppRouterTs(),
251
+ opts.force,
252
+ );
253
+ await this.ensureFile(
254
+ root,
255
+ "src/web/components/Hello.tsx",
256
+ webHelloComponentTsx(),
257
+ opts.force,
258
+ );
259
+ await this.ensureFile(
260
+ root,
261
+ "src/main.browser.ts",
262
+ mainBrowserTs(),
263
+ opts.force,
264
+ );
265
+ }
266
+
267
+ // ===========================================
268
+ // Test Directory
269
+ // ===========================================
270
+
271
+ /**
272
+ * Ensure test directory exists with a dummy test file.
273
+ */
274
+ public async ensureTestDir(root: string): Promise<void> {
275
+ const testDir = this.fs.join(root, "test");
276
+ const dummyPath = this.fs.join(testDir, "dummy.spec.ts");
277
+
278
+ if (!(await this.fs.exists(testDir))) {
279
+ await this.fs.mkdir(testDir, { recursive: true });
280
+ await this.fs.writeFile(dummyPath, dummySpecTs());
281
+ return;
282
+ }
283
+
284
+ const files = await this.fs.ls(testDir);
285
+ if (files.length === 0) {
286
+ await this.fs.writeFile(dummyPath, dummySpecTs());
287
+ }
288
+ }
289
+
290
+ // ===========================================
291
+ // Helpers
292
+ // ===========================================
293
+
294
+ /**
295
+ * Write a file, optionally overriding if it exists.
296
+ */
297
+ protected async ensureFile(
298
+ root: string,
299
+ relativePath: string,
300
+ content: string,
301
+ force?: boolean,
302
+ ): Promise<void> {
303
+ const fullPath = this.fs.join(root, relativePath);
304
+ if (force || !(await this.fs.exists(fullPath))) {
305
+ await this.fs.writeFile(fullPath, content);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check if a file exists in the given directory or any parent directory.
311
+ */
312
+ protected async existsInParents(
313
+ root: string,
314
+ filename: string,
315
+ ): Promise<boolean> {
316
+ let current = root;
317
+ while (true) {
318
+ if (await this.fs.exists(this.fs.join(current, filename))) {
319
+ return true;
320
+ }
321
+ const parent = dirname(current);
322
+ if (parent === current) {
323
+ // Reached filesystem root
324
+ return false;
325
+ }
326
+ current = parent;
327
+ }
328
+ }
329
+ }
@@ -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",
@@ -165,13 +165,6 @@ export class Alepha {
165
165
  ...state.env,
166
166
  ...process.env,
167
167
  };
168
-
169
- // remove empty env variables
170
- for (const key in state.env) {
171
- if (state.env[key] === "") {
172
- delete (state.env as any)[key];
173
- }
174
- }
175
168
  }
176
169
 
177
170
  // force production mode when building with vite
@@ -765,8 +758,9 @@ export class Alepha {
765
758
  }
766
759
 
767
760
  if (this.started) {
761
+ const mod = (service as WithModule)[MODULE]?.name;
768
762
  throw new ContainerLockedError(
769
- `Container is locked. No more services can be added. ${parent?.name} -> ${service.name}`,
763
+ `Container is locked. No more services can be added. Attempted to inject '${service.name}' from '${parent?.name}'. ${mod ? `Maybe register module '${mod}' in Alepha.` : ""}`,
770
764
  );
771
765
  }
772
766
  }