@classytic/arc 1.1.0 → 2.1.2

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 +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
  4. package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
  5. package/dist/HookSystem-BsGV-j2l.mjs +405 -0
  6. package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
  7. package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
  8. package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
  9. package/dist/adapters/index.d.mts +5 -0
  10. package/dist/adapters/index.mjs +3 -0
  11. package/dist/audit/index.d.mts +82 -0
  12. package/dist/audit/index.d.mts.map +1 -0
  13. package/dist/audit/index.mjs +276 -0
  14. package/dist/audit/index.mjs.map +1 -0
  15. package/dist/audit/mongodb.d.mts +5 -0
  16. package/dist/audit/mongodb.mjs +3 -0
  17. package/dist/audited-C3T5DTUx.mjs +141 -0
  18. package/dist/audited-C3T5DTUx.mjs.map +1 -0
  19. package/dist/auth/index.d.mts +189 -0
  20. package/dist/auth/index.d.mts.map +1 -0
  21. package/dist/auth/index.mjs +1102 -0
  22. package/dist/auth/index.mjs.map +1 -0
  23. package/dist/auth/redis-session.d.mts +44 -0
  24. package/dist/auth/redis-session.d.mts.map +1 -0
  25. package/dist/auth/redis-session.mjs +76 -0
  26. package/dist/auth/redis-session.mjs.map +1 -0
  27. package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
  28. package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
  29. package/dist/cache/index.d.mts +146 -0
  30. package/dist/cache/index.d.mts.map +1 -0
  31. package/dist/cache/index.mjs +92 -0
  32. package/dist/cache/index.mjs.map +1 -0
  33. package/dist/caching-Bl28lYsR.mjs +94 -0
  34. package/dist/caching-Bl28lYsR.mjs.map +1 -0
  35. package/dist/chunk-C7Uep-_p.mjs +20 -0
  36. package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
  37. package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
  38. package/dist/cli/commands/describe.d.mts +19 -0
  39. package/dist/cli/commands/describe.d.mts.map +1 -0
  40. package/dist/cli/commands/describe.mjs +239 -0
  41. package/dist/cli/commands/describe.mjs.map +1 -0
  42. package/dist/cli/commands/docs.d.mts +14 -0
  43. package/dist/cli/commands/docs.d.mts.map +1 -0
  44. package/dist/cli/commands/docs.mjs +53 -0
  45. package/dist/cli/commands/docs.mjs.map +1 -0
  46. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
  47. package/dist/cli/commands/generate.d.mts.map +1 -0
  48. package/dist/cli/commands/generate.mjs +358 -0
  49. package/dist/cli/commands/generate.mjs.map +1 -0
  50. package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
  51. package/dist/cli/commands/init.d.mts.map +1 -0
  52. package/dist/cli/commands/{init.js → init.mjs} +807 -616
  53. package/dist/cli/commands/init.mjs.map +1 -0
  54. package/dist/cli/commands/introspect.d.mts +11 -0
  55. package/dist/cli/commands/introspect.d.mts.map +1 -0
  56. package/dist/cli/commands/introspect.mjs +76 -0
  57. package/dist/cli/commands/introspect.mjs.map +1 -0
  58. package/dist/cli/index.d.mts +17 -0
  59. package/dist/cli/index.d.mts.map +1 -0
  60. package/dist/cli/index.mjs +157 -0
  61. package/dist/cli/index.mjs.map +1 -0
  62. package/dist/constants-DdXFXQtN.mjs +85 -0
  63. package/dist/constants-DdXFXQtN.mjs.map +1 -0
  64. package/dist/core/index.d.mts +5 -0
  65. package/dist/core/index.mjs +4 -0
  66. package/dist/createApp-CUgNqegw.mjs +560 -0
  67. package/dist/createApp-CUgNqegw.mjs.map +1 -0
  68. package/dist/defineResource-k0_BDn8v.mjs +2197 -0
  69. package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
  70. package/dist/discovery/index.d.mts +47 -0
  71. package/dist/discovery/index.d.mts.map +1 -0
  72. package/dist/discovery/index.mjs +110 -0
  73. package/dist/discovery/index.mjs.map +1 -0
  74. package/dist/docs/index.d.mts +163 -0
  75. package/dist/docs/index.d.mts.map +1 -0
  76. package/dist/docs/index.mjs +73 -0
  77. package/dist/docs/index.mjs.map +1 -0
  78. package/dist/elevation-BRy3yFWT.mjs +113 -0
  79. package/dist/elevation-BRy3yFWT.mjs.map +1 -0
  80. package/dist/elevation-B_2dRLVP.d.mts +88 -0
  81. package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
  82. package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
  83. package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
  84. package/dist/errorHandler-C1okiriz.mjs +109 -0
  85. package/dist/errorHandler-C1okiriz.mjs.map +1 -0
  86. package/dist/errors-B9bZok84.mjs +212 -0
  87. package/dist/errors-B9bZok84.mjs.map +1 -0
  88. package/dist/errors-ChKiFz62.d.mts +125 -0
  89. package/dist/errors-ChKiFz62.d.mts.map +1 -0
  90. package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
  91. package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
  92. package/dist/eventPlugin-DGR_B2on.mjs +230 -0
  93. package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
  94. package/dist/events/index.d.mts +54 -0
  95. package/dist/events/index.d.mts.map +1 -0
  96. package/dist/events/index.mjs +52 -0
  97. package/dist/events/index.mjs.map +1 -0
  98. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  99. package/dist/events/transports/redis-stream-entry.mjs +178 -0
  100. package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
  101. package/dist/events/transports/redis.d.mts +77 -0
  102. package/dist/events/transports/redis.d.mts.map +1 -0
  103. package/dist/events/transports/redis.mjs +125 -0
  104. package/dist/events/transports/redis.mjs.map +1 -0
  105. package/dist/externalPaths-DlINfKbP.d.mts +51 -0
  106. package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
  107. package/dist/factory/index.d.mts +64 -0
  108. package/dist/factory/index.d.mts.map +1 -0
  109. package/dist/factory/index.mjs +3 -0
  110. package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
  111. package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
  112. package/dist/fields-DyaDVX4J.d.mts +110 -0
  113. package/dist/fields-DyaDVX4J.d.mts.map +1 -0
  114. package/dist/fields-iagOozy0.mjs +115 -0
  115. package/dist/fields-iagOozy0.mjs.map +1 -0
  116. package/dist/hooks/index.d.mts +4 -0
  117. package/dist/hooks/index.mjs +3 -0
  118. package/dist/idempotency/index.d.mts +97 -0
  119. package/dist/idempotency/index.d.mts.map +1 -0
  120. package/dist/idempotency/index.mjs +320 -0
  121. package/dist/idempotency/index.mjs.map +1 -0
  122. package/dist/idempotency/mongodb.d.mts +2 -0
  123. package/dist/idempotency/mongodb.mjs +115 -0
  124. package/dist/idempotency/mongodb.mjs.map +1 -0
  125. package/dist/idempotency/redis.d.mts +2 -0
  126. package/dist/idempotency/redis.mjs +104 -0
  127. package/dist/idempotency/redis.mjs.map +1 -0
  128. package/dist/index.d.mts +261 -0
  129. package/dist/index.d.mts.map +1 -0
  130. package/dist/index.mjs +105 -0
  131. package/dist/index.mjs.map +1 -0
  132. package/dist/integrations/event-gateway.d.mts +47 -0
  133. package/dist/integrations/event-gateway.d.mts.map +1 -0
  134. package/dist/integrations/event-gateway.mjs +44 -0
  135. package/dist/integrations/event-gateway.mjs.map +1 -0
  136. package/dist/integrations/index.d.mts +5 -0
  137. package/dist/integrations/index.mjs +1 -0
  138. package/dist/integrations/jobs.d.mts +104 -0
  139. package/dist/integrations/jobs.d.mts.map +1 -0
  140. package/dist/integrations/jobs.mjs +124 -0
  141. package/dist/integrations/jobs.mjs.map +1 -0
  142. package/dist/integrations/streamline.d.mts +61 -0
  143. package/dist/integrations/streamline.d.mts.map +1 -0
  144. package/dist/integrations/streamline.mjs +126 -0
  145. package/dist/integrations/streamline.mjs.map +1 -0
  146. package/dist/integrations/websocket.d.mts +83 -0
  147. package/dist/integrations/websocket.d.mts.map +1 -0
  148. package/dist/integrations/websocket.mjs +289 -0
  149. package/dist/integrations/websocket.mjs.map +1 -0
  150. package/dist/interface-B01JvPVc.d.mts +78 -0
  151. package/dist/interface-B01JvPVc.d.mts.map +1 -0
  152. package/dist/interface-CZe8IkMf.d.mts +55 -0
  153. package/dist/interface-CZe8IkMf.d.mts.map +1 -0
  154. package/dist/interface-Ch8HU9uM.d.mts +1098 -0
  155. package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
  156. package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
  157. package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
  158. package/dist/keys-BqNejWup.mjs +43 -0
  159. package/dist/keys-BqNejWup.mjs.map +1 -0
  160. package/dist/logger-Df2O2WsW.mjs +79 -0
  161. package/dist/logger-Df2O2WsW.mjs.map +1 -0
  162. package/dist/memory-cQgelFOj.mjs +144 -0
  163. package/dist/memory-cQgelFOj.mjs.map +1 -0
  164. package/dist/migrations/index.d.mts +157 -0
  165. package/dist/migrations/index.d.mts.map +1 -0
  166. package/dist/migrations/index.mjs +261 -0
  167. package/dist/migrations/index.mjs.map +1 -0
  168. package/dist/mongodb-BfJVlUJH.mjs +94 -0
  169. package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
  170. package/dist/mongodb-CGzRbfAK.d.mts +119 -0
  171. package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
  172. package/dist/mongodb-JN-9JA7K.d.mts +72 -0
  173. package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
  174. package/dist/openapi-G3Cw7XuM.mjs +524 -0
  175. package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
  176. package/dist/org/index.d.mts +69 -0
  177. package/dist/org/index.d.mts.map +1 -0
  178. package/dist/org/index.mjs +514 -0
  179. package/dist/org/index.mjs.map +1 -0
  180. package/dist/org/types.d.mts +83 -0
  181. package/dist/org/types.d.mts.map +1 -0
  182. package/dist/org/types.mjs +1 -0
  183. package/dist/permissions/index.d.mts +279 -0
  184. package/dist/permissions/index.d.mts.map +1 -0
  185. package/dist/permissions/index.mjs +579 -0
  186. package/dist/permissions/index.mjs.map +1 -0
  187. package/dist/plugins/index.d.mts +173 -0
  188. package/dist/plugins/index.d.mts.map +1 -0
  189. package/dist/plugins/index.mjs +523 -0
  190. package/dist/plugins/index.mjs.map +1 -0
  191. package/dist/plugins/response-cache.d.mts +88 -0
  192. package/dist/plugins/response-cache.d.mts.map +1 -0
  193. package/dist/plugins/response-cache.mjs +284 -0
  194. package/dist/plugins/response-cache.mjs.map +1 -0
  195. package/dist/plugins/tracing-entry.d.mts +2 -0
  196. package/dist/plugins/tracing-entry.mjs +186 -0
  197. package/dist/plugins/tracing-entry.mjs.map +1 -0
  198. package/dist/pluralize-CEweyOEm.mjs +87 -0
  199. package/dist/pluralize-CEweyOEm.mjs.map +1 -0
  200. package/dist/policies/{index.d.ts → index.d.mts} +204 -169
  201. package/dist/policies/index.d.mts.map +1 -0
  202. package/dist/policies/index.mjs +322 -0
  203. package/dist/policies/index.mjs.map +1 -0
  204. package/dist/presets/{index.d.ts → index.d.mts} +63 -131
  205. package/dist/presets/index.d.mts.map +1 -0
  206. package/dist/presets/index.mjs +144 -0
  207. package/dist/presets/index.mjs.map +1 -0
  208. package/dist/presets/multiTenant.d.mts +25 -0
  209. package/dist/presets/multiTenant.d.mts.map +1 -0
  210. package/dist/presets/multiTenant.mjs +114 -0
  211. package/dist/presets/multiTenant.mjs.map +1 -0
  212. package/dist/presets-BITljm96.mjs +120 -0
  213. package/dist/presets-BITljm96.mjs.map +1 -0
  214. package/dist/presets-DzSMwlKj.d.mts +58 -0
  215. package/dist/presets-DzSMwlKj.d.mts.map +1 -0
  216. package/dist/prisma-DJbMt3yf.mjs +628 -0
  217. package/dist/prisma-DJbMt3yf.mjs.map +1 -0
  218. package/dist/prisma-Dg9GoVdj.d.mts +275 -0
  219. package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
  220. package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
  221. package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
  222. package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
  223. package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
  224. package/dist/redis-D-JAeLtm.d.mts +50 -0
  225. package/dist/redis-D-JAeLtm.d.mts.map +1 -0
  226. package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
  227. package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
  228. package/dist/registry/index.d.mts +12 -0
  229. package/dist/registry/index.d.mts.map +1 -0
  230. package/dist/registry/index.mjs +4 -0
  231. package/dist/requestContext-QQD6ROJc.mjs +56 -0
  232. package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
  233. package/dist/schemaConverter-BwrmWroW.mjs +99 -0
  234. package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
  235. package/dist/schemas/index.d.mts +64 -0
  236. package/dist/schemas/index.d.mts.map +1 -0
  237. package/dist/schemas/index.mjs +83 -0
  238. package/dist/schemas/index.mjs.map +1 -0
  239. package/dist/scope/index.d.mts +22 -0
  240. package/dist/scope/index.d.mts.map +1 -0
  241. package/dist/scope/index.mjs +66 -0
  242. package/dist/scope/index.mjs.map +1 -0
  243. package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
  244. package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
  245. package/dist/sse-B3c3_yZp.mjs +124 -0
  246. package/dist/sse-B3c3_yZp.mjs.map +1 -0
  247. package/dist/testing/index.d.mts +908 -0
  248. package/dist/testing/index.d.mts.map +1 -0
  249. package/dist/testing/index.mjs +1977 -0
  250. package/dist/testing/index.mjs.map +1 -0
  251. package/dist/tracing-Cc7vVQPp.d.mts +71 -0
  252. package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
  253. package/dist/typeGuards-DhMNLuvU.mjs +10 -0
  254. package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
  255. package/dist/types/index.d.mts +947 -0
  256. package/dist/types/index.d.mts.map +1 -0
  257. package/dist/types/index.mjs +15 -0
  258. package/dist/types/index.mjs.map +1 -0
  259. package/dist/types-Beqn1Un7.mjs +39 -0
  260. package/dist/types-Beqn1Un7.mjs.map +1 -0
  261. package/dist/types-CIgB7UUl.d.mts +446 -0
  262. package/dist/types-CIgB7UUl.d.mts.map +1 -0
  263. package/dist/types-aYB4V7uN.d.mts +87 -0
  264. package/dist/types-aYB4V7uN.d.mts.map +1 -0
  265. package/dist/utils/index.d.mts +748 -0
  266. package/dist/utils/index.d.mts.map +1 -0
  267. package/dist/utils/index.mjs +6 -0
  268. package/package.json +194 -68
  269. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  270. package/dist/adapters/index.d.ts +0 -237
  271. package/dist/adapters/index.js +0 -668
  272. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  273. package/dist/audit/index.d.ts +0 -195
  274. package/dist/audit/index.js +0 -319
  275. package/dist/auth/index.d.ts +0 -47
  276. package/dist/auth/index.js +0 -174
  277. package/dist/cli/commands/docs.d.ts +0 -11
  278. package/dist/cli/commands/docs.js +0 -474
  279. package/dist/cli/commands/generate.js +0 -334
  280. package/dist/cli/commands/introspect.d.ts +0 -8
  281. package/dist/cli/commands/introspect.js +0 -338
  282. package/dist/cli/index.d.ts +0 -4
  283. package/dist/cli/index.js +0 -3269
  284. package/dist/core/index.d.ts +0 -220
  285. package/dist/core/index.js +0 -2786
  286. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  287. package/dist/docs/index.d.ts +0 -166
  288. package/dist/docs/index.js +0 -658
  289. package/dist/errors-8WIxGS_6.d.ts +0 -122
  290. package/dist/events/index.d.ts +0 -117
  291. package/dist/events/index.js +0 -89
  292. package/dist/factory/index.d.ts +0 -38
  293. package/dist/factory/index.js +0 -1652
  294. package/dist/hooks/index.d.ts +0 -4
  295. package/dist/hooks/index.js +0 -199
  296. package/dist/idempotency/index.d.ts +0 -323
  297. package/dist/idempotency/index.js +0 -500
  298. package/dist/index-B4t03KQ0.d.ts +0 -1366
  299. package/dist/index.d.ts +0 -135
  300. package/dist/index.js +0 -4756
  301. package/dist/migrations/index.d.ts +0 -185
  302. package/dist/migrations/index.js +0 -274
  303. package/dist/org/index.d.ts +0 -129
  304. package/dist/org/index.js +0 -220
  305. package/dist/permissions/index.d.ts +0 -144
  306. package/dist/permissions/index.js +0 -103
  307. package/dist/plugins/index.d.ts +0 -46
  308. package/dist/plugins/index.js +0 -1069
  309. package/dist/policies/index.js +0 -196
  310. package/dist/presets/index.js +0 -384
  311. package/dist/presets/multiTenant.d.ts +0 -39
  312. package/dist/presets/multiTenant.js +0 -112
  313. package/dist/registry/index.d.ts +0 -16
  314. package/dist/registry/index.js +0 -253
  315. package/dist/testing/index.d.ts +0 -618
  316. package/dist/testing/index.js +0 -48020
  317. package/dist/types/index.d.ts +0 -4
  318. package/dist/types/index.js +0 -8
  319. package/dist/types-B99TBmFV.d.ts +0 -76
  320. package/dist/types-BvckRbs2.d.ts +0 -143
  321. package/dist/utils/index.d.ts +0 -679
  322. package/dist/utils/index.js +0 -931
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.mjs","names":["existsSync"],"sources":["../../../src/cli/commands/init.ts"],"sourcesContent":["/**\r\n * Arc CLI - Init Command\r\n *\r\n * Scaffolds a new Arc project with clean architecture:\r\n * - MongoKit or Custom adapter\r\n * - Multi-tenant or Single-tenant\r\n * - TypeScript or JavaScript\r\n *\r\n * Automatically installs dependencies using detected package manager.\r\n */\r\n\r\nimport * as fs from \"node:fs/promises\";\r\nimport { accessSync } from \"node:fs\";\r\nimport * as path from \"node:path\";\r\nimport * as readline from \"node:readline\";\r\nimport { execSync, spawn } from \"node:child_process\";\r\n\r\ntype PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport interface InitOptions {\r\n name?: string;\r\n adapter?: \"mongokit\" | \"custom\";\r\n auth?: \"jwt\" | \"better-auth\";\r\n tenant?: \"multi\" | \"single\";\r\n typescript?: boolean;\r\n edge?: boolean;\r\n skipInstall?: boolean;\r\n force?: boolean;\r\n}\r\n\r\ninterface ProjectConfig {\r\n name: string;\r\n adapter: \"mongokit\" | \"custom\";\r\n auth: \"jwt\" | \"better-auth\";\r\n tenant: \"multi\" | \"single\";\r\n typescript: boolean;\r\n edge: boolean;\r\n}\r\n\r\n// ============================================================================\r\n// Main Init Function\r\n// ============================================================================\r\n\r\n/**\r\n * Initialize a new Arc project\r\n */\r\nexport async function init(options: InitOptions = {}): Promise<void> {\r\n console.log(`\r\n╔═══════════════════════════════════════════════════════════════╗\r\n║ Arc Project Setup ║\r\n║ Resource-Oriented Backend Framework ║\r\n╚═══════════════════════════════════════════════════════════════╝\r\n`);\r\n\r\n // Gather configuration (from options or prompts)\r\n const config = await gatherConfig(options);\r\n\r\n console.log(`\\nCreating project: ${config.name}`);\r\n console.log(\r\n ` Adapter: ${config.adapter === \"mongokit\" ? \"MongoKit (MongoDB)\" : \"Custom\"}`,\r\n );\r\n console.log(\r\n ` Auth: ${config.auth === \"better-auth\" ? \"Better Auth (recommended)\" : \"Arc JWT\"}`,\r\n );\r\n console.log(\r\n ` Tenant: ${config.tenant === \"multi\" ? \"Multi-tenant\" : \"Single-tenant\"}`,\r\n );\r\n console.log(\r\n ` Language: ${config.typescript ? \"TypeScript\" : \"JavaScript\"}`,\r\n );\r\n console.log(\r\n ` Target: ${config.edge ? \"Edge/Serverless\" : \"Node.js Server\"}\\n`,\r\n );\r\n\r\n const projectPath = path.join(process.cwd(), config.name);\r\n\r\n // Check if directory exists\r\n try {\r\n await fs.access(projectPath);\r\n // If we reach here, the directory EXISTS\r\n if (!options.force) {\r\n throw new Error(\r\n `Directory \"${config.name}\" already exists. Use --force to overwrite.`,\r\n );\r\n }\r\n } catch (err) {\r\n // ENOENT = directory doesn't exist = good, fall through to scaffolding\r\n const isNotFound =\r\n err && typeof err === \"object\" && \"code\" in err && err.code === \"ENOENT\";\r\n if (!isNotFound) throw err;\r\n // else: directory doesn't exist, continue normally\r\n }\r\n\r\n // Detect package manager\r\n const packageManager = detectPackageManager();\r\n console.log(`Using package manager: ${packageManager}\\n`);\r\n\r\n // Create project structure (without dependencies in package.json)\r\n await createProjectStructure(projectPath, config);\r\n\r\n // Install dependencies unless --skip-install\r\n if (!options.skipInstall) {\r\n console.log(\"\\n📥 Installing dependencies...\\n\");\r\n await installDependencies(projectPath, config, packageManager);\r\n }\r\n\r\n // Print success message\r\n printSuccessMessage(config, options.skipInstall);\r\n}\r\n\r\n// ============================================================================\r\n// Package Manager Detection & Installation\r\n// ============================================================================\r\n\r\n/**\r\n * Detect which package manager to use\r\n * Priority: pnpm > yarn > bun > npm (based on lockfile or global availability)\r\n */\r\nfunction detectPackageManager(): PackageManager {\r\n // Check for lockfiles in current directory (user preference)\r\n try {\r\n const cwd = process.cwd();\r\n if (existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) return \"pnpm\";\r\n if (existsSync(path.join(cwd, \"yarn.lock\"))) return \"yarn\";\r\n if (existsSync(path.join(cwd, \"bun.lockb\"))) return \"bun\";\r\n if (existsSync(path.join(cwd, \"package-lock.json\"))) return \"npm\";\r\n } catch {\r\n // Ignore errors\r\n }\r\n\r\n // Check which package managers are available\r\n if (isCommandAvailable(\"pnpm\")) return \"pnpm\";\r\n if (isCommandAvailable(\"yarn\")) return \"yarn\";\r\n if (isCommandAvailable(\"bun\")) return \"bun\";\r\n\r\n // Default to npm\r\n return \"npm\";\r\n}\r\n\r\n/**\r\n * Check if a command is available in PATH\r\n */\r\nfunction isCommandAvailable(command: string): boolean {\r\n try {\r\n execSync(`${command} --version`, { stdio: \"ignore\" });\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Sync check if file exists (ESM-compatible — no require())\r\n */\r\nfunction existsSync(filePath: string): boolean {\r\n try {\r\n accessSync(filePath);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Install dependencies using the detected package manager\r\n */\r\nasync function installDependencies(\r\n projectPath: string,\r\n config: ProjectConfig,\r\n pm: PackageManager,\r\n): Promise<void> {\r\n // Build dependency lists\r\n const deps = [\r\n \"@classytic/arc@latest\",\r\n \"fastify@latest\",\r\n \"@fastify/cors@latest\",\r\n \"@fastify/helmet@latest\",\r\n \"@fastify/rate-limit@latest\",\r\n \"@fastify/sensible@latest\",\r\n \"@fastify/under-pressure@latest\",\r\n \"dotenv@latest\",\r\n ];\r\n\r\n if (config.auth === \"better-auth\") {\r\n deps.push(\"better-auth@latest\", \"mongodb@latest\");\r\n } else {\r\n deps.push(\"@fastify/jwt@latest\", \"bcryptjs@latest\");\r\n }\r\n\r\n if (config.adapter === \"mongokit\") {\r\n deps.push(\"@classytic/mongokit@latest\", \"mongoose@latest\");\r\n }\r\n\r\n const devDeps = [\"vitest@latest\", \"pino-pretty@latest\"];\r\n\r\n if (config.typescript) {\r\n devDeps.push(\"typescript@latest\", \"@types/node@latest\", \"tsx@latest\");\r\n }\r\n\r\n // Build install commands based on package manager\r\n const installCmd = getInstallCommand(pm, deps, false);\r\n const installDevCmd = getInstallCommand(pm, devDeps, true);\r\n\r\n // Run installation\r\n console.log(` Installing dependencies...`);\r\n await runCommand(installCmd, projectPath);\r\n\r\n console.log(` Installing dev dependencies...`);\r\n await runCommand(installDevCmd, projectPath);\r\n\r\n console.log(`\\nDependencies installed successfully.`);\r\n}\r\n\r\n/**\r\n * Get the install command for a package manager\r\n */\r\nfunction getInstallCommand(\r\n pm: PackageManager,\r\n packages: string[],\r\n isDev: boolean,\r\n): string {\r\n const pkgList = packages.join(\" \");\r\n\r\n switch (pm) {\r\n case \"pnpm\":\r\n return `pnpm add ${isDev ? \"-D\" : \"\"} ${pkgList}`;\r\n case \"yarn\":\r\n return `yarn add ${isDev ? \"-D\" : \"\"} ${pkgList}`;\r\n case \"bun\":\r\n return `bun add ${isDev ? \"-d\" : \"\"} ${pkgList}`;\r\n case \"npm\":\r\n default:\r\n return `npm install ${isDev ? \"--save-dev\" : \"\"} ${pkgList}`;\r\n }\r\n}\r\n\r\n/**\r\n * Run a shell command in a directory\r\n */\r\nfunction runCommand(command: string, cwd: string): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n const isWindows = process.platform === \"win32\";\r\n const shell = isWindows ? \"cmd\" : \"/bin/sh\";\r\n const shellFlag = isWindows ? \"/c\" : \"-c\";\r\n\r\n const child = spawn(shell, [shellFlag, command], {\r\n cwd,\r\n stdio: \"inherit\",\r\n env: { ...process.env, FORCE_COLOR: \"1\" },\r\n });\r\n\r\n child.on(\"close\", (code) => {\r\n if (code === 0) {\r\n resolve();\r\n } else {\r\n reject(new Error(`Command failed with exit code ${code}`));\r\n }\r\n });\r\n\r\n child.on(\"error\", reject);\r\n });\r\n}\r\n\r\n// ============================================================================\r\n// Configuration Gathering\r\n// ============================================================================\r\n\r\nasync function gatherConfig(options: InitOptions): Promise<ProjectConfig> {\r\n const rl = readline.createInterface({\r\n input: process.stdin,\r\n output: process.stdout,\r\n });\r\n\r\n const question = (prompt: string): Promise<string> =>\r\n new Promise((resolve) => rl.question(prompt, resolve));\r\n\r\n // Non-interactive mode: if a project name was provided via args, skip prompts\r\n // and use defaults for any unspecified options\r\n const nonInteractive = !!options.name;\r\n\r\n try {\r\n // Project name\r\n const name =\r\n options.name || (await question(\"Project name: \")) || \"my-arc-app\";\r\n\r\n // Adapter choice\r\n let adapter: \"mongokit\" | \"custom\" = options.adapter || \"mongokit\";\r\n if (!options.adapter && !nonInteractive) {\r\n const adapterChoice = await question(\r\n \"Database adapter [1=MongoKit (recommended), 2=Custom]: \",\r\n );\r\n adapter = adapterChoice === \"2\" ? \"custom\" : \"mongokit\";\r\n }\r\n\r\n // Auth strategy\r\n let auth: \"jwt\" | \"better-auth\" = options.auth || \"better-auth\";\r\n if (!options.auth && !nonInteractive) {\r\n const authChoice = await question(\r\n \"Auth strategy [1=Better Auth (recommended), 2=Arc JWT]: \",\r\n );\r\n auth = authChoice === \"2\" ? \"jwt\" : \"better-auth\";\r\n }\r\n\r\n // Tenant mode\r\n let tenant: \"multi\" | \"single\" = options.tenant || \"single\";\r\n if (!options.tenant && !nonInteractive) {\r\n const tenantChoice = await question(\r\n \"Tenant mode [1=Single-tenant, 2=Multi-tenant]: \",\r\n );\r\n tenant = tenantChoice === \"2\" ? \"multi\" : \"single\";\r\n }\r\n\r\n // TypeScript or JavaScript\r\n let typescript = options.typescript ?? true;\r\n if (options.typescript === undefined && !nonInteractive) {\r\n const tsChoice = await question(\r\n \"Language [1=TypeScript (recommended), 2=JavaScript]: \",\r\n );\r\n typescript = tsChoice !== \"2\";\r\n }\r\n\r\n // Environment/Target choice\r\n let edge = options.edge ?? false;\r\n if (options.edge === undefined && !nonInteractive) {\r\n const edgeChoice = await question(\r\n \"Deployment target [1=Node.js Server (default), 2=Edge/Serverless]: \",\r\n );\r\n edge = edgeChoice === \"2\";\r\n }\r\n\r\n return { name, adapter, auth, tenant, typescript, edge };\r\n } finally {\r\n rl.close();\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Project Structure Creation\r\n// ============================================================================\r\n\r\nasync function createProjectStructure(\r\n projectPath: string,\r\n config: ProjectConfig,\r\n): Promise<void> {\r\n const ext = config.typescript ? \"ts\" : \"js\";\r\n\r\n // Create directories - Clean architecture (organized by resource, no barrels)\r\n const dirs = [\r\n \"\",\r\n \"src\",\r\n \"src/config\", // Config & env loading (import first!)\r\n \"src/shared\", // Shared utilities (adapters, presets, permissions)\r\n \"src/shared/presets\", // Preset definitions\r\n \"src/plugins\", // App-specific plugins\r\n \"src/resources\", // Resource definitions\r\n ...(config.auth === \"jwt\"\r\n ? [\r\n \"src/resources/user\", // User resource (user.model, user.repository, etc.)\r\n \"src/resources/auth\", // Auth resource (auth.resource, auth.handlers, etc.)\r\n ]\r\n : []),\r\n \"src/resources/example\", // Example resource\r\n \"tests\",\r\n ];\r\n\r\n for (const dir of dirs) {\r\n await fs.mkdir(path.join(projectPath, dir), { recursive: true });\r\n console.log(` + Created: ${dir || \"/\"}`);\r\n }\r\n\r\n // Generate and write files\r\n const files: Record<string, string> = {\r\n \"package.json\": packageJsonTemplate(config),\r\n \".gitignore\": gitignoreTemplate(),\r\n \".env.example\": envExampleTemplate(config),\r\n \".env.dev\": envDevTemplate(config),\r\n \"README.md\": readmeTemplate(config),\r\n };\r\n\r\n // TypeScript config\r\n if (config.typescript) {\r\n files[\"tsconfig.json\"] = tsconfigTemplate();\r\n }\r\n\r\n // Vitest config (always needed for path alias resolution)\r\n files[\"vitest.config.ts\"] = vitestConfigTemplate(config);\r\n\r\n // Config files (env loader FIRST - imported before everything)\r\n files[`src/config/env.${ext}`] = envLoaderTemplate(config);\r\n files[`src/config/index.${ext}`] = configTemplate(config);\r\n\r\n // App factory + Entry point (separation for workers/tests)\r\n files[`src/app.${ext}`] = appTemplate(config);\r\n files[`src/index.${ext}`] = indexTemplate(config);\r\n\r\n // Shared utilities\r\n files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);\r\n files[`src/shared/adapter.${ext}`] =\r\n config.adapter === \"mongokit\"\r\n ? createAdapterTemplate(config)\r\n : customAdapterTemplate(config);\r\n files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);\r\n\r\n // Presets\r\n if (config.tenant === \"multi\") {\r\n files[`src/shared/presets/index.${ext}`] =\r\n presetsMultiTenantTemplate(config);\r\n files[`src/shared/presets/flexible-multi-tenant.${ext}`] =\r\n flexibleMultiTenantPresetTemplate(config);\r\n } else {\r\n files[`src/shared/presets/index.${ext}`] =\r\n presetsSingleTenantTemplate(config);\r\n }\r\n\r\n // Plugins (app-specific, easy to extend)\r\n files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);\r\n\r\n // Resources (organized by folder, no barrels - prefixed filenames)\r\n files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);\r\n\r\n // Auth setup — depends on strategy\r\n if (config.auth === \"better-auth\") {\r\n // Better Auth: single config file, no manual auth handlers\r\n files[`src/auth.${ext}`] = betterAuthSetupTemplate(config);\r\n } else {\r\n // JWT: manual user model + auth handlers\r\n files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);\r\n files[`src/resources/user/user.repository.${ext}`] =\r\n userRepositoryTemplate(config);\r\n files[`src/resources/user/user.controller.${ext}`] =\r\n userControllerTemplate(config);\r\n files[`src/resources/auth/auth.resource.${ext}`] =\r\n authResourceTemplate(config);\r\n files[`src/resources/auth/auth.handlers.${ext}`] =\r\n authHandlersTemplate(config);\r\n files[`src/resources/auth/auth.schemas.${ext}`] =\r\n authSchemasTemplate(config);\r\n }\r\n\r\n // Example resource (src/resources/example/)\r\n files[`src/resources/example/example.model.${ext}`] =\r\n exampleModelTemplate(config);\r\n files[`src/resources/example/example.repository.${ext}`] =\r\n exampleRepositoryTemplate(config);\r\n files[`src/resources/example/example.resource.${ext}`] =\r\n exampleResourceTemplate(config);\r\n files[`src/resources/example/example.controller.${ext}`] =\r\n exampleControllerTemplate(config);\r\n files[`src/resources/example/example.schemas.${ext}`] =\r\n exampleSchemasTemplate(config);\r\n\r\n // Tests\r\n files[`tests/example.test.${ext}`] = exampleTestTemplate(config);\r\n if (config.auth === \"jwt\") {\r\n files[`tests/auth.test.${ext}`] = authTestTemplate(config);\r\n }\r\n\r\n // Save project config for CLI tools (generate, etc.)\r\n files[\".arcrc\"] =\r\n JSON.stringify(\r\n {\r\n adapter: config.adapter,\r\n auth: config.auth,\r\n tenant: config.tenant,\r\n typescript: config.typescript,\r\n },\r\n null,\r\n 2,\r\n ) + \"\\n\";\r\n\r\n // Write all files\r\n for (const [filePath, content] of Object.entries(files)) {\r\n const fullPath = path.join(projectPath, filePath);\r\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\r\n await fs.writeFile(fullPath, content);\r\n console.log(` + Created: ${filePath}`);\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Templates\r\n// ============================================================================\r\n\r\nfunction packageJsonTemplate(config: ProjectConfig): string {\r\n // Minimal package.json - dependencies are installed via package manager\r\n const scripts: Record<string, string> = config.typescript\r\n ? {\r\n dev: \"tsx watch src/index.ts\",\r\n build: \"tsc\",\r\n start: \"node dist/index.js\",\r\n test: \"vitest run\",\r\n \"test:watch\": \"vitest\",\r\n }\r\n : {\r\n dev: \"node --watch src/index.js\",\r\n start: \"node src/index.js\",\r\n test: \"vitest run\",\r\n \"test:watch\": \"vitest\",\r\n };\r\n\r\n // Subpath imports for clean DX\r\n const imports: Record<string, string> = config.typescript\r\n ? {\r\n \"#config/*\": \"./dist/config/*\",\r\n \"#shared/*\": \"./dist/shared/*\",\r\n \"#resources/*\": \"./dist/resources/*\",\r\n \"#plugins/*\": \"./dist/plugins/*\",\r\n }\r\n : {\r\n \"#config/*\": \"./src/config/*\",\r\n \"#shared/*\": \"./src/shared/*\",\r\n \"#resources/*\": \"./src/resources/*\",\r\n \"#plugins/*\": \"./src/plugins/*\",\r\n };\r\n\r\n return JSON.stringify(\r\n {\r\n name: config.name,\r\n version: \"1.0.0\",\r\n type: \"module\",\r\n main: config.typescript ? \"dist/index.js\" : \"src/index.js\",\r\n imports,\r\n scripts,\r\n engines: {\r\n node: \">=20\",\r\n },\r\n },\r\n null,\r\n 2,\r\n );\r\n}\r\n\r\nfunction tsconfigTemplate(): string {\r\n return JSON.stringify(\r\n {\r\n compilerOptions: {\r\n target: \"ES2022\",\r\n module: \"NodeNext\",\r\n moduleResolution: \"NodeNext\",\r\n lib: [\"ES2022\"],\r\n outDir: \"./dist\",\r\n rootDir: \"./src\",\r\n strict: true,\r\n esModuleInterop: true,\r\n skipLibCheck: true,\r\n forceConsistentCasingInFileNames: true,\r\n declaration: true,\r\n declarationMap: true,\r\n sourceMap: true,\r\n resolveJsonModule: true,\r\n paths: {\r\n \"#shared/*\": [\"./src/shared/*\"],\r\n \"#resources/*\": [\"./src/resources/*\"],\r\n \"#config/*\": [\"./src/config/*\"],\r\n \"#plugins/*\": [\"./src/plugins/*\"],\r\n },\r\n },\r\n include: [\"src/**/*\"],\r\n exclude: [\"node_modules\", \"dist\"],\r\n },\r\n null,\r\n 2,\r\n );\r\n}\r\n\r\nfunction vitestConfigTemplate(config: ProjectConfig): string {\r\n const srcDir = config.typescript ? \"./src\" : \"./src\";\r\n\r\n return `import { defineConfig } from 'vitest/config';\r\nimport { resolve } from 'path';\r\n\r\nexport default defineConfig({\r\n test: {\r\n globals: true,\r\n environment: 'node',\r\n },\r\n resolve: {\r\n alias: {\r\n '#config': resolve(__dirname, '${srcDir}/config'),\r\n '#shared': resolve(__dirname, '${srcDir}/shared'),\r\n '#resources': resolve(__dirname, '${srcDir}/resources'),\r\n '#plugins': resolve(__dirname, '${srcDir}/plugins'),\r\n },\r\n },\r\n});\r\n`;\r\n}\r\n\r\nfunction gitignoreTemplate(): string {\r\n return `# Dependencies\r\nnode_modules/\r\n\r\n# Build\r\ndist/\r\n*.js.map\r\n\r\n# Environment\r\n.env\r\n.env.local\r\n.env.*.local\r\n\r\n# IDE\r\n.vscode/\r\n.idea/\r\n*.swp\r\n*.swo\r\n\r\n# OS\r\n.DS_Store\r\nThumbs.db\r\n\r\n# Logs\r\n*.log\r\nnpm-debug.log*\r\n\r\n# Test coverage\r\ncoverage/\r\n`;\r\n}\r\n\r\nfunction envExampleTemplate(config: ProjectConfig): string {\r\n let content = `# Server\r\nPORT=8040\r\nHOST=0.0.0.0\r\nNODE_ENV=development\r\n`;\r\n\r\n if (config.auth === \"better-auth\") {\r\n content += `\r\n# Better Auth\r\nBETTER_AUTH_SECRET=your-32-character-minimum-secret-here\r\nFRONTEND_URL=http://localhost:3000\r\n\r\n# Google OAuth (optional)\r\n# GOOGLE_CLIENT_ID=\r\n# GOOGLE_CLIENT_SECRET=\r\n`;\r\n } else {\r\n content += `\r\n# JWT\r\nJWT_SECRET=your-32-character-minimum-secret-here\r\nJWT_EXPIRES_IN=7d\r\n`;\r\n }\r\n\r\n content += `\r\n# CORS - Allowed origins\r\n# Options:\r\n# * = allow all origins (not recommended for production)\r\n# Comma-separated list = specific origins only\r\nCORS_ORIGINS=http://localhost:3000,http://localhost:5173\r\n`;\r\n\r\n if (config.adapter === \"mongokit\") {\r\n content += `\r\n# MongoDB\r\nMONGODB_URI=mongodb://localhost:27017/${config.name}\r\n`;\r\n }\r\n\r\n if (config.tenant === \"multi\") {\r\n content += `\r\n# Multi-tenant\r\nORG_HEADER=x-organization-id\r\n`;\r\n }\r\n\r\n return content;\r\n}\r\n\r\nfunction readmeTemplate(config: ProjectConfig): string {\r\n const ext = config.typescript ? \"ts\" : \"js\";\r\n\r\n return `# ${config.name}\r\n\r\nBuilt with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework\r\n\r\n## Quick Start\r\n\r\n\\`\\`\\`bash\r\n# Install dependencies\r\nnpm install\r\n\r\n# Start development server (uses .env.dev)\r\nnpm run dev\r\n\r\n# Run tests\r\nnpm test\r\n\\`\\`\\`\r\n\r\n## Project Structure\r\n\r\n\\`\\`\\`\r\nsrc/\r\n├── config/ # Configuration (loaded first)\r\n│ ├── env.${ext} # Env loader (import first!)\r\n│ └── index.${ext} # App config\r\n├── shared/ # Shared utilities\r\n│ ├── adapter.${ext} # ${config.adapter === \"mongokit\" ? \"MongoKit adapter factory\" : \"Custom adapter\"}\r\n│ ├── permissions.${ext} # Permission helpers\r\n│ └── presets/ # ${config.tenant === \"multi\" ? \"Multi-tenant presets\" : \"Standard presets\"}\r\n├── plugins/ # App-specific plugins\r\n│ └── index.${ext} # Plugin registry\r\n├── resources/ # API Resources\r\n│ ├── index.${ext} # Resource registry\r\n│ └── example/ # Example resource\r\n│ ├── index.${ext} # Resource definition\r\n│ ├── model.${ext} # Mongoose schema\r\n│ └── repository.${ext} # MongoKit repository\r\n├── app.${ext} # App factory (reusable)\r\n└── index.${ext} # Server entry point\r\ntests/\r\n└── example.test.${ext} # Example tests\r\n\\`\\`\\`\r\n\r\n## Architecture\r\n\r\n### Entry Points\r\n\r\n- **\\`src/index.${ext}\\`** - HTTP server entry point\r\n- **\\`src/app.${ext}\\`** - App factory (import for workers/tests)\r\n\r\n\\`\\`\\`${config.typescript ? \"typescript\" : \"javascript\"}\r\n// For workers or custom entry points:\r\nimport { createAppInstance } from './app.js';\r\n\r\nconst app = await createAppInstance();\r\n// Use app for your worker logic\r\n\\`\\`\\`\r\n\r\n### Adding Resources\r\n\r\n1. Create a new folder in \\`src/resources/\\`:\r\n\r\n\\`\\`\\`\r\nsrc/resources/product/\r\n├── index.${ext} # Resource definition\r\n├── model.${ext} # Mongoose schema\r\n└── repository.${ext} # MongoKit repository\r\n\\`\\`\\`\r\n\r\n2. Register in \\`src/resources/index.${ext}\\`:\r\n\r\n\\`\\`\\`${config.typescript ? \"typescript\" : \"javascript\"}\r\nimport productResource from './product/index.js';\r\n\r\nexport const resources = [\r\n exampleResource,\r\n productResource, // Add here\r\n];\r\n\\`\\`\\`\r\n\r\n### Adding Plugins\r\n\r\nAdd custom plugins in \\`src/plugins/index.${ext}\\`:\r\n\r\n\\`\\`\\`${config.typescript ? \"typescript\" : \"javascript\"}\r\nexport async function registerPlugins(app, deps) {\r\n const { config } = deps; // Explicit dependency injection\r\n\r\n await app.register(myCustomPlugin, { ...options });\r\n}\r\n\\`\\`\\`\r\n\r\n## CLI Commands\r\n\r\n\\`\\`\\`bash\r\n# Generate a new resource\r\narc generate resource product\r\n\r\n# Introspect existing schema\r\narc introspect\r\n\r\n# Generate API docs\r\narc docs\r\n\\`\\`\\`\r\n\r\n## Environment Files\r\n\r\n- \\`.env.development\\` / \\`.env.dev\\` - Development (default)\r\n- \\`.env.test\\` / \\`.env.qa\\` - Testing / QA\r\n- \\`.env.production\\` / \\`.env.prod\\` - Production\r\n- \\`.env\\` - Fallback\r\n\r\n## API Documentation\r\n\r\nAPI documentation is available via Scalar UI:\r\n\r\n- **Interactive UI**: [http://localhost:8040/docs](http://localhost:8040/docs)\r\n- **OpenAPI Spec**: [http://localhost:8040/_docs/openapi.json](http://localhost:8040/_docs/openapi.json)\r\n\r\n## API Endpoints\r\n\r\n| Method | Endpoint | Description |\r\n|--------|----------|-------------|\r\n| GET | /docs | API documentation (Scalar UI) |\r\n| GET | /_docs/openapi.json | OpenAPI 3.0 spec |\r\n| GET | /examples | List all |\r\n| GET | /examples/:id | Get by ID |\r\n| POST | /examples | Create |\r\n| PATCH | /examples/:id | Update |\r\n| DELETE | /examples/:id | Delete |\r\n`;\r\n}\r\n\r\nfunction indexTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * ${config.name} - Server Entry Point\r\n * Generated by Arc CLI\r\n *\r\n * This file starts the HTTP server.\r\n * For workers or other entry points, import createAppInstance from './app.js'\r\n */\r\n\r\n// Load environment FIRST (before any other imports)\r\nimport '#config/env.js';\r\n\r\nimport config from '#config/index.js';\r\n${config.adapter === \"mongokit\" ? \"import mongoose from 'mongoose';\" : \"\"}\r\nimport { createAppInstance } from './app.js';\r\n\r\nasync function main()${ts ? \": Promise<void>\" : \"\"} {\r\n console.log(\\`Environment: \\${config.env}\\`);\r\n${\r\n config.adapter === \"mongokit\"\r\n ? `\r\n // Connect to MongoDB\r\n await mongoose.connect(config.database.uri);\r\n console.log('Connected to MongoDB');\r\n`\r\n : \"\"\r\n}\r\n // Create and configure app\r\n const app = await createAppInstance();\r\n\r\n // Start server\r\n await app.listen({ port: config.server.port, host: config.server.host });\r\n console.log(\\`Server running at http://\\${config.server.host}:\\${config.server.port}\\`);\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error('Failed to start server:', err);\r\n process.exit(1);\r\n});\r\n`;\r\n}\r\n\r\nfunction appTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts\r\n ? \"import type { FastifyInstance } from 'fastify';\\n\"\r\n : \"\";\r\n\r\n const betterAuthImport =\r\n config.auth === \"better-auth\"\r\n ? `import { createBetterAuthAdapter } from '@classytic/arc/auth';\r\nimport { getAuth } from './auth.js';\r\n`\r\n : \"\";\r\n\r\n const authConfig =\r\n config.auth === \"better-auth\"\r\n ? config.tenant === \"multi\"\r\n ? `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth(), orgContext: true }) },`\r\n : `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth() }) },`\r\n : `auth: {\r\n type: 'jwt',\r\n jwt: { secret: config.jwt.secret },\r\n },`;\r\n\r\n return `/**\r\n * ${config.name} - App Factory\r\n * Generated by Arc CLI\r\n *\r\n * Creates and configures the Fastify app instance.\r\n * Can be imported by:\r\n * - index.ts (HTTP server)\r\n * - worker.ts (background workers)\r\n * - tests (integration tests)\r\n */\r\n\r\n${typeImport}import config from '#config/index.js';\r\nimport { createApp } from '@classytic/arc/factory';\r\n${betterAuthImport}\r\n// App-specific plugins\r\nimport { registerPlugins } from '#plugins/index.js';\r\n\r\n// Resource registry\r\nimport { registerResources } from '#resources/index.js';\r\n\r\n/**\r\n * Create a fully configured app instance\r\n *\r\n * @returns Configured Fastify instance ready to use\r\n */\r\nexport async function createAppInstance()${ts ? \": Promise<FastifyInstance>\" : \"\"} {\r\n // Create Arc app with base configuration\r\n const app = await createApp({\r\n preset: config.env === 'production' ? (${config.edge ? \"'edge'\" : \"'production'\"}) : 'development',\r\n ${authConfig}\r\n cors: {\r\n origin: config.cors.origins,\r\n methods: config.cors.methods,\r\n allowedHeaders: config.cors.allowedHeaders,\r\n credentials: config.cors.credentials,\r\n },\r\n trustProxy: true,\r\n });\r\n\r\n // Register app-specific plugins (explicit dependency injection)\r\n await registerPlugins(app, { config });\r\n\r\n // Register all resources\r\n await registerResources(app);\r\n\r\n return app;\r\n}\r\n\r\nexport default createAppInstance;\r\n`;\r\n}\r\n\r\nfunction envLoaderTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Environment Loader\r\n *\r\n * MUST be imported FIRST before any other imports.\r\n * Loads .env files based on NODE_ENV.\r\n *\r\n * Usage:\r\n * import './config/env.js'; // First line of entry point\r\n */\r\n\r\nimport dotenv from 'dotenv';\r\nimport { existsSync } from 'node:fs';\r\nimport { resolve } from 'node:path';\r\n\r\n/**\r\n * Normalize environment string to short form\r\n */\r\nfunction normalizeEnv(env${ts ? \": string | undefined\" : \"\"})${ts ? \": string\" : \"\"} {\r\n const normalized = (env || '').toLowerCase();\r\n if (normalized === 'production' || normalized === 'prod') return 'prod';\r\n if (normalized === 'test' || normalized === 'qa') return 'test';\r\n return 'dev';\r\n}\r\n\r\n// Determine environment\r\nconst env = normalizeEnv(process.env.NODE_ENV);\r\n\r\n// Load environment-specific .env file\r\nconst envFile = resolve(process.cwd(), \\`.env.\\${env}\\`);\r\nconst defaultEnvFile = resolve(process.cwd(), '.env');\r\n\r\nif (existsSync(envFile)) {\r\n dotenv.config({ path: envFile });\r\n console.log(\\`Loaded: .env.\\${env}\\`);\r\n} else if (existsSync(defaultEnvFile)) {\r\n dotenv.config({ path: defaultEnvFile });\r\n console.log('Loaded: .env');\r\n} else {\r\n console.warn('Warning: No .env file found');\r\n}\r\n\r\n// Export for reference\r\nexport const ENV = env;\r\n`;\r\n}\r\n\r\nfunction envDevTemplate(config: ProjectConfig): string {\r\n let content = `# Development Environment\r\nNODE_ENV=development\r\n\r\n# Server\r\nPORT=8040\r\nHOST=0.0.0.0\r\n`;\r\n\r\n if (config.auth === \"better-auth\") {\r\n content += `\r\n# Better Auth\r\nBETTER_AUTH_SECRET=dev-secret-change-in-production-min-32-chars\r\nFRONTEND_URL=http://localhost:3000\r\n\r\n# Google OAuth (optional — leave empty to disable)\r\nGOOGLE_CLIENT_ID=\r\nGOOGLE_CLIENT_SECRET=\r\n`;\r\n } else {\r\n content += `\r\n# JWT\r\nJWT_SECRET=dev-secret-change-in-production-min-32-chars\r\nJWT_EXPIRES_IN=7d\r\n`;\r\n }\r\n\r\n content += `\r\n# CORS - Allowed origins\r\n# Options:\r\n# * = allow all origins (not recommended for production)\r\n# Comma-separated list = specific origins only\r\nCORS_ORIGINS=http://localhost:3000,http://localhost:5173\r\n`;\r\n\r\n if (config.adapter === \"mongokit\") {\r\n content += `\r\n# MongoDB\r\nMONGODB_URI=mongodb://localhost:27017/${config.name}\r\n`;\r\n }\r\n\r\n if (config.tenant === \"multi\") {\r\n content += `\r\n# Multi-tenant\r\nORG_HEADER=x-organization-id\r\n`;\r\n }\r\n\r\n return content;\r\n}\r\n\r\nfunction pluginsIndexTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts\r\n ? \"import type { FastifyInstance } from 'fastify';\\n\"\r\n : \"\";\r\n const configType = ts ? \": { config: AppConfig }\" : \"\";\r\n const appType = ts ? \": FastifyInstance\" : \"\";\r\n\r\n let content = `/**\r\n * App Plugins Registry\r\n *\r\n * Register your app-specific plugins here.\r\n * Dependencies are passed explicitly (no shims, no magic).\r\n */\r\n\r\n${typeImport}${ts ? \"import type { AppConfig } from '../config/index.js';\\n\" : \"\"}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';\r\nimport { errorHandlerPlugin } from '@classytic/arc/plugins';\r\n`;\r\n\r\n content += `\r\n/**\r\n * Register all app-specific plugins\r\n *\r\n * @param app - Fastify instance\r\n * @param deps - Explicit dependencies (config, services, etc.)\r\n */\r\nexport async function registerPlugins(\r\n app${appType},\r\n deps${configType}\r\n)${ts ? \": Promise<void>\" : \"\"} {\r\n const { config } = deps;\r\n\r\n // Error handling (CastError → 400, validation → 422, duplicate → 409)\r\n await app.register(errorHandlerPlugin, {\r\n includeStack: config.isDev,\r\n });\r\n\r\n // API Documentation (Scalar UI)\r\n // OpenAPI spec: /_docs/openapi.json\r\n // Scalar UI: /docs\r\n await app.register(openApiPlugin, {\r\n title: '${config.name} API',\r\n version: '1.0.0',\r\n description: 'API documentation for ${config.name}',\r\n apiPrefix: '/api',\r\n });\r\n await app.register(scalarPlugin, {\r\n routePrefix: '/docs',\r\n theme: 'default',\r\n });\r\n\r\n // Add your custom plugins here:\r\n // await app.register(myCustomPlugin, { ...options });\r\n}\r\n`;\r\n\r\n return content;\r\n}\r\n\r\nfunction resourcesIndexTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts\r\n ? \"import type { FastifyInstance } from 'fastify';\\n\"\r\n : \"\";\r\n const appType = ts ? \": FastifyInstance\" : \"\";\r\n\r\n const authImports =\r\n config.auth === \"jwt\"\r\n ? `\r\n// Auth resources (register, login, /users/me)\r\nimport { authResource, userProfileResource } from './auth/auth.resource.js';\r\n`\r\n : `\r\n// Auth is handled by Better Auth — routes at /api/auth/*\r\n// No manual auth resource needed.\r\n`;\r\n\r\n const authResources =\r\n config.auth === \"jwt\"\r\n ? ` authResource,\r\n userProfileResource,\r\n `\r\n : ` `;\r\n\r\n return `/**\r\n * Resources Registry\r\n *\r\n * Central registry for all API resources.\r\n * All resources are mounted under /api prefix via Fastify scoping.\r\n */\r\n\r\n${typeImport}${authImports}\r\n// App resources\r\nimport exampleResource from './example/example.resource.js';\r\n\r\n// Add more resources here:\r\n// import productResource from './product/product.resource.js';\r\n\r\n/**\r\n * All registered resources\r\n */\r\nexport const resources = [\r\n${authResources}exampleResource,\r\n]${ts ? \" as const\" : \"\"};\r\n\r\n/**\r\n * Register all resources with the app under a common prefix.\r\n * Fastify scoping ensures all routes are mounted at /api/*.\r\n * The apiPrefix option in openApiPlugin keeps OpenAPI docs in sync.\r\n */\r\nexport async function registerResources(app${appType}, prefix = '/api')${ts ? \": Promise<void>\" : \"\"} {\r\n await app.register(async (scope) => {\r\n for (const resource of resources) {\r\n await scope.register(resource.toPlugin());\r\n }\r\n }, { prefix });\r\n}\r\n`;\r\n}\r\n\r\nfunction sharedIndexTemplate(_config: ProjectConfig): string {\r\n return `/**\r\n * Shared Utilities\r\n *\r\n * Central exports for resource definitions.\r\n * Import from here for clean, consistent code.\r\n */\r\n\r\n// Adapter factory\r\nexport { createAdapter } from './adapter.js';\r\n\r\n// Core Arc exports\r\nexport { createMongooseAdapter, defineResource } from '@classytic/arc';\r\n\r\n// Permission helpers (core + application-level)\r\nexport * from './permissions.js';\r\n\r\n// Presets\r\nexport * from './presets/index.js';\r\n`;\r\n}\r\n\r\nfunction createAdapterTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * MongoKit Adapter Factory\r\n *\r\n * Creates Arc adapters using MongoKit repositories.\r\n * The repository handles query parsing via MongoKit's built-in QueryParser.\r\n */\r\n\r\nimport { createMongooseAdapter } from '@classytic/arc';\r\n${ts ? \"import type { Model } from 'mongoose';\\nimport type { Repository } from '@classytic/mongokit';\" : \"\"}\r\n\r\n/**\r\n * Create a MongoKit-powered adapter for a resource\r\n *\r\n * Note: Query parsing is handled by MongoKit's Repository class.\r\n * Just pass the model and repository - Arc handles the rest.\r\n */\r\nexport function createAdapter${ts ? \"<TDoc = any>\" : \"\"}(\r\n model${ts ? \": Model<TDoc>\" : \"\"},\r\n repository${ts ? \": Repository<TDoc>\" : \"\"}\r\n) {\r\n return createMongooseAdapter({\r\n model,\r\n repository,\r\n });\r\n}\r\n`;\r\n}\r\n\r\nfunction customAdapterTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Custom Adapter Factory\r\n *\r\n * Implement your own database adapter here.\r\n */\r\n\r\nimport { createMongooseAdapter } from '@classytic/arc';\r\n${ts ? \"import type { Model } from 'mongoose';\" : \"\"}\r\n\r\n/**\r\n * Create a custom adapter for a resource\r\n *\r\n * Implement this based on your database choice:\r\n * - Prisma: Use @classytic/prismakit (coming soon)\r\n * - Drizzle: Create custom adapter\r\n * - Raw SQL: Create custom adapter\r\n */\r\nexport function createAdapter${ts ? \"<TDoc>\" : \"\"}(\r\n model${ts ? \": Model<TDoc>\" : \"\"},\r\n repository${ts ? \": any\" : \"\"}\r\n)${ts ? \": ReturnType<typeof createMongooseAdapter>\" : \"\"} {\r\n // SCAFFOLD: Replace with your custom adapter implementation\r\n return createMongooseAdapter({\r\n model,\r\n repository,\r\n });\r\n}\r\n`;\r\n}\r\n\r\nfunction presetsMultiTenantTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Arc Presets - Multi-Tenant Configuration\r\n *\r\n * Pre-configured presets for multi-tenant applications.\r\n * Includes both strict and flexible tenant isolation options.\r\n */\r\n\r\nimport {\r\n multiTenantPreset,\r\n ownedByUserPreset,\r\n softDeletePreset,\r\n slugLookupPreset,\r\n} from '@classytic/arc/presets';\r\n\r\n// Flexible preset for mixed public/private routes\r\nexport { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';\r\n\r\n/**\r\n * Organization-scoped preset (STRICT)\r\n * Always requires auth, always filters by organizationId.\r\n * Use for admin-only resources.\r\n */\r\nexport const orgScoped = multiTenantPreset({\r\n tenantField: 'organizationId',\r\n});\r\n\r\n/**\r\n * Owned by creator preset\r\n * Filters queries by createdBy field.\r\n */\r\nexport const ownedByCreator = ownedByUserPreset({\r\n ownerField: 'createdBy',\r\n});\r\n\r\n/**\r\n * Owned by user preset\r\n * For resources where userId references the owner.\r\n */\r\nexport const ownedByUser = ownedByUserPreset({\r\n ownerField: 'userId',\r\n});\r\n\r\n/**\r\n * Soft delete preset\r\n * Adds deletedAt filtering and restore endpoint.\r\n */\r\nexport const softDelete = softDeletePreset();\r\n\r\n/**\r\n * Slug lookup preset\r\n * Enables GET by slug in addition to ID.\r\n */\r\nexport const slugLookup = slugLookupPreset();\r\n\r\n// Export all presets\r\nexport const presets = {\r\n orgScoped,\r\n ownedByCreator,\r\n ownedByUser,\r\n softDelete,\r\n slugLookup,\r\n}${ts ? \" as const\" : \"\"};\r\n\r\nexport default presets;\r\n`;\r\n}\r\n\r\nfunction presetsSingleTenantTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Arc Presets - Single-Tenant Configuration\r\n *\r\n * Pre-configured presets for single-tenant applications.\r\n */\r\n\r\nimport {\r\n ownedByUserPreset,\r\n softDeletePreset,\r\n slugLookupPreset,\r\n} from '@classytic/arc/presets';\r\n\r\n/**\r\n * Owned by creator preset\r\n * Filters queries by createdBy field.\r\n */\r\nexport const ownedByCreator = ownedByUserPreset({\r\n ownerField: 'createdBy',\r\n});\r\n\r\n/**\r\n * Owned by user preset\r\n * For resources where userId references the owner.\r\n */\r\nexport const ownedByUser = ownedByUserPreset({\r\n ownerField: 'userId',\r\n});\r\n\r\n/**\r\n * Soft delete preset\r\n * Adds deletedAt filtering and restore endpoint.\r\n */\r\nexport const softDelete = softDeletePreset();\r\n\r\n/**\r\n * Slug lookup preset\r\n * Enables GET by slug in addition to ID.\r\n */\r\nexport const slugLookup = slugLookupPreset();\r\n\r\n// Export all presets\r\nexport const presets = {\r\n ownedByCreator,\r\n ownedByUser,\r\n softDelete,\r\n slugLookup,\r\n}${ts ? \" as const\" : \"\"};\r\n\r\nexport default presets;\r\n`;\r\n}\r\n\r\nfunction flexibleMultiTenantPresetTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeAnnotations = ts\r\n ? `\r\nimport { getOrgId, isElevated, isMember } from '@classytic/arc/scope';\r\nimport type { RequestScope } from '@classytic/arc/scope';\r\n\r\ninterface FlexibleMultiTenantOptions {\r\n tenantField?: string;\r\n}\r\n\r\ninterface PresetMiddlewares {\r\n list: ((request: any, reply: any) => Promise<void>)[];\r\n get: ((request: any, reply: any) => Promise<void>)[];\r\n create: ((request: any, reply: any) => Promise<void>)[];\r\n update: ((request: any, reply: any) => Promise<void>)[];\r\n delete: ((request: any, reply: any) => Promise<void>)[];\r\n}\r\n\r\ninterface Preset {\r\n [key: string]: unknown;\r\n name: string;\r\n middlewares: PresetMiddlewares;\r\n}\r\n`\r\n : `\r\nconst { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');\r\n`;\r\n\r\n return `/**\r\n * Flexible Multi-Tenant Preset\r\n *\r\n * Smarter tenant filtering that works with public + authenticated routes.\r\n *\r\n * Philosophy:\r\n * - No org scope → No filtering (public data, all orgs)\r\n * - Org scope present → Filter by org\r\n * - Elevated scope → No filter (platform admin sees all)\r\n *\r\n * Uses request.scope (RequestScope) from Arc's scope system.\r\n */\r\n${typeAnnotations}\r\n/**\r\n * Create flexible tenant filter middleware.\r\n * Only filters when org context is present.\r\n */\r\nfunction createFlexibleTenantFilter(tenantField${ts ? \": string\" : \"\"}) {\r\n return async (request${ts ? \": any\" : \"\"}, reply${ts ? \": any\" : \"\"}) => {\r\n const scope${ts ? \": RequestScope\" : \"\"} = request.scope ?? { kind: 'public' };\r\n\r\n // Elevated scope — platform admin sees all, no filter\r\n if (isElevated(scope)) {\r\n request.log?.debug?.({ msg: 'Elevated scope — no tenant filter' });\r\n return;\r\n }\r\n\r\n // Member scope — filter by org\r\n if (isMember(scope)) {\r\n request.query = request.query ?? {};\r\n request.query._policyFilters = {\r\n ...(request.query._policyFilters ?? {}),\r\n [tenantField]: scope.organizationId,\r\n };\r\n request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });\r\n return;\r\n }\r\n\r\n // Public / authenticated — no org context, show all data (public routes)\r\n request.log?.debug?.({ msg: 'No org context — showing all data' });\r\n };\r\n}\r\n\r\n/**\r\n * Create tenant injection middleware.\r\n * Injects tenant ID into request body on create.\r\n */\r\nfunction createTenantInjection(tenantField${ts ? \": string\" : \"\"}) {\r\n return async (request${ts ? \": any\" : \"\"}, reply${ts ? \": any\" : \"\"}) => {\r\n const scope${ts ? \": RequestScope\" : \"\"} = request.scope ?? { kind: 'public' };\r\n const orgId = getOrgId(scope);\r\n\r\n // Fail-closed: Require orgId for create operations\r\n if (!orgId) {\r\n return reply.code(403).send({\r\n success: false,\r\n error: 'Forbidden',\r\n message: 'Organization context required to create resources',\r\n });\r\n }\r\n\r\n if (request.body) {\r\n request.body[tenantField] = orgId;\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Flexible Multi-Tenant Preset\r\n *\r\n * @param options.tenantField - Field name in database (default: 'organizationId')\r\n */\r\nexport function flexibleMultiTenantPreset(options${ts ? \": FlexibleMultiTenantOptions = {}\" : \" = {}\"})${ts ? \": Preset\" : \"\"} {\r\n const { tenantField = 'organizationId' } = options;\r\n\r\n const tenantFilter = createFlexibleTenantFilter(tenantField);\r\n const tenantInjection = createTenantInjection(tenantField);\r\n\r\n return {\r\n name: 'flexibleMultiTenant',\r\n middlewares: {\r\n list: [tenantFilter],\r\n get: [tenantFilter],\r\n create: [tenantInjection],\r\n update: [tenantFilter],\r\n delete: [tenantFilter],\r\n },\r\n };\r\n}\r\n\r\nexport default flexibleMultiTenantPreset;\r\n`;\r\n}\r\n\r\nfunction permissionsTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts ? \",\\n type PermissionCheck,\" : \"\";\r\n const returnType = ts ? \": PermissionCheck\" : \"\";\r\n\r\n let content = `/**\r\n * Permission Helpers\r\n *\r\n * Clean, type-safe permission definitions for resources.\r\n */\r\n\r\nimport {\r\n requireAuth,\r\n requireRoles,\r\n requireOwnership,\r\n allowPublic,\r\n anyOf,\r\n allOf,\r\n denyAll,\r\n when${typeImport}\r\n} from '@classytic/arc/permissions';\r\n\r\n// Re-export core helpers\r\nexport {\r\n allowPublic,\r\n requireAuth,\r\n requireRoles,\r\n requireOwnership,\r\n allOf,\r\n anyOf,\r\n denyAll,\r\n when,\r\n};\r\n\r\n// ============================================================================\r\n// Permission Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Require any authenticated user\r\n */\r\nexport const requireAuthenticated = ()${returnType} =>\r\n requireRoles(['user', 'admin', 'superadmin']);\r\n\r\n/**\r\n * Require admin or superadmin\r\n */\r\nexport const requireAdmin = ()${returnType} =>\r\n requireRoles(['admin', 'superadmin']);\r\n\r\n/**\r\n * Require superadmin only\r\n */\r\nexport const requireSuperadmin = ()${returnType} =>\r\n requireRoles(['superadmin']);\r\n`;\r\n\r\n if (config.tenant === \"multi\") {\r\n if (config.auth === \"better-auth\") {\r\n // Better Auth: use requireOrgRole() which checks per-org member.role\r\n content += `\r\n// ============================================================================\r\n// Better Auth Organization & Team Permission Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Organization-level guards (per-org member.role):\r\n *\r\n * - requireOrgRole(['admin','owner']) — checks member.role in active org\r\n * - requireOrgMembership() — just checks if user is in the org (any role)\r\n * - requireTeamMembership() — checks if user is in the active team\r\n *\r\n * These are DIFFERENT from platform-level helpers above (requireRoles checks user.roles).\r\n * Platform superadmin automatically bypasses all org role checks.\r\n *\r\n * IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,\r\n * you MUST define ALL roles (owner, admin, member, + any custom) using the\r\n * same AC instance. BA's built-in defaults won't cover custom statements.\r\n * Omitting any role causes BA's hasPermission to fail silently for that role.\r\n *\r\n * @see multi-org-betterauth boilerplate (src/shared/access-control.ts) for the recommended pattern.\r\n */\r\nimport {\r\n requireOrgMembership,\r\n requireOrgRole,\r\n requireTeamMembership,\r\n} from '@classytic/arc/permissions';\r\nexport { requireOrgMembership, requireOrgRole, requireTeamMembership };\r\n\r\n/**\r\n * Require organization owner (checks member.role, not user.roles)\r\n */\r\nexport const requireOrgOwner = ()${returnType} =>\r\n requireOrgRole(['owner']);\r\n\r\n/**\r\n * Require organization manager or higher (checks member.role, not user.roles)\r\n */\r\nexport const requireOrgManager = ()${returnType} =>\r\n requireOrgRole(['manager', 'admin', 'owner']);\r\n\r\n/**\r\n * Require any organization member (any role)\r\n */\r\nexport const requireOrgStaff = ()${returnType} =>\r\n requireOrgMembership();\r\n`;\r\n } else {\r\n // JWT: no BA org plugin — use requireRoles() with user.roles\r\n content += `\r\n/**\r\n * Require organization owner (elevated scope auto-bypasses)\r\n */\r\nexport const requireOrgOwner = ()${returnType} =>\r\n requireRoles(['owner', 'admin', 'superadmin']);\r\n\r\n/**\r\n * Require organization manager or higher\r\n */\r\nexport const requireOrgManager = ()${returnType} =>\r\n requireRoles(['owner', 'manager', 'admin', 'superadmin']);\r\n\r\n/**\r\n * Require organization staff (any org member)\r\n */\r\nexport const requireOrgStaff = ()${returnType} =>\r\n requireRoles(['owner', 'manager', 'staff', 'admin', 'superadmin']);\r\n`;\r\n }\r\n }\r\n\r\n content += `\r\n// ============================================================================\r\n// Standard Permission Sets\r\n// ============================================================================\r\n\r\n/**\r\n * Public read, authenticated write (default for most resources)\r\n */\r\nexport const publicReadPermissions = {\r\n list: allowPublic(),\r\n get: allowPublic(),\r\n create: requireAuthenticated(),\r\n update: requireAuthenticated(),\r\n delete: requireAuthenticated(),\r\n};\r\n\r\n/**\r\n * All operations require authentication\r\n */\r\nexport const authenticatedPermissions = {\r\n list: requireAuth(),\r\n get: requireAuth(),\r\n create: requireAuth(),\r\n update: requireAuth(),\r\n delete: requireAuth(),\r\n};\r\n\r\n/**\r\n * Admin only permissions\r\n */\r\nexport const adminPermissions = {\r\n list: requireAdmin(),\r\n get: requireAdmin(),\r\n create: requireSuperadmin(),\r\n update: requireSuperadmin(),\r\n delete: requireSuperadmin(),\r\n};\r\n`;\r\n\r\n if (config.tenant === \"multi\") {\r\n content += `\r\n/**\r\n * Organization staff permissions\r\n */\r\nexport const orgStaffPermissions = {\r\n list: requireOrgStaff(),\r\n get: requireOrgStaff(),\r\n create: requireOrgManager(),\r\n update: requireOrgManager(),\r\n delete: requireOrgOwner(),\r\n};\r\n`;\r\n\r\n if (config.auth === \"better-auth\") {\r\n content += `\r\n/**\r\n * Team-scoped permissions (requires active team)\r\n * Uses Better Auth's team membership — flat groups, no team-level roles.\r\n */\r\nexport const teamScopedPermissions = {\r\n list: requireTeamMembership(),\r\n get: requireTeamMembership(),\r\n create: requireTeamMembership(),\r\n update: requireTeamMembership(),\r\n delete: requireOrgOwner(),\r\n};\r\n`;\r\n }\r\n }\r\n\r\n return content;\r\n}\r\n\r\nfunction configTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n const authTypeBlock =\r\n config.auth === \"better-auth\"\r\n ? `\r\n betterAuth: {\r\n secret: string;\r\n };\r\n frontend: {\r\n url: string;\r\n };`\r\n : `\r\n jwt: {\r\n secret: string;\r\n expiresIn: string;\r\n };`;\r\n\r\n let typeDefinition = \"\";\r\n if (ts) {\r\n typeDefinition = `\r\nexport interface AppConfig {\r\n env: string;\r\n isDev: boolean;\r\n isProd: boolean;\r\n server: {\r\n port: number;\r\n host: string;\r\n };${authTypeBlock}\r\n cors: {\r\n origins: string[] | boolean; // true = allow all ('*')\r\n methods: string[];\r\n allowedHeaders: string[];\r\n credentials: boolean;\r\n };${\r\n config.adapter === \"mongokit\"\r\n ? `\r\n database: {\r\n uri: string;\r\n };`\r\n : \"\"\r\n }${\r\n config.tenant === \"multi\"\r\n ? `\r\n org: {\r\n header: string;\r\n };`\r\n : \"\"\r\n }\r\n}\r\n`;\r\n }\r\n\r\n const authConfigBlock =\r\n config.auth === \"better-auth\"\r\n ? `\r\n betterAuth: {\r\n secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min-32-chars',\r\n },\r\n\r\n frontend: {\r\n url: process.env.FRONTEND_URL || 'http://localhost:3000',\r\n },`\r\n : `\r\n jwt: {\r\n secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',\r\n expiresIn: process.env.JWT_EXPIRES_IN || '7d',\r\n },`;\r\n\r\n return `/**\r\n * Application Configuration\r\n *\r\n * All config is loaded from environment variables.\r\n * ENV file is loaded by config/env.ts (imported first in entry points).\r\n */\r\n${typeDefinition}\r\nconst config${ts ? \": AppConfig\" : \"\"} = {\r\n env: process.env.NODE_ENV || 'development',\r\n isDev: (process.env.NODE_ENV || 'development') !== 'production',\r\n isProd: process.env.NODE_ENV === 'production',\r\n\r\n server: {\r\n port: parseInt(process.env.PORT || '8040', 10),\r\n host: process.env.HOST || '0.0.0.0',\r\n },\r\n${authConfigBlock}\r\n\r\n cors: {\r\n // '*' = allow all origins (true), otherwise comma-separated list\r\n origins:\r\n process.env.CORS_ORIGINS === '*'\r\n ? true\r\n : (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),\r\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],\r\n allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-request-id'],\r\n credentials: true,\r\n },\r\n${\r\n config.adapter === \"mongokit\"\r\n ? `\r\n database: {\r\n uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}',\r\n },\r\n`\r\n : \"\"\r\n}${\r\n config.tenant === \"multi\"\r\n ? `\r\n org: {\r\n header: process.env.ORG_HEADER || 'x-organization-id',\r\n },\r\n`\r\n : \"\"\r\n }};\r\n\r\nexport default config;\r\n`;\r\n}\r\n\r\nfunction exampleModelTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeExport = ts\r\n ? `\r\nexport type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;\r\nexport type ExampleModel = mongoose.Model<ExampleDocument>;\r\n`\r\n : \"\";\r\n\r\n return `/**\r\n * Example Model\r\n * Generated by Arc CLI\r\n */\r\n\r\nimport mongoose from 'mongoose';\r\n\r\nconst exampleSchema = new mongoose.Schema(\r\n {\r\n name: { type: String, required: true, trim: true },\r\n description: { type: String, trim: true },\r\n isActive: { type: Boolean, default: true, index: true },\r\n${config.tenant === \"multi\" ? \" organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },\\n\" : \"\"} createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },\r\n deletedAt: { type: Date, default: null, index: true },\r\n },\r\n {\r\n timestamps: true,\r\n toJSON: { virtuals: true },\r\n toObject: { virtuals: true },\r\n }\r\n);\r\n\r\n// Indexes for common queries\r\nexampleSchema.index({ name: 1 });\r\nexampleSchema.index({ deletedAt: 1, isActive: 1 });\r\n${config.tenant === \"multi\" ? \"exampleSchema.index({ organizationId: 1, deletedAt: 1 });\\n\" : \"\"}${typeExport}\r\nconst Example = mongoose.model${ts ? \"<ExampleDocument>\" : \"\"}('Example', exampleSchema);\r\n\r\nexport default Example;\r\n`;\r\n}\r\n\r\nfunction exampleRepositoryTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts\r\n ? \"import type { ExampleDocument } from './example.model.js';\\n\"\r\n : \"\";\r\n const generic = ts ? \"<ExampleDocument>\" : \"\";\r\n\r\n return `/**\r\n * Example Repository\r\n * Generated by Arc CLI\r\n *\r\n * MongoKit repository with plugins for:\r\n * - Soft delete (deletedAt filtering)\r\n * - Custom business logic methods\r\n */\r\n\r\nimport {\r\n Repository,\r\n softDeletePlugin,\r\n methodRegistryPlugin,\r\n} from '@classytic/mongokit';\r\n${typeImport}import Example from './example.model.js';\r\n\r\nclass ExampleRepository extends Repository${generic} {\r\n constructor() {\r\n super(Example, [\r\n methodRegistryPlugin(), // Required for plugin method registration\r\n softDeletePlugin(), // Soft delete support\r\n ]);\r\n }\r\n\r\n /**\r\n * Find all active (non-deleted) records\r\n */\r\n async findActive() {\r\n return this.Model.find({ isActive: true, deletedAt: null }).lean();\r\n }\r\n${\r\n config.tenant === \"multi\"\r\n ? `\r\n /**\r\n * Find active records for an organization\r\n */\r\n async findActiveByOrg(organizationId${ts ? \": string\" : \"\"}) {\r\n return this.Model.find({\r\n organizationId,\r\n isActive: true,\r\n deletedAt: null,\r\n }).lean();\r\n }\r\n`\r\n : \"\"\r\n}\r\n // Note: softDeletePlugin provides restore() and getDeleted() methods automatically\r\n}\r\n\r\nconst exampleRepository = new ExampleRepository();\r\n\r\nexport default exampleRepository;\r\nexport { ExampleRepository };\r\n`;\r\n}\r\n\r\nfunction exampleResourceTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Example Resource\r\n * Generated by Arc CLI\r\n *\r\n * A complete resource with:\r\n * - Model (Mongoose schema)\r\n * - Repository (MongoKit with plugins)\r\n * - Permissions (role-based access)\r\n * - Presets (soft delete${config.tenant === \"multi\" ? \", multi-tenant\" : \"\"})\r\n */\r\n\r\nimport { defineResource } from '@classytic/arc';\r\nimport { createAdapter } from '#shared/adapter.js';\r\nimport { ${config.tenant === \"multi\" ? \"orgStaffPermissions\" : \"publicReadPermissions\"} } from '#shared/permissions.js';\r\n${config.tenant === \"multi\" ? \"import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\\n\" : \"\"}import Example${ts ? \", { type ExampleDocument }\" : \"\"} from './example.model.js';\r\nimport exampleRepository from './example.repository.js';\r\nimport exampleController from './example.controller.js';\r\n\r\nconst exampleResource = defineResource${ts ? \"<ExampleDocument>\" : \"\"}({\r\n name: 'example',\r\n displayName: 'Examples',\r\n prefix: '/examples',\r\n\r\n adapter: createAdapter(Example, exampleRepository),\r\n controller: exampleController,\r\n\r\n presets: [\r\n 'softDelete',${\r\n config.tenant === \"multi\"\r\n ? `\r\n flexibleMultiTenantPreset({ tenantField: 'organizationId' }),`\r\n : \"\"\r\n }\r\n ],\r\n\r\n permissions: ${config.tenant === \"multi\" ? \"orgStaffPermissions\" : \"publicReadPermissions\"},\r\n\r\n // Add custom routes here:\r\n // additionalRoutes: [\r\n // {\r\n // method: 'GET',\r\n // path: '/custom',\r\n // summary: 'Custom endpoint',\r\n // handler: async (request, reply) => { ... },\r\n // },\r\n // ],\r\n});\r\n\r\nexport default exampleResource;\r\n`;\r\n}\r\n\r\nfunction exampleControllerTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Example Controller\r\n * Generated by Arc CLI\r\n *\r\n * BaseController provides CRUD operations with:\r\n * - Automatic pagination\r\n * - Query parsing\r\n * - Validation\r\n */\r\n\r\nimport { BaseController } from '@classytic/arc';\r\nimport exampleRepository from './example.repository.js';\r\nimport { exampleSchemaOptions } from './example.schemas.js';\r\n\r\nclass ExampleController extends BaseController {\r\n constructor() {\r\n super(exampleRepository${ts ? \" as any\" : \"\"}, {\r\n schemaOptions: exampleSchemaOptions,${\r\n config.tenant === \"multi\"\r\n ? `\r\n tenantField: 'organizationId', // Configurable tenant field for multi-tenant`\r\n : `\r\n // tenantField: 'organizationId', // For multi-tenant apps`\r\n }\r\n });\r\n }\r\n\r\n // Add custom controller methods here:\r\n // async customAction(request, reply) {\r\n // // Custom logic\r\n // }\r\n}\r\n\r\nconst exampleController = new ExampleController();\r\nexport default exampleController;\r\n`;\r\n}\r\n\r\nfunction exampleSchemasTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const multiTenantFields = config.tenant === \"multi\";\r\n\r\n return `/**\r\n * Example Schemas\r\n * Generated by Arc CLI\r\n *\r\n * Schema options for controller validation and query parsing\r\n */\r\n\r\nimport Example from './example.model.js';\r\nimport { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';\r\n\r\n/**\r\n * CRUD Schemas with Field Rules\r\n * Auto-generated from Mongoose model\r\n */\r\nconst crudSchemas = buildCrudSchemasFromModel(Example, {\r\n strictAdditionalProperties: true,\r\n fieldRules: {\r\n // Mark fields as system-managed (excluded from create/update)\r\n // deletedAt: { systemManaged: true },\r\n },\r\n query: {\r\n filterableFields: {\r\n isActive: 'boolean',${\r\n multiTenantFields\r\n ? `\r\n organizationId: 'ObjectId',`\r\n : \"\"\r\n }\r\n createdAt: 'date',\r\n },\r\n },\r\n});\r\n\r\n// Schema options for controller\r\nexport const exampleSchemaOptions${ts ? \": any\" : \"\"} = {\r\n query: {${\r\n multiTenantFields\r\n ? `\r\n allowedPopulate: ['organizationId'],`\r\n : \"\"\r\n }\r\n filterableFields: {\r\n isActive: 'boolean',${\r\n multiTenantFields\r\n ? `\r\n organizationId: 'ObjectId',`\r\n : \"\"\r\n }\r\n createdAt: 'date',\r\n },\r\n },\r\n};\r\n\r\nexport default crudSchemas;\r\n`;\r\n}\r\n\r\nfunction exampleTestTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Example Resource Tests\r\n * Generated by Arc CLI\r\n *\r\n * Run tests: npm test\r\n * Watch mode: npm run test:watch\r\n */\r\n\r\nimport { describe, it, expect, beforeAll, afterAll } from 'vitest';\r\n${config.adapter === \"mongokit\" ? \"import mongoose from 'mongoose';\\n\" : \"\"}import { createAppInstance } from '../src/app.js';\r\n${ts ? \"import type { FastifyInstance } from 'fastify';\\n\" : \"\"}\r\ndescribe('Example Resource', () => {\r\n let app${ts ? \": FastifyInstance\" : \"\"};\r\n\r\n beforeAll(async () => {\r\n${\r\n config.adapter === \"mongokit\"\r\n ? ` // Connect to test database\r\n const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';\r\n await mongoose.connect(testDbUri);\r\n`\r\n : \"\"\r\n}\r\n // Create app instance\r\n app = await createAppInstance();\r\n await app.ready();\r\n });\r\n\r\n afterAll(async () => {\r\n await app.close();\r\n${config.adapter === \"mongokit\" ? \" await mongoose.connection.close();\" : \"\"}\r\n });\r\n\r\n describe('GET /examples', () => {\r\n it('should return a list of examples', async () => {\r\n const response = await app.inject({\r\n method: 'GET',\r\n url: '/examples',\r\n });\r\n\r\n expect(response.statusCode).toBe(200);\r\n const body = JSON.parse(response.body);\r\n expect(body).toHaveProperty('docs');\r\n expect(Array.isArray(body.docs)).toBe(true);\r\n });\r\n });\r\n\r\n describe('POST /examples', () => {\r\n it('should require authentication', async () => {\r\n const response = await app.inject({\r\n method: 'POST',\r\n url: '/examples',\r\n payload: { name: 'Test Example' },\r\n });\r\n\r\n // Should fail without auth token\r\n expect(response.statusCode).toBe(401);\r\n });\r\n });\r\n\r\n // Add more tests as needed:\r\n // - GET /examples/:id\r\n // - PATCH /examples/:id\r\n // - DELETE /examples/:id\r\n // - Custom endpoints\r\n});\r\n`;\r\n}\r\n\r\n// ============================================================================\r\n// User & Auth Templates\r\n// ============================================================================\r\n\r\nfunction userModelTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n const orgRoles =\r\n config.tenant === \"multi\"\r\n ? `\r\n// Organization roles (for multi-tenant)\r\nconst ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;\r\ntype OrgRole = typeof ORG_ROLES[number];\r\n`\r\n : \"\";\r\n\r\n const orgInterface =\r\n config.tenant === \"multi\"\r\n ? `\r\ntype UserOrganization = {\r\n organizationId: Types.ObjectId;\r\n organizationName: string;\r\n roles: OrgRole[];\r\n joinedAt: Date;\r\n};\r\n`\r\n : \"\";\r\n\r\n const orgSchema =\r\n config.tenant === \"multi\"\r\n ? `\r\n // Multi-org support\r\n organizations: [{\r\n organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },\r\n organizationName: { type: String, required: true },\r\n roles: { type: [String], enum: ORG_ROLES, default: [] },\r\n joinedAt: { type: Date, default: () => new Date() },\r\n }],\r\n`\r\n : \"\";\r\n\r\n const orgMethods =\r\n config.tenant === \"multi\"\r\n ? `\r\n// Organization methods\r\nuserSchema.methods.getOrgRoles = function(orgId${ts ? \": Types.ObjectId | string\" : \"\"}) {\r\n const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());\r\n return org?.roles || [];\r\n};\r\n\r\nuserSchema.methods.hasOrgAccess = function(orgId${ts ? \": Types.ObjectId | string\" : \"\"}) {\r\n return this.organizations.some(o => o.organizationId.toString() === orgId.toString());\r\n};\r\n\r\nuserSchema.methods.addOrganization = function(\r\n organizationId${ts ? \": Types.ObjectId\" : \"\"},\r\n organizationName${ts ? \": string\" : \"\"},\r\n roles${ts ? \": OrgRole[]\" : \"\"} = []\r\n) {\r\n const existing = this.organizations.find(o => o.organizationId.toString() === organizationId.toString());\r\n if (existing) {\r\n existing.organizationName = organizationName;\r\n existing.roles = [...new Set([...existing.roles, ...roles])];\r\n } else {\r\n this.organizations.push({ organizationId, organizationName, roles, joinedAt: new Date() });\r\n }\r\n return this;\r\n};\r\n\r\nuserSchema.methods.removeOrganization = function(organizationId${ts ? \": Types.ObjectId\" : \"\"}) {\r\n this.organizations = this.organizations.filter(o => o.organizationId.toString() !== organizationId.toString());\r\n return this;\r\n};\r\n\r\n// Index for org queries\r\nuserSchema.index({ 'organizations.organizationId': 1 });\r\n`\r\n : \"\";\r\n\r\n const userType = ts\r\n ? `\r\ntype PlatformRole = 'user' | 'admin' | 'superadmin';\r\n\r\ntype User = {\r\n name: string;\r\n email: string;\r\n password: string;\r\n roles: PlatformRole[];${\r\n config.tenant === \"multi\"\r\n ? `\r\n organizations: UserOrganization[];`\r\n : \"\"\r\n }\r\n resetPasswordToken?: string;\r\n resetPasswordExpires?: Date;\r\n};\r\n\r\ntype UserMethods = {\r\n matchPassword: (enteredPassword: string) => Promise<boolean>;${\r\n config.tenant === \"multi\"\r\n ? `\r\n getOrgRoles: (orgId: Types.ObjectId | string) => OrgRole[];\r\n hasOrgAccess: (orgId: Types.ObjectId | string) => boolean;\r\n addOrganization: (orgId: Types.ObjectId, name: string, roles?: OrgRole[]) => UserDocument;\r\n removeOrganization: (orgId: Types.ObjectId) => UserDocument;`\r\n : \"\"\r\n }\r\n};\r\n\r\nexport type UserDocument = HydratedDocument<User, UserMethods>;\r\nexport type UserModel = Model<User, {}, UserMethods>;\r\n`\r\n : \"\";\r\n\r\n return `/**\r\n * User Model\r\n * Generated by Arc CLI\r\n */\r\n\r\nimport bcrypt from 'bcryptjs';\r\nimport mongoose${ts ? \", { type HydratedDocument, type Model, type Types }\" : \"\"} from 'mongoose';\r\n${orgRoles}\r\nconst { Schema } = mongoose;\r\n${orgInterface}${userType}\r\nconst userSchema = new Schema${ts ? \"<User, UserModel, UserMethods>\" : \"\"}(\r\n {\r\n name: { type: String, required: true, trim: true },\r\n email: {\r\n type: String,\r\n required: true,\r\n unique: true,\r\n lowercase: true,\r\n trim: true,\r\n },\r\n password: { type: String, required: true },\r\n\r\n // Platform roles\r\n roles: {\r\n type: [String],\r\n enum: ['user', 'admin', 'superadmin'],\r\n default: ['user'],\r\n },\r\n${orgSchema}\r\n // Password reset\r\n resetPasswordToken: String,\r\n resetPasswordExpires: Date,\r\n },\r\n { timestamps: true }\r\n);\r\n\r\n// Password hashing\r\nuserSchema.pre('save', async function() {\r\n if (!this.isModified('password')) return;\r\n const salt = await bcrypt.genSalt(10);\r\n this.password = await bcrypt.hash(this.password, salt);\r\n});\r\n\r\n// Password comparison\r\nuserSchema.methods.matchPassword = async function(enteredPassword${ts ? \": string\" : \"\"}) {\r\n return bcrypt.compare(enteredPassword, this.password);\r\n};\r\n${orgMethods}\r\n// Exclude password in JSON\r\nuserSchema.set('toJSON', {\r\n transform: (_doc, ret${ts ? \": any\" : \"\"}) => {\r\n delete ret.password;\r\n delete ret.resetPasswordToken;\r\n delete ret.resetPasswordExpires;\r\n return ret;\r\n },\r\n});\r\n\r\nconst User = mongoose.models.User${ts ? \" as UserModel\" : \"\"} || mongoose.model${ts ? \"<User, UserModel>\" : \"\"}('User', userSchema);\r\nexport default User;\r\n`;\r\n}\r\n\r\nfunction userRepositoryTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeImport = ts\r\n ? \"import type { UserDocument } from './user.model.js';\\nimport type { ClientSession, Types } from 'mongoose';\\n\"\r\n : \"\";\r\n\r\n return `/**\r\n * User Repository\r\n * Generated by Arc CLI\r\n *\r\n * MongoKit repository with plugins for common operations\r\n */\r\n\r\nimport {\r\n Repository,\r\n methodRegistryPlugin,\r\n mongoOperationsPlugin,\r\n} from '@classytic/mongokit';\r\n${typeImport}import User from './user.model.js';\r\n\r\n${ts ? \"type ID = string | Types.ObjectId;\\n\" : \"\"}\r\nclass UserRepository extends Repository${ts ? \"<UserDocument>\" : \"\"} {\r\n constructor() {\r\n super(User${ts ? \" as any\" : \"\"}, [\r\n methodRegistryPlugin(),\r\n mongoOperationsPlugin(),\r\n ]);\r\n }\r\n\r\n /**\r\n * Find user by email\r\n */\r\n async findByEmail(email${ts ? \": string\" : \"\"}) {\r\n return this.Model.findOne({ email: email.toLowerCase().trim() });\r\n }\r\n\r\n /**\r\n * Find user by reset token\r\n */\r\n async findByResetToken(token${ts ? \": string\" : \"\"}) {\r\n return this.Model.findOne({\r\n resetPasswordToken: token,\r\n resetPasswordExpires: { $gt: Date.now() },\r\n });\r\n }\r\n\r\n /**\r\n * Check if email exists\r\n */\r\n async emailExists(email${ts ? \": string\" : \"\"})${ts ? \": Promise<boolean>\" : \"\"} {\r\n const result = await this.Model.exists({ email: email.toLowerCase().trim() });\r\n return !!result;\r\n }\r\n\r\n /**\r\n * Update user password (triggers hash middleware)\r\n */\r\n async updatePassword(userId${ts ? \": ID\" : \"\"}, newPassword${ts ? \": string\" : \"\"}, options${ts ? \": { session?: ClientSession }\" : \"\"} = {}) {\r\n const user = await this.Model.findById(userId).session(options.session ?? null);\r\n if (!user) throw new Error('User not found');\r\n\r\n user.password = newPassword;\r\n user.resetPasswordToken = undefined;\r\n user.resetPasswordExpires = undefined;\r\n await user.save({ session: options.session ?? undefined });\r\n return user;\r\n }\r\n\r\n /**\r\n * Set reset token\r\n */\r\n async setResetToken(userId${ts ? \": ID\" : \"\"}, token${ts ? \": string\" : \"\"}, expiresAt${ts ? \": Date\" : \"\"}) {\r\n return this.Model.findByIdAndUpdate(\r\n userId,\r\n { resetPasswordToken: token, resetPasswordExpires: expiresAt },\r\n { new: true }\r\n );\r\n }\r\n${\r\n config.tenant === \"multi\"\r\n ? `\r\n /**\r\n * Find users by organization\r\n */\r\n async findByOrganization(organizationId${ts ? \": ID\" : \"\"}) {\r\n return this.Model.find({ 'organizations.organizationId': organizationId })\r\n .select('-password -resetPasswordToken -resetPasswordExpires')\r\n .lean();\r\n }\r\n`\r\n : \"\"\r\n}\r\n}\r\n\r\nconst userRepository = new UserRepository();\r\nexport default userRepository;\r\nexport { UserRepository };\r\n`;\r\n}\r\n\r\nfunction userControllerTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * User Controller\r\n * Generated by Arc CLI\r\n *\r\n * BaseController for user management operations.\r\n * Used by auth resource for /users/me endpoints.\r\n */\r\n\r\nimport { BaseController } from '@classytic/arc';\r\nimport userRepository from './user.repository.js';\r\n\r\nclass UserController extends BaseController {\r\n constructor() {\r\n super(userRepository${ts ? \" as any\" : \"\"});\r\n }\r\n\r\n // Custom user operations can be added here\r\n}\r\n\r\nconst userController = new UserController();\r\nexport default userController;\r\n`;\r\n}\r\n\r\nfunction authResourceTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Auth Resource\r\n * Generated by Arc CLI\r\n *\r\n * Combined auth + user profile endpoints:\r\n * - POST /auth/register\r\n * - POST /auth/login\r\n * - POST /auth/refresh\r\n * - POST /auth/forgot-password\r\n * - POST /auth/reset-password\r\n * - GET /users/me\r\n * - PATCH /users/me\r\n */\r\n\r\nimport { defineResource } from '@classytic/arc';\r\nimport { allowPublic, requireAuth } from '@classytic/arc/permissions';\r\nimport { createAdapter } from '#shared/adapter.js';\r\nimport User from '../user/user.model.js';\r\nimport userRepository from '../user/user.repository.js';\r\nimport * as handlers from './auth.handlers.js';\r\nimport * as schemas from './auth.schemas.js';\r\n\r\n/**\r\n * Auth Resource - handles authentication\r\n */\r\nexport const authResource = defineResource({\r\n name: 'auth',\r\n displayName: 'Authentication',\r\n tag: 'Authentication',\r\n prefix: '/auth',\r\n\r\n adapter: createAdapter(User${ts ? \" as any\" : \"\"}, userRepository${ts ? \" as any\" : \"\"}),\r\n disableDefaultRoutes: true,\r\n\r\n additionalRoutes: [\r\n {\r\n method: 'POST',\r\n path: '/register',\r\n summary: 'Register new user',\r\n permissions: allowPublic(),\r\n handler: handlers.register,\r\n wrapHandler: false,\r\n schema: { body: schemas.registerBody, response: { 201: schemas.successResponse } },\r\n },\r\n {\r\n method: 'POST',\r\n path: '/login',\r\n summary: 'User login',\r\n permissions: allowPublic(),\r\n handler: handlers.login,\r\n wrapHandler: false,\r\n schema: { body: schemas.loginBody, response: { 200: schemas.loginResponse } },\r\n },\r\n {\r\n method: 'POST',\r\n path: '/refresh',\r\n summary: 'Refresh access token',\r\n permissions: allowPublic(),\r\n handler: handlers.refreshToken,\r\n wrapHandler: false,\r\n schema: { body: schemas.refreshBody, response: { 200: schemas.tokenResponse } },\r\n },\r\n {\r\n method: 'POST',\r\n path: '/forgot-password',\r\n summary: 'Request password reset',\r\n permissions: allowPublic(),\r\n handler: handlers.forgotPassword,\r\n wrapHandler: false,\r\n schema: { body: schemas.forgotBody, response: { 200: schemas.successResponse } },\r\n },\r\n {\r\n method: 'POST',\r\n path: '/reset-password',\r\n summary: 'Reset password with token',\r\n permissions: allowPublic(),\r\n handler: handlers.resetPassword,\r\n wrapHandler: false,\r\n schema: { body: schemas.resetBody, response: { 200: schemas.successResponse } },\r\n },\r\n ],\r\n});\r\n\r\n/**\r\n * User Profile Resource - handles /users/me\r\n */\r\nexport const userProfileResource = defineResource({\r\n name: 'user-profile',\r\n displayName: 'User Profile',\r\n tag: 'User Profile',\r\n prefix: '/users',\r\n\r\n adapter: createAdapter(User${ts ? \" as any\" : \"\"}, userRepository${ts ? \" as any\" : \"\"}),\r\n disableDefaultRoutes: true,\r\n\r\n additionalRoutes: [\r\n {\r\n method: 'GET',\r\n path: '/me',\r\n summary: 'Get current user profile',\r\n permissions: requireAuth(),\r\n handler: handlers.getUserProfile,\r\n wrapHandler: false,\r\n schema: { response: { 200: schemas.userProfileResponse } },\r\n },\r\n {\r\n method: 'PATCH',\r\n path: '/me',\r\n summary: 'Update current user profile',\r\n permissions: requireAuth(),\r\n handler: handlers.updateUserProfile,\r\n wrapHandler: false,\r\n schema: { body: schemas.updateUserBody, response: { 200: schemas.userProfileResponse } },\r\n },\r\n ],\r\n});\r\n\r\nexport default authResource;\r\n`;\r\n}\r\n\r\nfunction betterAuthSetupTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const mongoImport =\r\n config.adapter === \"mongokit\"\r\n ? `import mongoose from 'mongoose';\r\nimport { mongodbAdapter } from 'better-auth/adapters/mongodb';`\r\n : \"\";\r\n\r\n const dbAdapter =\r\n config.adapter === \"mongokit\"\r\n ? config.typescript\r\n ? `database: mongodbAdapter(mongoose.connection.getClient().db() as any),`\r\n : `database: mongodbAdapter(mongoose.connection.getClient().db()),`\r\n : `// Configure your database adapter here\r\n // See: https://www.better-auth.com/docs/concepts/database`;\r\n\r\n const orgPlugin =\r\n config.tenant === \"multi\"\r\n ? `\r\nimport { organization } from 'better-auth/plugins/organization';\r\nimport { bearer } from 'better-auth/plugins/bearer';`\r\n : \"\";\r\n\r\n const orgPluginUsage =\r\n config.tenant === \"multi\"\r\n ? `\r\n plugins: [\r\n bearer(),\r\n organization({\r\n allowUserToCreateOrganization: true,\r\n creatorRole: 'owner',\r\n teams: {\r\n enabled: true,\r\n },\r\n }),\r\n ],`\r\n : \"\";\r\n\r\n const googleProvider = `\r\n // Google OAuth (enabled when env vars are set)\r\n ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET\r\n ? {\r\n socialProviders: {\r\n google: {\r\n clientId: process.env.GOOGLE_CLIENT_ID,\r\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\r\n },\r\n },\r\n }\r\n : {}),`;\r\n\r\n return `/**\r\n * Better Auth Configuration\r\n * Generated by Arc CLI\r\n *\r\n * Authentication is handled entirely by Better Auth.\r\n * Routes are registered automatically at /api/auth/*\r\n *\r\n * Better Auth manages these collections:\r\n * - user, session, account${config.tenant === \"multi\" ? \", organization, member, invitation, team, teamMember\" : \"\"}\r\n *\r\n * @see https://www.better-auth.com/docs\r\n */\r\n\r\nimport { betterAuth } from 'better-auth';\r\n${mongoImport}${orgPlugin}\r\nimport config from '#config/index.js';\r\n\r\nlet _auth${ts ? \": ReturnType<typeof betterAuth> | null\" : \"\"} = null;\r\n\r\n/**\r\n * Get the Better Auth instance (lazy singleton)\r\n *\r\n * Must be called AFTER database connection is established.\r\n */\r\nexport function getAuth()${ts ? \": ReturnType<typeof betterAuth>\" : \"\"} {\r\n if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {\r\n throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');\r\n }\r\n\r\n if (!_auth) {\r\n _auth = betterAuth({\r\n secret: config.betterAuth.secret,\r\n baseURL: process.env.BETTER_AUTH_URL || \\`http://localhost:\\${config.server.port}\\`,\r\n basePath: '/api/auth',\r\n\r\n ${dbAdapter}\r\n${\r\n config.tenant === \"multi\"\r\n ? `\r\n user: {\r\n additionalFields: {\r\n roles: {\r\n type: 'string[]',\r\n defaultValue: ['user'],\r\n required: false,\r\n input: false, // Cannot be set during signup\r\n },\r\n },\r\n },\r\n`\r\n : \"\"\r\n}\r\n emailAndPassword: {\r\n enabled: true,\r\n minPasswordLength: 6,\r\n },\r\n${googleProvider}\r\n${orgPluginUsage}\r\n session: {\r\n cookieCache: {\r\n enabled: true,\r\n maxAge: 5 * 60, // 5 minutes\r\n },\r\n },\r\n\r\n trustedOrigins: [config.frontend.url],\r\n\r\n rateLimit: {\r\n enabled: process.env.NODE_ENV === 'production',\r\n },\r\n });\r\n }\r\n\r\n return _auth;\r\n}\r\n\r\nexport default getAuth;\r\n`;\r\n}\r\n\r\nfunction authHandlersTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n const typeAnnotations = ts\r\n ? `\r\nimport type { FastifyRequest, FastifyReply } from 'fastify';\r\n// Load Arc auth type augmentations (adds request.server.auth typings)\r\nimport '@classytic/arc/auth';\r\n`\r\n : \"\";\r\n\r\n return `/**\r\n * Auth Handlers\r\n * Generated by Arc CLI\r\n *\r\n * Uses Arc's built-in JWT utilities via fastify.auth (provided by @fastify/jwt v10).\r\n * No standalone jsonwebtoken dependency needed.\r\n */\r\n\r\nimport userRepository from '../user/user.repository.js';\r\n${typeAnnotations}\r\n\r\n/**\r\n * Register new user\r\n */\r\nexport async function register(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const { name, email, password } = request.body${ts ? \" as any\" : \"\"};\r\n\r\n // Check if email exists\r\n if (await userRepository.emailExists(email)) {\r\n return reply.code(400).send({ success: false, message: 'Email already registered' });\r\n }\r\n\r\n // Create user\r\n await userRepository.create({ name, email, password, roles: ['user'] });\r\n\r\n return reply.code(201).send({ success: true, message: 'User registered successfully' });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Register error');\r\n return reply.code(500).send({ success: false, message: 'Registration failed' });\r\n }\r\n}\r\n\r\n/**\r\n * Login user\r\n */\r\nexport async function login(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const { email, password } = request.body${ts ? \" as any\" : \"\"};\r\n\r\n const user = await userRepository.findByEmail(email);\r\n if (!user || !(await user.matchPassword(password))) {\r\n return reply.code(401).send({ success: false, message: 'Invalid credentials' });\r\n }\r\n\r\n const tokens = request.server.auth.issueTokens({ id: user._id.toString(), roles: user.roles });\r\n\r\n return reply.send({\r\n success: true,\r\n user: { id: user._id, name: user.name, email: user.email, roles: user.roles },\r\n ...tokens,\r\n });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Login error');\r\n return reply.code(500).send({ success: false, message: 'Login failed' });\r\n }\r\n}\r\n\r\n/**\r\n * Refresh access token\r\n */\r\nexport async function refreshToken(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const { token } = request.body${ts ? \" as any\" : \"\"};\r\n if (!token) {\r\n return reply.code(401).send({ success: false, message: 'Refresh token required' });\r\n }\r\n\r\n const decoded = request.server.auth.verifyRefreshToken(token)${ts ? \" as { id: string }\" : \"\"};\r\n const tokens = request.server.auth.issueTokens({ id: decoded.id });\r\n\r\n return reply.send({ success: true, ...tokens });\r\n } catch {\r\n return reply.code(401).send({ success: false, message: 'Invalid refresh token' });\r\n }\r\n}\r\n\r\n/**\r\n * Forgot password\r\n */\r\nexport async function forgotPassword(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const { email } = request.body${ts ? \" as any\" : \"\"};\r\n const user = await userRepository.findByEmail(email);\r\n\r\n if (user) {\r\n const { randomBytes } = await import('node:crypto');\r\n const token = randomBytes(32).toString('hex');\r\n const expires = new Date(Date.now() + 3600000); // 1 hour\r\n await userRepository.setResetToken(user._id, token, expires);\r\n // SCAFFOLD: Integrate your email provider to send the reset link\r\n request.log.info(\\`Password reset requested for \\${email}\\`);\r\n }\r\n\r\n // Always return success to prevent email enumeration\r\n return reply.send({ success: true, message: 'If email exists, reset link sent' });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Forgot password error');\r\n return reply.code(500).send({ success: false, message: 'Failed to process request' });\r\n }\r\n}\r\n\r\n/**\r\n * Reset password\r\n */\r\nexport async function resetPassword(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const { token, newPassword } = request.body${ts ? \" as any\" : \"\"};\r\n const user = await userRepository.findByResetToken(token);\r\n\r\n if (!user) {\r\n return reply.code(400).send({ success: false, message: 'Invalid or expired token' });\r\n }\r\n\r\n await userRepository.updatePassword(user._id, newPassword);\r\n return reply.send({ success: true, message: 'Password has been reset' });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Reset password error');\r\n return reply.code(500).send({ success: false, message: 'Failed to reset password' });\r\n }\r\n}\r\n\r\n/**\r\n * Get current user profile\r\n */\r\nexport async function getUserProfile(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const userId = (request${ts ? \" as any\" : \"\"}).user?._id || (request${ts ? \" as any\" : \"\"}).user?.id;\r\n const user = await userRepository.getById(userId);\r\n\r\n if (!user) {\r\n return reply.code(404).send({ success: false, message: 'User not found' });\r\n }\r\n\r\n return reply.send({ success: true, data: user });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Get profile error');\r\n return reply.code(500).send({ success: false, message: 'Failed to get profile' });\r\n }\r\n}\r\n\r\n/**\r\n * Update current user profile\r\n */\r\nexport async function updateUserProfile(request${ts ? \": FastifyRequest\" : \"\"}, reply${ts ? \": FastifyReply\" : \"\"}) {\r\n try {\r\n const userId = (request${ts ? \" as any\" : \"\"}).user?._id || (request${ts ? \" as any\" : \"\"}).user?.id;\r\n const updates = { ...request.body${ts ? \" as any\" : \"\"} };\r\n\r\n // Prevent updating protected fields\r\n if ('password' in updates) delete updates.password;\r\n if ('roles' in updates) delete updates.roles;\r\n if ('organizations' in updates) delete updates.organizations;\r\n\r\n const user = await userRepository.Model.findByIdAndUpdate(userId, updates, { new: true });\r\n return reply.send({ success: true, data: user });\r\n } catch (error) {\r\n request.log.error({ err: error }, 'Update profile error');\r\n return reply.code(500).send({ success: false, message: 'Failed to update profile' });\r\n }\r\n}\r\n`;\r\n}\r\n\r\nfunction authSchemasTemplate(_config: ProjectConfig): string {\r\n return `/**\r\n * Auth Schemas\r\n * Generated by Arc CLI\r\n */\r\n\r\nexport const registerBody = {\r\n type: 'object',\r\n required: ['name', 'email', 'password'],\r\n properties: {\r\n name: { type: 'string', minLength: 2 },\r\n email: { type: 'string', format: 'email' },\r\n password: { type: 'string', minLength: 6 },\r\n },\r\n};\r\n\r\nexport const loginBody = {\r\n type: 'object',\r\n required: ['email', 'password'],\r\n properties: {\r\n email: { type: 'string', format: 'email' },\r\n password: { type: 'string' },\r\n },\r\n};\r\n\r\nexport const refreshBody = {\r\n type: 'object',\r\n required: ['token'],\r\n properties: {\r\n token: { type: 'string' },\r\n },\r\n};\r\n\r\nexport const forgotBody = {\r\n type: 'object',\r\n required: ['email'],\r\n properties: {\r\n email: { type: 'string', format: 'email' },\r\n },\r\n};\r\n\r\nexport const resetBody = {\r\n type: 'object',\r\n required: ['token', 'newPassword'],\r\n properties: {\r\n token: { type: 'string' },\r\n newPassword: { type: 'string', minLength: 6 },\r\n },\r\n};\r\n\r\nexport const updateUserBody = {\r\n type: 'object',\r\n properties: {\r\n name: { type: 'string', minLength: 2 },\r\n email: { type: 'string', format: 'email' },\r\n },\r\n};\r\n\r\n// Response schemas (enables fast-json-stringify serialization)\r\n\r\nexport const successResponse = {\r\n type: 'object',\r\n properties: {\r\n success: { type: 'boolean' },\r\n message: { type: 'string' },\r\n },\r\n};\r\n\r\nexport const loginResponse = {\r\n type: 'object',\r\n properties: {\r\n success: { type: 'boolean' },\r\n user: {\r\n type: 'object',\r\n properties: {\r\n id: { type: 'string' },\r\n name: { type: 'string' },\r\n email: { type: 'string' },\r\n roles: { type: 'array', items: { type: 'string' } },\r\n },\r\n },\r\n accessToken: { type: 'string' },\r\n refreshToken: { type: 'string' },\r\n },\r\n};\r\n\r\nexport const tokenResponse = {\r\n type: 'object',\r\n properties: {\r\n success: { type: 'boolean' },\r\n accessToken: { type: 'string' },\r\n refreshToken: { type: 'string' },\r\n },\r\n};\r\n\r\nexport const userProfileResponse = {\r\n type: 'object',\r\n properties: {\r\n success: { type: 'boolean' },\r\n data: { type: 'object', additionalProperties: true },\r\n },\r\n};\r\n`;\r\n}\r\n\r\nfunction authTestTemplate(config: ProjectConfig): string {\r\n const ts = config.typescript;\r\n\r\n return `/**\r\n * Auth Tests\r\n * Generated by Arc CLI\r\n */\r\n\r\nimport { describe, it, expect, beforeAll, afterAll } from 'vitest';\r\n${config.adapter === \"mongokit\" ? \"import mongoose from 'mongoose';\\n\" : \"\"}import { createAppInstance } from '../src/app.js';\r\n${ts ? \"import type { FastifyInstance } from 'fastify';\\n\" : \"\"}\r\ndescribe('Auth', () => {\r\n let app${ts ? \": FastifyInstance\" : \"\"};\r\n const testUser = {\r\n name: 'Test User',\r\n email: 'test@example.com',\r\n password: 'password123',\r\n };\r\n\r\n beforeAll(async () => {\r\n${\r\n config.adapter === \"mongokit\"\r\n ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';\r\n await mongoose.connect(testDbUri);\r\n // Clean up test data\r\n await mongoose.connection.collection('users').deleteMany({ email: testUser.email });\r\n`\r\n : \"\"\r\n}\r\n app = await createAppInstance();\r\n await app.ready();\r\n });\r\n\r\n afterAll(async () => {\r\n${\r\n config.adapter === \"mongokit\"\r\n ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });\r\n await mongoose.connection.close();\r\n`\r\n : \"\"\r\n} await app.close();\r\n });\r\n\r\n describe('POST /auth/register', () => {\r\n it('should register a new user', async () => {\r\n const response = await app.inject({\r\n method: 'POST',\r\n url: '/auth/register',\r\n payload: testUser,\r\n });\r\n\r\n expect(response.statusCode).toBe(201);\r\n const body = JSON.parse(response.body);\r\n expect(body.success).toBe(true);\r\n });\r\n\r\n it('should reject duplicate email', async () => {\r\n const response = await app.inject({\r\n method: 'POST',\r\n url: '/auth/register',\r\n payload: testUser,\r\n });\r\n\r\n expect(response.statusCode).toBe(400);\r\n });\r\n });\r\n\r\n describe('POST /auth/login', () => {\r\n it('should login with valid credentials', async () => {\r\n const response = await app.inject({\r\n method: 'POST',\r\n url: '/auth/login',\r\n payload: { email: testUser.email, password: testUser.password },\r\n });\r\n\r\n expect(response.statusCode).toBe(200);\r\n const body = JSON.parse(response.body);\r\n expect(body.success).toBe(true);\r\n expect(body.accessToken).toBeDefined();\r\n expect(body.refreshToken).toBeDefined();\r\n });\r\n\r\n it('should reject invalid credentials', async () => {\r\n const response = await app.inject({\r\n method: 'POST',\r\n url: '/auth/login',\r\n payload: { email: testUser.email, password: 'wrongpassword' },\r\n });\r\n\r\n expect(response.statusCode).toBe(401);\r\n });\r\n });\r\n\r\n describe('GET /users/me', () => {\r\n it('should require authentication', async () => {\r\n const response = await app.inject({\r\n method: 'GET',\r\n url: '/users/me',\r\n });\r\n\r\n expect(response.statusCode).toBe(401);\r\n });\r\n });\r\n});\r\n`;\r\n}\r\n\r\n// ============================================================================\r\n// Success Message\r\n// ============================================================================\r\n\r\nfunction printSuccessMessage(\r\n config: ProjectConfig,\r\n skipInstall?: boolean,\r\n): void {\r\n const installStep = skipInstall ? ` npm install\\n` : \"\";\r\n const ext = config.typescript ? \"ts\" : \"js\";\r\n\r\n const authInfo =\r\n config.auth === \"better-auth\"\r\n ? `\r\nAuth (Better Auth):\r\n\r\n Auth routes: http://localhost:8040/api/auth/*\r\n Better Auth handles: registration, login, sessions, OAuth\r\n Config file: src/auth.${ext}\r\n`\r\n : `\r\nAuth (JWT):\r\n\r\n POST /auth/register # Register\r\n POST /auth/login # Login (returns JWT)\r\n POST /auth/refresh # Refresh token\r\n GET /users/me # Current user profile\r\n`;\r\n\r\n console.log(`\r\n╔═══════════════════════════════════════════════════════════════╗\r\n║ Project Created ║\r\n╚═══════════════════════════════════════════════════════════════╝\r\n\r\nNext steps:\r\n\r\n cd ${config.name}\r\n${installStep} npm run dev # Uses .env.dev automatically\r\n${authInfo}\r\nAPI Documentation:\r\n\r\n http://localhost:8040/docs # Scalar UI\r\n http://localhost:8040/_docs/openapi.json # OpenAPI spec\r\n\r\nRun tests:\r\n\r\n npm test # Run once\r\n npm run test:watch # Watch mode\r\n\r\nAdd resources:\r\n\r\n arc generate resource product\r\n\r\nProject structure:\r\n\r\n src/\r\n ├── app.${ext} # App factory (for workers/tests)\r\n ├── index.${ext} # Server entry${config.auth === \"better-auth\" ? `\\n ├── auth.${ext} # Better Auth config` : \"\"}\r\n ├── config/ # Configuration\r\n ├── shared/ # Adapters, presets, permissions\r\n ├── plugins/ # App plugins (DI pattern)\r\n └── resources/ # API resources\r\n\r\nDocumentation:\r\n https://github.com/classytic/arc\r\n`);\r\n}\r\n\r\nexport default init;\r\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/cli/commands/introspect.d.ts
2
+ /**
3
+ * Arc CLI - Introspect Command
4
+ *
5
+ * Shows all registered resources and their configuration.
6
+ * Requires an entry file that exports defineResource() results.
7
+ */
8
+ declare function introspect(args: string[]): Promise<void>;
9
+ //#endregion
10
+ export { introspect as default, introspect };
11
+ //# sourceMappingURL=introspect.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect.d.mts","names":[],"sources":["../../../src/cli/commands/introspect.ts"],"mappings":";;AAsBA;;;;;iBAAsB,UAAA,CAAW,IAAA,aAAiB,OAAA"}
@@ -0,0 +1,76 @@
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-DsN4KJjV.mjs";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ //#region src/cli/commands/introspect.ts
6
+ /**
7
+ * Arc CLI - Introspect Command
8
+ *
9
+ * Shows all registered resources and their configuration.
10
+ * Requires an entry file that exports defineResource() results.
11
+ */
12
+ function describePermission(value) {
13
+ if (!value) return "none";
14
+ if (typeof value === "function") return value.name ? `${value.name}()` : "[anonymous permission function]";
15
+ if (Array.isArray(value)) return `[${value.join(", ")}]`;
16
+ if (typeof value === "object") return "[permission object]";
17
+ return String(value);
18
+ }
19
+ async function introspect(args) {
20
+ console.log("Introspecting Arc resources...\n");
21
+ try {
22
+ const entryPath = args[0];
23
+ if (!entryPath) {
24
+ console.log("Usage: arc introspect <entry-file>\n");
25
+ console.log("Where entry-file exports your defineResource() results.");
26
+ console.log("Example: arc introspect ./src/resources.js");
27
+ return;
28
+ }
29
+ const entryModule = await import(pathToFileURL(resolve(process.cwd(), entryPath)).href);
30
+ const registry = new ResourceRegistry();
31
+ let registered = 0;
32
+ function tryRegister(value) {
33
+ if (value && typeof value === "object" && "name" in value && "_registryMeta" in value && "toPlugin" in value) {
34
+ registry.register(value, value._registryMeta ?? {});
35
+ registered++;
36
+ }
37
+ }
38
+ for (const exported of Object.values(entryModule)) if (Array.isArray(exported)) exported.forEach(tryRegister);
39
+ else tryRegister(exported);
40
+ if (registered === 0) {
41
+ console.log("No resource definitions found in entry file.");
42
+ console.log("\nMake sure your file exports defineResource() results:");
43
+ console.log(" export const productResource = defineResource({ ... });");
44
+ return;
45
+ }
46
+ const resources = registry.getAll();
47
+ console.log(`Found ${resources.length} resource(s):\n`);
48
+ resources.forEach((resource, index) => {
49
+ console.log(`${index + 1}. ${resource.name}`);
50
+ console.log(` Display Name: ${resource.displayName}`);
51
+ console.log(` Prefix: ${resource.prefix}`);
52
+ console.log(` Module: ${resource.module || "none"}`);
53
+ if (resource.permissions) {
54
+ console.log(` Permissions:`);
55
+ Object.entries(resource.permissions).forEach(([op, permission]) => {
56
+ console.log(` ${op}: ${describePermission(permission)}`);
57
+ });
58
+ }
59
+ if (resource.presets && resource.presets.length > 0) console.log(` Presets: ${resource.presets.join(", ")}`);
60
+ if (resource.additionalRoutes && resource.additionalRoutes.length > 0) console.log(` Additional Routes: ${resource.additionalRoutes.length}`);
61
+ console.log("");
62
+ });
63
+ const stats = registry.getStats();
64
+ console.log("Summary:");
65
+ console.log(` Total Resources: ${stats.totalResources}`);
66
+ console.log(` With Presets: ${resources.filter((r) => r.presets?.length > 0).length}`);
67
+ console.log(` With Custom Routes: ${resources.filter((r) => r.additionalRoutes && r.additionalRoutes.length > 0).length}`);
68
+ } catch (error) {
69
+ if (error instanceof Error) throw error;
70
+ throw new Error(String(error));
71
+ }
72
+ }
73
+
74
+ //#endregion
75
+ export { introspect as default, introspect };
76
+ //# sourceMappingURL=introspect.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspect.mjs","names":[],"sources":["../../../src/cli/commands/introspect.ts"],"sourcesContent":["/**\n * Arc CLI - Introspect Command\n *\n * Shows all registered resources and their configuration.\n * Requires an entry file that exports defineResource() results.\n */\n\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { ResourceRegistry } from '../../registry/index.js';\nimport type { RegistryEntry } from '../../types/index.js';\n\nfunction describePermission(value: unknown): string {\n if (!value) return 'none';\n if (typeof value === 'function') {\n return value.name ? `${value.name}()` : '[anonymous permission function]';\n }\n if (Array.isArray(value)) return `[${value.join(', ')}]`;\n if (typeof value === 'object') return '[permission object]';\n return String(value);\n}\n\nexport async function introspect(args: string[]): Promise<void> {\n console.log('Introspecting Arc resources...\\n');\n\n try {\n const entryPath = args[0];\n if (!entryPath) {\n console.log('Usage: arc introspect <entry-file>\\n');\n console.log('Where entry-file exports your defineResource() results.');\n console.log('Example: arc introspect ./src/resources.js');\n return;\n }\n\n // Dynamically import user's entry file (pathToFileURL needed for Windows)\n const entryFileUrl = pathToFileURL(resolve(process.cwd(), entryPath)).href;\n const entryModule = await import(entryFileUrl);\n\n // Collect ResourceDefinition objects from exports (they have _registryMeta + toPlugin)\n // Also handles arrays of resources (e.g. `export const resources = [r1, r2]`)\n const registry = new ResourceRegistry();\n let registered = 0;\n\n function tryRegister(value: unknown): void {\n if (\n value &&\n typeof value === 'object' &&\n 'name' in value &&\n '_registryMeta' in value &&\n 'toPlugin' in value\n ) {\n registry.register(value as any, (value as any)._registryMeta ?? {});\n registered++;\n }\n }\n\n for (const exported of Object.values(entryModule)) {\n if (Array.isArray(exported)) {\n exported.forEach(tryRegister);\n } else {\n tryRegister(exported);\n }\n }\n\n if (registered === 0) {\n console.log('No resource definitions found in entry file.');\n console.log('\\nMake sure your file exports defineResource() results:');\n console.log(' export const productResource = defineResource({ ... });');\n return;\n }\n\n const resources: RegistryEntry[] = registry.getAll();\n\n console.log(`Found ${resources.length} resource(s):\\n`);\n\n resources.forEach((resource, index) => {\n console.log(`${index + 1}. ${resource.name}`);\n console.log(` Display Name: ${resource.displayName}`);\n console.log(` Prefix: ${resource.prefix}`);\n console.log(` Module: ${resource.module || 'none'}`);\n\n if (resource.permissions) {\n console.log(` Permissions:`);\n Object.entries(resource.permissions).forEach(([op, permission]) => {\n console.log(` ${op}: ${describePermission(permission)}`);\n });\n }\n\n if (resource.presets && resource.presets.length > 0) {\n console.log(` Presets: ${resource.presets.join(', ')}`);\n }\n\n if (resource.additionalRoutes && resource.additionalRoutes.length > 0) {\n console.log(` Additional Routes: ${resource.additionalRoutes.length}`);\n }\n\n console.log('');\n });\n\n // Summary\n const stats = registry.getStats();\n console.log('Summary:');\n console.log(` Total Resources: ${stats.totalResources}`);\n console.log(` With Presets: ${resources.filter((r) => r.presets?.length > 0).length}`);\n console.log(\n ` With Custom Routes: ${resources.filter((r) => r.additionalRoutes && r.additionalRoutes.length > 0).length}`\n );\n } catch (error: unknown) {\n if (error instanceof Error) throw error;\n throw new Error(String(error));\n }\n}\n\nexport default introspect;\n"],"mappings":";;;;;;;;;;;AAYA,SAAS,mBAAmB,OAAwB;AAClD,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI,OAAO,UAAU,WACnB,QAAO,MAAM,OAAO,GAAG,MAAM,KAAK,MAAM;AAE1C,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,IAAI,MAAM,KAAK,KAAK,CAAC;AACtD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAO,OAAO,MAAM;;AAGtB,eAAsB,WAAW,MAA+B;AAC9D,SAAQ,IAAI,mCAAmC;AAE/C,KAAI;EACF,MAAM,YAAY,KAAK;AACvB,MAAI,CAAC,WAAW;AACd,WAAQ,IAAI,uCAAuC;AACnD,WAAQ,IAAI,0DAA0D;AACtE,WAAQ,IAAI,6CAA6C;AACzD;;EAKF,MAAM,cAAc,MAAM,OADL,cAAc,QAAQ,QAAQ,KAAK,EAAE,UAAU,CAAC,CAAC;EAKtE,MAAM,WAAW,IAAI,kBAAkB;EACvC,IAAI,aAAa;EAEjB,SAAS,YAAY,OAAsB;AACzC,OACE,SACA,OAAO,UAAU,YACjB,UAAU,SACV,mBAAmB,SACnB,cAAc,OACd;AACA,aAAS,SAAS,OAAe,MAAc,iBAAiB,EAAE,CAAC;AACnE;;;AAIJ,OAAK,MAAM,YAAY,OAAO,OAAO,YAAY,CAC/C,KAAI,MAAM,QAAQ,SAAS,CACzB,UAAS,QAAQ,YAAY;MAE7B,aAAY,SAAS;AAIzB,MAAI,eAAe,GAAG;AACpB,WAAQ,IAAI,+CAA+C;AAC3D,WAAQ,IAAI,0DAA0D;AACtE,WAAQ,IAAI,4DAA4D;AACxE;;EAGF,MAAM,YAA6B,SAAS,QAAQ;AAEpD,UAAQ,IAAI,SAAS,UAAU,OAAO,iBAAiB;AAEvD,YAAU,SAAS,UAAU,UAAU;AACrC,WAAQ,IAAI,GAAG,QAAQ,EAAE,IAAI,SAAS,OAAO;AAC7C,WAAQ,IAAI,oBAAoB,SAAS,cAAc;AACvD,WAAQ,IAAI,cAAc,SAAS,SAAS;AAC5C,WAAQ,IAAI,cAAc,SAAS,UAAU,SAAS;AAEtD,OAAI,SAAS,aAAa;AACxB,YAAQ,IAAI,kBAAkB;AAC9B,WAAO,QAAQ,SAAS,YAAY,CAAC,SAAS,CAAC,IAAI,gBAAgB;AACjE,aAAQ,IAAI,QAAQ,GAAG,IAAI,mBAAmB,WAAW,GAAG;MAC5D;;AAGJ,OAAI,SAAS,WAAW,SAAS,QAAQ,SAAS,EAChD,SAAQ,IAAI,eAAe,SAAS,QAAQ,KAAK,KAAK,GAAG;AAG3D,OAAI,SAAS,oBAAoB,SAAS,iBAAiB,SAAS,EAClE,SAAQ,IAAI,yBAAyB,SAAS,iBAAiB,SAAS;AAG1E,WAAQ,IAAI,GAAG;IACf;EAGF,MAAM,QAAQ,SAAS,UAAU;AACjC,UAAQ,IAAI,WAAW;AACvB,UAAQ,IAAI,sBAAsB,MAAM,iBAAiB;AACzD,UAAQ,IAAI,mBAAmB,UAAU,QAAQ,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,SAAS;AACvF,UAAQ,IACN,yBAAyB,UAAU,QAAQ,MAAM,EAAE,oBAAoB,EAAE,iBAAiB,SAAS,EAAE,CAAC,SACvG;UACM,OAAgB;AACvB,MAAI,iBAAiB,MAAO,OAAM;AAClC,QAAM,IAAI,MAAM,OAAO,MAAM,CAAC"}
@@ -0,0 +1,17 @@
1
+ import describe from "./commands/describe.mjs";
2
+ import { exportDocs } from "./commands/docs.mjs";
3
+ import generate from "./commands/generate.mjs";
4
+ import init from "./commands/init.mjs";
5
+ import introspect from "./commands/introspect.mjs";
6
+
7
+ //#region src/cli/commands/doctor.d.ts
8
+ /**
9
+ * Arc CLI - Doctor Command
10
+ *
11
+ * Health check utility that validates the development environment.
12
+ * Checks Node.js version, dependencies, configuration, and env variables.
13
+ */
14
+ declare function doctor(_args?: string[]): Promise<void>;
15
+ //#endregion
16
+ export { describe, doctor, exportDocs, generate, init, introspect };
17
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cli/commands/doctor.ts"],"mappings":";;;;;;;;;;;;;iBAgBsB,MAAA,CAAO,KAAA,cAAuB,OAAA"}
@@ -0,0 +1,157 @@
1
+ import generate from "./commands/generate.mjs";
2
+ import init from "./commands/init.mjs";
3
+ import introspect from "./commands/introspect.mjs";
4
+ import describe from "./commands/describe.mjs";
5
+ import { exportDocs } from "./commands/docs.mjs";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+
9
+ //#region src/cli/commands/doctor.ts
10
+ /**
11
+ * Arc CLI - Doctor Command
12
+ *
13
+ * Health check utility that validates the development environment.
14
+ * Checks Node.js version, dependencies, configuration, and env variables.
15
+ */
16
+ async function doctor(_args = []) {
17
+ console.log("\nArc Doctor\n");
18
+ const results = [];
19
+ const cwd = process.cwd();
20
+ const nodeVersion = process.versions.node;
21
+ if (parseInt(nodeVersion.split(".")[0] ?? "0", 10) >= 22) results.push({
22
+ status: "pass",
23
+ label: `Node.js ${nodeVersion}`,
24
+ detail: "required: >=22"
25
+ });
26
+ else results.push({
27
+ status: "fail",
28
+ label: `Node.js ${nodeVersion}`,
29
+ detail: "required: >=22"
30
+ });
31
+ const pkg = findPackageJson(cwd);
32
+ const allDeps = {
33
+ ...pkg?.dependencies ?? {},
34
+ ...pkg?.devDependencies ?? {}
35
+ };
36
+ const arcVersion = allDeps["@classytic/arc"];
37
+ if (arcVersion) results.push({
38
+ status: "pass",
39
+ label: `@classytic/arc ${arcVersion}`
40
+ });
41
+ else results.push({
42
+ status: "warn",
43
+ label: "@classytic/arc not found in dependencies"
44
+ });
45
+ const fastifyVersion = allDeps["fastify"];
46
+ if (fastifyVersion) {
47
+ const clean = fastifyVersion.replace(/^[\^~>=<]+/, "");
48
+ if (parseInt(clean.split(".")[0] ?? "0", 10) >= 5) results.push({
49
+ status: "pass",
50
+ label: `fastify ${fastifyVersion}`,
51
+ detail: "required: ^5.0.0"
52
+ });
53
+ else results.push({
54
+ status: "fail",
55
+ label: `fastify ${fastifyVersion}`,
56
+ detail: "required: ^5.0.0 — Arc requires Fastify 5"
57
+ });
58
+ } else results.push({
59
+ status: "fail",
60
+ label: "fastify not found in dependencies",
61
+ detail: "required: ^5.0.0"
62
+ });
63
+ const tsconfigPath = resolve(cwd, "tsconfig.json");
64
+ if (existsSync(tsconfigPath)) try {
65
+ const stripped = readFileSync(tsconfigPath, "utf-8").replace(/\/\/.*$/gm, "");
66
+ const moduleRes = JSON.parse(stripped)?.compilerOptions?.moduleResolution;
67
+ if (moduleRes && ![
68
+ "nodenext",
69
+ "node16",
70
+ "bundler"
71
+ ].includes(moduleRes.toLowerCase())) results.push({
72
+ status: "warn",
73
+ label: "tsconfig.json found",
74
+ detail: `moduleResolution "${moduleRes}" — recommend "NodeNext" or "Bundler"`
75
+ });
76
+ else results.push({
77
+ status: "pass",
78
+ label: "tsconfig.json found"
79
+ });
80
+ } catch {
81
+ results.push({
82
+ status: "pass",
83
+ label: "tsconfig.json found"
84
+ });
85
+ }
86
+ else results.push({
87
+ status: "warn",
88
+ label: "tsconfig.json not found"
89
+ });
90
+ for (const dep of [
91
+ {
92
+ name: "@fastify/rate-limit",
93
+ purpose: "rate limiting"
94
+ },
95
+ {
96
+ name: "@fastify/helmet",
97
+ purpose: "security headers"
98
+ },
99
+ {
100
+ name: "@fastify/cors",
101
+ purpose: "CORS support"
102
+ }
103
+ ]) if (allDeps[dep.name]) results.push({
104
+ status: "pass",
105
+ label: `${dep.name} installed`
106
+ });
107
+ else results.push({
108
+ status: "warn",
109
+ label: `${dep.name} not installed`,
110
+ detail: `${dep.purpose} disabled`
111
+ });
112
+ if (allDeps["better-auth"]) results.push({
113
+ status: "pass",
114
+ label: `better-auth ${allDeps["better-auth"]}`
115
+ });
116
+ for (const env of [{
117
+ name: "MONGO_URI",
118
+ severity: "warn",
119
+ detail: "required at runtime for MongoDB"
120
+ }, {
121
+ name: "BETTER_AUTH_SECRET",
122
+ severity: "warn",
123
+ detail: "required for Better Auth session encryption"
124
+ }]) if (process.env[env.name]) results.push({
125
+ status: "pass",
126
+ label: `${env.name} set`
127
+ });
128
+ else results.push({
129
+ status: env.severity,
130
+ label: `${env.name} not set`,
131
+ detail: env.detail
132
+ });
133
+ let passCount = 0;
134
+ let warnCount = 0;
135
+ let failCount = 0;
136
+ for (const r of results) {
137
+ const icon = r.status === "pass" ? "[pass]" : r.status === "warn" ? "[warn]" : "[FAIL]";
138
+ const detail = r.detail ? ` (${r.detail})` : "";
139
+ console.log(` ${icon} ${r.label}${detail}`);
140
+ if (r.status === "pass") passCount++;
141
+ else if (r.status === "warn") warnCount++;
142
+ else failCount++;
143
+ }
144
+ console.log(`\n${passCount} passed, ${warnCount} warnings, ${failCount} failures\n`);
145
+ if (failCount > 0) process.exitCode = 1;
146
+ }
147
+ function findPackageJson(dir) {
148
+ const paths = [join(dir, "package.json"), join(dir, "..", "package.json")];
149
+ for (const p of paths) try {
150
+ if (existsSync(p)) return JSON.parse(readFileSync(p, "utf-8"));
151
+ } catch {}
152
+ return null;
153
+ }
154
+
155
+ //#endregion
156
+ export { describe, doctor, exportDocs, generate, init, introspect };
157
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/commands/doctor.ts"],"sourcesContent":["/**\n * Arc CLI - Doctor Command\n *\n * Health check utility that validates the development environment.\n * Checks Node.js version, dependencies, configuration, and env variables.\n */\n\nimport { readFileSync, existsSync } from 'node:fs';\nimport { resolve, join } from 'node:path';\n\ninterface CheckResult {\n status: 'pass' | 'warn' | 'fail';\n label: string;\n detail?: string;\n}\n\nexport async function doctor(_args: string[] = []): Promise<void> {\n console.log('\\nArc Doctor\\n');\n\n const results: CheckResult[] = [];\n const cwd = process.cwd();\n\n // 1. Node.js version\n const nodeVersion = process.versions.node;\n const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);\n if (nodeMajor >= 22) {\n results.push({ status: 'pass', label: `Node.js ${nodeVersion}`, detail: 'required: >=22' });\n } else {\n results.push({ status: 'fail', label: `Node.js ${nodeVersion}`, detail: 'required: >=22' });\n }\n\n // 2. Find nearest package.json\n const pkg = findPackageJson(cwd);\n const allDeps = {\n ...(pkg?.dependencies ?? {}),\n ...(pkg?.devDependencies ?? {}),\n };\n\n // 3. Arc version\n const arcVersion = allDeps['@classytic/arc'];\n if (arcVersion) {\n results.push({ status: 'pass', label: `@classytic/arc ${arcVersion}` });\n } else {\n results.push({ status: 'warn', label: '@classytic/arc not found in dependencies' });\n }\n\n // 4. Fastify version\n const fastifyVersion = allDeps['fastify'];\n if (fastifyVersion) {\n const clean = fastifyVersion.replace(/^[\\^~>=<]+/, '');\n const major = parseInt(clean.split('.')[0] ?? '0', 10);\n if (major >= 5) {\n results.push({ status: 'pass', label: `fastify ${fastifyVersion}`, detail: 'required: ^5.0.0' });\n } else {\n results.push({ status: 'fail', label: `fastify ${fastifyVersion}`, detail: 'required: ^5.0.0 — Arc requires Fastify 5' });\n }\n } else {\n results.push({ status: 'fail', label: 'fastify not found in dependencies', detail: 'required: ^5.0.0' });\n }\n\n // 5. tsconfig.json\n const tsconfigPath = resolve(cwd, 'tsconfig.json');\n if (existsSync(tsconfigPath)) {\n try {\n const raw = readFileSync(tsconfigPath, 'utf-8');\n // Strip single-line comments for JSON parsing\n const stripped = raw.replace(/\\/\\/.*$/gm, '');\n const tsconfig = JSON.parse(stripped);\n const moduleRes = tsconfig?.compilerOptions?.moduleResolution;\n if (moduleRes && !['nodenext', 'node16', 'bundler'].includes(moduleRes.toLowerCase())) {\n results.push({ status: 'warn', label: 'tsconfig.json found', detail: `moduleResolution \"${moduleRes}\" — recommend \"NodeNext\" or \"Bundler\"` });\n } else {\n results.push({ status: 'pass', label: 'tsconfig.json found' });\n }\n } catch {\n results.push({ status: 'pass', label: 'tsconfig.json found' });\n }\n } else {\n results.push({ status: 'warn', label: 'tsconfig.json not found' });\n }\n\n // 6. Peer / optional dependencies\n const optionalDeps: Array<{ name: string; purpose: string }> = [\n { name: '@fastify/rate-limit', purpose: 'rate limiting' },\n { name: '@fastify/helmet', purpose: 'security headers' },\n { name: '@fastify/cors', purpose: 'CORS support' },\n ];\n\n for (const dep of optionalDeps) {\n if (allDeps[dep.name]) {\n results.push({ status: 'pass', label: `${dep.name} installed` });\n } else {\n results.push({ status: 'warn', label: `${dep.name} not installed`, detail: `${dep.purpose} disabled` });\n }\n }\n\n // 7. Better Auth detection\n if (allDeps['better-auth']) {\n results.push({ status: 'pass', label: `better-auth ${allDeps['better-auth']}` });\n }\n\n // 8. Environment variables\n const envChecks: Array<{ name: string; severity: 'warn' | 'fail'; detail: string }> = [\n { name: 'MONGO_URI', severity: 'warn', detail: 'required at runtime for MongoDB' },\n { name: 'BETTER_AUTH_SECRET', severity: 'warn', detail: 'required for Better Auth session encryption' },\n ];\n\n for (const env of envChecks) {\n if (process.env[env.name]) {\n results.push({ status: 'pass', label: `${env.name} set` });\n } else {\n results.push({ status: env.severity, label: `${env.name} not set`, detail: env.detail });\n }\n }\n\n // Print results\n let passCount = 0;\n let warnCount = 0;\n let failCount = 0;\n\n for (const r of results) {\n const icon = r.status === 'pass' ? '[pass]' : r.status === 'warn' ? '[warn]' : '[FAIL]';\n const detail = r.detail ? ` (${r.detail})` : '';\n console.log(` ${icon} ${r.label}${detail}`);\n\n if (r.status === 'pass') passCount++;\n else if (r.status === 'warn') warnCount++;\n else failCount++;\n }\n\n console.log(`\\n${passCount} passed, ${warnCount} warnings, ${failCount} failures\\n`);\n\n if (failCount > 0) {\n process.exitCode = 1;\n }\n}\n\nfunction findPackageJson(dir: string): Record<string, Record<string, string>> | null {\n const paths = [\n join(dir, 'package.json'),\n join(dir, '..', 'package.json'),\n ];\n\n for (const p of paths) {\n try {\n if (existsSync(p)) {\n return JSON.parse(readFileSync(p, 'utf-8'));\n }\n } catch {\n // Skip\n }\n }\n return null;\n}\n"],"mappings":"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"}