mega-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.env +127 -0
  2. package/.env.example +186 -0
  3. package/.prettierrc.json +8 -0
  4. package/CHANGELOG.md +259 -0
  5. package/LICENSE +21 -0
  6. package/README.md +153 -0
  7. package/bin/mega-ws-hub.js +15 -0
  8. package/bin/mega.js +38 -0
  9. package/docker-compose.yml +201 -0
  10. package/eslint.config.js +57 -0
  11. package/infra/otel-collector-config.yaml +43 -0
  12. package/jsconfig.json +18 -0
  13. package/package.json +121 -0
  14. package/sample/crud/.env +18 -0
  15. package/sample/crud/.env.example +50 -0
  16. package/sample/crud/README.md +85 -0
  17. package/sample/crud/apps/main/app.config.js +114 -0
  18. package/sample/crud/apps/main/channels/chat-bus.js +115 -0
  19. package/sample/crud/apps/main/channels/chat-channel.js +145 -0
  20. package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
  21. package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
  22. package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
  23. package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
  24. package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
  25. package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
  26. package/sample/crud/apps/main/controllers/note-controller.js +116 -0
  27. package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
  28. package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
  29. package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
  30. package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
  31. package/sample/crud/apps/main/controllers/user-controller.js +34 -0
  32. package/sample/crud/apps/main/controllers/web-controller.js +137 -0
  33. package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
  34. package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
  35. package/sample/crud/apps/main/jobs/email-job.js +72 -0
  36. package/sample/crud/apps/main/locales/client/en.json +3 -0
  37. package/sample/crud/apps/main/locales/client/ko.json +3 -0
  38. package/sample/crud/apps/main/locales/server/en.json +316 -0
  39. package/sample/crud/apps/main/locales/server/ko.json +316 -0
  40. package/sample/crud/apps/main/middleware/web-auth.js +40 -0
  41. package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
  42. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
  43. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
  44. package/sample/crud/apps/main/models/note.js +71 -0
  45. package/sample/crud/apps/main/models/user.js +86 -0
  46. package/sample/crud/apps/main/public/css/app.css +101 -0
  47. package/sample/crud/apps/main/public/css/guide.css +137 -0
  48. package/sample/crud/apps/main/public/js/app.js +54 -0
  49. package/sample/crud/apps/main/public/js/perf.js +129 -0
  50. package/sample/crud/apps/main/public/js/theme-init.js +12 -0
  51. package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
  52. package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
  53. package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
  54. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  55. package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  56. package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
  57. package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
  58. package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
  59. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
  60. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
  61. package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
  62. package/sample/crud/apps/main/routes/auth.js +15 -0
  63. package/sample/crud/apps/main/routes/cron.js +14 -0
  64. package/sample/crud/apps/main/routes/guide.js +25 -0
  65. package/sample/crud/apps/main/routes/jobs.js +14 -0
  66. package/sample/crud/apps/main/routes/logs.js +28 -0
  67. package/sample/crud/apps/main/routes/metrics.js +13 -0
  68. package/sample/crud/apps/main/routes/notes.js +19 -0
  69. package/sample/crud/apps/main/routes/perf.js +47 -0
  70. package/sample/crud/apps/main/routes/redis.js +14 -0
  71. package/sample/crud/apps/main/routes/tracing.js +14 -0
  72. package/sample/crud/apps/main/routes/upload.js +16 -0
  73. package/sample/crud/apps/main/routes/users.js +54 -0
  74. package/sample/crud/apps/main/routes/web.js +23 -0
  75. package/sample/crud/apps/main/routes/worker.js +15 -0
  76. package/sample/crud/apps/main/routes/ws.js +30 -0
  77. package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
  78. package/sample/crud/apps/main/services/auth-service.js +74 -0
  79. package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
  80. package/sample/crud/apps/main/services/guide-service.js +145 -0
  81. package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
  82. package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
  83. package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
  84. package/sample/crud/apps/main/services/note-service.js +75 -0
  85. package/sample/crud/apps/main/services/perf-service.js +302 -0
  86. package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
  87. package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
  88. package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
  89. package/sample/crud/apps/main/services/user-service.js +65 -0
  90. package/sample/crud/apps/main/views/auth/login.ejs +57 -0
  91. package/sample/crud/apps/main/views/auth/register.ejs +71 -0
  92. package/sample/crud/apps/main/views/cron/index.ejs +92 -0
  93. package/sample/crud/apps/main/views/guide/index.ejs +24 -0
  94. package/sample/crud/apps/main/views/guide/page.ejs +64 -0
  95. package/sample/crud/apps/main/views/home.ejs +82 -0
  96. package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
  97. package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
  98. package/sample/crud/apps/main/views/logs/index.ejs +80 -0
  99. package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
  100. package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
  101. package/sample/crud/apps/main/views/notes/list.ejs +74 -0
  102. package/sample/crud/apps/main/views/notes/new.ejs +45 -0
  103. package/sample/crud/apps/main/views/perf/index.ejs +90 -0
  104. package/sample/crud/apps/main/views/redis/index.ejs +65 -0
  105. package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
  106. package/sample/crud/apps/main/views/upload/index.ejs +79 -0
  107. package/sample/crud/apps/main/views/users/edit.ejs +48 -0
  108. package/sample/crud/apps/main/views/users/list.ejs +81 -0
  109. package/sample/crud/apps/main/views/users/new.ejs +48 -0
  110. package/sample/crud/apps/main/views/worker/index.ejs +70 -0
  111. package/sample/crud/apps/main/views/ws/index.ejs +62 -0
  112. package/sample/crud/apps/main/workers/hash-worker.js +17 -0
  113. package/sample/crud/apps/main/workers/hash.task.js +22 -0
  114. package/sample/crud/ecosystem.config.cjs +9 -0
  115. package/sample/crud/mega.config.js +105 -0
  116. package/sample/crud/package-lock.json +5665 -0
  117. package/sample/crud/package.json +28 -0
  118. package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
  119. package/sample/crud/test/apps/main/auth-service.test.js +93 -0
  120. package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
  121. package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
  122. package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
  123. package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
  124. package/sample/crud/test/apps/main/email-job.test.js +76 -0
  125. package/sample/crud/test/apps/main/guide-service.test.js +68 -0
  126. package/sample/crud/test/apps/main/hash-task.test.js +30 -0
  127. package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
  128. package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
  129. package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
  130. package/sample/crud/test/apps/main/note-service.test.js +68 -0
  131. package/sample/crud/test/apps/main/perf-service.test.js +121 -0
  132. package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
  133. package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
  134. package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
  135. package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
  136. package/sample/crud/test/apps/main/user-service.test.js +65 -0
  137. package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
  138. package/sample/crud/vitest.config.js +8 -0
  139. package/sample/crud/yarn.lock +2142 -0
  140. package/sample/simple/.env.example +15 -0
  141. package/sample/simple/README.md +52 -0
  142. package/sample/simple/apps/main/app.config.js +35 -0
  143. package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
  144. package/sample/simple/apps/main/locales/client/en.json +3 -0
  145. package/sample/simple/apps/main/locales/client/ko.json +3 -0
  146. package/sample/simple/apps/main/locales/server/en.json +23 -0
  147. package/sample/simple/apps/main/locales/server/ko.json +23 -0
  148. package/sample/simple/apps/main/public/css/app.css +101 -0
  149. package/sample/simple/apps/main/public/hello.txt +1 -0
  150. package/sample/simple/apps/main/public/js/app.js +54 -0
  151. package/sample/simple/apps/main/public/js/theme-init.js +12 -0
  152. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  153. package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
  154. package/sample/simple/apps/main/routes/index.js +9 -0
  155. package/sample/simple/apps/main/routes/pages.js +12 -0
  156. package/sample/simple/apps/main/views/index.ejs +56 -0
  157. package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
  158. package/sample/simple/ecosystem.config.cjs +10 -0
  159. package/sample/simple/mega.config.js +27 -0
  160. package/sample/simple/package-lock.json +1851 -0
  161. package/sample/simple/package.json +25 -0
  162. package/sample/simple/test/apps/main/index.test.js +13 -0
  163. package/sample/simple/vitest.config.js +8 -0
  164. package/src/adapters/adapter-manager.js +305 -0
  165. package/src/adapters/adapter-options.js +208 -0
  166. package/src/adapters/file-adapter.js +350 -0
  167. package/src/adapters/file-session-adapter.js +363 -0
  168. package/src/adapters/index.js +38 -0
  169. package/src/adapters/maria-adapter.js +425 -0
  170. package/src/adapters/mega-adapter.js +511 -0
  171. package/src/adapters/mega-bus-adapter.js +81 -0
  172. package/src/adapters/mega-cache-adapter.js +94 -0
  173. package/src/adapters/mega-db-adapter.js +72 -0
  174. package/src/adapters/mega-lock-adapter.js +118 -0
  175. package/src/adapters/mega-log-sink-adapter.js +46 -0
  176. package/src/adapters/mega-session-adapter.js +72 -0
  177. package/src/adapters/mongo-adapter.js +396 -0
  178. package/src/adapters/nats-adapter.js +370 -0
  179. package/src/adapters/postgres-adapter.js +341 -0
  180. package/src/adapters/redis-adapter.js +331 -0
  181. package/src/adapters/redis-session-adapter.js +261 -0
  182. package/src/adapters/redlock-adapter.js +385 -0
  183. package/src/adapters/registry.js +157 -0
  184. package/src/adapters/sqlite-adapter.js +309 -0
  185. package/src/auth/index.js +103 -0
  186. package/src/cli/commands/console-cmd.js +56 -0
  187. package/src/cli/commands/new.js +101 -0
  188. package/src/cli/commands/routes.js +107 -0
  189. package/src/cli/commands/scaffold.js +120 -0
  190. package/src/cli/commands/test-cmd.js +45 -0
  191. package/src/cli/generators/index.js +368 -0
  192. package/src/cli/index.js +472 -0
  193. package/src/cli/template-engine.js +72 -0
  194. package/src/cli/ws-hub.js +582 -0
  195. package/src/core/ajv-mapper.js +80 -0
  196. package/src/core/boot.js +323 -0
  197. package/src/core/cluster-metrics.js +278 -0
  198. package/src/core/config-loader.js +115 -0
  199. package/src/core/config-validator.js +322 -0
  200. package/src/core/ctx-builder.js +253 -0
  201. package/src/core/envelope.js +88 -0
  202. package/src/core/error-mapper.js +116 -0
  203. package/src/core/formbody.js +69 -0
  204. package/src/core/hub-link.js +552 -0
  205. package/src/core/i18n.js +525 -0
  206. package/src/core/index.js +63 -0
  207. package/src/core/mega-app.js +1138 -0
  208. package/src/core/mega-cluster.js +232 -0
  209. package/src/core/mega-server.js +176 -0
  210. package/src/core/mega-service.js +41 -0
  211. package/src/core/migration-runner.js +196 -0
  212. package/src/core/multipart.js +282 -0
  213. package/src/core/openapi.js +114 -0
  214. package/src/core/router.js +388 -0
  215. package/src/core/routes-loader.js +57 -0
  216. package/src/core/scope-registry.js +53 -0
  217. package/src/core/security.js +275 -0
  218. package/src/core/services-loader.js +98 -0
  219. package/src/core/session-cleanup-schedule.js +57 -0
  220. package/src/core/session-store.js +55 -0
  221. package/src/core/session.js +414 -0
  222. package/src/core/static-assets.js +126 -0
  223. package/src/core/template.js +294 -0
  224. package/src/core/workers-manager.js +193 -0
  225. package/src/core/ws-compression.js +112 -0
  226. package/src/core/ws-controller.js +109 -0
  227. package/src/core/ws-message.js +176 -0
  228. package/src/core/ws-upgrade.js +445 -0
  229. package/src/errors/config-error.js +16 -0
  230. package/src/errors/http-errors.js +130 -0
  231. package/src/errors/index.js +19 -0
  232. package/src/errors/mega-error.js +34 -0
  233. package/src/eslint-plugin/index.js +15 -0
  234. package/src/eslint-plugin/no-direct-model-import.js +113 -0
  235. package/src/index.js +131 -0
  236. package/src/lib/asp/config.js +83 -0
  237. package/src/lib/asp/crypto.js +145 -0
  238. package/src/lib/asp/errors.js +49 -0
  239. package/src/lib/asp/nonce-cache.js +94 -0
  240. package/src/lib/asp/plugin.js +263 -0
  241. package/src/lib/asp/ws-terminator.js +101 -0
  242. package/src/lib/env-mapper.js +222 -0
  243. package/src/lib/hub-protocol.js +322 -0
  244. package/src/lib/index.js +42 -0
  245. package/src/lib/logger/telegram-core.js +150 -0
  246. package/src/lib/logger/telegram-transport.js +126 -0
  247. package/src/lib/mega-brute-force.js +225 -0
  248. package/src/lib/mega-circuit-breaker.js +412 -0
  249. package/src/lib/mega-cron.js +169 -0
  250. package/src/lib/mega-hash.js +179 -0
  251. package/src/lib/mega-health.js +91 -0
  252. package/src/lib/mega-job-queue.js +600 -0
  253. package/src/lib/mega-job-worker.js +295 -0
  254. package/src/lib/mega-job.js +140 -0
  255. package/src/lib/mega-logger.js +128 -0
  256. package/src/lib/mega-metrics.js +661 -0
  257. package/src/lib/mega-plugin.js +650 -0
  258. package/src/lib/mega-retry.js +95 -0
  259. package/src/lib/mega-schedule.js +507 -0
  260. package/src/lib/mega-shutdown.js +176 -0
  261. package/src/lib/mega-tracing.js +715 -0
  262. package/src/lib/mega-worker.js +653 -0
  263. package/src/lib/worker-runner/process-entry.js +30 -0
  264. package/src/lib/worker-runner/task-dispatch.js +72 -0
  265. package/src/lib/worker-runner/thread-entry.js +26 -0
  266. package/src/models/index.js +7 -0
  267. package/src/models/mega-model.js +151 -0
  268. package/src/test/index.js +288 -0
  269. package/templates/adapter/code.tpl +40 -0
  270. package/templates/adapter/test.tpl +13 -0
  271. package/templates/app/app.config.tpl +10 -0
  272. package/templates/app/route.tpl +10 -0
  273. package/templates/app/test.tpl +13 -0
  274. package/templates/channel/code.tpl +38 -0
  275. package/templates/channel/test.tpl +19 -0
  276. package/templates/controller/code.tpl +16 -0
  277. package/templates/controller/route.tpl +9 -0
  278. package/templates/controller/test.tpl +14 -0
  279. package/templates/job/code.tpl +23 -0
  280. package/templates/job/test.tpl +17 -0
  281. package/templates/locale/code.tpl +3 -0
  282. package/templates/locale/test.tpl +13 -0
  283. package/templates/middleware/code.tpl +13 -0
  284. package/templates/middleware/test.tpl +11 -0
  285. package/templates/migration/code.tpl +20 -0
  286. package/templates/migration/test.tpl +14 -0
  287. package/templates/model/code.tpl +21 -0
  288. package/templates/model/test.tpl +29 -0
  289. package/templates/project/app.config.tpl +8 -0
  290. package/templates/project/app.config.views.tpl +37 -0
  291. package/templates/project/ecosystem.config.tpl +10 -0
  292. package/templates/project/env.tpl +12 -0
  293. package/templates/project/gitignore.tpl +8 -0
  294. package/templates/project/locales/client/en.json.tpl +3 -0
  295. package/templates/project/locales/client/ko.json.tpl +3 -0
  296. package/templates/project/locales/server/en.json.tpl +17 -0
  297. package/templates/project/locales/server/ko.json.tpl +17 -0
  298. package/templates/project/mega.config.tpl +11 -0
  299. package/templates/project/package.tpl +25 -0
  300. package/templates/project/public/css/app.css +101 -0
  301. package/templates/project/public/js/app.js +54 -0
  302. package/templates/project/public/js/theme-init.js +12 -0
  303. package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
  304. package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
  305. package/templates/project/readme.tpl +48 -0
  306. package/templates/project/route.test.tpl +13 -0
  307. package/templates/project/route.test.views.tpl +15 -0
  308. package/templates/project/route.tpl +10 -0
  309. package/templates/project/route.views.tpl +10 -0
  310. package/templates/project/views/index.ejs.tpl +58 -0
  311. package/templates/project/views/layout.ejs.tpl +73 -0
  312. package/templates/project/vitest.config.tpl +8 -0
  313. package/templates/route/code.tpl +11 -0
  314. package/templates/route/test.tpl +26 -0
  315. package/templates/schedule/code.tpl +19 -0
  316. package/templates/schedule/test.tpl +17 -0
  317. package/templates/service/code.tpl +18 -0
  318. package/templates/service/test.tpl +17 -0
  319. package/templates/worker/code.tpl +14 -0
  320. package/templates/worker/task.tpl +13 -0
  321. package/templates/worker/test.tpl +18 -0
  322. package/vitest.config.js +33 -0
