alepha 0.14.2 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/dist/api/audits/index.browser.js +5 -5
  2. package/dist/api/audits/index.browser.js.map +1 -1
  3. package/dist/api/audits/index.d.ts +784 -784
  4. package/dist/api/audits/index.d.ts.map +1 -1
  5. package/dist/api/audits/index.js +13 -13
  6. package/dist/api/audits/index.js.map +1 -1
  7. package/dist/api/files/index.browser.js +5 -5
  8. package/dist/api/files/index.browser.js.map +1 -1
  9. package/dist/api/files/index.d.ts +57 -57
  10. package/dist/api/files/index.d.ts.map +1 -1
  11. package/dist/api/files/index.js +71 -71
  12. package/dist/api/files/index.js.map +1 -1
  13. package/dist/api/jobs/index.browser.js +5 -5
  14. package/dist/api/jobs/index.browser.js.map +1 -1
  15. package/dist/api/jobs/index.d.ts +165 -165
  16. package/dist/api/jobs/index.d.ts.map +1 -1
  17. package/dist/api/jobs/index.js +10 -10
  18. package/dist/api/jobs/index.js.map +1 -1
  19. package/dist/api/notifications/index.browser.js +10 -10
  20. package/dist/api/notifications/index.browser.js.map +1 -1
  21. package/dist/api/notifications/index.d.ts +583 -171
  22. package/dist/api/notifications/index.d.ts.map +1 -1
  23. package/dist/api/notifications/index.js +12 -12
  24. package/dist/api/notifications/index.js.map +1 -1
  25. package/dist/api/parameters/index.browser.js +163 -10
  26. package/dist/api/parameters/index.browser.js.map +1 -1
  27. package/dist/api/parameters/index.d.ts +281 -276
  28. package/dist/api/parameters/index.d.ts.map +1 -1
  29. package/dist/api/parameters/index.js +196 -91
  30. package/dist/api/parameters/index.js.map +1 -1
  31. package/dist/api/users/index.browser.js +19 -19
  32. package/dist/api/users/index.browser.js.map +1 -1
  33. package/dist/api/users/index.d.ts +1137 -1123
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +827 -596
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/verifications/index.browser.js +6 -6
  38. package/dist/api/verifications/index.browser.js.map +1 -1
  39. package/dist/api/verifications/index.d.ts +13 -13
  40. package/dist/api/verifications/index.d.ts.map +1 -1
  41. package/dist/api/verifications/index.js +6 -6
  42. package/dist/api/verifications/index.js.map +1 -1
  43. package/dist/bin/index.d.ts +1 -2
  44. package/dist/bin/index.js +0 -1
  45. package/dist/bin/index.js.map +1 -1
  46. package/dist/cli/index.d.ts +137 -112
  47. package/dist/cli/index.d.ts.map +1 -1
  48. package/dist/cli/index.js +371 -259
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/command/index.d.ts +45 -5
  51. package/dist/command/index.d.ts.map +1 -1
  52. package/dist/command/index.js +97 -17
  53. package/dist/command/index.js.map +1 -1
  54. package/dist/core/index.browser.js +14 -18
  55. package/dist/core/index.browser.js.map +1 -1
  56. package/dist/core/index.d.ts +29 -0
  57. package/dist/core/index.d.ts.map +1 -1
  58. package/dist/core/index.js +14 -18
  59. package/dist/core/index.js.map +1 -1
  60. package/dist/core/index.native.js +14 -18
  61. package/dist/core/index.native.js.map +1 -1
  62. package/dist/fake/index.js +195 -168
  63. package/dist/fake/index.js.map +1 -1
  64. package/dist/file/index.d.ts +8 -0
  65. package/dist/file/index.d.ts.map +1 -1
  66. package/dist/file/index.js +3 -0
  67. package/dist/file/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts.map +1 -1
  69. package/dist/orm/index.d.ts +32 -32
  70. package/dist/orm/index.d.ts.map +1 -1
  71. package/dist/orm/index.js +12 -12
  72. package/dist/orm/index.js.map +1 -1
  73. package/dist/security/index.d.ts +1 -1
  74. package/dist/security/index.d.ts.map +1 -1
  75. package/dist/security/index.js +1 -1
  76. package/dist/security/index.js.map +1 -1
  77. package/dist/server/auth/index.d.ts +171 -155
  78. package/dist/server/auth/index.d.ts.map +1 -1
  79. package/dist/server/auth/index.js +0 -1
  80. package/dist/server/auth/index.js.map +1 -1
  81. package/dist/server/compress/index.d.ts.map +1 -1
  82. package/dist/server/compress/index.js +2 -0
  83. package/dist/server/compress/index.js.map +1 -1
  84. package/dist/server/core/index.d.ts.map +1 -1
  85. package/dist/server/core/index.js +1 -1
  86. package/dist/server/core/index.js.map +1 -1
  87. package/dist/server/links/index.browser.js +22 -6
  88. package/dist/server/links/index.browser.js.map +1 -1
  89. package/dist/server/links/index.d.ts +46 -44
  90. package/dist/server/links/index.d.ts.map +1 -1
  91. package/dist/server/links/index.js +24 -41
  92. package/dist/server/links/index.js.map +1 -1
  93. package/dist/server/security/index.d.ts +9 -9
  94. package/dist/server/swagger/index.d.ts +2 -1
  95. package/dist/server/swagger/index.d.ts.map +1 -1
  96. package/dist/server/swagger/index.js +8 -3
  97. package/dist/server/swagger/index.js.map +1 -1
  98. package/dist/vite/index.d.ts.map +1 -1
  99. package/dist/vite/index.js +12 -4
  100. package/dist/vite/index.js.map +1 -1
  101. package/dist/websocket/index.d.ts +7 -7
  102. package/package.json +7 -7
  103. package/src/api/audits/controllers/{AuditController.ts → AdminAuditController.ts} +5 -6
  104. package/src/api/audits/entities/audits.ts +5 -5
  105. package/src/api/audits/index.browser.ts +1 -1
  106. package/src/api/audits/index.ts +3 -3
  107. package/src/api/audits/primitives/$audit.spec.ts +276 -0
  108. package/src/api/audits/services/AuditService.spec.ts +495 -0
  109. package/src/api/files/__tests__/$bucket.spec.ts +91 -0
  110. package/src/api/files/controllers/AdminFileStatsController.spec.ts +166 -0
  111. package/src/api/files/controllers/{StorageStatsController.ts → AdminFileStatsController.ts} +2 -2
  112. package/src/api/files/controllers/FileController.spec.ts +558 -0
  113. package/src/api/files/controllers/FileController.ts +4 -5
  114. package/src/api/files/entities/files.ts +5 -5
  115. package/src/api/files/index.browser.ts +1 -1
  116. package/src/api/files/index.ts +4 -4
  117. package/src/api/files/jobs/FileJobs.spec.ts +52 -0
  118. package/src/api/files/services/FileService.spec.ts +109 -0
  119. package/src/api/jobs/__tests__/JobController.spec.ts +343 -0
  120. package/src/api/jobs/controllers/{JobController.ts → AdminJobController.ts} +2 -2
  121. package/src/api/jobs/entities/jobExecutions.ts +5 -5
  122. package/src/api/jobs/index.ts +3 -3
  123. package/src/api/jobs/primitives/$job.spec.ts +476 -0
  124. package/src/api/notifications/controllers/{NotificationController.ts → AdminNotificationController.ts} +4 -5
  125. package/src/api/notifications/entities/notifications.ts +5 -5
  126. package/src/api/notifications/index.browser.ts +1 -1
  127. package/src/api/notifications/index.ts +4 -4
  128. package/src/api/parameters/controllers/{ConfigController.ts → AdminConfigController.ts} +46 -107
  129. package/src/api/parameters/entities/parameters.ts +7 -17
  130. package/src/api/parameters/index.ts +3 -3
  131. package/src/api/parameters/primitives/$config.spec.ts +356 -0
  132. package/src/api/parameters/schemas/activateConfigBodySchema.ts +12 -0
  133. package/src/api/parameters/schemas/checkScheduledResponseSchema.ts +8 -0
  134. package/src/api/parameters/schemas/configCurrentResponseSchema.ts +13 -0
  135. package/src/api/parameters/schemas/configHistoryResponseSchema.ts +9 -0
  136. package/src/api/parameters/schemas/configNameParamSchema.ts +10 -0
  137. package/src/api/parameters/schemas/configNamesResponseSchema.ts +8 -0
  138. package/src/api/parameters/schemas/configTreeNodeSchema.ts +13 -0
  139. package/src/api/parameters/schemas/configVersionParamSchema.ts +9 -0
  140. package/src/api/parameters/schemas/configVersionResponseSchema.ts +9 -0
  141. package/src/api/parameters/schemas/configsByStatusResponseSchema.ts +9 -0
  142. package/src/api/parameters/schemas/createConfigVersionBodySchema.ts +24 -0
  143. package/src/api/parameters/schemas/index.ts +15 -0
  144. package/src/api/parameters/schemas/parameterResponseSchema.ts +26 -0
  145. package/src/api/parameters/schemas/parameterStatusSchema.ts +13 -0
  146. package/src/api/parameters/schemas/rollbackConfigBodySchema.ts +15 -0
  147. package/src/api/parameters/schemas/statusParamSchema.ts +9 -0
  148. package/src/api/users/__tests__/EmailVerification.spec.ts +369 -0
  149. package/src/api/users/__tests__/PasswordReset.spec.ts +550 -0
  150. package/src/api/users/controllers/AdminIdentityController.spec.ts +365 -0
  151. package/src/api/users/controllers/{IdentityController.ts → AdminIdentityController.ts} +3 -4
  152. package/src/api/users/controllers/AdminSessionController.spec.ts +274 -0
  153. package/src/api/users/controllers/{SessionController.ts → AdminSessionController.ts} +3 -4
  154. package/src/api/users/controllers/AdminUserController.spec.ts +372 -0
  155. package/src/api/users/controllers/AdminUserController.ts +116 -0
  156. package/src/api/users/controllers/UserController.ts +4 -107
  157. package/src/api/users/controllers/UserRealmController.ts +3 -0
  158. package/src/api/users/entities/identities.ts +6 -6
  159. package/src/api/users/entities/sessions.ts +6 -6
  160. package/src/api/users/entities/users.ts +9 -9
  161. package/src/api/users/index.ts +9 -6
  162. package/src/api/users/primitives/$userRealm.ts +13 -8
  163. package/src/api/users/services/CredentialService.spec.ts +509 -0
  164. package/src/api/users/services/CredentialService.ts +46 -0
  165. package/src/api/users/services/IdentityService.ts +15 -0
  166. package/src/api/users/services/RegistrationService.spec.ts +630 -0
  167. package/src/api/users/services/RegistrationService.ts +18 -0
  168. package/src/api/users/services/SessionService.spec.ts +301 -0
  169. package/src/api/users/services/SessionService.ts +110 -1
  170. package/src/api/users/services/UserService.ts +67 -2
  171. package/src/api/verifications/__tests__/CodeVerification.spec.ts +318 -0
  172. package/src/api/verifications/__tests__/LinkVerification.spec.ts +279 -0
  173. package/src/api/verifications/entities/verifications.ts +6 -6
  174. package/src/api/verifications/jobs/VerificationJobs.spec.ts +50 -0
  175. package/src/batch/__tests__/startup-buffering.spec.ts +458 -0
  176. package/src/batch/primitives/$batch.spec.ts +766 -0
  177. package/src/batch/providers/BatchProvider.spec.ts +786 -0
  178. package/src/bin/index.ts +0 -1
  179. package/src/bucket/__tests__/shared.ts +194 -0
  180. package/src/bucket/primitives/$bucket.spec.ts +104 -0
  181. package/src/bucket/providers/FileStorageProvider.spec.ts +13 -0
  182. package/src/bucket/providers/LocalFileStorageProvider.spec.ts +77 -0
  183. package/src/bucket/providers/MemoryFileStorageProvider.spec.ts +82 -0
  184. package/src/cache/core/__tests__/shared.ts +377 -0
  185. package/src/cache/core/primitives/$cache.spec.ts +111 -0
  186. package/src/cache/redis/__tests__/cache-redis.spec.ts +70 -0
  187. package/src/cli/apps/AlephaCli.ts +25 -4
  188. package/src/cli/commands/dev.ts +19 -7
  189. package/src/cli/commands/gen/changelog.spec.ts +315 -0
  190. package/src/cli/commands/{changelog.ts → gen/changelog.ts} +9 -9
  191. package/src/cli/commands/gen/openapi.ts +71 -0
  192. package/src/cli/commands/gen.ts +18 -0
  193. package/src/cli/commands/init.ts +2 -0
  194. package/src/cli/commands/root.ts +12 -3
  195. package/src/cli/commands/typecheck.ts +5 -0
  196. package/src/cli/index.ts +2 -1
  197. package/src/cli/services/AlephaCliUtils.ts +71 -32
  198. package/src/cli/services/GitMessageParser.ts +1 -1
  199. package/src/command/helpers/Asker.spec.ts +127 -0
  200. package/src/command/helpers/Runner.spec.ts +126 -0
  201. package/src/command/primitives/$command.spec.ts +1588 -0
  202. package/src/command/providers/CliProvider.ts +74 -24
  203. package/src/core/Alepha.ts +45 -0
  204. package/src/core/__tests__/Alepha-emit.spec.ts +22 -0
  205. package/src/core/__tests__/Alepha-graph.spec.ts +93 -0
  206. package/src/core/__tests__/Alepha-has.spec.ts +41 -0
  207. package/src/core/__tests__/Alepha-inject.spec.ts +93 -0
  208. package/src/core/__tests__/Alepha-register.spec.ts +81 -0
  209. package/src/core/__tests__/Alepha-start.spec.ts +176 -0
  210. package/src/core/__tests__/Alepha-with.spec.ts +14 -0
  211. package/src/core/__tests__/TypeBox-usecases.spec.ts +35 -0
  212. package/src/core/__tests__/TypeBoxLocale.spec.ts +15 -0
  213. package/src/core/__tests__/descriptor.spec.ts +34 -0
  214. package/src/core/__tests__/fixtures/A.ts +5 -0
  215. package/src/core/__tests__/pagination.spec.ts +77 -0
  216. package/src/core/helpers/jsonSchemaToTypeBox.ts +2 -2
  217. package/src/core/primitives/$atom.spec.ts +43 -0
  218. package/src/core/primitives/$hook.spec.ts +130 -0
  219. package/src/core/primitives/$inject.spec.ts +175 -0
  220. package/src/core/primitives/$module.spec.ts +115 -0
  221. package/src/core/providers/CodecManager.spec.ts +740 -0
  222. package/src/core/providers/EventManager.spec.ts +762 -0
  223. package/src/core/providers/EventManager.ts +4 -0
  224. package/src/core/providers/StateManager.spec.ts +365 -0
  225. package/src/core/providers/TypeProvider.spec.ts +1607 -0
  226. package/src/core/providers/TypeProvider.ts +20 -26
  227. package/src/datetime/primitives/$interval.spec.ts +103 -0
  228. package/src/datetime/providers/DateTimeProvider.spec.ts +86 -0
  229. package/src/email/primitives/$email.spec.ts +175 -0
  230. package/src/email/providers/LocalEmailProvider.spec.ts +341 -0
  231. package/src/fake/__tests__/keyName.example.ts +40 -0
  232. package/src/fake/__tests__/keyName.spec.ts +152 -0
  233. package/src/fake/__tests__/module.example.ts +32 -0
  234. package/src/fake/providers/FakeProvider.spec.ts +438 -0
  235. package/src/file/providers/FileSystemProvider.ts +8 -0
  236. package/src/file/providers/NodeFileSystemProvider.spec.ts +418 -0
  237. package/src/file/providers/NodeFileSystemProvider.ts +5 -0
  238. package/src/file/services/FileDetector.spec.ts +591 -0
  239. package/src/lock/core/__tests__/shared.ts +190 -0
  240. package/src/lock/core/providers/MemoryLockProvider.spec.ts +25 -0
  241. package/src/lock/redis/providers/RedisLockProvider.spec.ts +25 -0
  242. package/src/logger/__tests__/SimpleFormatterProvider.spec.ts +109 -0
  243. package/src/logger/primitives/$logger.spec.ts +108 -0
  244. package/src/logger/services/Logger.spec.ts +295 -0
  245. package/src/mcp/__tests__/errors.spec.ts +175 -0
  246. package/src/mcp/__tests__/integration.spec.ts +450 -0
  247. package/src/mcp/helpers/jsonrpc.spec.ts +380 -0
  248. package/src/mcp/primitives/$prompt.spec.ts +468 -0
  249. package/src/mcp/primitives/$resource.spec.ts +390 -0
  250. package/src/mcp/primitives/$tool.spec.ts +406 -0
  251. package/src/mcp/providers/McpServerProvider.spec.ts +797 -0
  252. package/src/orm/__tests__/$repository-crud.spec.ts +276 -0
  253. package/src/orm/__tests__/$repository-hooks.spec.ts +325 -0
  254. package/src/orm/__tests__/$repository-orderBy.spec.ts +128 -0
  255. package/src/orm/__tests__/$repository-pagination-sort.spec.ts +149 -0
  256. package/src/orm/__tests__/$repository-save.spec.ts +37 -0
  257. package/src/orm/__tests__/ModelBuilder-integration.spec.ts +490 -0
  258. package/src/orm/__tests__/ModelBuilder-types.spec.ts +186 -0
  259. package/src/orm/__tests__/PostgresProvider.spec.ts +46 -0
  260. package/src/orm/__tests__/delete-returning.spec.ts +256 -0
  261. package/src/orm/__tests__/deletedAt.spec.ts +80 -0
  262. package/src/orm/__tests__/enums.spec.ts +315 -0
  263. package/src/orm/__tests__/execute.spec.ts +72 -0
  264. package/src/orm/__tests__/fixtures/bigEntitySchema.ts +65 -0
  265. package/src/orm/__tests__/fixtures/userEntitySchema.ts +27 -0
  266. package/src/orm/__tests__/joins.spec.ts +1114 -0
  267. package/src/orm/__tests__/page.spec.ts +287 -0
  268. package/src/orm/__tests__/primaryKey.spec.ts +87 -0
  269. package/src/orm/__tests__/query-date-encoding.spec.ts +402 -0
  270. package/src/orm/__tests__/ref-auto-onDelete.spec.ts +156 -0
  271. package/src/orm/__tests__/references.spec.ts +102 -0
  272. package/src/orm/__tests__/security.spec.ts +710 -0
  273. package/src/orm/__tests__/sqlite.spec.ts +111 -0
  274. package/src/orm/__tests__/string-operators.spec.ts +429 -0
  275. package/src/orm/__tests__/timestamps.spec.ts +388 -0
  276. package/src/orm/__tests__/validation.spec.ts +183 -0
  277. package/src/orm/__tests__/version.spec.ts +64 -0
  278. package/src/orm/helpers/parseQueryString.spec.ts +196 -0
  279. package/src/orm/primitives/$repository.spec.ts +137 -0
  280. package/src/orm/primitives/$sequence.spec.ts +29 -0
  281. package/src/orm/primitives/$transaction.spec.ts +82 -0
  282. package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -3
  283. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
  284. package/src/orm/providers/drivers/CloudflareD1Provider.ts +1 -1
  285. package/src/orm/providers/drivers/DatabaseProvider.ts +1 -1
  286. package/src/orm/providers/drivers/NodePostgresProvider.ts +3 -3
  287. package/src/orm/providers/drivers/NodeSqliteProvider.ts +1 -1
  288. package/src/orm/providers/drivers/PglitePostgresProvider.ts +2 -2
  289. package/src/orm/services/ModelBuilder.spec.ts +575 -0
  290. package/src/orm/services/Repository.spec.ts +137 -0
  291. package/src/queue/core/__tests__/shared.ts +143 -0
  292. package/src/queue/core/providers/MemoryQueueProvider.spec.ts +23 -0
  293. package/src/queue/core/providers/WorkerProvider.spec.ts +378 -0
  294. package/src/queue/redis/providers/RedisQueueProvider.spec.ts +23 -0
  295. package/src/redis/__tests__/redis.spec.ts +58 -0
  296. package/src/retry/primitives/$retry.spec.ts +234 -0
  297. package/src/retry/providers/RetryProvider.spec.ts +438 -0
  298. package/src/router/__tests__/match.spec.ts +252 -0
  299. package/src/router/providers/RouterProvider.spec.ts +197 -0
  300. package/src/scheduler/__tests__/$scheduler-cron.spec.ts +25 -0
  301. package/src/scheduler/__tests__/$scheduler-interval.spec.ts +25 -0
  302. package/src/scheduler/__tests__/shared.ts +77 -0
  303. package/src/security/__tests__/bug-1-wildcard-after-start.spec.ts +229 -0
  304. package/src/security/__tests__/bug-2-password-validation.spec.ts +245 -0
  305. package/src/security/__tests__/bug-3-regex-vulnerability.spec.ts +407 -0
  306. package/src/security/__tests__/bug-4-oauth2-validation.spec.ts +439 -0
  307. package/src/security/__tests__/multi-layer-permissions.spec.ts +522 -0
  308. package/src/security/primitives/$permission.spec.ts +30 -0
  309. package/src/security/primitives/$permission.ts +2 -2
  310. package/src/security/primitives/$realm.spec.ts +101 -0
  311. package/src/security/primitives/$role.spec.ts +52 -0
  312. package/src/security/primitives/$serviceAccount.spec.ts +61 -0
  313. package/src/security/providers/SecurityProvider.spec.ts +350 -0
  314. package/src/server/auth/providers/ServerAuthProvider.ts +0 -2
  315. package/src/server/cache/providers/ServerCacheProvider.spec.ts +942 -0
  316. package/src/server/compress/providers/ServerCompressProvider.spec.ts +31 -0
  317. package/src/server/compress/providers/ServerCompressProvider.ts +2 -0
  318. package/src/server/cookies/providers/ServerCookiesProvider.spec.ts +253 -0
  319. package/src/server/core/__tests__/ServerRouterProvider-getRoutes.spec.ts +334 -0
  320. package/src/server/core/__tests__/ServerRouterProvider-requestId.spec.ts +129 -0
  321. package/src/server/core/primitives/$action.spec.ts +191 -0
  322. package/src/server/core/primitives/$route.spec.ts +65 -0
  323. package/src/server/core/providers/ServerBodyParserProvider.spec.ts +93 -0
  324. package/src/server/core/providers/ServerLoggerProvider.spec.ts +100 -0
  325. package/src/server/core/providers/ServerProvider.ts +3 -1
  326. package/src/server/core/services/HttpClient.spec.ts +123 -0
  327. package/src/server/core/services/UserAgentParser.spec.ts +111 -0
  328. package/src/server/cors/providers/ServerCorsProvider.spec.ts +481 -0
  329. package/src/server/health/providers/ServerHealthProvider.spec.ts +22 -0
  330. package/src/server/helmet/providers/ServerHelmetProvider.spec.ts +105 -0
  331. package/src/server/links/__tests__/$action.spec.ts +238 -0
  332. package/src/server/links/__tests__/fixtures/CrudApp.ts +122 -0
  333. package/src/server/links/__tests__/requestId.spec.ts +120 -0
  334. package/src/server/links/primitives/$remote.spec.ts +228 -0
  335. package/src/server/links/providers/LinkProvider.spec.ts +54 -0
  336. package/src/server/links/providers/LinkProvider.ts +49 -3
  337. package/src/server/links/providers/ServerLinksProvider.ts +1 -53
  338. package/src/server/links/schemas/apiLinksResponseSchema.ts +7 -0
  339. package/src/server/metrics/providers/ServerMetricsProvider.spec.ts +25 -0
  340. package/src/server/multipart/providers/ServerMultipartProvider.spec.ts +528 -0
  341. package/src/server/proxy/primitives/$proxy.spec.ts +87 -0
  342. package/src/server/rate-limit/__tests__/ActionRateLimit.spec.ts +211 -0
  343. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +344 -0
  344. package/src/server/security/__tests__/BasicAuth.spec.ts +684 -0
  345. package/src/server/security/__tests__/ServerSecurityProvider-realm.spec.ts +388 -0
  346. package/src/server/security/providers/ServerSecurityProvider.spec.ts +123 -0
  347. package/src/server/static/primitives/$serve.spec.ts +193 -0
  348. package/src/server/swagger/__tests__/ui.spec.ts +52 -0
  349. package/src/server/swagger/primitives/$swagger.spec.ts +193 -0
  350. package/src/server/swagger/providers/ServerSwaggerProvider.ts +18 -8
  351. package/src/sms/primitives/$sms.spec.ts +165 -0
  352. package/src/sms/providers/LocalSmsProvider.spec.ts +224 -0
  353. package/src/sms/providers/MemorySmsProvider.spec.ts +193 -0
  354. package/src/thread/primitives/$thread.spec.ts +186 -0
  355. package/src/topic/core/__tests__/shared.ts +144 -0
  356. package/src/topic/core/providers/MemoryTopicProvider.spec.ts +23 -0
  357. package/src/topic/redis/providers/RedisTopicProvider.spec.ts +23 -0
  358. package/src/vite/plugins/viteAlephaDev.ts +16 -4
  359. package/src/vite/tasks/runAlepha.ts +7 -1
  360. package/src/websocket/__tests__/$websocket-new.spec.ts +195 -0
  361. package/src/websocket/primitives/$channel.spec.ts +30 -0
