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.
- package/README.md +9 -0
- package/bin/mega-ws-hub.js +2 -2
- package/package.json +33 -9
- package/sample/crud/.env +10 -1
- package/sample/crud/.env.example +10 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/locales/server/en.json +12 -1
- package/sample/crud/apps/main/locales/server/ko.json +12 -1
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +10 -2
- package/sample/crud/package.json +3 -3
- package/sample/crud/scripts/start-ws-hub.sh +20 -6
- package/sample/simple/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +44 -3
- package/src/adapters/file-adapter.js +9 -5
- package/src/adapters/file-session-adapter.js +4 -3
- package/src/adapters/maria-adapter.js +33 -7
- package/src/adapters/mega-cache-adapter.js +83 -6
- package/src/adapters/mega-db-adapter.js +10 -1
- package/src/adapters/mongo-adapter.js +40 -8
- package/src/adapters/postgres-adapter.js +33 -6
- package/src/adapters/redis-adapter.js +7 -3
- package/src/adapters/sqlite-adapter.js +26 -3
- package/src/cli/commands/console-cmd.js +3 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +173 -33
- package/src/cli/generators/index.js +140 -3
- package/src/cli/index.js +437 -155
- package/src/cli/watch.js +188 -0
- package/src/core/ajv-mapper.js +30 -3
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +65 -3
- package/src/core/envelope.js +119 -12
- package/src/core/hub-link.js +89 -18
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +7 -3
- package/src/core/mega-app.js +253 -505
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +131 -0
- package/src/core/router.js +70 -65
- package/src/core/scope-registry.js +1 -0
- package/src/core/security.js +70 -12
- package/src/core/session-store.js +14 -1
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +636 -0
- package/src/core/ws-roster.js +50 -8
- package/src/core/ws-upgrade.js +223 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-circuit-breaker.js +5 -3
- package/src/lib/mega-health.js +35 -4
- package/src/lib/mega-job-queue.js +151 -34
- package/src/lib/mega-job.js +37 -1
- package/src/lib/mega-metrics.js +31 -13
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +33 -6
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +139 -15
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/adr/code.tpl +23 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +93 -0
- package/types/adapters/file-adapter.d.ts +105 -0
- package/types/adapters/file-session-adapter.d.ts +103 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +117 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +73 -0
- package/types/adapters/mega-db-adapter.d.ts +50 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +150 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +141 -0
- package/types/adapters/redis-adapter.d.ts +78 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +112 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +122 -0
- package/types/cli/index.d.ts +234 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/cli/watch.d.ts +59 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +103 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +266 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +93 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +25 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +108 -0
- package/types/core/ws-upgrade.d.ts +260 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +243 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +48 -0
- package/types/lib/mega-job-queue.d.ts +188 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +145 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +129 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +259 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
package/src/cli/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* `mega` CLI 디스패처 —
|
|
3
|
+
* `mega` CLI 디스패처 — 전 빌트인 명령을 단일 commander program 으로 파싱한다(ADR-195, ADR-123 개정).
|
|
4
4
|
*
|
|
5
|
-
* 명령
|
|
6
|
-
*
|
|
7
|
-
* - `mega
|
|
8
|
-
* - `mega
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* 명령 표는 {@link buildProgram} 이 정본이다(도움말 자동 생성 — 수기 USAGE 드리프트 제거):
|
|
6
|
+
* - `mega start`(별칭 `serve`) / `worker` / `scheduler` / `migrate[:down|:status]` — 런타임 호스트.
|
|
7
|
+
* - `mega new` / `generate`(g) / `routes` / `test` / `console` — scaffold/dev(commands/scaffold.js).
|
|
8
|
+
* - `mega <plugin:command>` — 빌트인 표({@link BUILTIN_COMMANDS}) 밖 명령은 플러그인 dispatch 로
|
|
9
|
+
* fallthrough(ADR-123 계약 유지). `mega help` 가 프로젝트 플러그인 명령을 동적 병기한다.
|
|
10
|
+
*
|
|
11
|
+
* `runCli` 는 `process.exit` 를 부르지 않고 exit code 를 반환하며(테스트 가능), commander 가 아닌
|
|
12
|
+
* 예외(MegaConfigError·부팅 실패)는 bin 으로 전파해 정리 경로(MegaShutdown.now)를 태운다.
|
|
12
13
|
*
|
|
13
14
|
* worker/scheduler 의 **잡·스케줄 등록 소스 = `config.jobs` / `config.schedules`**(정적 배열) + 플러그인
|
|
14
15
|
* `host.listJobs()` / `host.listSchedules()`(동적)를 {@link collectRegistrations}(ADR-123)가 합친다 —
|
|
@@ -18,59 +19,35 @@
|
|
|
18
19
|
*
|
|
19
20
|
* @module cli/index
|
|
20
21
|
*/
|
|
22
|
+
// 무거운 런타임 그래프(boot→fastify·OTel·pino·ejs·보안 플러그인, 클러스터, 마이그레이션 러너)는
|
|
23
|
+
// 정적 import 하지 않는다 — `mega help`/`g`/`new` 같은 비부팅 명령이 전체 그래프를 평가해 4배
|
|
24
|
+
// 느려지는 것을 막기 위해, 각 런타임 명령의 action/호스트 함수 내부에서 dynamic import 한다.
|
|
25
|
+
// 모듈 레벨 싱글톤(adapter-manager·MegaShutdown 등)은 dynamic import 도 같은 인스턴스를 본다.
|
|
21
26
|
import { existsSync } from 'node:fs'
|
|
22
27
|
import { join } from 'node:path'
|
|
23
28
|
import os from 'node:os'
|
|
24
29
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
25
|
-
import { bootApp, prepareRuntime } from '../core/boot.js'
|
|
26
|
-
import { MegaCluster } from '../core/mega-cluster.js'
|
|
27
|
-
import { installPrimaryAggregator, installWorkerResponder } from '../core/cluster-metrics.js'
|
|
28
30
|
import { loadAndValidateConfig } from '../core/config-loader.js'
|
|
29
|
-
import { migrateUp, migrateDown, migrateStatus } from '../core/migration-runner.js'
|
|
30
31
|
import { buildFromGlobalConfig, connectAll, disconnectAll, get as getAdapter } from '../adapters/adapter-manager.js'
|
|
32
|
+
import { MegaDbAdapter } from '../adapters/mega-db-adapter.js'
|
|
31
33
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
32
34
|
import { MegaPluginHost, loadPlugins } from '../lib/mega-plugin.js'
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
mega migrate:status [--db KEY] 적용/미적용 마이그레이션 목록
|
|
50
|
-
mega new <project> [--force] sample/crud 데모앱(14기능) 전체를 스캐폴드
|
|
51
|
-
mega g <kind> <name> [--app A] [--version vN] 코드+테스트 생성 (별칭: generate)
|
|
52
|
-
[--kind K] [--lng L] [--force]
|
|
53
|
-
mega routes [--root DIR] 등록 라우트 트리 출력
|
|
54
|
-
mega test [--root DIR] [-- vitest args...] vitest 실행
|
|
55
|
-
mega console [--root DIR] 앱 컨텍스트 REPL
|
|
56
|
-
mega <plugin:command> [args...] 플러그인 등록 CLI 명령 실행
|
|
57
|
-
mega help 이 도움말
|
|
58
|
-
|
|
59
|
-
generator kinds:
|
|
60
|
-
app controller channel service model middleware route schedule job worker locale adapter migration
|
|
61
|
-
|
|
62
|
-
Options:
|
|
63
|
-
--root DIR 프로젝트 루트(기본: 현재 디렉토리)
|
|
64
|
-
--port N listen 포트(start, 기본: server/global 설정 또는 3000)
|
|
65
|
-
--host H listen 호스트(start)
|
|
66
|
-
--cluster X 워커 프로세스 수(start). 정수 N 또는 max(CPU 코어 수). 우선순위:
|
|
67
|
-
--cluster > MEGA_CLUSTER_WORKERS env > server.cluster config. 1/미지정=단일 프로세스.
|
|
68
|
-
--watch dev watch 모드(start). 파일 변경 시 자동 재시작 — node 내장 --watch 로 자신을 재실행한다
|
|
69
|
-
(nodemon 불요). 단일 프로세스 강제 + NODE_ENV 미설정 시 development. apps/·mega.config.js 감시.
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
/** 프로젝트 `.env` 를 로드하지 않는 명령 — 스캐폴드 생성(new/g)·도움말. 그 외 런타임/부팅 명령은 로드. */
|
|
73
|
-
const NO_ENV_COMMANDS = new Set(['new', 'g', 'generate', 'help'])
|
|
35
|
+
import { MegaShutdown, DEFAULT_HARD_KILL_MS } from '../lib/mega-shutdown.js'
|
|
36
|
+
import { Command } from 'commander'
|
|
37
|
+
import { registerScaffoldCommands, commanderErrorToExitCode } from './commands/scaffold.js'
|
|
38
|
+
|
|
39
|
+
/** 빌트인 명령 이름·별칭 전체 — 이 표 밖의 첫 positional 은 플러그인 명령 dispatch 로 fallthrough 한다
|
|
40
|
+
* (ADR-123 계약 유지). `buildProgram` 의 commander 등록과 동기를 유지할 것. */
|
|
41
|
+
export const BUILTIN_COMMANDS = new Set([
|
|
42
|
+
'start', 'serve', 'worker', 'scheduler',
|
|
43
|
+
'migrate', 'migrate:down', 'migrate:status', 'migrate:generate',
|
|
44
|
+
'new', 'generate', 'g', 'routes', 'test', 'console', 'help',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
/** 프로젝트 `.env` 를 로드하지 않는 명령(commander 정식 이름 기준 — 별칭 `g` 는 `generate` 로 해석됨).
|
|
48
|
+
* 스캐폴드 생성은 대상 프로젝트가 아직 없거나 무관하므로 제외(cwd 의 무관한 .env 로드 방지). `help` 는
|
|
49
|
+
* runCli 가 commander 진입 전에 처리한다. 그 외 런타임/부팅 명령은 로드(ADR-152). */
|
|
50
|
+
const NO_ENV_COMMANDS = new Set(['new', 'generate'])
|
|
74
51
|
|
|
75
52
|
/**
|
|
76
53
|
* 프로젝트 루트의 `.env` 를 `process.env` 로 로드한다(파일이 있을 때만). Node 내장
|
|
@@ -94,7 +71,8 @@ export function loadProjectEnv(projectRoot, logger) {
|
|
|
94
71
|
|
|
95
72
|
/**
|
|
96
73
|
* 인자 토큰을 `{ _: positionals[], flags: {} }` 로 파싱. `--key=value` / `--key value` / `--flag`(=true).
|
|
97
|
-
*
|
|
74
|
+
* commander 일원화(ADR-195) 이후에도 두 용도로 남는다 — ① runCli 의 사전 스캔(`--root`·명령어 판별,
|
|
75
|
+
* commander 진입 전), ② 플러그인 CLI 명령 핸들러 인자 계약(`handler({ _, flags })`, 03-api-spec §11).
|
|
98
76
|
* @param {string[]} argv - `process.argv.slice(2)` 형태.
|
|
99
77
|
* @returns {{ _: string[], flags: Record<string, string | boolean> }}
|
|
100
78
|
*/
|
|
@@ -206,6 +184,44 @@ export function resolveHost(setting) {
|
|
|
206
184
|
return setting
|
|
207
185
|
}
|
|
208
186
|
|
|
187
|
+
/**
|
|
188
|
+
* `--root` 플래그를 검증된 프로젝트 루트로 해석한다(fail-closed — {@link resolveHost} 와 동형, OQ-029②).
|
|
189
|
+
*
|
|
190
|
+
* 미지정은 cwd 폴백에 위임하려 `undefined`. 값 없는 베어 `--root`(=`true`)·빈 값 `--root=`(=`''`)은
|
|
191
|
+
* throw — 빈 루트를 통과시키면 `join('', '.env')`/`loadAndValidateConfig('')` 가 cwd 상대로 silent
|
|
192
|
+
* 오동작해 운영자가 의도한 루트 지정 실패를 모른다(P4/P7). 비어 있지 않은 문자열은 그대로.
|
|
193
|
+
*
|
|
194
|
+
* @param {string | boolean | undefined | null} setting - `--root` 플래그 값.
|
|
195
|
+
* @returns {string | undefined} 검증된 루트, 또는 미지정이면 `undefined`(cwd 폴백 위임).
|
|
196
|
+
* @throws {MegaConfigError} `config.invalid_root` — 값 없음/빈 값.
|
|
197
|
+
*/
|
|
198
|
+
export function resolveRoot(setting) {
|
|
199
|
+
if (setting === undefined || setting === null) return undefined
|
|
200
|
+
if (typeof setting !== 'string' || setting === '') {
|
|
201
|
+
throw new MegaConfigError('config.invalid_root', '--root requires a value (e.g. --root ./my-app).', { details: { setting } })
|
|
202
|
+
}
|
|
203
|
+
return setting
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `--db` 플래그를 검증된 마이그레이션 대상 키로 해석한다(fail-closed — {@link resolveRoot} 와 동형, OQ-029②).
|
|
208
|
+
*
|
|
209
|
+
* 미지정은 자동 선택(`resolveMigrationAdapter` 의 유일 db/primary 규칙)에 위임하려 `undefined`. 값 없는
|
|
210
|
+
* 베어 `--db`(=`true`)·빈 값 `--db=`(=`''`)은 throw — 빈 키가 자동 선택으로 silent 강등되면 운영자가
|
|
211
|
+
* 지정 실패를 모른 채 다른 db 에 마이그레이션을 적용할 수 있다(P4/P7).
|
|
212
|
+
*
|
|
213
|
+
* @param {string | boolean | undefined | null} setting - `--db` 플래그 값.
|
|
214
|
+
* @returns {string | undefined} 검증된 db 키, 또는 미지정이면 `undefined`(자동 선택 위임).
|
|
215
|
+
* @throws {MegaConfigError} `config.invalid_db` — 값 없음/빈 값.
|
|
216
|
+
*/
|
|
217
|
+
export function resolveDb(setting) {
|
|
218
|
+
if (setting === undefined || setting === null) return undefined
|
|
219
|
+
if (typeof setting !== 'string' || setting === '') {
|
|
220
|
+
throw new MegaConfigError('config.invalid_db', '--db requires a value (e.g. --db primary).', { details: { setting } })
|
|
221
|
+
}
|
|
222
|
+
return setting
|
|
223
|
+
}
|
|
224
|
+
|
|
209
225
|
/**
|
|
210
226
|
* `mega start --watch` 가 자신을 `node --watch` 로 재실행해야 하는지 (ADR-182). `--watch` 플래그가 있고
|
|
211
227
|
* 아직 watch 하에 재실행되지 않았을 때만 true — 가드 env(`MEGA_WATCH_REEXEC`)로 무한재귀를 막는다.
|
|
@@ -218,37 +234,35 @@ export function shouldReexecForWatch(flags, env) {
|
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
/**
|
|
221
|
-
* `mega start --watch`
|
|
237
|
+
* `mega start --watch` supervisor 의 **자식 인자**를 빌드한다 (ADR-220, ADR-182 개정).
|
|
222
238
|
*
|
|
223
|
-
* - `--watch`
|
|
239
|
+
* - `--watch`/`--watch-ignore`(값 포함)는 부모(supervisor) 전용 — 자식 인자에서 제거한다
|
|
240
|
+
* (가드 env 와 이중 안전으로 무한재귀 방지).
|
|
224
241
|
* - **단일 프로세스 강제**(`--cluster 1`) — 멀티워커 watch 카오스 방지(기존 `--cluster` 값은 무시).
|
|
225
|
-
* - watchPaths 가 있으면 `--watch-path=…`(이게 모듈 그래프 watch 를 **대체**하므로 앱 소스·전역설정 경로를
|
|
226
|
-
* 명시해야 한다 — 실측 확인). 없으면 plain `--watch`(모듈 그래프 폴백).
|
|
227
242
|
*
|
|
228
|
-
* @param {
|
|
229
|
-
* @
|
|
230
|
-
* @param {string[]} o.watchPaths - 감시할 절대경로(존재하는 것만).
|
|
231
|
-
* @param {string} o.selfPath - mega 진입 스크립트(`process.argv[1]`).
|
|
232
|
-
* @param {string} o.execPath - node 바이너리(`process.execPath`).
|
|
233
|
-
* @returns {{ command: string, args: string[] }}
|
|
243
|
+
* @param {string[]} argv - 원본 mega argv(예: `['start','--watch','--port','3000']`).
|
|
244
|
+
* @returns {string[]} 자식 `mega` 인자(예: `['start','--port','3000','--cluster','1']`).
|
|
234
245
|
*/
|
|
235
|
-
export function
|
|
236
|
-
|
|
237
|
-
Array.isArray(watchPaths) && watchPaths.length > 0 ? watchPaths.map((p) => `--watch-path=${p}`) : ['--watch']
|
|
238
|
-
/** @type {string[]} mega 인자 — `--watch` 제거 + `--cluster`(값 포함) 제거. */
|
|
246
|
+
export function buildWatchChildArgs(argv) {
|
|
247
|
+
/** @type {string[]} */
|
|
239
248
|
const megaArgs = []
|
|
240
249
|
for (let i = 0; i < argv.length; i++) {
|
|
241
250
|
const tok = argv[i]
|
|
242
251
|
if (tok === '--watch' || tok.startsWith('--watch=')) continue
|
|
243
|
-
if (tok === '--
|
|
252
|
+
if (tok === '--watch-ignore') {
|
|
244
253
|
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++ // 값 토큰도 건너뜀.
|
|
245
254
|
continue
|
|
246
255
|
}
|
|
256
|
+
if (tok.startsWith('--watch-ignore=')) continue
|
|
257
|
+
if (tok === '--cluster') {
|
|
258
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) i++
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
247
261
|
if (tok.startsWith('--cluster=')) continue
|
|
248
262
|
megaArgs.push(tok)
|
|
249
263
|
}
|
|
250
264
|
megaArgs.push('--cluster', '1') // 단일 프로세스 강제.
|
|
251
|
-
return
|
|
265
|
+
return megaArgs
|
|
252
266
|
}
|
|
253
267
|
|
|
254
268
|
/**
|
|
@@ -268,58 +282,106 @@ export function buildWatchCommand({ argv, watchPaths, selfPath, execPath }) {
|
|
|
268
282
|
export async function runCli(argv, { out = console.log, err = console.error, cwd, logger, spawn = nodeSpawn, selfPath = process.argv[1] } = {}) {
|
|
269
283
|
const { _, flags } = parseArgs(argv)
|
|
270
284
|
const command = _[0]
|
|
271
|
-
|
|
285
|
+
// `--root` 는 fail-closed 검증(OQ-029② — 베어/빈 값은 throw, 미지정만 cwd 폴백).
|
|
286
|
+
const projectRoot = resolveRoot(flags.root) ?? cwd ?? process.cwd()
|
|
272
287
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
288
|
+
// 도움말 — commander 자동 생성(USAGE) + 프로젝트 플러그인 명령 동적 병기(ADR-195). 명령 없이
|
|
289
|
+
// 호출(`mega`)은 도움말 + 실패 코드(쉘 스크립트 친화), 명시적 help/`--help` 는 0.
|
|
290
|
+
// `mega help <command>`(인자 있는 help)·`mega <command> --help` 는 commander 가 명령별 도움말을 낸다.
|
|
291
|
+
if (command === undefined || (command === 'help' && _.length === 1)) {
|
|
292
|
+
out(USAGE)
|
|
293
|
+
await appendPluginCommandsHelp(projectRoot, out, logger)
|
|
294
|
+
return command === 'help' || flags.help === true ? 0 : 1
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 플러그인 등록 CLI 명령 fallthrough(예: `mega sample:ping`) — 빌트인 표 밖의 명령만(ADR-123 계약).
|
|
298
|
+
// 이름 컨벤션은 `<plugin>:<verb>` 이지만 강제는 아니다 — host 에 등록된 임의 이름을 그대로 조회한다.
|
|
299
|
+
if (!BUILTIN_COMMANDS.has(command)) {
|
|
276
300
|
loadProjectEnv(projectRoot, logger)
|
|
301
|
+
/** @type {{ found: boolean, code: number }} */
|
|
302
|
+
let dispatched
|
|
303
|
+
try {
|
|
304
|
+
dispatched = await dispatchPluginCommand(projectRoot, command, { _: _.slice(1), flags }, logger)
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// config 부재/오류(프로젝트 밖 등)로 플러그인 확인 자체가 불가 — unknown command 에 원인을 병기해
|
|
307
|
+
// config 에러로 오도되지 않게 보고한다(F6 audit §2.2-⑥).
|
|
308
|
+
err(`mega: unknown command '${command}' (플러그인 명령 확인 실패: ${/** @type {any} */ (e).message ?? e}).\n`)
|
|
309
|
+
err(USAGE)
|
|
310
|
+
return 1
|
|
311
|
+
}
|
|
312
|
+
if (dispatched.found) return dispatched.code
|
|
313
|
+
err(`mega: unknown command '${command}'.\n`)
|
|
314
|
+
err(USAGE)
|
|
315
|
+
return 1
|
|
277
316
|
}
|
|
278
317
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
318
|
+
// 빌트인 명령 — 단일 commander program 으로 파싱·디스패치(ADR-195). action 은 setExit 로 종료 코드를
|
|
319
|
+
// 보고하고, commander 가 아닌 예외(MegaConfigError·부팅 실패 등)는 그대로 전파해 bin 의 정리 경로
|
|
320
|
+
// (MegaShutdown.now)를 태운다.
|
|
321
|
+
let exitCode = 0
|
|
322
|
+
const program = buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, setExit: (c) => (exitCode = c) })
|
|
323
|
+
try {
|
|
324
|
+
await program.parseAsync(argv, { from: 'user' })
|
|
325
|
+
return exitCode
|
|
326
|
+
} catch (e) {
|
|
327
|
+
return commanderErrorToExitCode(e, err, { rethrowUnknown: true })
|
|
283
328
|
}
|
|
329
|
+
}
|
|
284
330
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
331
|
+
/**
|
|
332
|
+
* `mega start`/`serve` 본체 — dev watch 재실행 또는 부팅+listen(단일/클러스터).
|
|
333
|
+
* @param {{ port?: string, host?: string, cluster?: string | boolean, watch?: boolean, watchIgnore?: string }} opts - commander 옵션.
|
|
334
|
+
* @param {{ argv: string[], out: (msg: string) => void, projectRoot: string, logger?: { debug?: Function, info?: Function, warn?: Function }, spawn: Function, selfPath: string }} ctx
|
|
335
|
+
* @returns {Promise<number>} exit code.
|
|
336
|
+
*/
|
|
337
|
+
async function runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }) {
|
|
338
|
+
{
|
|
339
|
+
// dev watch (ADR-220, ADR-182 개정) — `--watch` 면 자체 supervisor 가 자식(`mega start`)을 띄우고
|
|
340
|
+
// fs.watch(recursive) + ignore 패턴 + debounce 로 재시작을 조율한다. node 내장 --watch 는 ignore 가
|
|
341
|
+
// 없어 런타임 산출물(locales saveMissing·업로드·journal)이 재시작 루프를 만들었다. 단일 프로세스
|
|
342
|
+
// 강제 + NODE_ENV 미설정 시 development 기본 + 가드 env 로 무한재귀 방지는 기존과 동일.
|
|
343
|
+
if (shouldReexecForWatch({ watch: opts.watch === true }, process.env)) {
|
|
344
|
+
const { mergeWatchIgnore, startWatchSupervisor, defaultWatchPaths } = await import('./watch.js')
|
|
345
|
+
// config 의 watch.ignore 는 best-effort — dev 루프는 config 가 깨진 동안에도 살아 있어야
|
|
346
|
+
// 하므로(고치면 재시작) 로드 실패 시 디폴트 ignore 로 진행하고 이유를 알린다.
|
|
347
|
+
/** @type {{ ignore?: unknown, ignoreDefaults?: unknown }} */
|
|
348
|
+
let watchConfig = {}
|
|
349
|
+
try {
|
|
350
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
351
|
+
watchConfig = /** @type {any} */ (global).watch ?? {}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
out(`mega: watch — mega.config.js 를 읽지 못해 디폴트 ignore 로 감시합니다(${/** @type {any} */ (e).message?.split('\n')[0] ?? e}).`)
|
|
354
|
+
}
|
|
355
|
+
const ignore = mergeWatchIgnore(watchConfig, typeof opts.watchIgnore === 'string' ? opts.watchIgnore : undefined)
|
|
356
|
+
const watchPaths = defaultWatchPaths(projectRoot)
|
|
357
|
+
const childArgs = buildWatchChildArgs(argv)
|
|
358
|
+
out(`mega: watch mode (single process) — watching [${watchPaths.join(', ')}], ignore ${ignore.length} patterns`)
|
|
359
|
+
if (ignore.length === 0) {
|
|
360
|
+
// 숨은 디폴트가 없으므로(ADR-220) 미설정은 "모든 변경이 재시작" — locales(saveMissing 자동
|
|
361
|
+
// 기입)·업로드 같은 런타임 쓰기 영역이 있으면 루프가 날 수 있어 가시화한다(P4).
|
|
362
|
+
out('mega: watch ignore 가 비어 있습니다 — locales/업로드 등 런타임 쓰기 폴더가 있으면 .env 의 WATCH_IGNORE(또는 config watch.ignore) 설정을 권장합니다.')
|
|
305
363
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
364
|
+
return await startWatchSupervisor({
|
|
365
|
+
projectRoot,
|
|
366
|
+
watchPaths,
|
|
367
|
+
ignore,
|
|
368
|
+
out,
|
|
369
|
+
spawnChild: () =>
|
|
370
|
+
spawn(process.execPath, [selfPath, ...childArgs], {
|
|
371
|
+
stdio: 'inherit',
|
|
372
|
+
env: { ...process.env, MEGA_WATCH_REEXEC: '1', NODE_ENV: process.env.NODE_ENV ?? 'development' },
|
|
373
|
+
}),
|
|
312
374
|
})
|
|
313
375
|
}
|
|
314
376
|
|
|
315
377
|
// listen 포트/호스트는 부팅(어댑터 connect) 전에 fail-closed 검증한다 — 잘못된 값을 늦게(listen 시점)
|
|
316
378
|
// cryptic 에러로 만나거나 silent 강등(특권/랜덤 포트)하지 않도록(per ADR-167 후속).
|
|
317
|
-
const port = resolvePort(
|
|
318
|
-
const host = resolveHost(
|
|
379
|
+
const port = resolvePort(opts.port)
|
|
380
|
+
const host = resolveHost(opts.host)
|
|
319
381
|
|
|
320
382
|
// cluster 워커 수 해석(ADR-154) — `--cluster` 플래그 > `MEGA_CLUSTER_WORKERS` env > `server.cluster`
|
|
321
383
|
// config 순. 앞선 소스가 정해지면 config 로드를 생략한다(env/플래그만으로 동작 — port 패턴과 동일).
|
|
322
|
-
let clusterSetting =
|
|
384
|
+
let clusterSetting = opts.cluster ?? process.env.MEGA_CLUSTER_WORKERS
|
|
323
385
|
if (clusterSetting === undefined) {
|
|
324
386
|
const { global } = await loadAndValidateConfig(projectRoot)
|
|
325
387
|
clusterSetting = /** @type {any} */ (global).server?.cluster
|
|
@@ -333,10 +395,20 @@ export async function runCli(argv, { out = console.log, err = console.error, cwd
|
|
|
333
395
|
else out(`mega: ${msg}`)
|
|
334
396
|
}
|
|
335
397
|
|
|
398
|
+
// 런타임 그래프 lazy 로드 — 부팅 명령에서만 프레임워크 전체를 지불한다(모듈 상단 주석).
|
|
399
|
+
const [{ bootApp }, { MegaCluster }, { installPrimaryAggregator, installWorkerResponder }, { buildLogger }] =
|
|
400
|
+
await Promise.all([
|
|
401
|
+
import('../core/boot.js'),
|
|
402
|
+
import('../core/mega-cluster.js'),
|
|
403
|
+
import('../core/cluster-metrics.js'),
|
|
404
|
+
import('../lib/mega-logger.js'),
|
|
405
|
+
])
|
|
406
|
+
|
|
336
407
|
// 워커 부팅 함수 — 단일/클러스터 모드 공통. 클러스터에선 각 워커 프로세스가 이 함수를 실행한다.
|
|
337
408
|
const bootListen = async () => {
|
|
338
409
|
const { server, appLogger } = await bootApp(projectRoot, { listen: true, port, host, logger })
|
|
339
|
-
|
|
410
|
+
// 'server' stage — HTTP/WS 수용을 가장 먼저 끊는다(진행 중 요청·WS drain 포함).
|
|
411
|
+
MegaShutdown.register('mega-cli:server', async () => server.close(), { stage: 'server' })
|
|
340
412
|
MegaShutdown.setupSignals()
|
|
341
413
|
// 클러스터 워커면 메트릭 collect 응답기 설치(ADR-163) — 마스터의 집계 요청에 자기 메트릭 회신.
|
|
342
414
|
// 단일 프로세스면 no-op(cluster.isWorker=false).
|
|
@@ -353,7 +425,9 @@ export async function runCli(argv, { out = console.log, err = console.error, cwd
|
|
|
353
425
|
|
|
354
426
|
// 클러스터 모드 — 마스터는 워커 N개 fork·respawn·graceful 협응(MegaCluster), 각 워커가 bootApp+listen.
|
|
355
427
|
// Node cluster 가 마스터의 공유 listen 소켓을 워커들에 분배하므로 SO_REUSEPORT 불필요(ADR-030/154).
|
|
356
|
-
|
|
428
|
+
// 마스터 grace 는 워커 shutdown 예산 상한(MegaShutdown hardKill 60s) + 마진으로 위계화한다 —
|
|
429
|
+
// 마스터 기본값(30s)을 그대로 쓰면 워커가 hook(어댑터 disconnect 등)을 다 돌기 전에 SIGKILL 된다.
|
|
430
|
+
const mega = new MegaCluster({ instances: workers, gracePeriodMs: DEFAULT_HARD_KILL_MS + 5_000 })
|
|
357
431
|
// 마스터 조율 로그(SIGINT 수신·워커 종료/respawn 등)를 pino 로(ADR-180). 마스터는 bootApp 을 안 해
|
|
358
432
|
// pino 인스턴스가 없으므로 config 로 직접 만든다. 워커는 bootApp 의 appLogger 를 쓰니 primary 에서만
|
|
359
433
|
// 만들어(워커당 중복 transport worker 회피) start 전에 주입한다.
|
|
@@ -374,42 +448,187 @@ export async function runCli(argv, { out = console.log, err = console.error, cwd
|
|
|
374
448
|
}
|
|
375
449
|
return 0
|
|
376
450
|
}
|
|
451
|
+
}
|
|
377
452
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
453
|
+
/**
|
|
454
|
+
* 빌트인 명령 전체를 담은 commander program 을 만든다(ADR-195 — 파서 단일화의 정본).
|
|
455
|
+
* {@link BUILTIN_COMMANDS} 와 동기 유지. action 은 `process.exit` 없이 `setExit(code)` 로 보고한다.
|
|
456
|
+
*
|
|
457
|
+
* @param {object} deps
|
|
458
|
+
* @param {string[]} deps.argv - 원본 argv(`--watch` 재실행 명령 빌드용).
|
|
459
|
+
* @param {(msg: string) => void} deps.out
|
|
460
|
+
* @param {(msg: string) => void} deps.err
|
|
461
|
+
* @param {string} deps.projectRoot - 이미 `--root` 해석된 기준 디렉토리.
|
|
462
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
|
|
463
|
+
* @param {Function} deps.spawn
|
|
464
|
+
* @param {string} deps.selfPath
|
|
465
|
+
* @param {(code: number) => void} deps.setExit
|
|
466
|
+
* @returns {import('commander').Command}
|
|
467
|
+
*/
|
|
468
|
+
function buildProgram({ argv, out, err, projectRoot, logger, spawn, selfPath, setExit }) {
|
|
469
|
+
const program = new Command()
|
|
470
|
+
program
|
|
471
|
+
.name('mega')
|
|
472
|
+
.description('MEGA-FRAMEWORK CLI')
|
|
473
|
+
.exitOverride()
|
|
474
|
+
// 도움말 폭 고정 — 터미널 폭에 따라 줄바꿈이 달라지면 USAGE 가 비결정적이 된다(테스트·문서 안정).
|
|
475
|
+
.configureHelp({ helpWidth: 100 })
|
|
476
|
+
.configureOutput({
|
|
477
|
+
writeOut: (s) => out(s.replace(/\n+$/, '')),
|
|
478
|
+
writeErr: (s) => err(s.replace(/\n+$/, '')),
|
|
479
|
+
})
|
|
480
|
+
.option('--root <dir>', '프로젝트 루트(기본: 현재 디렉토리)')
|
|
383
481
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
482
|
+
// 프로젝트 `.env` 자동 로드(ADR-152) — 스캐폴드 생성(new/generate)만 제외(NO_ENV_COMMANDS).
|
|
483
|
+
// preAction 시점이라 각 명령의 config 로드보다 항상 먼저다.
|
|
484
|
+
program.hook('preAction', (_thisCmd, actionCmd) => {
|
|
485
|
+
if (!NO_ENV_COMMANDS.has(actionCmd.name())) loadProjectEnv(projectRoot, logger)
|
|
486
|
+
})
|
|
389
487
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
488
|
+
program
|
|
489
|
+
.command('start')
|
|
490
|
+
.alias('serve')
|
|
491
|
+
.description('앱 부팅 + HTTP listen')
|
|
492
|
+
.option('--port <n>', 'listen 포트(기본: server/global 설정 또는 3000)')
|
|
493
|
+
.option('--host <h>', 'listen 호스트')
|
|
494
|
+
.option(
|
|
495
|
+
'--cluster [n]',
|
|
496
|
+
"워커 프로세스 수 — 정수 N 또는 'max'(CPU 코어 수, 베어 플래그도 max). 우선순위: 플래그 > MEGA_CLUSTER_WORKERS env > server.cluster config. 1/미지정 = 단일 프로세스",
|
|
497
|
+
)
|
|
498
|
+
.option('--watch', 'dev watch — 파일 변경 시 자동 재시작(단일 프로세스 강제). locales/views/public/uploads/var/.mega/.env* 등은 디폴트 ignore')
|
|
499
|
+
.option('--watch-ignore <globs>', 'watch 추가 ignore(콤마 구분 glob, 디폴트에 더해짐). config watch.ignore 와 병합')
|
|
500
|
+
.action(async (/** @type {any} */ opts) => {
|
|
501
|
+
setExit(await runStart(opts, { argv, out, projectRoot, logger, spawn, selfPath }))
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
program
|
|
505
|
+
.command('worker')
|
|
506
|
+
.description('잡 소비 워커 런타임 호스트')
|
|
507
|
+
.action(async () => {
|
|
508
|
+
await runWorkerHost(projectRoot, logger)
|
|
509
|
+
out('mega: worker started')
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
program
|
|
513
|
+
.command('scheduler')
|
|
514
|
+
.description('분산 스케줄러 호스트')
|
|
515
|
+
.action(async () => {
|
|
516
|
+
await runSchedulerHost(projectRoot, logger)
|
|
517
|
+
out('mega: scheduler started')
|
|
518
|
+
})
|
|
398
519
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
520
|
+
// 마이그레이션 러너(ADR-149) — 일회성 호스트 3종. `--db` 는 fail-closed 해석(resolveDb).
|
|
521
|
+
const migrateAction = (/** @type {'up'|'down'|'status'} */ direction) =>
|
|
522
|
+
async (/** @type {any} */ opts) => {
|
|
523
|
+
await runMigrateHost(projectRoot, { direction, dbKey: resolveDb(opts.db), logger, out })
|
|
524
|
+
}
|
|
525
|
+
program
|
|
526
|
+
.command('migrate')
|
|
527
|
+
.description('pending 마이그레이션 일괄 적용(up)')
|
|
528
|
+
.option('--db <key>', '대상 db(services.databases 키, 기본: 유일 선언 db 또는 primary)')
|
|
529
|
+
.action(migrateAction('up'))
|
|
530
|
+
program
|
|
531
|
+
.command('migrate:down')
|
|
532
|
+
.description('마지막 적용 마이그레이션 1개 롤백(down)')
|
|
533
|
+
.option('--db <key>', '대상 db')
|
|
534
|
+
.action(migrateAction('down'))
|
|
535
|
+
program
|
|
536
|
+
.command('migrate:status')
|
|
537
|
+
.description('적용/미적용 마이그레이션 목록')
|
|
538
|
+
.option('--db <key>', '대상 db')
|
|
539
|
+
.action(migrateAction('status'))
|
|
540
|
+
// 자동 생성(ADR-204) — journal diff 기반. DB 연결 불요, 적용은 기존 `mega migrate` 가 담당.
|
|
541
|
+
program
|
|
542
|
+
.command('migrate:generate')
|
|
543
|
+
.description('모델 static schema ↔ journal snapshot diff 로 마이그레이션 파일 자동 생성')
|
|
544
|
+
.argument('[name]', '마이그레이션 이름(kebab-case, 기본: 변경 내용에서 자동 추론)')
|
|
545
|
+
.option('--renames <map>', '비인터랙티브 rename 매핑 — "old:new[,old2:new2]" (후보 불일치 시 fail-fast)')
|
|
546
|
+
.option('--dry-run', 'SQL 출력만(파일·journal 변경 없음)')
|
|
547
|
+
.option('--check', 'CI 드리프트 게이트 — 변경 있으면 exit 1, 파일 생성 없음')
|
|
548
|
+
.option('--concurrent', '인덱스 전용 생성을 온라인(no-tx)으로 — postgres CONCURRENTLY / maria ALGORITHM=INPLACE,LOCK=NONE / mongo 는 4.2+ 항상 online(표기); sqlite 미지원(거부)')
|
|
549
|
+
.option('--allow-destroy-drops', '파괴적 산출의 명시 옵트인 — 비인터랙티브 rename 후보의 drop+add 진행 + 모델 제거/table 변경의 테이블(컬렉션) 전체 DROP(ADR-210 게이트)')
|
|
550
|
+
.option('--adapter <key>', '특정 adapter(services.databases 키)만 처리(기본: 전부)')
|
|
551
|
+
.option('--app <name>', '마이그레이션 파일을 둘 앱(기본: main)', 'main')
|
|
552
|
+
.action(async (/** @type {string | undefined} */ name, /** @type {any} */ opts) => {
|
|
553
|
+
const { runMigrateGenerate } = await import('../core/migration/generate.js')
|
|
554
|
+
const r = await runMigrateGenerate(projectRoot, {
|
|
555
|
+
name,
|
|
556
|
+
renames: opts.renames,
|
|
557
|
+
dryRun: opts.dryRun === true,
|
|
558
|
+
check: opts.check === true,
|
|
559
|
+
concurrent: opts.concurrent === true,
|
|
560
|
+
allowDestroyDrops: opts.allowDestroyDrops === true,
|
|
561
|
+
adapter: opts.adapter,
|
|
562
|
+
app: opts.app,
|
|
563
|
+
out,
|
|
564
|
+
logger,
|
|
565
|
+
})
|
|
566
|
+
// --check 드리프트 발견은 CI 게이트용 비정상 종료("generate 안 돌림" 검출).
|
|
567
|
+
if (opts.check === true && r.changed) setExit(1)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
// scaffold/dev 명령군(new/generate/routes/test/console) — 정의는 commands/scaffold.js 가 정본.
|
|
571
|
+
// 빌트인이 아닌 generator kind 는 플러그인 scaffold manifest 를 조회한다(`mega.scaffold.register`,
|
|
572
|
+
// 03-api-spec §11). 이 폴백 경로에 한해 `.env` 를 로드한다 — 플러그인은 대상 프로젝트의 config 에
|
|
573
|
+
// 선언되므로(NO_ENV_COMMANDS 의 "프로젝트 무관" 전제가 깨짐) process.env 참조가 채워져 있어야 한다.
|
|
574
|
+
const resolvePluginGenerator = async (/** @type {string} */ kind) => {
|
|
575
|
+
loadProjectEnv(projectRoot, logger)
|
|
576
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
577
|
+
const host = new MegaPluginHost({ logger })
|
|
578
|
+
await loadPlugins(/** @type {any} */ (global).plugins, host, { projectRoot, logger })
|
|
579
|
+
return host.getGenerator(kind)
|
|
403
580
|
}
|
|
581
|
+
registerScaffoldCommands(program, { out, projectRoot, logger, resolvePluginGenerator, setExit })
|
|
404
582
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const dispatched = await dispatchPluginCommand(projectRoot, command, { _: _.slice(1), flags }, logger)
|
|
408
|
-
if (dispatched.found) return dispatched.code
|
|
583
|
+
return program
|
|
584
|
+
}
|
|
409
585
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
586
|
+
/** 도움말 생성용 no-op deps — action 은 호출되지 않으므로 안전. @returns {Parameters<typeof buildProgram>[0]} */
|
|
587
|
+
function helpProgramDeps() {
|
|
588
|
+
const noop = () => {}
|
|
589
|
+
return { argv: [], out: noop, err: noop, projectRoot: '', logger: undefined, spawn: nodeSpawn, selfPath: '', setExit: noop }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** CLI 사용법 텍스트 — commander 자동 생성(ADR-195) + 플러그인 명령 안내. 명령·옵션 정의가 곧 도움말의
|
|
593
|
+
* 단일 정본이라 수기 USAGE 와의 드리프트가 없다. */
|
|
594
|
+
export const USAGE = `${buildProgram(helpProgramDeps()).helpInformation()}
|
|
595
|
+
플러그인 명령:
|
|
596
|
+
mega <plugin:command> [args...] mega.config.js 의 plugins 가 등록한 CLI 명령 실행 (mega help 가 프로젝트 안에서 목록 병기)`
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* `mega help` 에 프로젝트 플러그인 등록물(CLI 명령 + scaffold generator manifest)을 동적으로 병기한다
|
|
600
|
+
* (ADR-195/199). 프로젝트 밖/config 오류면 병기만 생략 — help 는 어디서든 떠야 하므로 비치명적이다
|
|
601
|
+
* (P4: debug 로그로 가시화).
|
|
602
|
+
* @param {string} projectRoot
|
|
603
|
+
* @param {(msg: string) => void} out
|
|
604
|
+
* @param {{ debug?: Function }} [logger]
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
async function appendPluginCommandsHelp(projectRoot, out, logger) {
|
|
608
|
+
try {
|
|
609
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
610
|
+
const host = new MegaPluginHost({ logger })
|
|
611
|
+
await loadPlugins(/** @type {any} */ (global).plugins, host, { projectRoot, logger })
|
|
612
|
+
const names = host.listCommands()
|
|
613
|
+
if (names.length > 0) {
|
|
614
|
+
out('\n등록된 플러그인 명령:')
|
|
615
|
+
for (const n of names) {
|
|
616
|
+
const desc = host.getCommand(n)?.description ?? ''
|
|
617
|
+
out(` mega ${n}${desc ? ` ${desc}` : ''}`)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const generators = host.listGenerators()
|
|
621
|
+
if (generators.length > 0) {
|
|
622
|
+
out('\n등록된 플러그인 generator:')
|
|
623
|
+
for (const g of generators) {
|
|
624
|
+
const desc = host.getGenerator(g)?.description ?? ''
|
|
625
|
+
out(` mega g ${g} <name>${desc ? ` ${desc}` : ''}`)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// help 는 프로젝트 밖(config 부재)에서도 떠야 한다 — 플러그인 목록 병기만 생략(비치명적).
|
|
630
|
+
logger?.debug?.({ err: e }, 'cli.help plugin listing skipped')
|
|
631
|
+
}
|
|
413
632
|
}
|
|
414
633
|
|
|
415
634
|
/**
|
|
@@ -455,18 +674,19 @@ export function collectRegistrations(global, host, kind) {
|
|
|
455
674
|
* 주입(테스트)이 있으면 그쪽을 그대로 쓰고 pino 는 만들지 않는다(side-effect 회피).
|
|
456
675
|
* @param {Object} global - global config(`logger` 보유 가능).
|
|
457
676
|
* @param {{ info?: Function, warn?: Function } | undefined} injectedLogger - 주입 로거(테스트) 또는 undefined.
|
|
458
|
-
* @returns {{ info?: Function, warn?: Function } | null} 호스트 로그에 쓸 로거(없으면 null).
|
|
677
|
+
* @returns {Promise<{ info?: Function, warn?: Function } | null>} 호스트 로그에 쓸 로거(없으면 null).
|
|
459
678
|
*/
|
|
460
|
-
function wireHostLogger(global, injectedLogger) {
|
|
679
|
+
async function wireHostLogger(global, injectedLogger) {
|
|
461
680
|
if (injectedLogger) return injectedLogger
|
|
681
|
+
const { buildLogger } = await import('../lib/mega-logger.js') // pino 그래프 — 호스트 경로에서만 로드.
|
|
462
682
|
const appLogger = buildLogger(/** @type {any} */ (global).logger)
|
|
463
683
|
if (appLogger) {
|
|
464
|
-
// 종료 시퀀스 로그를 pino 로(ADR-178) + graceful 시 로그 버퍼 flush.
|
|
465
|
-
//
|
|
684
|
+
// 종료 시퀀스 로그를 pino 로(ADR-178) + graceful 시 로그 버퍼 flush. 'logs' stage 라 종료
|
|
685
|
+
// 시퀀스의 마지막에 drain 된다(다른 stage 의 hook 들이 남긴 로그까지 포함).
|
|
466
686
|
MegaShutdown.setLogger(appLogger)
|
|
467
687
|
MegaShutdown.register('mega-logger', async () => {
|
|
468
688
|
await new Promise((resolve) => appLogger.flush(() => resolve(undefined)))
|
|
469
|
-
})
|
|
689
|
+
}, { stage: 'logs' })
|
|
470
690
|
}
|
|
471
691
|
return appLogger
|
|
472
692
|
}
|
|
@@ -476,12 +696,17 @@ function wireHostLogger(global, injectedLogger) {
|
|
|
476
696
|
* 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
|
|
477
697
|
* @param {string} projectRoot
|
|
478
698
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
479
|
-
* @returns {Promise<MegaJobWorker>}
|
|
699
|
+
* @returns {Promise<import('../lib/mega-job-worker.js').MegaJobWorker>}
|
|
480
700
|
*/
|
|
481
701
|
export async function runWorkerHost(projectRoot, logger) {
|
|
482
|
-
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx).
|
|
702
|
+
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx). 런타임 그래프는 lazy.
|
|
703
|
+
const [{ prepareRuntime }, { MegaJobWorker }, MegaMetrics] = await Promise.all([
|
|
704
|
+
import('../core/boot.js'),
|
|
705
|
+
import('../lib/mega-job-worker.js'),
|
|
706
|
+
import('../lib/mega-metrics.js'),
|
|
707
|
+
])
|
|
483
708
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
484
|
-
const log = wireHostLogger(global, logger)
|
|
709
|
+
const log = await wireHostLogger(global, logger)
|
|
485
710
|
const worker = new MegaJobWorker({ ctx })
|
|
486
711
|
// 잡 메트릭 (ADR-132) — prepareRuntime 이 health.exposeMetrics 시 이미 MegaMetrics.init 했고,
|
|
487
712
|
// 옵트인 OFF 면 subscribeJobs 는 no-op() 을 반환한다. start 전에 구독해 enqueue(dispatch)부터 잡는다. 구독은
|
|
@@ -493,9 +718,10 @@ export async function runWorkerHost(projectRoot, logger) {
|
|
|
493
718
|
for (const JobClass of jobs) worker.register(/** @type {any} */ (JobClass))
|
|
494
719
|
log?.info?.({ count: jobs.length }, 'worker.jobs registered')
|
|
495
720
|
await worker.start()
|
|
721
|
+
// 'jobs' stage — 새 잡 수신을 멈추고 진행 중 잡을 완료시킨 뒤 어댑터가 끊기게 한다.
|
|
496
722
|
MegaShutdown.register('mega-worker', async () => {
|
|
497
723
|
await worker.stop()
|
|
498
|
-
})
|
|
724
|
+
}, { stage: 'jobs' })
|
|
499
725
|
MegaShutdown.setupSignals()
|
|
500
726
|
return worker
|
|
501
727
|
}
|
|
@@ -504,11 +730,15 @@ export async function runWorkerHost(projectRoot, logger) {
|
|
|
504
730
|
* `mega scheduler` 호스트 골격 — config + 어댑터 connect + ctx + `MegaScheduler` 인스턴스 + graceful.
|
|
505
731
|
* @param {string} projectRoot
|
|
506
732
|
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
507
|
-
* @returns {Promise<MegaScheduler>}
|
|
733
|
+
* @returns {Promise<import('../lib/mega-schedule.js').MegaScheduler>}
|
|
508
734
|
*/
|
|
509
735
|
export async function runSchedulerHost(projectRoot, logger) {
|
|
736
|
+
const [{ prepareRuntime }, { MegaScheduler }] = await Promise.all([
|
|
737
|
+
import('../core/boot.js'),
|
|
738
|
+
import('../lib/mega-schedule.js'),
|
|
739
|
+
])
|
|
510
740
|
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
511
|
-
const log = wireHostLogger(global, logger)
|
|
741
|
+
const log = await wireHostLogger(global, logger)
|
|
512
742
|
const scheduler = new MegaScheduler({ ctx })
|
|
513
743
|
// 등록 소스(ADR-123) = config.schedules(정적) + 플러그인 host.listSchedules()(동적). register 가
|
|
514
744
|
// cron 미선언·중복을 부팅 시 fail-fast.
|
|
@@ -516,9 +746,10 @@ export async function runSchedulerHost(projectRoot, logger) {
|
|
|
516
746
|
for (const TaskClass of schedules) scheduler.register(/** @type {any} */ (TaskClass))
|
|
517
747
|
log?.info?.({ count: schedules.length }, 'scheduler.schedules registered')
|
|
518
748
|
scheduler.start()
|
|
749
|
+
// 'jobs' stage — cron 트리거를 멈추고 실행 중 핸들러를 완료시킨다(ClosingScheduler).
|
|
519
750
|
MegaShutdown.register('mega-scheduler', async () => {
|
|
520
751
|
await scheduler.stop()
|
|
521
|
-
})
|
|
752
|
+
}, { stage: 'jobs' })
|
|
522
753
|
MegaShutdown.setupSignals()
|
|
523
754
|
return scheduler
|
|
524
755
|
}
|
|
@@ -526,6 +757,11 @@ export async function runSchedulerHost(projectRoot, logger) {
|
|
|
526
757
|
/**
|
|
527
758
|
* 마이그레이션 대상 DB 어댑터 해석 — `services.databases` 의 globalKey 로 조회한다. `--db <key>` 미지정
|
|
528
759
|
* 시 유일 선언 db, 그게 아니면 `primary`, 둘 다 아니면(다중·미선언) fail-fast 로 명시를 요구한다.
|
|
760
|
+
*
|
|
761
|
+
* 러너는 SQL `query` 기반이라(ADR-149 ⑥) `query` 도메인 메서드를 구현하지 않은 어댑터(mongodb 등
|
|
762
|
+
* Document DB)는 대상이 될 수 없다 — 베이스의 `adapter.not_implemented` 가 늦게 터지는 대신 여기서
|
|
763
|
+
* `migrate.unsupported_driver` 로 명시 fail-fast 한다(ADR-190).
|
|
764
|
+
*
|
|
529
765
|
* @param {Object} global - global config.
|
|
530
766
|
* @param {string} [dbKey] - `--db` 플래그.
|
|
531
767
|
* @returns {import('../adapters/mega-adapter.js').MegaAdapter}
|
|
@@ -550,7 +786,20 @@ export function resolveMigrationAdapter(global, dbKey) {
|
|
|
550
786
|
details: { db: key, declared: keys },
|
|
551
787
|
})
|
|
552
788
|
}
|
|
553
|
-
|
|
789
|
+
const adapter = getAdapter('db', key)
|
|
790
|
+
// mongodb 는 SQL 이 아니라 도큐먼트 API 마이그레이션 대상(ADR-209) — 러너 부기는 호스트가
|
|
791
|
+
// mongo 셈({@link module:core/migration/mongo-migration-db})으로 번역하므로 query 검사를 거치지 않는다.
|
|
792
|
+
if (dbs[key]?.driver === 'mongodb') return adapter
|
|
793
|
+
// 구체 어댑터가 query 를 override 하지 않았으면 SQL 마이그레이션 실행 불가(Document DB 등) — 명시 거부.
|
|
794
|
+
if (/** @type {any} */ (adapter).query === MegaDbAdapter.prototype.query) {
|
|
795
|
+
throw new MegaConfigError(
|
|
796
|
+
'migrate.unsupported_driver',
|
|
797
|
+
`db '${key}' (driver '${dbs[key]?.driver ?? '?'}') 는 SQL query 를 지원하지 않아 마이그레이션 대상이 될 수 없습니다 — ` +
|
|
798
|
+
'마이그레이션은 postgres/mariadb/sqlite(SQL) + mongodb(도큐먼트 API, ADR-209)만 지원합니다(ADR-149).',
|
|
799
|
+
{ details: { db: key, driver: dbs[key]?.driver ?? null } },
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
return adapter
|
|
554
803
|
}
|
|
555
804
|
|
|
556
805
|
/**
|
|
@@ -558,11 +807,35 @@ export function resolveMigrationAdapter(global, dbKey) {
|
|
|
558
807
|
* disconnect. 워커/wsHub/메트릭은 띄우지 않는다(마이그레이션엔 불필요·이벤트루프 유지 회피).
|
|
559
808
|
* `MegaShutdown.now`(process.exit)를 쓰지 않고 직접 disconnect 해 반환값으로 결과를 돌려준다(테스트 가능).
|
|
560
809
|
*
|
|
810
|
+
* up/down 은 driver 별 마이그레이션 락({@link module:core/migration-lock}, ADR-190)으로 직렬화한다 —
|
|
811
|
+
* 동시 `mega migrate`(멀티 인스턴스 배포 훅)의 중복 적용 방지. status 는 읽기 전용이라 락 없이 실행.
|
|
812
|
+
*
|
|
561
813
|
* @param {string} projectRoot
|
|
562
814
|
* @param {{ direction?: 'up'|'down'|'status', dbKey?: string, logger?: { debug?: Function, info?: Function, warn?: Function }, out?: (msg: string) => void }} [opts]
|
|
563
|
-
* @returns {Promise<{ applied: string[] } | { rolledBack: string|null } | { applied: string[], pending: string[] }>}
|
|
815
|
+
* @returns {Promise<{ applied: string[] } | { rolledBack: string|null } | { applied: string[], pending: string[], modified: string[] }>}
|
|
564
816
|
*/
|
|
565
817
|
export async function runMigrateHost(projectRoot, { direction = 'up', dbKey, logger, out = console.log } = {}) {
|
|
818
|
+
// bin 경유 실행은 logger 가 없다 — 러너의 운영 경고(checksum 드리프트·no-tx 등)가 무음으로
|
|
819
|
+
// 사라지지 않도록 CLI 출력으로 폴백한다(배포 훅이 드리프트 상태를 조용히 통과하는 것 방지).
|
|
820
|
+
// pino 시그니처 양형(`(obj, msg)` / `(msg)`)을 모두 수용한다 — 락 모듈은 문자열 단독으로 호출.
|
|
821
|
+
const fmt = (/** @type {any} */ o, /** @type {string | undefined} */ msg) => {
|
|
822
|
+
const text = msg ?? (typeof o === 'string' ? o : '')
|
|
823
|
+
const suffix = typeof o === 'object' && o !== null && typeof o.migration === 'string' ? ` — ${o.migration}` : ''
|
|
824
|
+
return `${text}${suffix}`
|
|
825
|
+
}
|
|
826
|
+
const log =
|
|
827
|
+
logger ??
|
|
828
|
+
{
|
|
829
|
+
info: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] ${fmt(o, msg)}`),
|
|
830
|
+
warn: (/** @type {any} */ o, /** @type {string} */ msg) => out(`[migrate] 경고: ${fmt(o, msg)}`),
|
|
831
|
+
}
|
|
832
|
+
// 빌트인 어댑터 배럴을 명시 로드(ADR-150) — migrate 는 boot.js 를 거치지 않고 buildFromGlobalConfig
|
|
833
|
+
// 를 직접 부르므로, boot 정적 import 가 사라진 lazy CLI 에선 여기서 driver 자기등록을 보장해야 한다.
|
|
834
|
+
const [{ migrateUp, migrateDown, migrateStatus }, { withMigrationLock }] = await Promise.all([
|
|
835
|
+
import('../core/migration-runner.js'),
|
|
836
|
+
import('../core/migration-lock.js'),
|
|
837
|
+
import('../adapters/index.js'),
|
|
838
|
+
])
|
|
566
839
|
const { global, apps } = await loadAndValidateConfig(projectRoot)
|
|
567
840
|
// 플러그인 install 을 어댑터 build 보다 먼저(config-driven driver 제약, boot.js 주석 참조).
|
|
568
841
|
const host = new MegaPluginHost({ logger })
|
|
@@ -570,21 +843,30 @@ export async function runMigrateHost(projectRoot, { direction = 'up', dbKey, log
|
|
|
570
843
|
buildFromGlobalConfig(global)
|
|
571
844
|
await connectAll({ logger })
|
|
572
845
|
try {
|
|
573
|
-
const
|
|
846
|
+
const adapter = /** @type {any} */ (resolveMigrationAdapter(global, dbKey))
|
|
847
|
+
// mongodb 는 러너 부기 SQL 을 collection 연산으로 번역하는 셈을 끼운다 — 러너/마이그레이션
|
|
848
|
+
// 파일 모두 이 객체를 받는다(파일의 db 는 사실상 native Db, ADR-209). 락은 어댑터 기준.
|
|
849
|
+
let db = adapter
|
|
850
|
+
if (adapter?.getStats?.().driver === 'mongodb') {
|
|
851
|
+
const { createMongoMigrationDb } = await import('../core/migration/mongo-migration-db.js')
|
|
852
|
+
db = createMongoMigrationDb(adapter.native)
|
|
853
|
+
}
|
|
574
854
|
const appNames = apps.map((a) => a.name)
|
|
575
855
|
if (direction === 'status') {
|
|
576
856
|
const s = await migrateStatus({ db, projectRoot, appNames })
|
|
577
857
|
out(`migrations — applied: ${s.applied.length}, pending: ${s.pending.length}`)
|
|
578
858
|
for (const n of s.applied) out(` [x] ${n}`)
|
|
579
859
|
for (const n of s.pending) out(` [ ] ${n}`)
|
|
860
|
+
// 적용 후 수정된(checksum 불일치) 파일 — 실행된 스키마와 어긋난 드리프트 경고(ADR-190).
|
|
861
|
+
for (const n of s.modified) out(` [!] ${n} (modified after apply — checksum mismatch)`)
|
|
580
862
|
return s
|
|
581
863
|
}
|
|
582
864
|
if (direction === 'down') {
|
|
583
|
-
const r = await migrateDown({ db, projectRoot, appNames, log
|
|
865
|
+
const r = await withMigrationLock(adapter, () => migrateDown({ db, projectRoot, appNames, log }), { logger: log })
|
|
584
866
|
out(r.rolledBack ? `rolled back: ${r.rolledBack}` : 'nothing to roll back')
|
|
585
867
|
return r
|
|
586
868
|
}
|
|
587
|
-
const r = await migrateUp({ db, projectRoot, appNames, log
|
|
869
|
+
const r = await withMigrationLock(adapter, () => migrateUp({ db, projectRoot, appNames, log }), { logger: log })
|
|
588
870
|
out(r.applied.length ? `applied ${r.applied.length} migration(s): ${r.applied.join(', ')}` : 'no pending migrations')
|
|
589
871
|
return r
|
|
590
872
|
} finally {
|