@@ -0,0 +1,101 @@
1
+ // @ts-check
2
+ /**
3
+ * `mega new <project>` — 빈 폴더에서 멀티앱 hello world 까지 한 명령 스캐폴드 (roadmap §336, ADR-142).
4
+ *
5
+ * 기본 앱 `main` + `*.localhost` 호스트 + README · `.env.example` · `.gitignore` · `package.json`
6
+ * (scripts + concurrently devDep + PM2 `ecosystem.config.cjs`) 를 만든다. `ejsMate` 옵트인이면 EJS +
7
+ * ejs-mate 뷰 골격(ADR-011/136) + i18next 다국어(ADR-037~039) + Bootstrap 5 디자인(ADR-151)을 더한다 —
8
+ * 뷰 렌더 라우트, ko/en 로케일, 그리고 토큰 치환 없이 그대로 복사하는 정적 자산(vendored Bootstrap +
9
+ * 브랜드 CSS/JS). 신규 dep 설치는 하지 않고(네트워크 회피) 안내만 출력한다.
10
+ *
11
+ * @module cli/commands/new
12
+ */
13
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
+ import { dirname, join, resolve } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { nameVariants, renderTemplate } from '../template-engine.js'
17
+
18
+ /** templates/project 루트. src/cli/commands/new.js 기준 3단계 위 + templates/project. */
19
+ const PROJECT_TEMPLATES = resolve(dirname(fileURLToPath(import.meta.url)), '../../../templates/project')
20
+
21
+ /** 프로젝트 템플릿을 읽어 토큰 치환. @param {string} file @param {Record<string,string>} vars @returns {string} */
22
+ function renderProject(file, vars) {
23
+ return renderTemplate(readFileSync(join(PROJECT_TEMPLATES, file), 'utf8'), vars)
24
+ }
25
+
26
+ /**
27
+ * 프로젝트를 스캐폴드한다(순수 — prompts 없이 opts 로만 동작, 테스트 가능).
28
+ * @param {string} targetDir - 프로젝트 루트 절대/상대 경로.
29
+ * @param {object} [opts]
30
+ * @param {string} [opts.name] - 프로젝트 이름(기본: targetDir 의 마지막 세그먼트).
31
+ * @param {boolean} [opts.ejsMate] - EJS + ejs-mate 뷰 골격 포함(기본 false).
32
+ * @param {boolean} [opts.force] - 기존 파일 덮어쓰기(기본 false — 건너뜀).
33
+ * @returns {{ root: string, written: string[], skipped: string[] }}
34
+ */
35
+ export function scaffoldProject(targetDir, { name, ejsMate = false, force = false } = {}) {
36
+ const root = resolve(targetDir)
37
+ const projectName = nameVariants(name ?? root.split(/[/\\]/).pop() ?? 'mega-app').kebab
38
+ const vars = { name: projectName, Name: nameVariants(projectName).pascal }
39
+
40
+ // 뷰 옵트인이면 GET / 를 뷰 렌더로 바꾸고(JSON hello 대신) 그에 맞는 테스트를 쓴다.
41
+ /** @type {Array<{ rel: string, tpl: string }>} */
42
+ const files = [
43
+ { rel: 'mega.config.js', tpl: 'mega.config.tpl' },
44
+ { rel: 'apps/main/app.config.js', tpl: ejsMate ? 'app.config.views.tpl' : 'app.config.tpl' },
45
+ { rel: 'apps/main/routes/index.js', tpl: ejsMate ? 'route.views.tpl' : 'route.tpl' },
46
+ { rel: 'test/apps/main/index.test.js', tpl: ejsMate ? 'route.test.views.tpl' : 'route.test.tpl' },
47
+ { rel: 'package.json', tpl: 'package.tpl' },
48
+ { rel: 'README.md', tpl: 'readme.tpl' },
49
+ { rel: '.env.example', tpl: 'env.tpl' },
50
+ { rel: '.gitignore', tpl: 'gitignore.tpl' },
51
+ { rel: 'vitest.config.js', tpl: 'vitest.config.tpl' },
52
+ { rel: 'ecosystem.config.cjs', tpl: 'ecosystem.config.tpl' },
53
+ ]
54
+ if (ejsMate) {
55
+ files.push(
56
+ { rel: 'apps/main/views/layouts/main.ejs', tpl: 'views/layout.ejs.tpl' },
57
+ { rel: 'apps/main/views/index.ejs', tpl: 'views/index.ejs.tpl' },
58
+ { rel: 'apps/main/locales/server/ko.json', tpl: 'locales/server/ko.json.tpl' },
59
+ { rel: 'apps/main/locales/server/en.json', tpl: 'locales/server/en.json.tpl' },
60
+ { rel: 'apps/main/locales/client/ko.json', tpl: 'locales/client/ko.json.tpl' },
61
+ { rel: 'apps/main/locales/client/en.json', tpl: 'locales/client/en.json.tpl' },
62
+ )
63
+ }
64
+
65
+ // 정적 자산 — 토큰 치환 없이 바이트 그대로 복사한다(vendored Bootstrap 5 min 번들 + 브랜드 CSS/JS).
66
+ // 이들은 템플릿이 아니라 정적 파일이므로 renderTemplate(`{{token}}`)을 거치지 않는다.
67
+ /** @type {Array<{ rel: string, src: string }>} */
68
+ const assets = ejsMate
69
+ ? [
70
+ { rel: 'apps/main/public/vendor/bootstrap/bootstrap.min.css', src: 'public/vendor/bootstrap/bootstrap.min.css' },
71
+ { rel: 'apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js', src: 'public/vendor/bootstrap/bootstrap.bundle.min.js' },
72
+ { rel: 'apps/main/public/css/app.css', src: 'public/css/app.css' },
73
+ { rel: 'apps/main/public/js/app.js', src: 'public/js/app.js' },
74
+ { rel: 'apps/main/public/js/theme-init.js', src: 'public/js/theme-init.js' },
75
+ ]
76
+ : []
77
+
78
+ /** @type {string[]} */ const written = []
79
+ /** @type {string[]} */ const skipped = []
80
+ for (const { rel, tpl } of files) {
81
+ const out = join(root, rel)
82
+ if (existsSync(out) && !force) {
83
+ skipped.push(out)
84
+ continue
85
+ }
86
+ mkdirSync(dirname(out), { recursive: true })
87
+ writeFileSync(out, renderProject(tpl, vars))
88
+ written.push(out)
89
+ }
90
+ for (const { rel, src } of assets) {
91
+ const out = join(root, rel)
92
+ if (existsSync(out) && !force) {
93
+ skipped.push(out)
94
+ continue
95
+ }
96
+ mkdirSync(dirname(out), { recursive: true })
97
+ copyFileSync(join(PROJECT_TEMPLATES, src), out)
98
+ written.push(out)
99
+ }
100
+ return { root, written, skipped }
101
+ }
@@ -0,0 +1,107 @@
1
+ // @ts-check
2
+ /**
3
+ * `mega routes` — 앱별 등록 라우트 트리 출력 (roadmap §338, ADR-142).
4
+ *
5
+ * 서버를 띄우지 않고(부팅·어댑터 connect·listen 없이) 라우트만 수집한다 — 각 `apps/<name>/routes/*.js`
6
+ * 를 import 해 **기록용 라우터 stub** 으로 `router.http.<method>(path)`/`router.ws(path)` 호출을 잡는다.
7
+ * (loadRoutes 의 자동 스캔과 동형이되 부작용 없음 — 운영 환경에서도 안전.)
8
+ *
9
+ * @module cli/commands/routes
10
+ */
11
+ import { readdirSync, statSync } from 'node:fs'
12
+ import { join, resolve } from 'node:path'
13
+ import { pathToFileURL } from 'node:url'
14
+ import { loadAndValidateConfig } from '../../core/config-loader.js'
15
+
16
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']
17
+
18
+ /**
19
+ * 라우트 등록을 기록하는 라우터 stub.
20
+ * @param {Array<{ method: string, path: string, file: string }>} sink @param {string} file
21
+ * @returns {Record<string, any>}
22
+ */
23
+ function makeRecordingRouter(sink, file) {
24
+ /** @type {Record<string, any>} */
25
+ const http = {}
26
+ for (const m of HTTP_METHODS) {
27
+ http[m] = (/** @type {string} */ path) => sink.push({ method: m.toUpperCase(), path: String(path), file })
28
+ }
29
+ return {
30
+ http,
31
+ ws: (/** @type {string} */ path) => sink.push({ method: 'WS', path: String(path), file }),
32
+ use: () => {}, // 파일 레벨 미들웨어(router.use) — 수집엔 불필요한 no-op.
33
+ // 실 Router 의 `app` getter 미러(standalone Router 면 null, router.js). 라우트 모듈이 등록 시점에
34
+ // `router.app` 을 읽어도 undefined 가 아니라 null 이라 옵셔널 가드(`router.app?.x`)가 동형 동작(ADR-167).
35
+ app: null,
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 프로젝트의 앱별 라우트를 수집한다.
41
+ * @param {string} projectRoot
42
+ * @returns {Promise<Array<{ app: string, hosts: string[], routes: Array<{ method: string, path: string, file: string }> }>>}
43
+ */
44
+ export async function collectRoutes(projectRoot) {
45
+ const { apps } = await loadAndValidateConfig(projectRoot)
46
+ /** @type {Array<{ app: string, hosts: string[], routes: any[] }>} */
47
+ const result = []
48
+ for (const { name, config } of apps) {
49
+ const routesDir = join(projectRoot, 'apps', name, 'routes')
50
+ /** @type {any[]} */
51
+ const routes = []
52
+ /** @type {string[]} */
53
+ let files = []
54
+ try {
55
+ files = readdirSync(routesDir)
56
+ .filter((f) => f.endsWith('.js') && !f.endsWith('.test.js'))
57
+ .sort()
58
+ } catch (err) {
59
+ if (/** @type {any} */ (err).code !== 'ENOENT') throw err
60
+ }
61
+ for (const file of files) {
62
+ const abs = resolve(routesDir, file)
63
+ // `.js` 로 끝나는 디렉토리 등 비파일은 스킵한다(import 가 디렉토리에 던짐) — loadRoutes 의 statSync
64
+ // isFile 가드와 동형(routes-loader.js, ADR-167).
65
+ if (!statSync(abs).isFile()) continue
66
+ const mod = await import(pathToFileURL(abs).href)
67
+ if (typeof mod.default !== 'function') continue
68
+ // async 라우트 모듈(`export default async (router) => {...}`)도 끝까지 기다린다 — await 안 하면
69
+ // await 경계 뒤 등록 라우트가 수집 전에 누락된다. loadRoutes 도 `await mod.default(router)`(ADR-167).
70
+ await mod.default(makeRecordingRouter(routes, file))
71
+ }
72
+ result.push({ app: name, hosts: /** @type {any} */ (config).hosts ?? [], routes })
73
+ }
74
+ return result
75
+ }
76
+
77
+ /**
78
+ * 수집한 라우트를 사람이 읽는 트리 문자열로 포맷.
79
+ * @param {Array<{ app: string, hosts: string[], routes: Array<{ method: string, path: string }> }>} data
80
+ * @returns {string}
81
+ */
82
+ export function formatRoutes(data) {
83
+ /** @type {string[]} */
84
+ const lines = []
85
+ for (const { app, hosts, routes } of data) {
86
+ lines.push(`${app} [${hosts.join(', ') || 'no hosts'}]`)
87
+ if (routes.length === 0) {
88
+ lines.push(' (no routes)')
89
+ continue
90
+ }
91
+ const sorted = [...routes].sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
92
+ for (const r of sorted) lines.push(` ${r.method.padEnd(7)} ${r.path}`)
93
+ }
94
+ return lines.join('\n')
95
+ }
96
+
97
+ /**
98
+ * `mega routes` 명령 본체.
99
+ * @param {string} projectRoot
100
+ * @param {{ out?: (msg: string) => void }} [deps]
101
+ * @returns {Promise<number>} exit code.
102
+ */
103
+ export async function runRoutesCommand(projectRoot, { out = console.log } = {}) {
104
+ const data = await collectRoutes(projectRoot)
105
+ out(formatRoutes(data))
106
+ return 0
107
+ }
@@ -0,0 +1,120 @@
1
+ // @ts-check
2
+ /**
3
+ * scaffold/dev 명령군 (`new` / `generate`(g) / `routes` / `test` / `console`)을 commander 로 묶는다
4
+ * (ADR-142 — CLI 파서 dep 채택, ADR-123 의 zero-dep 런타임 명령과 공존). 런타임 명령(start/worker/
5
+ * scheduler/plugin)은 기존 zero-dep 디스패치(`src/cli/index.js`)가 그대로 처리한다.
6
+ *
7
+ * @module cli/commands/scaffold
8
+ */
9
+ import { Command } from 'commander'
10
+ import prompts from 'prompts'
11
+ import { generate, GENERATOR_KINDS } from '../generators/index.js'
12
+ import { scaffoldProject } from './new.js'
13
+ import { runRoutesCommand } from './routes.js'
14
+ import { runTestCommand } from './test-cmd.js'
15
+ import { startConsole } from './console-cmd.js'
16
+
17
+ /** scaffold/dev 명령 이름(별칭 포함). runCli 가 이 집합으로 라우팅한다. */
18
+ export const SCAFFOLD_COMMANDS = new Set(['new', 'generate', 'g', 'routes', 'test', 'console'])
19
+
20
+ /**
21
+ * 생성 결과를 사람이 읽게 출력.
22
+ * @param {(msg: string) => void} out @param {{ written: string[], skipped: string[] }} r @param {string} root
23
+ */
24
+ function reportFiles(out, r, root) {
25
+ for (const f of r.written) out(` create ${f.startsWith(root) ? f.slice(root.length + 1) : f}`)
26
+ for (const f of r.skipped) out(` skip ${f.startsWith(root) ? f.slice(root.length + 1) : f} (exists — use --force)`)
27
+ }
28
+
29
+ /**
30
+ * scaffold/dev 명령을 실행한다. commander 로 파싱하되 `process.exit` 를 부르지 않고 exit code 를 반환한다
31
+ * (runCli 계약 정합).
32
+ * @param {string[]} argv - `process.argv.slice(2)` (첫 토큰 = 명령).
33
+ * @param {object} deps
34
+ * @param {(msg: string) => void} deps.out
35
+ * @param {(msg: string) => void} deps.err
36
+ * @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
37
+ * @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
38
+ * @returns {Promise<number>} exit code.
39
+ */
40
+ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }) {
41
+ let exitCode = 0
42
+ const program = new Command()
43
+ program.name('mega').exitOverride()
44
+ program.configureOutput({
45
+ writeOut: (s) => out(s.replace(/\n+$/, '')),
46
+ writeErr: (s) => err(s.replace(/\n+$/, '')),
47
+ })
48
+ // --root 는 runCli 가 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
49
+ program.option('--root <dir>', '프로젝트 루트(runCli 가 해석)')
50
+
51
+ program
52
+ .command('new <project>')
53
+ .description('빈 폴더에서 멀티앱 hello world 스캐폴드')
54
+ .option('--views', 'EJS + ejs-mate 뷰 골격 포함')
55
+ .option('--force', '기존 파일 덮어쓰기')
56
+ .action(async (/** @type {string} */ project, /** @type {any} */ opts) => {
57
+ // --views 미지정 + 대화형 터미널이면 ejs-mate 옵트인을 묻는다(비-TTY/CI 는 기본 false).
58
+ let ejsMate = opts.views === true
59
+ if (opts.views === undefined && process.stdin.isTTY) {
60
+ const ans = /** @type {any} */ (await prompts({ type: 'confirm', name: 'views', message: 'EJS + ejs-mate 뷰 골격을 포함할까요?', initial: false }))
61
+ ejsMate = ans.views === true
62
+ }
63
+ const target = project === '.' ? projectRoot : `${projectRoot}/${project}`
64
+ const r = scaffoldProject(target, { name: project === '.' ? undefined : project, ejsMate, force: opts.force === true })
65
+ out(`mega: scaffolded project at ${r.root}`)
66
+ reportFiles(out, r, r.root)
67
+ out('\n다음 단계:\n cd ' + project + '\n npm install\n npm run dev')
68
+ })
69
+
70
+ program
71
+ .command('generate <kind> <name>')
72
+ .alias('g')
73
+ .description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')})`)
74
+ .option('--app <app>', '대상 앱', 'main')
75
+ .option('--version <v>', '컨트롤러 API 버전(예 v2, ADR-069)')
76
+ .option('--kind <adapterKind>', 'adapter 종류(db|cache|bus|session|log)')
77
+ .option('--lng <lng>', 'locale 언어(기본 en)')
78
+ .option('--force', '기존 파일 덮어쓰기')
79
+ .action((/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
80
+ const r = generate(kind, name, { app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true }, projectRoot)
81
+ out(`mega: generated ${r.kind} '${r.name}'`)
82
+ reportFiles(out, r, projectRoot)
83
+ if (r.written.length === 0) exitCode = 1
84
+ })
85
+
86
+ program
87
+ .command('routes')
88
+ .description('등록된 라우트 트리 출력')
89
+ .action(async () => {
90
+ exitCode = await runRoutesCommand(projectRoot, { out })
91
+ })
92
+
93
+ program
94
+ .command('test')
95
+ .description('vitest 실행(추가 인자 그대로 전달)')
96
+ .allowUnknownOption()
97
+ .argument('[args...]', 'vitest 인자')
98
+ .action(async (/** @type {string[]} */ args) => {
99
+ exitCode = await runTestCommand(projectRoot, args ?? [], { out })
100
+ })
101
+
102
+ program
103
+ .command('console')
104
+ .description('앱 컨텍스트를 로딩한 REPL')
105
+ .action(async () => {
106
+ await startConsole(projectRoot, { logger, out })
107
+ })
108
+
109
+ try {
110
+ await program.parseAsync(argv, { from: 'user' })
111
+ return exitCode
112
+ } catch (e) {
113
+ // commander 의 help/version 출력은 정상 종료(exitOverride 가 던지는 특수 코드).
114
+ const code = /** @type {any} */ (e).exitCode
115
+ if (/** @type {any} */ (e).code === 'commander.helpDisplayed' || /** @type {any} */ (e).code === 'commander.help') return 0
116
+ if (typeof code === 'number') return code
117
+ err(`mega: ${/** @type {any} */ (e).message ?? e}`)
118
+ return 1
119
+ }
120
+ }
@@ -0,0 +1,45 @@
1
+ // @ts-check
2
+ /**
3
+ * `mega test [...args]` — 프로젝트의 vitest 래퍼 (roadmap §338, ADR-142).
4
+ *
5
+ * 프로젝트 루트에서 `vitest run` 을 실행하고 추가 인자를 그대로 넘긴다. 별도 테스트 러너 dep 없이
6
+ * 프로젝트가 설치한 vitest 를 `npx` 로 호출한다(생성 package.json 의 devDependency).
7
+ *
8
+ * @module cli/commands/test-cmd
9
+ */
10
+ import { spawn as nodeSpawn } from 'node:child_process'
11
+ import os from 'node:os'
12
+
13
+ /**
14
+ * 자식 프로세스 종료를 exit code 로 환산한다. 정상 종료면 그 code(number)를, **시그널로 죽었으면**
15
+ * (code=null·signal=문자열) `128+signo`(bash 관례)를 돌려준다 — `code ?? 0` 으로 0(성공)을 돌려주면
16
+ * 비정상 종료를 통과로 오판하므로 금지(fail-closed, P4, ADR-167).
17
+ * @param {number|null} code - close 이벤트의 종료 code.
18
+ * @param {NodeJS.Signals|null} signal - close 이벤트의 종료 시그널.
19
+ * @returns {number}
20
+ */
21
+ function exitCodeFromClose(code, signal) {
22
+ if (typeof code === 'number') return code
23
+ const signo = signal ? (os.constants.signals[signal] ?? 0) : 0
24
+ return signo ? 128 + signo : 1
25
+ }
26
+
27
+ /**
28
+ * vitest 를 자식 프로세스로 실행한다.
29
+ * @param {string} projectRoot - 실행 cwd.
30
+ * @param {string[]} [args] - vitest 에 넘길 추가 인자.
31
+ * @param {object} [deps]
32
+ * @param {typeof nodeSpawn} [deps.spawn] - 주입용(테스트).
33
+ * @param {(msg: string) => void} [deps.out]
34
+ * @returns {Promise<number>} vitest exit code.
35
+ */
36
+ export function runTestCommand(projectRoot, args = [], { spawn = nodeSpawn, out = console.log } = {}) {
37
+ out('mega: running vitest...')
38
+ return new Promise((resolvePromise, reject) => {
39
+ const child = spawn('npx', ['vitest', 'run', ...args], { cwd: projectRoot, stdio: 'inherit', shell: false })
40
+ child.on('error', reject)
41
+ child.on('close', (/** @type {number|null} */ code, /** @type {NodeJS.Signals|null} */ signal) =>
42
+ resolvePromise(exitCodeFromClose(code, signal)),
43
+ )
44
+ })
45
+ }