@@ -0,0 +1,1588 @@
1
+ import { Alepha, AlephaError, t } from "alepha";
2
+ import {
3
+ LogDestinationProvider,
4
+ MemoryDestinationProvider,
5
+ } from "alepha/logger";
6
+ import { describe, expect, test, vi } from "vitest";
7
+ import { $command, CommandError, cliOptions } from "../index.ts";
8
+
9
+ describe("$command", () => {
10
+ const setupTestCommands = async (
11
+ argv?: string[],
12
+ before?: (alepha: Alepha) => any,
13
+ ) => {
14
+ const mockHandlers = {
15
+ greet: vi.fn(),
16
+ root: vi.fn(),
17
+ deploy: vi.fn(),
18
+ };
19
+
20
+ class TestCommands {
21
+ greet = $command({
22
+ description: "A simple greeting command.",
23
+ aliases: ["g"],
24
+ flags: t.object({
25
+ name: t.text({ description: "Name to greet." }),
26
+ times: t.optional(t.integer({ default: 1 })),
27
+ }),
28
+ handler: mockHandlers.greet,
29
+ });
30
+
31
+ deploy = $command({
32
+ description: "Deploys the application.",
33
+ flags: t.object({
34
+ production: t.optional(
35
+ t.boolean({ description: "Deploy to production." }),
36
+ ),
37
+ "api-key": t.text({
38
+ description: "API key for deployment.",
39
+ aliases: ["key"],
40
+ }),
41
+ }),
42
+ handler: mockHandlers.deploy,
43
+ });
44
+ }
45
+
46
+ const alepha = Alepha.create({
47
+ env: {
48
+ LOG_LEVEL: "info",
49
+ NO_COLOR: "true",
50
+ },
51
+ })
52
+ .with({
53
+ provide: LogDestinationProvider,
54
+ use: MemoryDestinationProvider,
55
+ })
56
+ .with(TestCommands);
57
+
58
+ if (argv) {
59
+ alepha.store.mut(cliOptions, (old) => ({
60
+ ...old,
61
+ argv,
62
+ }));
63
+ }
64
+
65
+ await before?.(alepha);
66
+ try {
67
+ await alepha.start();
68
+ } catch (error) {
69
+ if (error instanceof AlephaError && error.cause instanceof CommandError) {
70
+ throw error.cause;
71
+ }
72
+ throw error;
73
+ }
74
+
75
+ return {
76
+ alepha,
77
+ mockHandlers,
78
+ mockLogger: alepha.inject(MemoryDestinationProvider),
79
+ };
80
+ };
81
+
82
+ describe("Command Execution", () => {
83
+ test("should execute a matched command with correct flags", async () => {
84
+ const { mockHandlers } = await setupTestCommands([
85
+ "greet",
86
+ "--name=Alepha",
87
+ ]);
88
+
89
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
90
+ const [callArgs] = mockHandlers.greet.mock.calls[0];
91
+ expect(callArgs.flags).toEqual({ name: "Alepha", times: 1 });
92
+ expect(callArgs.run).toBeDefined();
93
+ });
94
+
95
+ test("should execute a command using its alias", async () => {
96
+ const { mockHandlers } = await setupTestCommands(["g", "--name=World"]);
97
+
98
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
99
+ expect(mockHandlers.greet.mock.calls[0][0].flags.name).toBe("World");
100
+ });
101
+
102
+ test("should execute the root command when no command name is provided", async () => {
103
+ const mockHandlers = {
104
+ root: vi.fn(),
105
+ };
106
+
107
+ await setupTestCommands([], (a) => {
108
+ a.with(
109
+ class Ext {
110
+ root = $command({
111
+ name: "", // root command has an empty name
112
+ description: "Root command",
113
+ handler: mockHandlers.root,
114
+ });
115
+ },
116
+ );
117
+ });
118
+
119
+ expect(mockHandlers.root).toHaveBeenCalledOnce();
120
+ });
121
+
122
+ test("should not execute any command if no matching command and no root command", async () => {
123
+ const { mockHandlers } = await setupTestCommands(["--some-flag"]);
124
+
125
+ expect(mockHandlers.greet).not.toHaveBeenCalled();
126
+ expect(mockHandlers.deploy).not.toHaveBeenCalled();
127
+ });
128
+ });
129
+
130
+ describe("Flag Parsing", () => {
131
+ test("should parse string and boolean flags", async () => {
132
+ const { mockHandlers } = await setupTestCommands([
133
+ "deploy",
134
+ "--production",
135
+ "--api-key=xyz-123",
136
+ ]);
137
+
138
+ expect(mockHandlers.deploy).toHaveBeenCalledOnce();
139
+ expect(mockHandlers.deploy.mock.calls[0][0].flags).toEqual({
140
+ production: true,
141
+ "api-key": "xyz-123",
142
+ });
143
+ });
144
+
145
+ test("should use flag aliases", async () => {
146
+ const { mockHandlers } = await setupTestCommands([
147
+ "deploy",
148
+ "--key=abc-456",
149
+ ]);
150
+
151
+ expect(mockHandlers.deploy).toHaveBeenCalledOnce();
152
+ expect(mockHandlers.deploy.mock.calls[0][0].flags).toEqual({
153
+ "api-key": "abc-456",
154
+ });
155
+ });
156
+
157
+ test("should apply default values for optional flags", async () => {
158
+ const { mockHandlers } = await setupTestCommands([
159
+ "greet",
160
+ "--name=Tester",
161
+ ]);
162
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
163
+ expect(mockHandlers.greet.mock.calls[0][0].flags).toEqual({
164
+ name: "Tester",
165
+ times: 1,
166
+ });
167
+ });
168
+
169
+ test("should correctly parse and cast integer flags", async () => {
170
+ const { mockHandlers } = await setupTestCommands([
171
+ "greet",
172
+ "--name=Tester",
173
+ "--times=5",
174
+ ]);
175
+
176
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
177
+ expect(mockHandlers.greet.mock.calls[0][0].flags.times).toBe(5);
178
+ });
179
+
180
+ test("should parse space-separated flag values", async () => {
181
+ const { mockHandlers } = await setupTestCommands([
182
+ "greet",
183
+ "--name",
184
+ "Alepha",
185
+ ]);
186
+
187
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
188
+ expect(mockHandlers.greet.mock.calls[0][0].flags).toEqual({
189
+ name: "Alepha",
190
+ times: 1,
191
+ });
192
+ });
193
+
194
+ test("should parse space-separated flag values with aliases", async () => {
195
+ const { mockHandlers } = await setupTestCommands([
196
+ "deploy",
197
+ "--key",
198
+ "my-secret-key",
199
+ ]);
200
+
201
+ expect(mockHandlers.deploy).toHaveBeenCalledOnce();
202
+ expect(mockHandlers.deploy.mock.calls[0][0].flags).toEqual({
203
+ "api-key": "my-secret-key",
204
+ });
205
+ });
206
+
207
+ test("should mix space-separated and equals syntax", async () => {
208
+ const { mockHandlers } = await setupTestCommands([
209
+ "greet",
210
+ "--name",
211
+ "World",
212
+ "--times=3",
213
+ ]);
214
+
215
+ expect(mockHandlers.greet).toHaveBeenCalledOnce();
216
+ expect(mockHandlers.greet.mock.calls[0][0].flags).toEqual({
217
+ name: "World",
218
+ times: 3,
219
+ });
220
+ });
221
+
222
+ test("should parse space-separated with boolean flag before it", async () => {
223
+ const { mockHandlers } = await setupTestCommands([
224
+ "deploy",
225
+ "--production",
226
+ "--api-key",
227
+ "xyz-123",
228
+ ]);
229
+
230
+ expect(mockHandlers.deploy).toHaveBeenCalledOnce();
231
+ expect(mockHandlers.deploy.mock.calls[0][0].flags).toEqual({
232
+ production: true,
233
+ "api-key": "xyz-123",
234
+ });
235
+ });
236
+
237
+ test("should handle space-separated with short flag alias", async () => {
238
+ const mockHandler = vi.fn();
239
+
240
+ class TestCommand {
241
+ cmd = $command({
242
+ description: "Test command",
243
+ flags: t.object({
244
+ output: t.text({ alias: "o" }),
245
+ }),
246
+ handler: mockHandler,
247
+ });
248
+ }
249
+
250
+ await setupTestCommands(["cmd", "-o", "output.txt"], (a) =>
251
+ a.with(TestCommand),
252
+ );
253
+
254
+ expect(mockHandler).toHaveBeenCalledOnce();
255
+ expect(mockHandler.mock.calls[0][0].flags).toEqual({
256
+ output: "output.txt",
257
+ });
258
+ });
259
+
260
+ test("should parse JSON object with space-separated syntax", async () => {
261
+ const mockHandler = vi.fn();
262
+
263
+ class TestCommand {
264
+ cmd = $command({
265
+ description: "Test command with JSON flag",
266
+ flags: t.object({
267
+ data: t.object({
268
+ name: t.string(),
269
+ age: t.number(),
270
+ }),
271
+ }),
272
+ handler: mockHandler,
273
+ });
274
+ }
275
+
276
+ await setupTestCommands(
277
+ ["cmd", "--data", '{"name":"John","age":30}'],
278
+ (a) => a.with(TestCommand),
279
+ );
280
+
281
+ expect(mockHandler).toHaveBeenCalledOnce();
282
+ expect(mockHandler.mock.calls[0][0].flags).toEqual({
283
+ data: { name: "John", age: 30 },
284
+ });
285
+ });
286
+
287
+ test("should parse JSON array with space-separated syntax", async () => {
288
+ const mockHandler = vi.fn();
289
+
290
+ class TestCommand {
291
+ cmd = $command({
292
+ description: "Test command with array flag",
293
+ flags: t.object({
294
+ items: t.array(t.string()),
295
+ }),
296
+ handler: mockHandler,
297
+ });
298
+ }
299
+
300
+ await setupTestCommands(["cmd", "--items", '["a","b","c"]'], (a) =>
301
+ a.with(TestCommand),
302
+ );
303
+
304
+ expect(mockHandler).toHaveBeenCalledOnce();
305
+ expect(mockHandler.mock.calls[0][0].flags).toEqual({
306
+ items: ["a", "b", "c"],
307
+ });
308
+ });
309
+
310
+ test("should throw error when space-separated value is missing (next arg is a flag)", async () => {
311
+ await expect(() =>
312
+ setupTestCommands(["greet", "--name", "--times=5"]),
313
+ ).rejects.toThrow("Flag --name requires a value");
314
+ });
315
+
316
+ test("should throw error when space-separated value is missing (end of args)", async () => {
317
+ await expect(() =>
318
+ setupTestCommands(["greet", "--name"]),
319
+ ).rejects.toThrow("Flag --name requires a value");
320
+ });
321
+ });
322
+
323
+ describe("Error Handling", () => {
324
+ test("should log an error for an unknown command", async () => {
325
+ const { mockLogger } = await setupTestCommands(["non-existent-command"]);
326
+
327
+ const errorLog = mockLogger.logs.find((l) => l.level === "ERROR");
328
+ expect(errorLog).toBeDefined();
329
+ expect(errorLog?.message).toBe("Unknown command: 'non-existent-command'");
330
+ // It should also print help
331
+ expect(mockLogger.logs.some((l) => l.message.includes("Commands:"))).toBe(
332
+ true,
333
+ );
334
+ });
335
+
336
+ test("should throw a CommandError for missing flag values", async () => {
337
+ await expect(() =>
338
+ setupTestCommands(["greet", "--name"]),
339
+ ).rejects.toThrow("Flag --name requires a value");
340
+ });
341
+
342
+ test("should throw a CommandError for invalid flag types", async () => {
343
+ await expect(() =>
344
+ setupTestCommands(["greet", "--name=Test", "--times=not-a-number"]),
345
+ ).rejects.toThrow("Invalid flag: /times must be integer");
346
+ });
347
+ });
348
+
349
+ describe("Help Message", () => {
350
+ test("should print general help with --help flag", async () => {
351
+ const { mockLogger } = await setupTestCommands(["--help"], (alepha) => {
352
+ alepha.store.mut(cliOptions, (old) => ({
353
+ ...old,
354
+ name: "my-cli",
355
+ description: "My awesome CLI tool.",
356
+ }));
357
+ });
358
+
359
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
360
+ expect(output).toContain("My awesome CLI tool.");
361
+ expect(output).toContain("Commands:");
362
+ expect(output).toContain("my-cli greet, g");
363
+ expect(output).toContain("my-cli deploy");
364
+ expect(output).toContain("Flags:");
365
+ expect(output).toContain("-h, --help");
366
+ });
367
+
368
+ test("should print command-specific help", async () => {
369
+ const { mockLogger } = await setupTestCommands(
370
+ ["greet", "-h"],
371
+ (alepha) =>
372
+ alepha.store.mut(cliOptions, (old) => ({
373
+ ...old,
374
+ name: "my-cli",
375
+ })),
376
+ );
377
+
378
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
379
+ expect(output).toContain("Usage: my-cli greet");
380
+ expect(output).toContain("A simple greeting command.");
381
+ expect(output).toContain("Flags:");
382
+ expect(output).toContain("--name");
383
+ expect(output).toContain("--times");
384
+ expect(output).toContain("-h, --help");
385
+ });
386
+ });
387
+
388
+ describe("Arguments Parsing", () => {
389
+ test("should parse single string argument", async () => {
390
+ const mockHandler = vi.fn();
391
+
392
+ class TestCommand {
393
+ cmd = $command({
394
+ description: "Test command with string arg",
395
+ args: t.text(),
396
+ handler: mockHandler,
397
+ });
398
+ }
399
+
400
+ const { alepha } = await setupTestCommands(["cmd", "hello"], (a) =>
401
+ a.with(TestCommand),
402
+ );
403
+
404
+ expect(mockHandler).toHaveBeenCalledOnce();
405
+ const [callArgs] = mockHandler.mock.calls[0];
406
+ expect(callArgs.args).toBe("hello");
407
+ });
408
+
409
+ test("should parse single number argument", async () => {
410
+ const mockHandler = vi.fn();
411
+
412
+ class TestCommand {
413
+ cmd = $command({
414
+ description: "Test command with number arg",
415
+ args: t.number(),
416
+ handler: mockHandler,
417
+ });
418
+ }
419
+
420
+ await setupTestCommands(["cmd", "42.5"], (a) => a.with(TestCommand));
421
+
422
+ expect(mockHandler).toHaveBeenCalledOnce();
423
+ expect(mockHandler.mock.calls[0][0].args).toBe(42.5);
424
+ });
425
+
426
+ test("should parse single integer argument", async () => {
427
+ const mockHandler = vi.fn();
428
+
429
+ class TestCommand {
430
+ cmd = $command({
431
+ description: "Test command with integer arg",
432
+ args: t.integer(),
433
+ handler: mockHandler,
434
+ });
435
+ }
436
+
437
+ await setupTestCommands(["cmd", "42"], (a) => a.with(TestCommand));
438
+
439
+ expect(mockHandler).toHaveBeenCalledOnce();
440
+ expect(mockHandler.mock.calls[0][0].args).toBe(42);
441
+ });
442
+
443
+ test("should parse single boolean argument", async () => {
444
+ const mockHandler = vi.fn();
445
+
446
+ class TestCommand {
447
+ cmd = $command({
448
+ description: "Test command with boolean arg",
449
+ args: t.boolean(),
450
+ handler: mockHandler,
451
+ });
452
+ }
453
+
454
+ await setupTestCommands(["cmd", "true"], (a) => a.with(TestCommand));
455
+
456
+ expect(mockHandler).toHaveBeenCalledOnce();
457
+ expect(mockHandler.mock.calls[0][0].args).toBe(true);
458
+
459
+ mockHandler.mockClear();
460
+ await setupTestCommands(["cmd", "false"], (a) => a.with(TestCommand));
461
+ expect(mockHandler.mock.calls[0][0].args).toBe(false);
462
+ });
463
+
464
+ test("should handle optional string argument when provided", async () => {
465
+ const mockHandler = vi.fn();
466
+
467
+ class TestCommand {
468
+ cmd = $command({
469
+ description: "Test command with optional string arg",
470
+ args: t.optional(t.text()),
471
+ handler: mockHandler,
472
+ });
473
+ }
474
+
475
+ await setupTestCommands(["cmd", "optional-value"], (a) =>
476
+ a.with(TestCommand),
477
+ );
478
+
479
+ expect(mockHandler).toHaveBeenCalledOnce();
480
+ expect(mockHandler.mock.calls[0][0].args).toBe("optional-value");
481
+ });
482
+
483
+ test("should handle optional string argument when not provided", async () => {
484
+ const mockHandler = vi.fn();
485
+
486
+ class TestCommand {
487
+ cmd = $command({
488
+ description: "Test command with optional string arg",
489
+ args: t.optional(t.text()),
490
+ handler: mockHandler,
491
+ });
492
+ }
493
+
494
+ await setupTestCommands(["cmd"], (a) => a.with(TestCommand));
495
+
496
+ expect(mockHandler).toHaveBeenCalledOnce();
497
+ expect(mockHandler.mock.calls[0][0].args).toBeUndefined();
498
+ });
499
+
500
+ test("should parse tuple arguments with multiple types", async () => {
501
+ const mockHandler = vi.fn();
502
+
503
+ class TestCommand {
504
+ cmd = $command({
505
+ description: "Test command with tuple args",
506
+ args: t.tuple([t.text(), t.number()]),
507
+ handler: mockHandler,
508
+ });
509
+ }
510
+
511
+ await setupTestCommands(["cmd", "hello", "42"], (a) =>
512
+ a.with(TestCommand),
513
+ );
514
+
515
+ expect(mockHandler).toHaveBeenCalledOnce();
516
+ expect(mockHandler.mock.calls[0][0].args).toEqual(["hello", 42]);
517
+ });
518
+
519
+ test("should parse tuple with optional arguments", async () => {
520
+ const mockHandler = vi.fn();
521
+
522
+ class TestCommand {
523
+ cmd = $command({
524
+ description: "Test command with tuple containing optional arg",
525
+ args: t.tuple([t.text(), t.optional(t.number())]),
526
+ handler: mockHandler,
527
+ });
528
+ }
529
+
530
+ await setupTestCommands(["cmd", "hello"], (a) => a.with(TestCommand));
531
+
532
+ expect(mockHandler).toHaveBeenCalledOnce();
533
+ expect(mockHandler.mock.calls[0][0].args).toEqual(["hello", undefined]);
534
+ });
535
+
536
+ test("should handle arguments with flags mixed in", async () => {
537
+ const mockHandler = vi.fn();
538
+
539
+ class TestCommand {
540
+ cmd = $command({
541
+ description: "Test command with args and flags",
542
+ flags: t.object({
543
+ verbose: t.optional(t.boolean()),
544
+ }),
545
+ args: t.tuple([t.text(), t.number()]),
546
+ handler: mockHandler,
547
+ });
548
+ }
549
+
550
+ await setupTestCommands(["cmd", "--verbose", "hello", "42"], (a) =>
551
+ a.with(TestCommand),
552
+ );
553
+
554
+ expect(mockHandler).toHaveBeenCalledOnce();
555
+ const [callArgs] = mockHandler.mock.calls[0];
556
+ expect(callArgs.flags).toEqual({ verbose: true });
557
+ expect(callArgs.args).toEqual(["hello", 42]);
558
+ });
559
+
560
+ test("should handle arguments with space-separated flags", async () => {
561
+ const mockHandler = vi.fn();
562
+
563
+ class TestCommand {
564
+ cmd = $command({
565
+ description: "Test command with args and space-separated flags",
566
+ flags: t.object({
567
+ output: t.text(),
568
+ }),
569
+ args: t.text(),
570
+ handler: mockHandler,
571
+ });
572
+ }
573
+
574
+ await setupTestCommands(["cmd", "--output", "file.txt", "my-arg"], (a) =>
575
+ a.with(TestCommand),
576
+ );
577
+
578
+ expect(mockHandler).toHaveBeenCalledOnce();
579
+ const [callArgs] = mockHandler.mock.calls[0];
580
+ expect(callArgs.flags).toEqual({ output: "file.txt" });
581
+ expect(callArgs.args).toBe("my-arg");
582
+ });
583
+
584
+ test("should handle tuple arguments with space-separated flags interspersed", async () => {
585
+ const mockHandler = vi.fn();
586
+
587
+ class TestCommand {
588
+ cmd = $command({
589
+ description: "Test command with tuple args and space-separated flags",
590
+ flags: t.object({
591
+ config: t.text(),
592
+ verbose: t.optional(t.boolean()),
593
+ }),
594
+ args: t.tuple([t.text(), t.number()]),
595
+ handler: mockHandler,
596
+ });
597
+ }
598
+
599
+ await setupTestCommands(
600
+ ["cmd", "--config", "app.json", "hello", "--verbose", "42"],
601
+ (a) => a.with(TestCommand),
602
+ );
603
+
604
+ expect(mockHandler).toHaveBeenCalledOnce();
605
+ const [callArgs] = mockHandler.mock.calls[0];
606
+ expect(callArgs.flags).toEqual({ config: "app.json", verbose: true });
607
+ expect(callArgs.args).toEqual(["hello", 42]);
608
+ });
609
+
610
+ test("should pass fs and glob to handler", async () => {
611
+ const mockHandler = vi.fn();
612
+
613
+ class TestCommand {
614
+ cmd = $command({
615
+ description: "Test command to check fs and glob",
616
+ handler: mockHandler,
617
+ });
618
+ }
619
+
620
+ await setupTestCommands(["cmd"], (a) => a.with(TestCommand));
621
+
622
+ expect(mockHandler).toHaveBeenCalledOnce();
623
+ const [callArgs] = mockHandler.mock.calls[0];
624
+ expect(callArgs.fs).toBeDefined();
625
+ expect(callArgs.glob).toBeDefined();
626
+ });
627
+ });
628
+
629
+ describe("Arguments Error Handling", () => {
630
+ test("should throw error for missing required argument", async () => {
631
+ class TestCommand {
632
+ cmd = $command({
633
+ description: "Test command with required arg",
634
+ args: t.text(),
635
+ handler: vi.fn(),
636
+ });
637
+ }
638
+
639
+ await expect(() =>
640
+ setupTestCommands(["cmd"], (a) => a.with(TestCommand)),
641
+ ).rejects.toThrow("Missing required argument");
642
+ });
643
+
644
+ test("should throw error for invalid number argument", async () => {
645
+ class TestCommand {
646
+ cmd = $command({
647
+ description: "Test command with number arg",
648
+ args: t.number(),
649
+ handler: vi.fn(),
650
+ });
651
+ }
652
+
653
+ await expect(() =>
654
+ setupTestCommands(["cmd", "not-a-number"], (a) => a.with(TestCommand)),
655
+ ).rejects.toThrow('Expected number, got "not-a-number"');
656
+ });
657
+
658
+ test("should throw error for invalid integer argument", async () => {
659
+ class TestCommand {
660
+ cmd = $command({
661
+ description: "Test command with integer arg",
662
+ args: t.integer(),
663
+ handler: vi.fn(),
664
+ });
665
+ }
666
+
667
+ await expect(() =>
668
+ setupTestCommands(["cmd", "42.5"], (a) => a.with(TestCommand)),
669
+ ).rejects.toThrow('Expected integer, got "42.5"');
670
+ });
671
+
672
+ test("should throw error for invalid boolean argument", async () => {
673
+ class TestCommand {
674
+ cmd = $command({
675
+ description: "Test command with boolean arg",
676
+ args: t.boolean(),
677
+ handler: vi.fn(),
678
+ });
679
+ }
680
+
681
+ await expect(() =>
682
+ setupTestCommands(["cmd", "not-a-boolean"], (a) => a.with(TestCommand)),
683
+ ).rejects.toThrow('Expected boolean, got "not-a-boolean"');
684
+ });
685
+
686
+ test("should throw error for missing required tuple argument", async () => {
687
+ class TestCommand {
688
+ cmd = $command({
689
+ description: "Test command with tuple args",
690
+ args: t.tuple([t.text(), t.number()]),
691
+ handler: vi.fn(),
692
+ });
693
+ }
694
+
695
+ await expect(() =>
696
+ setupTestCommands(["cmd", "hello"], (a) => a.with(TestCommand)),
697
+ ).rejects.toThrow("Missing required argument at position 2");
698
+ });
699
+ });
700
+
701
+ describe("Root Command with Arguments", () => {
702
+ test("should parse single string argument for root command", async () => {
703
+ const mockHandler = vi.fn();
704
+
705
+ class RootCommand {
706
+ root = $command({
707
+ name: "",
708
+ description: "Root command with string arg",
709
+ args: t.text(),
710
+ handler: mockHandler,
711
+ });
712
+ }
713
+
714
+ await setupTestCommands(["hello"], (a) => a.with(RootCommand));
715
+
716
+ expect(mockHandler).toHaveBeenCalledOnce();
717
+ const [callArgs] = mockHandler.mock.calls[0];
718
+ expect(callArgs.args).toBe("hello");
719
+ });
720
+
721
+ test("should parse optional argument for root command when provided", async () => {
722
+ const mockHandler = vi.fn();
723
+
724
+ class RootCommand {
725
+ root = $command({
726
+ name: "",
727
+ description: "Root command with optional arg",
728
+ args: t.optional(t.text()),
729
+ handler: mockHandler,
730
+ });
731
+ }
732
+
733
+ await setupTestCommands(["my-value"], (a) => a.with(RootCommand));
734
+
735
+ expect(mockHandler).toHaveBeenCalledOnce();
736
+ expect(mockHandler.mock.calls[0][0].args).toBe("my-value");
737
+ });
738
+
739
+ test("should handle optional argument for root command when not provided", async () => {
740
+ const mockHandler = vi.fn();
741
+
742
+ class RootCommand {
743
+ root = $command({
744
+ name: "",
745
+ description: "Root command with optional arg",
746
+ args: t.optional(t.text()),
747
+ handler: mockHandler,
748
+ });
749
+ }
750
+
751
+ await setupTestCommands([], (a) => a.with(RootCommand));
752
+
753
+ expect(mockHandler).toHaveBeenCalledOnce();
754
+ expect(mockHandler.mock.calls[0][0].args).toBeUndefined();
755
+ });
756
+
757
+ test("should parse tuple arguments for root command", async () => {
758
+ const mockHandler = vi.fn();
759
+
760
+ class RootCommand {
761
+ root = $command({
762
+ name: "",
763
+ description: "Root command with tuple args",
764
+ args: t.tuple([t.text(), t.number()]),
765
+ handler: mockHandler,
766
+ });
767
+ }
768
+
769
+ await setupTestCommands(["hello", "42"], (a) => a.with(RootCommand));
770
+
771
+ expect(mockHandler).toHaveBeenCalledOnce();
772
+ expect(mockHandler.mock.calls[0][0].args).toEqual(["hello", 42]);
773
+ });
774
+
775
+ test("should parse root command args with flags mixed in", async () => {
776
+ const mockHandler = vi.fn();
777
+
778
+ class RootCommand {
779
+ root = $command({
780
+ name: "",
781
+ description: "Root command with args and flags",
782
+ flags: t.object({
783
+ verbose: t.optional(t.boolean()),
784
+ }),
785
+ args: t.text(),
786
+ handler: mockHandler,
787
+ });
788
+ }
789
+
790
+ await setupTestCommands(["--verbose", "my-arg"], (a) =>
791
+ a.with(RootCommand),
792
+ );
793
+
794
+ expect(mockHandler).toHaveBeenCalledOnce();
795
+ const [callArgs] = mockHandler.mock.calls[0];
796
+ expect(callArgs.flags).toEqual({ verbose: true });
797
+ expect(callArgs.args).toBe("my-arg");
798
+ });
799
+
800
+ test("should parse root command with space-separated flags and args", async () => {
801
+ const mockHandler = vi.fn();
802
+
803
+ class RootCommand {
804
+ root = $command({
805
+ name: "",
806
+ description: "Root command with space-separated flags",
807
+ flags: t.object({
808
+ config: t.text(),
809
+ }),
810
+ args: t.text(),
811
+ handler: mockHandler,
812
+ });
813
+ }
814
+
815
+ await setupTestCommands(["--config", "app.json", "my-arg"], (a) =>
816
+ a.with(RootCommand),
817
+ );
818
+
819
+ expect(mockHandler).toHaveBeenCalledOnce();
820
+ const [callArgs] = mockHandler.mock.calls[0];
821
+ expect(callArgs.flags).toEqual({ config: "app.json" });
822
+ expect(callArgs.args).toBe("my-arg");
823
+ });
824
+
825
+ test("should throw error for missing required root command argument", async () => {
826
+ class RootCommand {
827
+ root = $command({
828
+ name: "",
829
+ description: "Root command with required arg",
830
+ args: t.text(),
831
+ handler: vi.fn(),
832
+ });
833
+ }
834
+
835
+ await expect(() =>
836
+ setupTestCommands([], (a) => a.with(RootCommand)),
837
+ ).rejects.toThrow("Missing required argument");
838
+ });
839
+ });
840
+
841
+ describe("Root Command Global Flags", () => {
842
+ test("should display root command flags as global flags in help", async () => {
843
+ const mockRootHandler = vi.fn();
844
+
845
+ class RootCommand {
846
+ root = $command({
847
+ name: "",
848
+ description: "Root command",
849
+ flags: t.object({
850
+ verbose: t.optional(
851
+ t.boolean({ description: "Enable verbose output." }),
852
+ ),
853
+ config: t.optional(
854
+ t.text({
855
+ description: "Path to config file.",
856
+ alias: "c",
857
+ }),
858
+ ),
859
+ }),
860
+ handler: mockRootHandler,
861
+ });
862
+ }
863
+
864
+ const { mockLogger } = await setupTestCommands(["--help"], (alepha) => {
865
+ alepha.store.mut(cliOptions, (old) => ({
866
+ ...old,
867
+ name: "my-cli",
868
+ description: "My awesome CLI tool.",
869
+ }));
870
+ alepha.with(RootCommand);
871
+ });
872
+
873
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
874
+ expect(output).toContain("Flags:");
875
+ expect(output).toContain("--verbose");
876
+ expect(output).toContain("Enable verbose output.");
877
+ expect(output).toContain("--config");
878
+ expect(output).toContain("Path to config file.");
879
+ expect(output).toContain("-h, --help");
880
+ });
881
+
882
+ test("should parse root command flags correctly", async () => {
883
+ const mockRootHandler = vi.fn();
884
+
885
+ class RootCommand {
886
+ root = $command({
887
+ name: "",
888
+ description: "Root command",
889
+ flags: t.object({
890
+ verbose: t.optional(t.boolean({ description: "Verbose mode." })),
891
+ output: t.text({ description: "Output file." }),
892
+ }),
893
+ handler: mockRootHandler,
894
+ });
895
+ }
896
+
897
+ await setupTestCommands(["--verbose", "--output=test.txt"], (alepha) => {
898
+ alepha.with(RootCommand);
899
+ });
900
+
901
+ expect(mockRootHandler).toHaveBeenCalledOnce();
902
+ const [callArgs] = mockRootHandler.mock.calls[0];
903
+ expect(callArgs.flags).toEqual({
904
+ verbose: true,
905
+ output: "test.txt",
906
+ });
907
+ });
908
+ });
909
+
910
+ describe("Help Message with Arguments", () => {
911
+ test("should show argument usage in help for single string argument", async () => {
912
+ class TestCommand {
913
+ cmd = $command({
914
+ description: "Test command with string arg",
915
+ args: t.text(),
916
+ handler: vi.fn(),
917
+ });
918
+ }
919
+
920
+ const { mockLogger } = await setupTestCommands(
921
+ ["cmd", "--help"],
922
+ (alepha) => {
923
+ alepha.store.mut(cliOptions, (old) => ({
924
+ ...old,
925
+ name: "test-cli",
926
+ }));
927
+ alepha.with(TestCommand);
928
+ },
929
+ );
930
+
931
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
932
+ expect(output).toContain("Usage: test-cli cmd <arg1>");
933
+ });
934
+
935
+ test("should show argument usage in help for optional argument", async () => {
936
+ class TestCommand {
937
+ cmd = $command({
938
+ description: "Test command with optional arg",
939
+ args: t.optional(t.text()),
940
+ handler: vi.fn(),
941
+ });
942
+ }
943
+
944
+ const { mockLogger } = await setupTestCommands(
945
+ ["cmd", "--help"],
946
+ (alepha) => {
947
+ alepha.store.mut(cliOptions, (old) => ({
948
+ ...old,
949
+ name: "test-cli",
950
+ }));
951
+ alepha.with(TestCommand);
952
+ },
953
+ );
954
+
955
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
956
+ expect(output).toContain("Usage: test-cli cmd [arg1]");
957
+ });
958
+
959
+ test("should show argument usage in help for tuple arguments", async () => {
960
+ class TestCommand {
961
+ cmd = $command({
962
+ description: "Test command with tuple args",
963
+ args: t.tuple([t.text(), t.optional(t.number())]),
964
+ handler: vi.fn(),
965
+ });
966
+ }
967
+
968
+ const { mockLogger } = await setupTestCommands(
969
+ ["cmd", "--help"],
970
+ (alepha) => {
971
+ alepha.store.mut(cliOptions, (old) => ({
972
+ ...old,
973
+ name: "test-cli",
974
+ }));
975
+ alepha.with(TestCommand);
976
+ },
977
+ );
978
+
979
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
980
+ expect(output).toContain("Usage: test-cli cmd <arg1> [arg2: number]");
981
+ });
982
+ });
983
+
984
+ describe("Subcommands", () => {
985
+ test("should execute subcommand with space-separated syntax", async () => {
986
+ const mockHandlers = {
987
+ parent: vi.fn(),
988
+ child: vi.fn(),
989
+ };
990
+
991
+ class TestCommands {
992
+ child = $command({
993
+ name: "vercel",
994
+ description: "Deploy to Vercel",
995
+ handler: mockHandlers.child,
996
+ });
997
+
998
+ parent = $command({
999
+ name: "deploy",
1000
+ description: "Deploy the application",
1001
+ children: [this.child],
1002
+ handler: mockHandlers.parent,
1003
+ });
1004
+ }
1005
+
1006
+ await setupTestCommands(["deploy", "vercel"], (a) =>
1007
+ a.with(TestCommands),
1008
+ );
1009
+
1010
+ expect(mockHandlers.child).toHaveBeenCalledOnce();
1011
+ expect(mockHandlers.parent).not.toHaveBeenCalled();
1012
+ });
1013
+
1014
+ test("should execute parent handler when subcommand is not provided", async () => {
1015
+ const mockHandlers = {
1016
+ parent: vi.fn(),
1017
+ child: vi.fn(),
1018
+ };
1019
+
1020
+ class TestCommands {
1021
+ child = $command({
1022
+ name: "vercel",
1023
+ description: "Deploy to Vercel",
1024
+ handler: mockHandlers.child,
1025
+ });
1026
+
1027
+ parent = $command({
1028
+ name: "deploy",
1029
+ description: "Deploy the application",
1030
+ children: [this.child],
1031
+ handler: mockHandlers.parent,
1032
+ });
1033
+ }
1034
+
1035
+ await setupTestCommands(["deploy"], (a) => a.with(TestCommands));
1036
+
1037
+ expect(mockHandlers.parent).toHaveBeenCalledOnce();
1038
+ expect(mockHandlers.child).not.toHaveBeenCalled();
1039
+ });
1040
+
1041
+ test("should provide help() function in handler args", async () => {
1042
+ let helpFn: (() => void) | undefined;
1043
+
1044
+ class TestCommands {
1045
+ child = $command({
1046
+ name: "vercel",
1047
+ description: "Deploy to Vercel",
1048
+ handler: vi.fn(),
1049
+ });
1050
+
1051
+ parent = $command({
1052
+ name: "deploy",
1053
+ description: "Deploy the application",
1054
+ children: [this.child],
1055
+ handler: ({ help }) => {
1056
+ helpFn = help;
1057
+ help();
1058
+ },
1059
+ });
1060
+ }
1061
+
1062
+ const { mockLogger } = await setupTestCommands(["deploy"], (alepha) => {
1063
+ alepha.store.mut(cliOptions, (old) => ({
1064
+ ...old,
1065
+ name: "my-cli",
1066
+ }));
1067
+ alepha.with(TestCommands);
1068
+ });
1069
+
1070
+ expect(helpFn).toBeDefined();
1071
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1072
+ expect(output).toContain("Usage: my-cli deploy <command>");
1073
+ expect(output).toContain("Commands:");
1074
+ expect(output).toContain("vercel");
1075
+ });
1076
+
1077
+ test("should pass flags to subcommand", async () => {
1078
+ const mockHandler = vi.fn();
1079
+
1080
+ class TestCommands {
1081
+ child = $command({
1082
+ name: "vercel",
1083
+ description: "Deploy to Vercel",
1084
+ flags: t.object({
1085
+ prod: t.optional(t.boolean()),
1086
+ }),
1087
+ handler: mockHandler,
1088
+ });
1089
+
1090
+ parent = $command({
1091
+ name: "deploy",
1092
+ description: "Deploy the application",
1093
+ children: [this.child],
1094
+ handler: vi.fn(),
1095
+ });
1096
+ }
1097
+
1098
+ await setupTestCommands(["deploy", "vercel", "--prod"], (a) =>
1099
+ a.with(TestCommands),
1100
+ );
1101
+
1102
+ expect(mockHandler).toHaveBeenCalledOnce();
1103
+ expect(mockHandler.mock.calls[0][0].flags).toEqual({ prod: true });
1104
+ });
1105
+
1106
+ test("should pass arguments to subcommand", async () => {
1107
+ const mockHandler = vi.fn();
1108
+
1109
+ class TestCommands {
1110
+ child = $command({
1111
+ name: "surge",
1112
+ description: "Deploy to Surge",
1113
+ args: t.optional(t.text()),
1114
+ handler: mockHandler,
1115
+ });
1116
+
1117
+ parent = $command({
1118
+ name: "deploy",
1119
+ description: "Deploy the application",
1120
+ children: [this.child],
1121
+ handler: vi.fn(),
1122
+ });
1123
+ }
1124
+
1125
+ await setupTestCommands(["deploy", "surge", "my-domain.surge.sh"], (a) =>
1126
+ a.with(TestCommands),
1127
+ );
1128
+
1129
+ expect(mockHandler).toHaveBeenCalledOnce();
1130
+ expect(mockHandler.mock.calls[0][0].args).toBe("my-domain.surge.sh");
1131
+ });
1132
+
1133
+ test("should show subcommands in help output", async () => {
1134
+ class TestCommands {
1135
+ child1 = $command({
1136
+ name: "vercel",
1137
+ description: "Deploy to Vercel",
1138
+ handler: vi.fn(),
1139
+ });
1140
+
1141
+ child2 = $command({
1142
+ name: "cloudflare",
1143
+ description: "Deploy to Cloudflare",
1144
+ handler: vi.fn(),
1145
+ });
1146
+
1147
+ parent = $command({
1148
+ name: "deploy",
1149
+ description: "Deploy the application",
1150
+ children: [this.child1, this.child2],
1151
+ handler: vi.fn(),
1152
+ });
1153
+ }
1154
+
1155
+ const { mockLogger } = await setupTestCommands(
1156
+ ["deploy", "--help"],
1157
+ (alepha) => {
1158
+ alepha.store.mut(cliOptions, (old) => ({
1159
+ ...old,
1160
+ name: "my-cli",
1161
+ }));
1162
+ alepha.with(TestCommands);
1163
+ },
1164
+ );
1165
+
1166
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1167
+ expect(output).toContain("Usage: my-cli deploy <command>");
1168
+ expect(output).toContain("Commands:");
1169
+ expect(output).toContain("my-cli deploy vercel");
1170
+ expect(output).toContain("my-cli deploy cloudflare");
1171
+ expect(output).toContain("Deploy to Vercel");
1172
+ expect(output).toContain("Deploy to Cloudflare");
1173
+ });
1174
+
1175
+ test("should support colon notation for backwards compatibility", async () => {
1176
+ const mockHandler = vi.fn();
1177
+
1178
+ class TestCommands {
1179
+ legacyCommand = $command({
1180
+ name: "db:migrate",
1181
+ description: "Run database migrations",
1182
+ handler: mockHandler,
1183
+ });
1184
+ }
1185
+
1186
+ await setupTestCommands(["db:migrate"], (a) => a.with(TestCommands));
1187
+
1188
+ expect(mockHandler).toHaveBeenCalledOnce();
1189
+ });
1190
+
1191
+ test("should not show child commands in top-level help", async () => {
1192
+ class TestCommands {
1193
+ child = $command({
1194
+ name: "vercel",
1195
+ description: "Deploy to Vercel",
1196
+ handler: vi.fn(),
1197
+ });
1198
+
1199
+ parent = $command({
1200
+ name: "deploy",
1201
+ description: "Deploy the application",
1202
+ children: [this.child],
1203
+ handler: vi.fn(),
1204
+ });
1205
+ }
1206
+
1207
+ const { mockLogger } = await setupTestCommands(["--help"], (alepha) => {
1208
+ alepha.store.mut(cliOptions, (old) => ({
1209
+ ...old,
1210
+ name: "my-cli",
1211
+ }));
1212
+ alepha.with(TestCommands);
1213
+ });
1214
+
1215
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1216
+ expect(output).toContain("my-cli deploy <command>");
1217
+ // vercel should not be listed as a top-level command
1218
+ expect(output).not.toMatch(/^\s*my-cli vercel/m);
1219
+ });
1220
+
1221
+ test("should execute subcommand by alias", async () => {
1222
+ const mockHandler = vi.fn();
1223
+
1224
+ class TestCommands {
1225
+ child = $command({
1226
+ name: "vercel",
1227
+ aliases: ["v"],
1228
+ description: "Deploy to Vercel",
1229
+ handler: mockHandler,
1230
+ });
1231
+
1232
+ parent = $command({
1233
+ name: "deploy",
1234
+ description: "Deploy the application",
1235
+ children: [this.child],
1236
+ handler: vi.fn(),
1237
+ });
1238
+ }
1239
+
1240
+ await setupTestCommands(["deploy", "v"], (a) => a.with(TestCommands));
1241
+
1242
+ expect(mockHandler).toHaveBeenCalledOnce();
1243
+ });
1244
+ });
1245
+
1246
+ describe("Environment Variables", () => {
1247
+ test("should pass validated env vars to handler", async () => {
1248
+ let receivedEnv: any;
1249
+
1250
+ class TestCommands {
1251
+ cmd = $command({
1252
+ name: "test",
1253
+ env: t.object({
1254
+ MY_TOKEN: t.text({ description: "API token" }),
1255
+ }),
1256
+ handler: ({ env }) => {
1257
+ receivedEnv = env;
1258
+ },
1259
+ });
1260
+ }
1261
+
1262
+ // Set env var before running
1263
+ process.env.MY_TOKEN = "secret123";
1264
+
1265
+ try {
1266
+ await setupTestCommands(["test"], (a) => a.with(TestCommands));
1267
+ expect(receivedEnv).toEqual({ MY_TOKEN: "secret123" });
1268
+ } finally {
1269
+ delete process.env.MY_TOKEN;
1270
+ }
1271
+ });
1272
+
1273
+ test("should throw error for missing required env var", async () => {
1274
+ class TestCommands {
1275
+ cmd = $command({
1276
+ name: "test",
1277
+ env: t.object({
1278
+ REQUIRED_VAR: t.text({ description: "Required variable" }),
1279
+ }),
1280
+ handler: vi.fn(),
1281
+ });
1282
+ }
1283
+
1284
+ // Ensure env var is not set
1285
+ delete process.env.REQUIRED_VAR;
1286
+
1287
+ await expect(
1288
+ setupTestCommands(["test"], (a) => a.with(TestCommands)),
1289
+ ).rejects.toThrow("Missing required environment variable: REQUIRED_VAR");
1290
+ });
1291
+
1292
+ test("should handle optional env vars", async () => {
1293
+ let receivedEnv: any;
1294
+
1295
+ class TestCommands {
1296
+ cmd = $command({
1297
+ name: "test",
1298
+ env: t.object({
1299
+ OPTIONAL_VAR: t.optional(t.text({ description: "Optional" })),
1300
+ }),
1301
+ handler: ({ env }) => {
1302
+ receivedEnv = env;
1303
+ },
1304
+ });
1305
+ }
1306
+
1307
+ // Ensure env var is not set
1308
+ delete process.env.OPTIONAL_VAR;
1309
+
1310
+ await setupTestCommands(["test"], (a) => a.with(TestCommands));
1311
+ expect(receivedEnv).toEqual({});
1312
+ });
1313
+
1314
+ test("should handle optional env vars with defaults", async () => {
1315
+ let receivedEnv: any;
1316
+
1317
+ class TestCommands {
1318
+ cmd = $command({
1319
+ name: "test",
1320
+ env: t.object({
1321
+ MY_VAR: t.optional(t.text({ default: "default_value" })),
1322
+ }),
1323
+ handler: ({ env }) => {
1324
+ receivedEnv = env;
1325
+ },
1326
+ });
1327
+ }
1328
+
1329
+ // Ensure env var is not set
1330
+ delete process.env.MY_VAR;
1331
+
1332
+ await setupTestCommands(["test"], (a) => a.with(TestCommands));
1333
+ expect(receivedEnv).toEqual({ MY_VAR: "default_value" });
1334
+ });
1335
+
1336
+ test("should show env vars in help output", async () => {
1337
+ class TestCommands {
1338
+ cmd = $command({
1339
+ name: "test",
1340
+ env: t.object({
1341
+ API_TOKEN: t.text({ description: "API token for authentication" }),
1342
+ ORG_ID: t.optional(t.text({ description: "Organization ID" })),
1343
+ }),
1344
+ handler: vi.fn(),
1345
+ });
1346
+ }
1347
+
1348
+ const { mockLogger } = await setupTestCommands(
1349
+ ["test", "--help"],
1350
+ (alepha) => {
1351
+ alepha.store.mut(cliOptions, (old) => ({
1352
+ ...old,
1353
+ name: "my-cli",
1354
+ }));
1355
+ alepha.with(TestCommands);
1356
+ },
1357
+ );
1358
+
1359
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1360
+ expect(output).toContain("Env:");
1361
+ expect(output).toContain("API_TOKEN");
1362
+ expect(output).toContain("API token for authentication");
1363
+ expect(output).toContain("ORG_ID");
1364
+ expect(output).toContain("(optional)");
1365
+ });
1366
+
1367
+ test("should throw error for multiple missing env vars", async () => {
1368
+ class TestCommands {
1369
+ cmd = $command({
1370
+ name: "test",
1371
+ env: t.object({
1372
+ VAR_ONE: t.text(),
1373
+ VAR_TWO: t.text(),
1374
+ }),
1375
+ handler: vi.fn(),
1376
+ });
1377
+ }
1378
+
1379
+ delete process.env.VAR_ONE;
1380
+ delete process.env.VAR_TWO;
1381
+
1382
+ await expect(
1383
+ setupTestCommands(["test"], (a) => a.with(TestCommands)),
1384
+ ).rejects.toThrow(
1385
+ "Missing required environment variables: VAR_ONE, VAR_TWO",
1386
+ );
1387
+ });
1388
+ });
1389
+
1390
+ describe("mode option", () => {
1391
+ test("should pass mode value to handler when --mode flag is used", async () => {
1392
+ const handler = vi.fn();
1393
+
1394
+ class TestCommands {
1395
+ build = $command({
1396
+ name: "build",
1397
+ mode: true,
1398
+ handler,
1399
+ });
1400
+ }
1401
+
1402
+ await setupTestCommands(["build", "--mode", "production"], (a) =>
1403
+ a.with(TestCommands),
1404
+ );
1405
+
1406
+ expect(handler).toHaveBeenCalledWith(
1407
+ expect.objectContaining({
1408
+ mode: "production",
1409
+ }),
1410
+ );
1411
+ });
1412
+
1413
+ test("should pass mode value with -m shorthand", async () => {
1414
+ const handler = vi.fn();
1415
+
1416
+ class TestCommands {
1417
+ build = $command({
1418
+ name: "build",
1419
+ mode: true,
1420
+ handler,
1421
+ });
1422
+ }
1423
+
1424
+ await setupTestCommands(["build", "-m", "staging"], (a) =>
1425
+ a.with(TestCommands),
1426
+ );
1427
+
1428
+ expect(handler).toHaveBeenCalledWith(
1429
+ expect.objectContaining({
1430
+ mode: "staging",
1431
+ }),
1432
+ );
1433
+ });
1434
+
1435
+ test("should pass mode value with --mode=value syntax", async () => {
1436
+ const handler = vi.fn();
1437
+
1438
+ class TestCommands {
1439
+ build = $command({
1440
+ name: "build",
1441
+ mode: true,
1442
+ handler,
1443
+ });
1444
+ }
1445
+
1446
+ await setupTestCommands(["build", "--mode=development"], (a) =>
1447
+ a.with(TestCommands),
1448
+ );
1449
+
1450
+ expect(handler).toHaveBeenCalledWith(
1451
+ expect.objectContaining({
1452
+ mode: "development",
1453
+ }),
1454
+ );
1455
+ });
1456
+
1457
+ test("should pass undefined mode when flag is not provided", async () => {
1458
+ const handler = vi.fn();
1459
+
1460
+ class TestCommands {
1461
+ build = $command({
1462
+ name: "build",
1463
+ mode: true,
1464
+ handler,
1465
+ });
1466
+ }
1467
+
1468
+ await setupTestCommands(["build"], (a) => a.with(TestCommands));
1469
+
1470
+ expect(handler).toHaveBeenCalledWith(
1471
+ expect.objectContaining({
1472
+ mode: undefined,
1473
+ }),
1474
+ );
1475
+ });
1476
+
1477
+ test("should throw error when --mode is used without value", async () => {
1478
+ class TestCommands {
1479
+ build = $command({
1480
+ name: "build",
1481
+ mode: true,
1482
+ handler: vi.fn(),
1483
+ });
1484
+ }
1485
+
1486
+ await expect(
1487
+ setupTestCommands(["build", "--mode"], (a) => a.with(TestCommands)),
1488
+ ).rejects.toThrow("Flag --mode requires a value.");
1489
+ });
1490
+
1491
+ test("should show --mode flag in help when mode option is enabled", async () => {
1492
+ class TestCommands {
1493
+ build = $command({
1494
+ name: "build",
1495
+ description: "Build the project",
1496
+ mode: true,
1497
+ handler: vi.fn(),
1498
+ });
1499
+ }
1500
+
1501
+ const { mockLogger } = await setupTestCommands(["build", "-h"], (a) =>
1502
+ a.with(TestCommands),
1503
+ );
1504
+
1505
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1506
+ expect(output).toContain("-m, --mode");
1507
+ expect(output).toContain("Environment mode");
1508
+ });
1509
+
1510
+ test("should not show --mode flag when mode option is disabled", async () => {
1511
+ class TestCommands {
1512
+ build = $command({
1513
+ name: "build",
1514
+ description: "Build the project",
1515
+ handler: vi.fn(),
1516
+ });
1517
+ }
1518
+
1519
+ const { mockLogger } = await setupTestCommands(["build", "-h"], (a) =>
1520
+ a.with(TestCommands),
1521
+ );
1522
+
1523
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1524
+ expect(output).not.toContain("-m, --mode");
1525
+ });
1526
+
1527
+ test("should use string mode as default when no --mode flag provided", async () => {
1528
+ const handler = vi.fn();
1529
+
1530
+ class TestCommands {
1531
+ deploy = $command({
1532
+ name: "deploy",
1533
+ mode: "production",
1534
+ handler,
1535
+ });
1536
+ }
1537
+
1538
+ await setupTestCommands(["deploy"], (a) => a.with(TestCommands));
1539
+
1540
+ expect(handler).toHaveBeenCalledWith(
1541
+ expect.objectContaining({
1542
+ mode: "production",
1543
+ }),
1544
+ );
1545
+ });
1546
+
1547
+ test("should override string default mode when --mode flag provided", async () => {
1548
+ const handler = vi.fn();
1549
+
1550
+ class TestCommands {
1551
+ deploy = $command({
1552
+ name: "deploy",
1553
+ mode: "production",
1554
+ handler,
1555
+ });
1556
+ }
1557
+
1558
+ await setupTestCommands(["deploy", "--mode", "staging"], (a) =>
1559
+ a.with(TestCommands),
1560
+ );
1561
+
1562
+ expect(handler).toHaveBeenCalledWith(
1563
+ expect.objectContaining({
1564
+ mode: "staging",
1565
+ }),
1566
+ );
1567
+ });
1568
+
1569
+ test("should show default mode in help when mode is a string", async () => {
1570
+ class TestCommands {
1571
+ deploy = $command({
1572
+ name: "deploy",
1573
+ description: "Deploy the project",
1574
+ mode: "production",
1575
+ handler: vi.fn(),
1576
+ });
1577
+ }
1578
+
1579
+ const { mockLogger } = await setupTestCommands(["deploy", "-h"], (a) =>
1580
+ a.with(TestCommands),
1581
+ );
1582
+
1583
+ const output = mockLogger.logs.map((l) => l.message).join("\n");
1584
+ expect(output).toContain("-m, --mode");
1585
+ expect(output).toContain("default: production");
1586
+ });
1587
+ });
1588
+ });