mega-framework 0.1.6 → 0.1.8

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 (248) hide show
  1. package/README.md +9 -0
  2. package/bin/mega-ws-hub.js +2 -2
  3. package/package.json +33 -9
  4. package/sample/crud/.env +10 -1
  5. package/sample/crud/.env.example +10 -1
  6. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  7. package/sample/crud/.mega/journal/snapshot.json +261 -0
  8. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  9. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  10. package/sample/crud/apps/main/locales/server/en.json +12 -1
  11. package/sample/crud/apps/main/locales/server/ko.json +12 -1
  12. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  13. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  14. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  15. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  16. package/sample/crud/apps/main/models/note-model.js +79 -0
  17. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  18. package/sample/crud/apps/main/models/user-model.js +146 -0
  19. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  20. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  21. package/sample/crud/apps/main/routes/users.js +55 -10
  22. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  23. package/sample/crud/apps/main/services/auth-service.js +39 -24
  24. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  25. package/sample/crud/apps/main/services/note-service.js +6 -6
  26. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  27. package/sample/crud/apps/main/services/user-service.js +62 -21
  28. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  29. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  30. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  31. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  32. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  33. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  34. package/sample/crud/mega.config.js +10 -2
  35. package/sample/crud/package.json +3 -3
  36. package/sample/crud/scripts/start-ws-hub.sh +20 -6
  37. package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/sample/simple/package.json +2 -2
  39. package/src/adapters/adapter-manager.js +2 -1
  40. package/src/adapters/adapter-options.js +44 -3
  41. package/src/adapters/file-adapter.js +9 -5
  42. package/src/adapters/file-session-adapter.js +4 -3
  43. package/src/adapters/maria-adapter.js +33 -7
  44. package/src/adapters/mega-cache-adapter.js +83 -6
  45. package/src/adapters/mega-db-adapter.js +10 -1
  46. package/src/adapters/mongo-adapter.js +40 -8
  47. package/src/adapters/postgres-adapter.js +33 -6
  48. package/src/adapters/redis-adapter.js +7 -3
  49. package/src/adapters/sqlite-adapter.js +26 -3
  50. package/src/cli/commands/console-cmd.js +3 -1
  51. package/src/cli/commands/new.js +13 -3
  52. package/src/cli/commands/scaffold.js +173 -33
  53. package/src/cli/generators/index.js +140 -3
  54. package/src/cli/index.js +437 -155
  55. package/src/cli/watch.js +188 -0
  56. package/src/core/ajv-mapper.js +30 -3
  57. package/src/core/boot.js +464 -245
  58. package/src/core/cluster-metrics.js +13 -4
  59. package/src/core/ctx-builder.js +65 -3
  60. package/src/core/envelope.js +119 -12
  61. package/src/core/hub-link.js +89 -18
  62. package/src/core/i18n.js +11 -1
  63. package/src/core/index.js +7 -3
  64. package/src/core/mega-app.js +253 -505
  65. package/src/core/mega-cluster.js +4 -1
  66. package/src/core/mega-server.js +40 -9
  67. package/src/core/migration/dialect-registry.js +107 -0
  68. package/src/core/migration/dialects/README.md +62 -0
  69. package/src/core/migration/dialects/maria.js +496 -0
  70. package/src/core/migration/dialects/mongo.js +824 -0
  71. package/src/core/migration/dialects/postgres.js +563 -0
  72. package/src/core/migration/dialects/sqlite.js +476 -0
  73. package/src/core/migration/differ.js +456 -0
  74. package/src/core/migration/generate.js +508 -0
  75. package/src/core/migration/journal.js +167 -0
  76. package/src/core/migration/model-scan.js +84 -0
  77. package/src/core/migration/mongo-migration-db.js +97 -0
  78. package/src/core/migration/schema-builder.js +400 -0
  79. package/src/core/migration/schema-validator.js +315 -0
  80. package/src/core/migration-lock.js +205 -0
  81. package/src/core/migration-runner.js +166 -38
  82. package/src/core/multipart.js +28 -5
  83. package/src/core/pipeline.js +131 -0
  84. package/src/core/router.js +70 -65
  85. package/src/core/scope-registry.js +1 -0
  86. package/src/core/security.js +70 -12
  87. package/src/core/session-store.js +14 -1
  88. package/src/core/workers-manager.js +12 -1
  89. package/src/core/ws-cluster.js +10 -3
  90. package/src/core/ws-message.js +48 -4
  91. package/src/core/ws-presence.js +636 -0
  92. package/src/core/ws-roster.js +50 -8
  93. package/src/core/ws-upgrade.js +223 -12
  94. package/src/index.js +1 -1
  95. package/src/lib/hub-protocol.js +29 -0
  96. package/src/lib/mega-circuit-breaker.js +5 -3
  97. package/src/lib/mega-health.js +35 -4
  98. package/src/lib/mega-job-queue.js +151 -34
  99. package/src/lib/mega-job.js +37 -1
  100. package/src/lib/mega-metrics.js +31 -13
  101. package/src/lib/mega-plugin.js +34 -3
  102. package/src/lib/mega-schedule.js +40 -22
  103. package/src/lib/mega-shutdown.js +114 -39
  104. package/src/lib/mega-tracing.js +66 -19
  105. package/src/lib/mega-worker.js +33 -6
  106. package/src/lib/otel-resource.js +36 -0
  107. package/src/{cli → lib}/ws-hub.js +139 -15
  108. package/src/models/crud-sql-builder.js +133 -0
  109. package/src/models/mega-model.js +82 -2
  110. package/src/models/model-crud.js +483 -0
  111. package/src/models/mongo-crud.js +285 -0
  112. package/templates/adr/code.tpl +23 -0
  113. package/templates/model/code-mongo.tpl +35 -0
  114. package/templates/model/code.tpl +15 -1
  115. package/templates/model/test-mongo.tpl +38 -0
  116. package/templates/model/test.tpl +4 -0
  117. package/types/adapters/adapter-manager.d.ts +95 -0
  118. package/types/adapters/adapter-options.d.ts +93 -0
  119. package/types/adapters/file-adapter.d.ts +105 -0
  120. package/types/adapters/file-session-adapter.d.ts +103 -0
  121. package/types/adapters/index.d.ts +20 -0
  122. package/types/adapters/maria-adapter.d.ts +117 -0
  123. package/types/adapters/mega-adapter.d.ts +215 -0
  124. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  125. package/types/adapters/mega-cache-adapter.d.ts +73 -0
  126. package/types/adapters/mega-db-adapter.d.ts +50 -0
  127. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  128. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  129. package/types/adapters/mega-session-adapter.d.ts +32 -0
  130. package/types/adapters/mongo-adapter.d.ts +150 -0
  131. package/types/adapters/nats-adapter.d.ts +108 -0
  132. package/types/adapters/postgres-adapter.d.ts +141 -0
  133. package/types/adapters/redis-adapter.d.ts +78 -0
  134. package/types/adapters/redis-session-adapter.d.ts +82 -0
  135. package/types/adapters/redlock-adapter.d.ts +149 -0
  136. package/types/adapters/registry.d.ts +46 -0
  137. package/types/adapters/sqlite-adapter.d.ts +112 -0
  138. package/types/auth/index.d.ts +24 -0
  139. package/types/cli/commands/console-cmd.d.ts +37 -0
  140. package/types/cli/commands/new.d.ts +16 -0
  141. package/types/cli/commands/routes.d.ts +36 -0
  142. package/types/cli/commands/scaffold.d.ts +78 -0
  143. package/types/cli/commands/test-cmd.d.ts +14 -0
  144. package/types/cli/generators/index.d.ts +122 -0
  145. package/types/cli/index.d.ts +234 -0
  146. package/types/cli/template-engine.d.ts +40 -0
  147. package/types/cli/watch.d.ts +59 -0
  148. package/types/core/ajv-mapper.d.ts +27 -0
  149. package/types/core/boot.d.ts +233 -0
  150. package/types/core/cluster-metrics.d.ts +52 -0
  151. package/types/core/config-loader.d.ts +13 -0
  152. package/types/core/config-validator.d.ts +30 -0
  153. package/types/core/ctx-builder.d.ts +103 -0
  154. package/types/core/envelope.d.ts +79 -0
  155. package/types/core/error-mapper.d.ts +17 -0
  156. package/types/core/formbody.d.ts +41 -0
  157. package/types/core/hub-link.d.ts +266 -0
  158. package/types/core/i18n.d.ts +178 -0
  159. package/types/core/index.d.ts +28 -0
  160. package/types/core/mega-app.d.ts +529 -0
  161. package/types/core/mega-cluster.d.ts +104 -0
  162. package/types/core/mega-server.d.ts +91 -0
  163. package/types/core/mega-service.d.ts +31 -0
  164. package/types/core/migration/dialect-registry.d.ts +22 -0
  165. package/types/core/migration/dialects/maria.d.ts +99 -0
  166. package/types/core/migration/dialects/mongo.d.ts +89 -0
  167. package/types/core/migration/dialects/postgres.d.ts +117 -0
  168. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  169. package/types/core/migration/differ.d.ts +47 -0
  170. package/types/core/migration/generate.d.ts +56 -0
  171. package/types/core/migration/journal.d.ts +52 -0
  172. package/types/core/migration/model-scan.d.ts +19 -0
  173. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  174. package/types/core/migration/schema-builder.d.ts +197 -0
  175. package/types/core/migration/schema-validator.d.ts +20 -0
  176. package/types/core/migration-lock.d.ts +33 -0
  177. package/types/core/migration-runner.d.ts +101 -0
  178. package/types/core/multipart.d.ts +86 -0
  179. package/types/core/openapi.d.ts +62 -0
  180. package/types/core/pipeline.d.ts +93 -0
  181. package/types/core/router.d.ts +159 -0
  182. package/types/core/routes-loader.d.ts +21 -0
  183. package/types/core/scope-registry.d.ts +14 -0
  184. package/types/core/security.d.ts +77 -0
  185. package/types/core/services-loader.d.ts +27 -0
  186. package/types/core/session-cleanup-schedule.d.ts +19 -0
  187. package/types/core/session-store.d.ts +25 -0
  188. package/types/core/session.d.ts +77 -0
  189. package/types/core/static-assets.d.ts +73 -0
  190. package/types/core/template.d.ts +106 -0
  191. package/types/core/workers-manager.d.ts +79 -0
  192. package/types/core/ws-cluster.d.ts +208 -0
  193. package/types/core/ws-compression.d.ts +112 -0
  194. package/types/core/ws-controller.d.ts +65 -0
  195. package/types/core/ws-message.d.ts +106 -0
  196. package/types/core/ws-presence.d.ts +273 -0
  197. package/types/core/ws-roster.d.ts +108 -0
  198. package/types/core/ws-upgrade.d.ts +260 -0
  199. package/types/errors/config-error.d.ts +10 -0
  200. package/types/errors/http-errors.d.ts +120 -0
  201. package/types/errors/index.d.ts +3 -0
  202. package/types/errors/mega-error.d.ts +32 -0
  203. package/types/index.d.ts +39 -0
  204. package/types/lib/asp/config.d.ts +49 -0
  205. package/types/lib/asp/crypto.d.ts +43 -0
  206. package/types/lib/asp/errors.d.ts +30 -0
  207. package/types/lib/asp/nonce-cache.d.ts +52 -0
  208. package/types/lib/asp/plugin.d.ts +30 -0
  209. package/types/lib/asp/ws-terminator.d.ts +45 -0
  210. package/types/lib/env-mapper.d.ts +14 -0
  211. package/types/lib/hub-protocol.d.ts +106 -0
  212. package/types/lib/index.d.ts +22 -0
  213. package/types/lib/logger/telegram-core.d.ts +104 -0
  214. package/types/lib/logger/telegram-transport.d.ts +45 -0
  215. package/types/lib/mega-brute-force.d.ts +66 -0
  216. package/types/lib/mega-circuit-breaker.d.ts +243 -0
  217. package/types/lib/mega-cron.d.ts +66 -0
  218. package/types/lib/mega-hash.d.ts +32 -0
  219. package/types/lib/mega-health.d.ts +48 -0
  220. package/types/lib/mega-job-queue.d.ts +188 -0
  221. package/types/lib/mega-job-worker.d.ts +130 -0
  222. package/types/lib/mega-job.d.ts +145 -0
  223. package/types/lib/mega-logger.d.ts +45 -0
  224. package/types/lib/mega-metrics.d.ts +285 -0
  225. package/types/lib/mega-plugin.d.ts +245 -0
  226. package/types/lib/mega-retry.d.ts +85 -0
  227. package/types/lib/mega-schedule.d.ts +260 -0
  228. package/types/lib/mega-shutdown.d.ts +135 -0
  229. package/types/lib/mega-tracing.d.ts +224 -0
  230. package/types/lib/mega-worker.d.ts +129 -0
  231. package/types/lib/otel-resource.d.ts +16 -0
  232. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  233. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  234. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  235. package/types/lib/ws-hub.d.ts +259 -0
  236. package/types/models/crud-sql-builder.d.ts +48 -0
  237. package/types/models/index.d.ts +1 -0
  238. package/types/models/mega-model.d.ts +138 -0
  239. package/types/models/model-crud.d.ts +82 -0
  240. package/types/models/mongo-crud.d.ts +59 -0
  241. package/types/test/index.d.ts +84 -0
  242. package/.env +0 -127
  243. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  244. package/sample/crud/apps/main/models/note.js +0 -71
  245. package/sample/crud/apps/main/models/user.js +0 -86
  246. package/sample/crud/package-lock.json +0 -5665
  247. package/sample/crud/yarn.lock +0 -2142
  248. package/sample/simple/package-lock.json +0 -1851
@@ -1,19 +1,27 @@
1
1
  // @ts-check
2
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`) 그대로 처리한다.
3
+ * scaffold/dev 명령군 (`new` / `generate`(g) / `routes` / `test` / `console`) commander 등록.
4
+ *
5
+ * ADR-142 commander 를 채택한 명령군이며, ADR-195(commander 전면 일원화) 이후 런타임 명령
6
+ * (start/worker/scheduler/migrate)과 **같은 program 트리**에 등록된다 — `registerScaffoldCommands` 를
7
+ * `cli/index.js` 의 `buildProgram` 이 호출한다. `runScaffoldCommand` 는 scaffold 명령군만 담은 독립
8
+ * program 을 돌리는 기존 진입점으로 유지한다(하위호환·단위 테스트 경계).
6
9
  *
7
10
  * @module cli/commands/scaffold
8
11
  */
9
12
  import { Command } from 'commander'
10
- import { generate, GENERATOR_KINDS } from '../generators/index.js'
13
+ import { existsSync } from 'node:fs'
14
+ import { join } from 'node:path'
15
+ import { pathToFileURL } from 'node:url'
16
+ import { execFile } from 'node:child_process'
17
+ import { promisify } from 'node:util'
18
+ import { generate, generateFromScaffoldDef, nextAdrNumber, GENERATOR_KINDS } from '../generators/index.js'
11
19
  import { scaffoldProject } from './new.js'
12
20
  import { runRoutesCommand } from './routes.js'
13
21
  import { runTestCommand } from './test-cmd.js'
14
22
  import { startConsole } from './console-cmd.js'
15
23
 
16
- /** scaffold/dev 명령 이름(별칭 포함). runCli 가 이 집합으로 라우팅한다. */
24
+ /** scaffold/dev 명령 이름(별칭 포함). */
17
25
  export const SCAFFOLD_COMMANDS = new Set(['new', 'generate', 'g', 'routes', 'test', 'console'])
18
26
 
19
27
  /**
@@ -25,28 +33,83 @@ function reportFiles(out, r, root) {
25
33
  for (const f of r.skipped) out(` skip ${f.startsWith(root) ? f.slice(root.length + 1) : f} (exists — use --force)`)
26
34
  }
27
35
 
36
+ const execFileAsync = promisify(execFile)
37
+
28
38
  /**
29
- * scaffold/dev 명령을 실행한다. commander 파싱하되 `process.exit` 부르지 않고 exit code 반환한다
30
- * (runCli 계약 정합).
31
- * @param {string[]} argv - `process.argv.slice(2)` (첫 토큰 = 명령).
39
+ * `g adr` 다음 번호를 **원격 포함**으로 해석한다(ADR-218 병렬 task 번호 충돌 회피).
40
+ * `git fetch origin` 후 `origin/main` 의 `docs/adr/` 파일명에서 번호를 모아 로컬 스캔
41
+ * ({@link nextAdrNumber})과 합산한다 형제 task 방금 push 한 ADR 이 로컬 작업트리에 없어도
42
+ * 번호가 건너뛰어진다. git 부재/오프라인/비-repo 는 로컬 스캔만으로 폴백(경고 1줄 — 스캐폴드는
43
+ * 어디서든 동작해야 하므로 fail 아님).
44
+ *
45
+ * @param {string} projectRoot
46
+ * @param {(msg: string) => void} out
47
+ * @returns {Promise<number>}
48
+ */
49
+ async function resolveAdrNumberWithRemote(projectRoot, out) {
50
+ /** @type {number[]} */
51
+ const remote = []
52
+ try {
53
+ await execFileAsync('git', ['fetch', 'origin', '--quiet'], { cwd: projectRoot })
54
+ const { stdout } = await execFileAsync('git', ['ls-tree', '--name-only', 'origin/main', 'docs/adr/'], { cwd: projectRoot })
55
+ for (const line of stdout.split('\n')) {
56
+ const m = line.match(/(\d{1,4})-[^/]+\.md$/)
57
+ if (m) remote.push(Number(m[1]))
58
+ }
59
+ } catch (err) {
60
+ // 오프라인/비-repo/origin 부재 — 로컬 스캔만으로 진행하되 충돌 가능성을 알린다(silent 금지).
61
+ out(`mega: 원격 ADR 번호 확인 실패(${/** @type {any} */ (err).message?.split('\n')[0] ?? err}) — 로컬 기준으로 번호를 할당합니다.`)
62
+ }
63
+ return nextAdrNumber(projectRoot, remote)
64
+ }
65
+
66
+ /**
67
+ * `g model --adapter <key>` 의 adapter 키/driver 해석 — mega.config.js 의 services.databases 를
68
+ * best-effort 로 읽는다(스캐폴드는 config 가 아직 없는 초기 프로젝트에서도 돌아야 하므로 로드
69
+ * 실패는 fail 이 아니라 기본값 + 경고 1줄). 키 미지정 시: 선언 db 가 1개면 그 키, 아니면 'primary'.
70
+ *
71
+ * @param {string} projectRoot @param {string | undefined} explicitKey
72
+ * @param {(msg: string) => void} out
73
+ * @returns {Promise<{ key: string, driver: string | undefined }>}
74
+ */
75
+ async function resolveModelAdapter(projectRoot, explicitKey, out) {
76
+ /** @type {Record<string, any>} */
77
+ let databases = {}
78
+ const configPath = join(projectRoot, 'mega.config.js')
79
+ if (existsSync(configPath)) {
80
+ try {
81
+ const mod = await import(pathToFileURL(configPath).href)
82
+ databases = mod.default?.services?.databases ?? {}
83
+ } catch (err) {
84
+ // config 문법 오류 등 — 스캐폴드는 계속 가능해야 하므로 기본 템플릿으로 폴백하되 명시 안내(P4).
85
+ out(`mega: mega.config.js 를 읽지 못해 adapter driver 를 해석하지 못했습니다(${/** @type {any} */ (err).message}) — SQL 템플릿으로 생성합니다.`)
86
+ }
87
+ }
88
+ const keys = Object.keys(databases)
89
+ const key = explicitKey ?? (keys.length === 1 ? keys[0] : 'primary')
90
+ if (explicitKey !== undefined && keys.length > 0 && !keys.includes(explicitKey)) {
91
+ out(`mega: --adapter '${explicitKey}' 가 services.databases 에 없습니다(선언: [${keys.join(', ')}]) — 키를 그대로 쓰고 SQL 템플릿으로 생성합니다.`)
92
+ }
93
+ return { key, driver: databases[key]?.driver }
94
+ }
95
+
96
+ /**
97
+ * scaffold/dev 명령 5종을 commander program 에 등록한다 — 명령 정의의 단일 정본(ADR-195).
98
+ * action 은 `process.exit` 를 부르지 않고 `setExit(code)` 로 종료 코드를 보고한다(runCli 계약).
99
+ *
100
+ * @param {import('commander').Command} program - 등록 대상 program.
32
101
  * @param {object} deps
33
102
  * @param {(msg: string) => void} deps.out
34
- * @param {(msg: string) => void} deps.err
35
103
  * @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
36
104
  * @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
37
- * @returns {Promise<number>} exit code.
105
+ * @param {(kind: string) => Promise<{ dir: string, files: Array<{ path: string, template: string }> } | undefined>} [deps.resolvePluginGenerator] -
106
+ * 빌트인이 아닌 kind 의 플러그인 scaffold manifest 조회(`mega.scaffold.register`, 03-api-spec §11).
107
+ * 호출측(runCli)이 config 로드 + 플러그인 install 을 감싼 함수를 주입한다 — 본 모듈이 cli/index.js 를
108
+ * import 하면 순환이라 주입식으로 푼다. 미주입/미발견이면 unknown kind 에러.
109
+ * @param {(code: number) => void} deps.setExit - 명령 종료 코드 보고 콜백.
110
+ * @returns {void}
38
111
  */
39
- export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }) {
40
- let exitCode = 0
41
- const program = new Command()
42
- program.name('mega').exitOverride()
43
- program.configureOutput({
44
- writeOut: (s) => out(s.replace(/\n+$/, '')),
45
- writeErr: (s) => err(s.replace(/\n+$/, '')),
46
- })
47
- // --root 는 runCli 가 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
48
- program.option('--root <dir>', '프로젝트 루트(runCli 가 해석)')
49
-
112
+ export function registerScaffoldCommands(program, { out, projectRoot, logger, resolvePluginGenerator, setExit }) {
50
113
  program
51
114
  .command('new <project>')
52
115
  .description('sample/crud 데모앱(14기능) 전체를 빈 폴더에 스캐폴드')
@@ -64,24 +127,58 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
64
127
  program
65
128
  .command('generate <kind> <name>')
66
129
  .alias('g')
67
- .description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')})`)
130
+ .description(`코드+테스트 생성 (kind: ${GENERATOR_KINDS.join('|')} 또는 플러그인 등록 generator)`)
68
131
  .option('--app <app>', '대상 앱', 'main')
69
132
  .option('--version <v>', '컨트롤러 API 버전(예 v2, ADR-069)')
70
133
  .option('--kind <adapterKind>', 'adapter 종류(db|cache|bus|session|log)')
134
+ .option('--adapter <key>', 'model 전용 — services.databases 키(driver 가 mongodb 면 mongo 템플릿, 기본: 유일 선언 db 또는 primary)')
71
135
  .option('--lng <lng>', 'locale 언어(기본 en)')
72
136
  .option('--force', '기존 파일 덮어쓰기')
73
- .action((/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
74
- const r = generate(kind, name, { app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true }, projectRoot)
137
+ .action(async (/** @type {string} */ kind, /** @type {string} */ name, /** @type {any} */ opts) => {
138
+ /** @type {{ kind: string, name: string, written: string[], skipped: string[] }} */
139
+ let r
140
+ if (GENERATOR_KINDS.includes(/** @type {any} */ (kind))) {
141
+ /** @type {{ key: string, driver: string | undefined }} */
142
+ let modelAdapter = { key: 'primary', driver: undefined }
143
+ if (kind === 'model') modelAdapter = await resolveModelAdapter(projectRoot, opts.adapter, out)
144
+ // adr 은 번호를 원격 포함으로 해석한다(병렬 task 충돌 회피, ADR-218).
145
+ /** @type {number | undefined} */
146
+ let adrNumber
147
+ if (kind === 'adr') adrNumber = await resolveAdrNumberWithRemote(projectRoot, out)
148
+ r = generate(
149
+ kind,
150
+ name,
151
+ { app: opts.app, version: opts.version, kind: opts.kind, lng: opts.lng, force: opts.force === true, adapter: modelAdapter.key, adapterDriver: modelAdapter.driver, adrNumber },
152
+ projectRoot,
153
+ )
154
+ } else {
155
+ // 빌트인이 아니면 플러그인 등록 generator(manifest) 조회(03-api-spec §11 `mega g <name>` 계약).
156
+ // config 로드 실패(프로젝트 밖 등)는 unknown kind 메시지에 원인을 병기해 오도 없이 보고한다.
157
+ /** @type {{ dir: string, files: Array<{ path: string, template: string }> } | undefined} */
158
+ let def
159
+ try {
160
+ def = await resolvePluginGenerator?.(kind)
161
+ } catch (e) {
162
+ throw new Error(
163
+ `Unknown generator '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}. ` +
164
+ `(플러그인 generator 확인 실패: ${/** @type {any} */ (e).message ?? e})`,
165
+ )
166
+ }
167
+ if (def === undefined) {
168
+ throw new Error(`Unknown generator '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')} (또는 플러그인 등록 generator).`)
169
+ }
170
+ r = generateFromScaffoldDef(kind, def, name, { app: opts.app, force: opts.force === true }, projectRoot)
171
+ }
75
172
  out(`mega: generated ${r.kind} '${r.name}'`)
76
173
  reportFiles(out, r, projectRoot)
77
- if (r.written.length === 0) exitCode = 1
174
+ if (r.written.length === 0) setExit(1)
78
175
  })
79
176
 
80
177
  program
81
178
  .command('routes')
82
179
  .description('등록된 라우트 트리 출력')
83
180
  .action(async () => {
84
- exitCode = await runRoutesCommand(projectRoot, { out })
181
+ setExit(await runRoutesCommand(projectRoot, { out }))
85
182
  })
86
183
 
87
184
  program
@@ -90,7 +187,7 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
90
187
  .allowUnknownOption()
91
188
  .argument('[args...]', 'vitest 인자')
92
189
  .action(async (/** @type {string[]} */ args) => {
93
- exitCode = await runTestCommand(projectRoot, args ?? [], { out })
190
+ setExit(await runTestCommand(projectRoot, args ?? [], { out }))
94
191
  })
95
192
 
96
193
  program
@@ -99,16 +196,59 @@ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger }
99
196
  .action(async () => {
100
197
  await startConsole(projectRoot, { logger, out })
101
198
  })
199
+ }
200
+
201
+ /**
202
+ * commander 의 exitOverride 예외를 exit code 로 환산한다 — help/version 출력은 정상 종료(0),
203
+ * CommanderError 는 자체 exitCode, 그 외 에러는 메시지 출력 후 1. commander 가 아닌 에러를 그대로
204
+ * 삼키지 않도록 `rethrowUnknown` 옵션을 둔다(runCli 가 부팅·config 에러를 bin 으로 전파할 때 사용).
205
+ *
206
+ * @param {unknown} e - parseAsync 가 던진 예외.
207
+ * @param {(msg: string) => void} err
208
+ * @param {{ rethrowUnknown?: boolean }} [opts] - true 면 CommanderError 가 아닌 예외를 재throw.
209
+ * @returns {number} exit code.
210
+ */
211
+ export function commanderErrorToExitCode(e, err, { rethrowUnknown = false } = {}) {
212
+ const anyErr = /** @type {any} */ (e)
213
+ if (typeof anyErr?.code === 'string' && anyErr.code.startsWith('commander.')) {
214
+ // help/version 출력은 정상 종료(exitOverride 가 던지는 특수 코드).
215
+ if (anyErr.code === 'commander.helpDisplayed' || anyErr.code === 'commander.help' || anyErr.code === 'commander.version') return 0
216
+ return typeof anyErr.exitCode === 'number' ? anyErr.exitCode : 1
217
+ }
218
+ if (rethrowUnknown) throw e
219
+ err(`mega: ${anyErr?.message ?? e}`)
220
+ return 1
221
+ }
222
+
223
+ /**
224
+ * scaffold/dev 명령만 담은 독립 program 으로 실행한다(하위호환 진입점 — 단위 테스트 경계 유지).
225
+ * commander 로 파싱하되 `process.exit` 를 부르지 않고 exit code 를 반환한다(runCli 계약 정합).
226
+ *
227
+ * @param {string[]} argv - `process.argv.slice(2)` (첫 토큰 = 명령).
228
+ * @param {object} deps
229
+ * @param {(msg: string) => void} deps.out
230
+ * @param {(msg: string) => void} deps.err
231
+ * @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
232
+ * @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
233
+ * @param {(kind: string) => Promise<{ dir: string, files: Array<{ path: string, template: string }> } | undefined>} [deps.resolvePluginGenerator]
234
+ * @returns {Promise<number>} exit code.
235
+ */
236
+ export async function runScaffoldCommand(argv, { out, err, projectRoot, logger, resolvePluginGenerator }) {
237
+ let exitCode = 0
238
+ const program = new Command()
239
+ program.name('mega').exitOverride()
240
+ program.configureOutput({
241
+ writeOut: (s) => out(s.replace(/\n+$/, '')),
242
+ writeErr: (s) => err(s.replace(/\n+$/, '')),
243
+ })
244
+ // --root 는 호출측(runCli)이 이미 해석함 — commander 가 "unknown option" 으로 막지 않도록 받아만 둔다.
245
+ program.option('--root <dir>', '프로젝트 루트(호출측이 해석)')
246
+ registerScaffoldCommands(program, { out, projectRoot, logger, resolvePluginGenerator, setExit: (c) => (exitCode = c) })
102
247
 
103
248
  try {
104
249
  await program.parseAsync(argv, { from: 'user' })
105
250
  return exitCode
106
251
  } catch (e) {
107
- // commander 의 help/version 출력은 정상 종료(exitOverride 가 던지는 특수 코드).
108
- const code = /** @type {any} */ (e).exitCode
109
- if (/** @type {any} */ (e).code === 'commander.helpDisplayed' || /** @type {any} */ (e).code === 'commander.help') return 0
110
- if (typeof code === 'number') return code
111
- err(`mega: ${/** @type {any} */ (e).message ?? e}`)
112
- return 1
252
+ return commanderErrorToExitCode(e, err)
113
253
  }
114
254
  }
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @module cli/generators
15
15
  */
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
16
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
17
17
  import { dirname, join, relative, resolve, sep } from 'node:path'
18
18
  import { fileURLToPath } from 'node:url'
19
19
  import { nameVariants, renderTemplate } from '../template-engine.js'
@@ -36,8 +36,23 @@ export const GENERATOR_KINDS = /** @type {const} */ ([
36
36
  'locale',
37
37
  'adapter',
38
38
  'migration',
39
+ 'adr',
39
40
  ])
40
41
 
42
+ /**
43
+ * 플러그인 scaffold manifest 의 토큰 계약(ADR-199) — `files[].path`/`files[].template` 의 `{{token}}`
44
+ * 에 쓸 수 있는 이름과 의미. 빌트인 generator 의 base 토큰과 동일 집합이라 템플릿 작성 관례가 하나다.
45
+ * 미정의 토큰은 renderTemplate 가 throw 한다(P4 — silent 치환 누락 방지).
46
+ * @type {Readonly<Record<string, string>>}
47
+ */
48
+ export const SCAFFOLD_TOKENS = Object.freeze({
49
+ Name: 'PascalCase 이름 (예: userCard → UserCard)',
50
+ name: 'kebab-case 이름 (user-card)',
51
+ camelName: 'camelCase 이름 (userCard)',
52
+ snake: 'snake_case 이름 (user_card)',
53
+ app: '대상 앱 이름 (--app, 기본 main)',
54
+ })
55
+
41
56
  /** adapter `--kind` → 베이스 클래스(mega-framework export 명). */
42
57
  const ADAPTER_BASES = /** @type {Record<string, string>} */ ({
43
58
  db: 'MegaDbAdapter',
@@ -121,8 +136,19 @@ export function planArtifacts(kind, rawName, opts, projectRoot) {
121
136
  }
122
137
 
123
138
  switch (kind) {
124
- case 'model':
125
- return pair({ codeRel: `apps/${app}/models/${v.kebab}.js`, vars: { table: v.snake } })
139
+ case 'model': {
140
+ // --adapter <key>: services.databases 키(static adapter 값). driver mongodb 해석되면
141
+ // mongo 변형 템플릿(_id 자동·도큐먼트 API — ADR-209)을 쓴다. 해석은 scaffold 명령이
142
+ // best-effort 로 수행해 opts.adapterDriver 로 전달한다(config 부재 시 SQL 템플릿 기본).
143
+ const adapter = typeof opts.adapter === 'string' && opts.adapter.length > 0 ? opts.adapter : 'primary'
144
+ const isMongo = opts.adapterDriver === 'mongodb'
145
+ return pair({
146
+ codeRel: `apps/${app}/models/${v.kebab}.js`,
147
+ vars: { table: v.snake, adapter },
148
+ codeTpl: isMongo ? 'code-mongo.tpl' : 'code.tpl',
149
+ testTpl: isMongo ? 'test-mongo.tpl' : 'test.tpl',
150
+ })
151
+ }
126
152
 
127
153
  case 'service':
128
154
  return pair({ codeRel: `apps/${app}/services/${v.kebab}-service.js`, vars: {} })
@@ -160,11 +186,67 @@ export function planArtifacts(kind, rawName, opts, projectRoot) {
160
186
  case 'app':
161
187
  return planApp(v, projectRoot, base)
162
188
 
189
+ case 'adr':
190
+ return planAdr(v, opts, projectRoot)
191
+
163
192
  default:
164
193
  throw new Error(`Unknown generator kind '${kind}'. Supported: ${GENERATOR_KINDS.join(', ')}.`)
165
194
  }
166
195
  }
167
196
 
197
+ /**
198
+ * 다음 ADR 번호를 해석한다 — `docs/adr/NNNN-*.md` 파일명 + 레거시 `docs/09` 헤딩(`### ADR-N:`) +
199
+ * 호출측이 모은 추가 번호(예: `git ls-tree origin/main` 의 원격 파일 — 병렬 task 충돌 회피)의
200
+ * 최댓값 + 1. 아무 ADR 도 없으면 1.
201
+ *
202
+ * @param {string} projectRoot
203
+ * @param {number[]} [extraNumbers] - 로컬 밖에서 관측한 번호(원격 스캔 등).
204
+ * @returns {number}
205
+ */
206
+ export function nextAdrNumber(projectRoot, extraNumbers = []) {
207
+ let max = 0
208
+ const adrDir = join(projectRoot, 'docs/adr')
209
+ if (existsSync(adrDir)) {
210
+ for (const f of readdirSync(adrDir)) {
211
+ const m = f.match(/^(\d{1,4})-.+\.md$/)
212
+ if (m) max = Math.max(max, Number(m[1]))
213
+ }
214
+ }
215
+ const legacy = join(projectRoot, 'docs/09-decisions-and-open-questions.md')
216
+ if (existsSync(legacy)) {
217
+ for (const m of readFileSync(legacy, 'utf8').matchAll(/^### ADR-(\d+)/gm)) {
218
+ max = Math.max(max, Number(m[1]))
219
+ }
220
+ }
221
+ for (const n of extraNumbers) {
222
+ if (Number.isInteger(n)) max = Math.max(max, n)
223
+ }
224
+ return max + 1
225
+ }
226
+
227
+ /** adr — `docs/adr/NNNN-<name>.md` 1개(코드/테스트 쌍 아님 — 프로젝트 결정 기록 문서, ADR-218).
228
+ * 번호는 opts.adrNumber(호출측이 원격 포함 해석) 우선, 미지정 시 로컬 스캔({@link nextAdrNumber}).
229
+ * @param {Variants} v @param {Record<string, any>} opts @param {string} projectRoot
230
+ * @returns {Artifact[]} */
231
+ function planAdr(v, opts, projectRoot) {
232
+ const number = Number.isInteger(opts.adrNumber) && opts.adrNumber > 0 ? opts.adrNumber : nextAdrNumber(projectRoot)
233
+ const padded = String(number).padStart(4, '0')
234
+ const d = new Date()
235
+ const p = (/** @type {number} */ n) => String(n).padStart(2, '0')
236
+ const vars = {
237
+ number: String(number),
238
+ title: v.words.join(' '),
239
+ date: `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`,
240
+ }
241
+ return [
242
+ {
243
+ outAbs: join(projectRoot, `docs/adr/${padded}-${v.kebab}.md`),
244
+ role: 'code',
245
+ content: renderTemplate(readTpl('adr', 'code.tpl'), vars),
246
+ },
247
+ ]
248
+ }
249
+
168
250
  /**
169
251
  * @typedef {{ kebab: string, pascal: string, camel: string, snake: string, words: string[] }} Variants
170
252
  * @typedef {Record<string, string>} BaseVars
@@ -347,6 +429,61 @@ export function writeArtifacts(artifacts, { force = false } = {}) {
347
429
  return { written, skipped }
348
430
  }
349
431
 
432
+ /**
433
+ * 플러그인 scaffold manifest(`mega.scaffold.register(name, { dir, files, description? })`,
434
+ * 03-api-spec §11 / ADR-199)를 artifact 목록으로 계획한다 — 빌트인 13종과 같은 plan→write 2단 분리.
435
+ * 토큰 계약은 {@link SCAFFOLD_TOKENS} 가 정본이며 미정의 토큰은 renderTemplate 가 throw(P4).
436
+ * `files[].path` 에도 토큰을 쓸 수 있다(예 `services/{{name}}-service.js`).
437
+ *
438
+ * @param {{ dir: string, files: Array<{ path: string, template: string }> }} def - 플러그인 등록 정의.
439
+ * @param {string} rawName - `mega g <kind> <name>` 의 name.
440
+ * @param {Record<string, any>} opts - { app }.
441
+ * @param {string} projectRoot - 출력 기준 루트. `def.dir` 는 이 루트 상대.
442
+ * @returns {Artifact[]}
443
+ * @throws {Error} def 파일 항목이 `{ path, template }` 문자열 쌍이 아니거나, 출력 경로가 projectRoot 를
444
+ * 벗어나면(경로 탐색 차단 — template.js resolveViewPath 정합) fail-fast.
445
+ */
446
+ export function planScaffoldDef(def, rawName, opts, projectRoot) {
447
+ const app = typeof opts.app === 'string' && opts.app.length > 0 ? opts.app : 'main'
448
+ const v = nameVariants(rawName)
449
+ const vars = { Name: v.pascal, name: v.kebab, camelName: v.camel, snake: v.snake, app }
450
+ const rootAbs = resolve(projectRoot)
451
+ const baseDir = resolve(rootAbs, def.dir)
452
+ /** @type {Artifact[]} */
453
+ const out = []
454
+ for (const file of def.files) {
455
+ if (!file || typeof file.path !== 'string' || file.path.length === 0 || typeof file.template !== 'string') {
456
+ throw new Error(`scaffold def: files entries must be { path: string, template: string }. Got ${JSON.stringify(file)}.`)
457
+ }
458
+ const outAbs = resolve(baseDir, renderTemplate(file.path, vars))
459
+ // 출력은 projectRoot 내부로 제한 — def.dir/path 의 `..` 가 프로젝트 밖에 쓰는 걸 차단.
460
+ if (outAbs !== rootAbs && !outAbs.startsWith(rootAbs + sep)) {
461
+ throw new Error(`scaffold def: output '${file.path}' escapes the project root (path traversal blocked).`)
462
+ }
463
+ out.push({ outAbs, role: 'code', content: renderTemplate(file.template, vars) })
464
+ }
465
+ return out
466
+ }
467
+
468
+ /**
469
+ * 플러그인 등록 scaffold generator 실행 — `mega g <plugin-generator> <name>` 의 본체. 빌트인 `generate`
470
+ * 와 같은 계획 → 쓰기 → 결과 계약(존재 파일 skip, `--force` 덮어쓰기).
471
+ * @param {string} kindName - 플러그인이 등록한 generator 이름(결과 보고용).
472
+ * @param {{ dir: string, files: Array<{ path: string, template: string }> }} def
473
+ * @param {string} rawName
474
+ * @param {object} [opts] - { app, force }
475
+ * @param {string} [projectRoot]
476
+ * @returns {{ kind: string, name: string, written: string[], skipped: string[] }}
477
+ */
478
+ export function generateFromScaffoldDef(kindName, def, rawName, opts = {}, projectRoot = process.cwd()) {
479
+ if (typeof rawName !== 'string' || rawName.trim().length === 0) {
480
+ throw new Error(`mega g ${kindName}: a name is required (e.g. 'mega g ${kindName} users').`)
481
+ }
482
+ const artifacts = planScaffoldDef(def, rawName, /** @type {any} */ (opts), projectRoot)
483
+ const { written, skipped } = writeArtifacts(artifacts, { force: /** @type {any} */ (opts).force === true })
484
+ return { kind: kindName, name: rawName, written, skipped }
485
+ }
486
+
350
487
  /**
351
488
  * `mega g <kind> <name>` 실행 — 계획 → 쓰기 → 결과 반환.
352
489
  * @param {string} kind