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.
- package/.env +127 -0
- package/.env.example +186 -0
- package/.prettierrc.json +8 -0
- package/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/mega-ws-hub.js +15 -0
- package/bin/mega.js +38 -0
- package/docker-compose.yml +201 -0
- package/eslint.config.js +57 -0
- package/infra/otel-collector-config.yaml +43 -0
- package/jsconfig.json +18 -0
- package/package.json +121 -0
- package/sample/crud/.env +18 -0
- package/sample/crud/.env.example +50 -0
- package/sample/crud/README.md +85 -0
- package/sample/crud/apps/main/app.config.js +114 -0
- package/sample/crud/apps/main/channels/chat-bus.js +115 -0
- package/sample/crud/apps/main/channels/chat-channel.js +145 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +144 -0
- package/sample/crud/apps/main/controllers/cron-controller.js +34 -0
- package/sample/crud/apps/main/controllers/guide-controller.js +37 -0
- package/sample/crud/apps/main/controllers/jobs-controller.js +43 -0
- package/sample/crud/apps/main/controllers/logs-controller.js +35 -0
- package/sample/crud/apps/main/controllers/metrics-controller.js +22 -0
- package/sample/crud/apps/main/controllers/note-controller.js +116 -0
- package/sample/crud/apps/main/controllers/perf-controller.js +38 -0
- package/sample/crud/apps/main/controllers/redis-controller.js +36 -0
- package/sample/crud/apps/main/controllers/tracing-controller.js +43 -0
- package/sample/crud/apps/main/controllers/upload-controller.js +98 -0
- package/sample/crud/apps/main/controllers/user-controller.js +34 -0
- package/sample/crud/apps/main/controllers/web-controller.js +137 -0
- package/sample/crud/apps/main/controllers/worker-controller.js +57 -0
- package/sample/crud/apps/main/controllers/ws-controller.js +29 -0
- package/sample/crud/apps/main/jobs/email-job.js +72 -0
- package/sample/crud/apps/main/locales/client/en.json +3 -0
- package/sample/crud/apps/main/locales/client/ko.json +3 -0
- package/sample/crud/apps/main/locales/server/en.json +316 -0
- package/sample/crud/apps/main/locales/server/ko.json +316 -0
- package/sample/crud/apps/main/middleware/web-auth.js +40 -0
- package/sample/crud/apps/main/middleware/ws-auth.js +48 -0
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +27 -0
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +30 -0
- package/sample/crud/apps/main/models/note.js +71 -0
- package/sample/crud/apps/main/models/user.js +86 -0
- package/sample/crud/apps/main/public/css/app.css +101 -0
- package/sample/crud/apps/main/public/css/guide.css +137 -0
- package/sample/crud/apps/main/public/js/app.js +54 -0
- package/sample/crud/apps/main/public/js/perf.js +129 -0
- package/sample/crud/apps/main/public/js/theme-init.js +12 -0
- package/sample/crud/apps/main/public/js/upload-demo.js +63 -0
- package/sample/crud/apps/main/public/js/worker-demo.js +92 -0
- package/sample/crud/apps/main/public/js/ws-chat.js +161 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/crud/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/crud/apps/main/public/vendor/highlight/github-dark.css +109 -0
- package/sample/crud/apps/main/public/vendor/highlight/github.css +118 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/README.md +19 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.d.ts +196 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm.js +1187 -0
- package/sample/crud/apps/main/public/vendor/mega-client-wasm/mega_client_wasm_bg.wasm +0 -0
- package/sample/crud/apps/main/routes/auth.js +15 -0
- package/sample/crud/apps/main/routes/cron.js +14 -0
- package/sample/crud/apps/main/routes/guide.js +25 -0
- package/sample/crud/apps/main/routes/jobs.js +14 -0
- package/sample/crud/apps/main/routes/logs.js +28 -0
- package/sample/crud/apps/main/routes/metrics.js +13 -0
- package/sample/crud/apps/main/routes/notes.js +19 -0
- package/sample/crud/apps/main/routes/perf.js +47 -0
- package/sample/crud/apps/main/routes/redis.js +14 -0
- package/sample/crud/apps/main/routes/tracing.js +14 -0
- package/sample/crud/apps/main/routes/upload.js +16 -0
- package/sample/crud/apps/main/routes/users.js +54 -0
- package/sample/crud/apps/main/routes/web.js +23 -0
- package/sample/crud/apps/main/routes/worker.js +15 -0
- package/sample/crud/apps/main/routes/ws.js +30 -0
- package/sample/crud/apps/main/schedules/cron-counter-schedule.js +30 -0
- package/sample/crud/apps/main/services/auth-service.js +74 -0
- package/sample/crud/apps/main/services/cron-demo-service.js +66 -0
- package/sample/crud/apps/main/services/guide-service.js +145 -0
- package/sample/crud/apps/main/services/jobs-demo-service.js +83 -0
- package/sample/crud/apps/main/services/logs-demo-service.js +59 -0
- package/sample/crud/apps/main/services/metrics-demo-service.js +144 -0
- package/sample/crud/apps/main/services/note-service.js +75 -0
- package/sample/crud/apps/main/services/perf-service.js +302 -0
- package/sample/crud/apps/main/services/redis-demo-service.js +75 -0
- package/sample/crud/apps/main/services/tracing-demo-service.js +69 -0
- package/sample/crud/apps/main/services/upload-demo-service.js +48 -0
- package/sample/crud/apps/main/services/user-service.js +65 -0
- package/sample/crud/apps/main/views/auth/login.ejs +57 -0
- package/sample/crud/apps/main/views/auth/register.ejs +71 -0
- package/sample/crud/apps/main/views/cron/index.ejs +92 -0
- package/sample/crud/apps/main/views/guide/index.ejs +24 -0
- package/sample/crud/apps/main/views/guide/page.ejs +64 -0
- package/sample/crud/apps/main/views/home.ejs +82 -0
- package/sample/crud/apps/main/views/jobs/index.ejs +113 -0
- package/sample/crud/apps/main/views/layouts/main.ejs +112 -0
- package/sample/crud/apps/main/views/logs/index.ejs +80 -0
- package/sample/crud/apps/main/views/metrics/index.ejs +123 -0
- package/sample/crud/apps/main/views/notes/edit.ejs +45 -0
- package/sample/crud/apps/main/views/notes/list.ejs +74 -0
- package/sample/crud/apps/main/views/notes/new.ejs +45 -0
- package/sample/crud/apps/main/views/perf/index.ejs +90 -0
- package/sample/crud/apps/main/views/redis/index.ejs +65 -0
- package/sample/crud/apps/main/views/tracing/index.ejs +106 -0
- package/sample/crud/apps/main/views/upload/index.ejs +79 -0
- package/sample/crud/apps/main/views/users/edit.ejs +48 -0
- package/sample/crud/apps/main/views/users/list.ejs +81 -0
- package/sample/crud/apps/main/views/users/new.ejs +48 -0
- package/sample/crud/apps/main/views/worker/index.ejs +70 -0
- package/sample/crud/apps/main/views/ws/index.ejs +62 -0
- package/sample/crud/apps/main/workers/hash-worker.js +17 -0
- package/sample/crud/apps/main/workers/hash.task.js +22 -0
- package/sample/crud/ecosystem.config.cjs +9 -0
- package/sample/crud/mega.config.js +105 -0
- package/sample/crud/package-lock.json +5665 -0
- package/sample/crud/package.json +28 -0
- package/sample/crud/test/apps/main/auth-flow.integration.test.js +177 -0
- package/sample/crud/test/apps/main/auth-service.test.js +93 -0
- package/sample/crud/test/apps/main/chat-bus.test.js +101 -0
- package/sample/crud/test/apps/main/chat-channel.test.js +144 -0
- package/sample/crud/test/apps/main/cron-demo-service.test.js +93 -0
- package/sample/crud/test/apps/main/demo-flow.integration.test.js +386 -0
- package/sample/crud/test/apps/main/email-job.test.js +76 -0
- package/sample/crud/test/apps/main/guide-service.test.js +68 -0
- package/sample/crud/test/apps/main/hash-task.test.js +30 -0
- package/sample/crud/test/apps/main/jobs-demo-service.test.js +88 -0
- package/sample/crud/test/apps/main/logs-demo-service.test.js +85 -0
- package/sample/crud/test/apps/main/metrics-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/note-service.test.js +68 -0
- package/sample/crud/test/apps/main/perf-service.test.js +121 -0
- package/sample/crud/test/apps/main/perf.integration.test.js +202 -0
- package/sample/crud/test/apps/main/redis-demo-service.test.js +98 -0
- package/sample/crud/test/apps/main/tracing-demo-service.test.js +90 -0
- package/sample/crud/test/apps/main/upload-demo-service.test.js +61 -0
- package/sample/crud/test/apps/main/user-service.test.js +65 -0
- package/sample/crud/test/apps/main/ws-chat.integration.test.js +232 -0
- package/sample/crud/vitest.config.js +8 -0
- package/sample/crud/yarn.lock +2142 -0
- package/sample/simple/.env.example +15 -0
- package/sample/simple/README.md +52 -0
- package/sample/simple/apps/main/app.config.js +35 -0
- package/sample/simple/apps/main/controllers/pages-controller.js +22 -0
- package/sample/simple/apps/main/locales/client/en.json +3 -0
- package/sample/simple/apps/main/locales/client/ko.json +3 -0
- package/sample/simple/apps/main/locales/server/en.json +23 -0
- package/sample/simple/apps/main/locales/server/ko.json +23 -0
- package/sample/simple/apps/main/public/css/app.css +101 -0
- package/sample/simple/apps/main/public/hello.txt +1 -0
- package/sample/simple/apps/main/public/js/app.js +54 -0
- package/sample/simple/apps/main/public/js/theme-init.js +12 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/sample/simple/apps/main/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/sample/simple/apps/main/routes/index.js +9 -0
- package/sample/simple/apps/main/routes/pages.js +12 -0
- package/sample/simple/apps/main/views/index.ejs +56 -0
- package/sample/simple/apps/main/views/layouts/main.ejs +74 -0
- package/sample/simple/ecosystem.config.cjs +10 -0
- package/sample/simple/mega.config.js +27 -0
- package/sample/simple/package-lock.json +1851 -0
- package/sample/simple/package.json +25 -0
- package/sample/simple/test/apps/main/index.test.js +13 -0
- package/sample/simple/vitest.config.js +8 -0
- package/src/adapters/adapter-manager.js +305 -0
- package/src/adapters/adapter-options.js +208 -0
- package/src/adapters/file-adapter.js +350 -0
- package/src/adapters/file-session-adapter.js +363 -0
- package/src/adapters/index.js +38 -0
- package/src/adapters/maria-adapter.js +425 -0
- package/src/adapters/mega-adapter.js +511 -0
- package/src/adapters/mega-bus-adapter.js +81 -0
- package/src/adapters/mega-cache-adapter.js +94 -0
- package/src/adapters/mega-db-adapter.js +72 -0
- package/src/adapters/mega-lock-adapter.js +118 -0
- package/src/adapters/mega-log-sink-adapter.js +46 -0
- package/src/adapters/mega-session-adapter.js +72 -0
- package/src/adapters/mongo-adapter.js +396 -0
- package/src/adapters/nats-adapter.js +370 -0
- package/src/adapters/postgres-adapter.js +341 -0
- package/src/adapters/redis-adapter.js +331 -0
- package/src/adapters/redis-session-adapter.js +261 -0
- package/src/adapters/redlock-adapter.js +385 -0
- package/src/adapters/registry.js +157 -0
- package/src/adapters/sqlite-adapter.js +309 -0
- package/src/auth/index.js +103 -0
- package/src/cli/commands/console-cmd.js +56 -0
- package/src/cli/commands/new.js +101 -0
- package/src/cli/commands/routes.js +107 -0
- package/src/cli/commands/scaffold.js +120 -0
- package/src/cli/commands/test-cmd.js +45 -0
- package/src/cli/generators/index.js +368 -0
- package/src/cli/index.js +472 -0
- package/src/cli/template-engine.js +72 -0
- package/src/cli/ws-hub.js +582 -0
- package/src/core/ajv-mapper.js +80 -0
- package/src/core/boot.js +323 -0
- package/src/core/cluster-metrics.js +278 -0
- package/src/core/config-loader.js +115 -0
- package/src/core/config-validator.js +322 -0
- package/src/core/ctx-builder.js +253 -0
- package/src/core/envelope.js +88 -0
- package/src/core/error-mapper.js +116 -0
- package/src/core/formbody.js +69 -0
- package/src/core/hub-link.js +552 -0
- package/src/core/i18n.js +525 -0
- package/src/core/index.js +63 -0
- package/src/core/mega-app.js +1138 -0
- package/src/core/mega-cluster.js +232 -0
- package/src/core/mega-server.js +176 -0
- package/src/core/mega-service.js +41 -0
- package/src/core/migration-runner.js +196 -0
- package/src/core/multipart.js +282 -0
- package/src/core/openapi.js +114 -0
- package/src/core/router.js +388 -0
- package/src/core/routes-loader.js +57 -0
- package/src/core/scope-registry.js +53 -0
- package/src/core/security.js +275 -0
- package/src/core/services-loader.js +98 -0
- package/src/core/session-cleanup-schedule.js +57 -0
- package/src/core/session-store.js +55 -0
- package/src/core/session.js +414 -0
- package/src/core/static-assets.js +126 -0
- package/src/core/template.js +294 -0
- package/src/core/workers-manager.js +193 -0
- package/src/core/ws-compression.js +112 -0
- package/src/core/ws-controller.js +109 -0
- package/src/core/ws-message.js +176 -0
- package/src/core/ws-upgrade.js +445 -0
- package/src/errors/config-error.js +16 -0
- package/src/errors/http-errors.js +130 -0
- package/src/errors/index.js +19 -0
- package/src/errors/mega-error.js +34 -0
- package/src/eslint-plugin/index.js +15 -0
- package/src/eslint-plugin/no-direct-model-import.js +113 -0
- package/src/index.js +131 -0
- package/src/lib/asp/config.js +83 -0
- package/src/lib/asp/crypto.js +145 -0
- package/src/lib/asp/errors.js +49 -0
- package/src/lib/asp/nonce-cache.js +94 -0
- package/src/lib/asp/plugin.js +263 -0
- package/src/lib/asp/ws-terminator.js +101 -0
- package/src/lib/env-mapper.js +222 -0
- package/src/lib/hub-protocol.js +322 -0
- package/src/lib/index.js +42 -0
- package/src/lib/logger/telegram-core.js +150 -0
- package/src/lib/logger/telegram-transport.js +126 -0
- package/src/lib/mega-brute-force.js +225 -0
- package/src/lib/mega-circuit-breaker.js +412 -0
- package/src/lib/mega-cron.js +169 -0
- package/src/lib/mega-hash.js +179 -0
- package/src/lib/mega-health.js +91 -0
- package/src/lib/mega-job-queue.js +600 -0
- package/src/lib/mega-job-worker.js +295 -0
- package/src/lib/mega-job.js +140 -0
- package/src/lib/mega-logger.js +128 -0
- package/src/lib/mega-metrics.js +661 -0
- package/src/lib/mega-plugin.js +650 -0
- package/src/lib/mega-retry.js +95 -0
- package/src/lib/mega-schedule.js +507 -0
- package/src/lib/mega-shutdown.js +176 -0
- package/src/lib/mega-tracing.js +715 -0
- package/src/lib/mega-worker.js +653 -0
- package/src/lib/worker-runner/process-entry.js +30 -0
- package/src/lib/worker-runner/task-dispatch.js +72 -0
- package/src/lib/worker-runner/thread-entry.js +26 -0
- package/src/models/index.js +7 -0
- package/src/models/mega-model.js +151 -0
- package/src/test/index.js +288 -0
- package/templates/adapter/code.tpl +40 -0
- package/templates/adapter/test.tpl +13 -0
- package/templates/app/app.config.tpl +10 -0
- package/templates/app/route.tpl +10 -0
- package/templates/app/test.tpl +13 -0
- package/templates/channel/code.tpl +38 -0
- package/templates/channel/test.tpl +19 -0
- package/templates/controller/code.tpl +16 -0
- package/templates/controller/route.tpl +9 -0
- package/templates/controller/test.tpl +14 -0
- package/templates/job/code.tpl +23 -0
- package/templates/job/test.tpl +17 -0
- package/templates/locale/code.tpl +3 -0
- package/templates/locale/test.tpl +13 -0
- package/templates/middleware/code.tpl +13 -0
- package/templates/middleware/test.tpl +11 -0
- package/templates/migration/code.tpl +20 -0
- package/templates/migration/test.tpl +14 -0
- package/templates/model/code.tpl +21 -0
- package/templates/model/test.tpl +29 -0
- package/templates/project/app.config.tpl +8 -0
- package/templates/project/app.config.views.tpl +37 -0
- package/templates/project/ecosystem.config.tpl +10 -0
- package/templates/project/env.tpl +12 -0
- package/templates/project/gitignore.tpl +8 -0
- package/templates/project/locales/client/en.json.tpl +3 -0
- package/templates/project/locales/client/ko.json.tpl +3 -0
- package/templates/project/locales/server/en.json.tpl +17 -0
- package/templates/project/locales/server/ko.json.tpl +17 -0
- package/templates/project/mega.config.tpl +11 -0
- package/templates/project/package.tpl +25 -0
- package/templates/project/public/css/app.css +101 -0
- package/templates/project/public/js/app.js +54 -0
- package/templates/project/public/js/theme-init.js +12 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.bundle.min.js +7 -0
- package/templates/project/public/vendor/bootstrap/bootstrap.min.css +6 -0
- package/templates/project/readme.tpl +48 -0
- package/templates/project/route.test.tpl +13 -0
- package/templates/project/route.test.views.tpl +15 -0
- package/templates/project/route.tpl +10 -0
- package/templates/project/route.views.tpl +10 -0
- package/templates/project/views/index.ejs.tpl +58 -0
- package/templates/project/views/layout.ejs.tpl +73 -0
- package/templates/project/vitest.config.tpl +8 -0
- package/templates/route/code.tpl +11 -0
- package/templates/route/test.tpl +26 -0
- package/templates/schedule/code.tpl +19 -0
- package/templates/schedule/test.tpl +17 -0
- package/templates/service/code.tpl +18 -0
- package/templates/service/test.tpl +17 -0
- package/templates/worker/code.tpl +14 -0
- package/templates/worker/task.tpl +13 -0
- package/templates/worker/test.tpl +18 -0
- package/vitest.config.js +33 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `mega` CLI 디스패처 — zero-dep(`process.argv` 직접 파싱) (ADR-123).
|
|
4
|
+
*
|
|
5
|
+
* 명령 수가 적고 MegaApp/MegaScheduler 의 "정적 클래스 + register" 패턴과 일관되게 commander/yargs
|
|
6
|
+
* 없이 직접 파싱한다(신규 의존성 0). 명령 표:
|
|
7
|
+
* - `mega start` (별칭 `serve`) — 부팅 orchestrator(`bootApp`) + HTTP listen + graceful 시그널.
|
|
8
|
+
* - `mega worker` — 잡 소비 워커 런타임(`MegaJobWorker`) 호스트 골격.
|
|
9
|
+
* - `mega scheduler` — 분산 스케줄러(`MegaScheduler`) 호스트 골격.
|
|
10
|
+
* - `mega <plugin:command>` — 플러그인이 `mega.cli.command(...)` 로 등록한 명령 dispatch.
|
|
11
|
+
* - `mega help` / `--help` — 도움말.
|
|
12
|
+
*
|
|
13
|
+
* worker/scheduler 의 **잡·스케줄 등록 소스 = `config.jobs` / `config.schedules`**(정적 배열) + 플러그인
|
|
14
|
+
* `host.listJobs()` / `host.listSchedules()`(동적)를 {@link collectRegistrations}(ADR-123)가 합친다 —
|
|
15
|
+
* 둘 다 scope-registry 의 GLOBAL_ONLY_KEYS 에 등록된 정식 글로벌 키다. 본 모듈(worker/scheduler 호스트)은
|
|
16
|
+
* config 로드 + 어댑터 connect + ctx 배선 + 인스턴스 생성·register + graceful shutdown 을 담당한다(CPU
|
|
17
|
+
* 워커 풀 `config.workers` 는 본 호스트가 아니라 `mega start`/boot 가 `ctx.workers` 로 배선).
|
|
18
|
+
*
|
|
19
|
+
* @module cli/index
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import os from 'node:os'
|
|
24
|
+
import { bootApp, prepareRuntime } from '../core/boot.js'
|
|
25
|
+
import { MegaCluster } from '../core/mega-cluster.js'
|
|
26
|
+
import { installPrimaryAggregator, installWorkerResponder } from '../core/cluster-metrics.js'
|
|
27
|
+
import { loadAndValidateConfig } from '../core/config-loader.js'
|
|
28
|
+
import { migrateUp, migrateDown, migrateStatus } from '../core/migration-runner.js'
|
|
29
|
+
import { buildFromGlobalConfig, connectAll, disconnectAll, get as getAdapter } from '../adapters/adapter-manager.js'
|
|
30
|
+
import { MegaConfigError } from '../errors/config-error.js'
|
|
31
|
+
import { MegaPluginHost, loadPlugins } from '../lib/mega-plugin.js'
|
|
32
|
+
import { MegaJobWorker } from '../lib/mega-job-worker.js'
|
|
33
|
+
import { MegaScheduler } from '../lib/mega-schedule.js'
|
|
34
|
+
import { MegaShutdown } from '../lib/mega-shutdown.js'
|
|
35
|
+
import * as MegaMetrics from '../lib/mega-metrics.js'
|
|
36
|
+
import { SCAFFOLD_COMMANDS, runScaffoldCommand } from './commands/scaffold.js'
|
|
37
|
+
|
|
38
|
+
/** CLI 사용법 텍스트. */
|
|
39
|
+
export const USAGE = `mega — MEGA-FRAMEWORK CLI
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
mega start [--port N] [--host H] [--cluster N|max] [--root DIR] 앱 부팅 + HTTP listen (별칭: serve)
|
|
43
|
+
mega worker [--root DIR] 잡 소비 워커 런타임 호스트
|
|
44
|
+
mega scheduler [--root DIR] 분산 스케줄러 호스트
|
|
45
|
+
mega migrate [--db KEY] [--root DIR] pending 마이그레이션 일괄 적용(up)
|
|
46
|
+
mega migrate:down [--db KEY] 마지막 적용 마이그레이션 1개 롤백(down)
|
|
47
|
+
mega migrate:status [--db KEY] 적용/미적용 마이그레이션 목록
|
|
48
|
+
mega new <project> [--views] [--force] 빈 폴더에서 멀티앱 hello world 스캐폴드
|
|
49
|
+
mega g <kind> <name> [--app A] [--version vN] 코드+테스트 생성 (별칭: generate)
|
|
50
|
+
[--kind K] [--lng L] [--force]
|
|
51
|
+
mega routes [--root DIR] 등록 라우트 트리 출력
|
|
52
|
+
mega test [--root DIR] [-- vitest args...] vitest 실행
|
|
53
|
+
mega console [--root DIR] 앱 컨텍스트 REPL
|
|
54
|
+
mega <plugin:command> [args...] 플러그인 등록 CLI 명령 실행
|
|
55
|
+
mega help 이 도움말
|
|
56
|
+
|
|
57
|
+
generator kinds:
|
|
58
|
+
app controller channel service model middleware route schedule job worker locale adapter migration
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--root DIR 프로젝트 루트(기본: 현재 디렉토리)
|
|
62
|
+
--port N listen 포트(start, 기본: server/global 설정 또는 3000)
|
|
63
|
+
--host H listen 호스트(start)
|
|
64
|
+
--cluster X 워커 프로세스 수(start). 정수 N 또는 max(CPU 코어 수). 우선순위:
|
|
65
|
+
--cluster > MEGA_CLUSTER_WORKERS env > server.cluster config. 1/미지정=단일 프로세스.
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
/** 프로젝트 `.env` 를 로드하지 않는 명령 — 스캐폴드 생성(new/g)·도움말. 그 외 런타임/부팅 명령은 로드. */
|
|
69
|
+
const NO_ENV_COMMANDS = new Set(['new', 'g', 'generate', 'help'])
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 프로젝트 루트의 `.env` 를 `process.env` 로 로드한다(파일이 있을 때만). Node 내장
|
|
73
|
+
* `process.loadEnvFile`(20.6+) 사용 — 신규 dep 0. **이미 설정된 실제 환경변수는 덮어쓰지 않는다**
|
|
74
|
+
* (`--env-file` 과 동일 우선순위 — 실 env 우선). config(`mega.config.js`)들이 `process.env.X` 를
|
|
75
|
+
* 참조하고 비밀은 `.env` 에만 두는 컨벤션(CLAUDE.md)을 CLI 가 보장한다(ADR-152). config 로드보다 먼저
|
|
76
|
+
* 호출해야 `process.env` 참조가 채워진다.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectRoot - `.env` 를 찾을 프로젝트 루트(`--root` 또는 cwd).
|
|
79
|
+
* @param {{ debug?: Function }} [logger]
|
|
80
|
+
* @returns {boolean} 로드했으면 true, `.env` 부재면 false.
|
|
81
|
+
* @throws {Error} `.env` 가 존재하나 파싱 실패 시(fail-fast — silent 무시 X).
|
|
82
|
+
*/
|
|
83
|
+
export function loadProjectEnv(projectRoot, logger) {
|
|
84
|
+
const envPath = join(projectRoot, '.env')
|
|
85
|
+
if (!existsSync(envPath)) return false
|
|
86
|
+
process.loadEnvFile(envPath)
|
|
87
|
+
logger?.debug?.({ envPath }, 'cli.env loaded')
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 인자 토큰을 `{ _: positionals[], flags: {} }` 로 파싱. `--key=value` / `--key value` / `--flag`(=true).
|
|
93
|
+
* zero-dep — 명령 표가 작아 직접 파싱(commander/yargs 회피).
|
|
94
|
+
* @param {string[]} argv - `process.argv.slice(2)` 형태.
|
|
95
|
+
* @returns {{ _: string[], flags: Record<string, string | boolean> }}
|
|
96
|
+
*/
|
|
97
|
+
export function parseArgs(argv) {
|
|
98
|
+
/** @type {string[]} */
|
|
99
|
+
const positionals = []
|
|
100
|
+
/** @type {Record<string, string | boolean>} */
|
|
101
|
+
const flags = {}
|
|
102
|
+
for (let i = 0; i < argv.length; i++) {
|
|
103
|
+
const tok = argv[i]
|
|
104
|
+
if (tok.startsWith('--')) {
|
|
105
|
+
const body = tok.slice(2)
|
|
106
|
+
const eq = body.indexOf('=')
|
|
107
|
+
if (eq !== -1) {
|
|
108
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1)
|
|
109
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
110
|
+
flags[body] = argv[++i]
|
|
111
|
+
} else {
|
|
112
|
+
flags[body] = true
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
positionals.push(tok)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { _: positionals, flags }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 사용 가능한 CPU 코어 수(최소 1). `cluster:'max'` 해석에 쓴다 — Node 22+ `availableParallelism`
|
|
123
|
+
* (컨테이너 cgroup 인식)을 우선하고, 미지원 런타임은 `cpus().length` 로 폴백한다.
|
|
124
|
+
* @returns {number}
|
|
125
|
+
*/
|
|
126
|
+
function defaultCpuCount() {
|
|
127
|
+
return Math.max(1, os.availableParallelism?.() ?? os.cpus().length)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* cluster 워커 수 설정값을 실제 워커 수로 해석한다(ADR-154 / 04-data-models §MegaServerConfig).
|
|
132
|
+
*
|
|
133
|
+
* 유효 도메인: **양의 정수** 또는 **'max'**(= CPU 코어 수). 그 외(0·음수·소수·'max' 아닌 문자열)는
|
|
134
|
+
* fail-closed 로 throw 한다 — 잘못된 값을 1로 조용히 강등하면 운영자가 클러스터가 안 뜬 줄 모른다(P4/P7).
|
|
135
|
+
* 워커가 1개로 해석되면(설정 `1`, 또는 단일 코어의 'max') 마스터+워커 2프로세스는 오버헤드만 더하므로
|
|
136
|
+
* **단일 프로세스(클러스터 비활성)** 로 본다 → `null` 반환.
|
|
137
|
+
*
|
|
138
|
+
* @param {string | number | boolean | undefined | null} setting - `--cluster` 플래그 / `MEGA_CLUSTER_WORKERS`
|
|
139
|
+
* env / `server.cluster` config 값. 미지정(`undefined`/`null`/`''`)은 단일 프로세스. 베어 `--cluster`(=true)는 'max'.
|
|
140
|
+
* @param {() => number} [cpuCount] - CPU 코어 수 공급자(테스트 주입용). 기본 {@link defaultCpuCount}.
|
|
141
|
+
* @returns {number | null} 워커 수(≥2) 또는 단일 프로세스면 `null`.
|
|
142
|
+
* @throws {MegaConfigError} `config.invalid_cluster` — 양의 정수도 'max' 도 아닌 값.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveClusterWorkers(setting, cpuCount = defaultCpuCount) {
|
|
145
|
+
if (setting === undefined || setting === null || setting === '') return null
|
|
146
|
+
let n
|
|
147
|
+
if (setting === true || setting === 'max' || setting === 'auto') {
|
|
148
|
+
n = cpuCount()
|
|
149
|
+
} else {
|
|
150
|
+
n = typeof setting === 'number' ? setting : Number(setting)
|
|
151
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
152
|
+
throw new MegaConfigError(
|
|
153
|
+
'config.invalid_cluster',
|
|
154
|
+
`cluster workers must be a positive integer or 'max', got "${setting}".`,
|
|
155
|
+
{ details: { setting } },
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return n <= 1 ? null : n
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `--port` 플래그를 검증된 listen 포트로 해석한다(fail-closed — {@link resolveClusterWorkers} 와 동형).
|
|
164
|
+
*
|
|
165
|
+
* 유효 도메인: **0~65535 정수**. 미지정(`undefined`)은 config/기본값에 위임하려 `undefined` 를 돌려준다.
|
|
166
|
+
* 값 없는 베어 `--port`(=`true`)·빈 값 `--port=`(=`''`)·정수 아님·범위 밖은 throw 한다 — 잘못된 값을
|
|
167
|
+
* 조용히 강등하면(`Number(true)=1` 특권포트, `Number('')=0` 랜덤포트, `Number('abc')=NaN` 은 어댑터
|
|
168
|
+
* connect 까지 부팅한 뒤 Node 가 `ERR_SOCKET_BAD_PORT` 로 늦게 throw) 운영자가 의도와 다른 포트를 쓰거나
|
|
169
|
+
* 늦은 cryptic 에러를 만난다(P4/P7). 명시적 `--port 0`(OS 랜덤 포트)은 정상 값으로 허용한다.
|
|
170
|
+
*
|
|
171
|
+
* @param {string | boolean | undefined | null} setting - `--port` 플래그 값.
|
|
172
|
+
* @returns {number | undefined} 검증된 포트, 또는 미지정이면 `undefined`(config/기본 위임).
|
|
173
|
+
* @throws {MegaConfigError} `config.invalid_port` — 값 없음/빈 값/정수 아님/범위 밖.
|
|
174
|
+
*/
|
|
175
|
+
export function resolvePort(setting) {
|
|
176
|
+
if (setting === undefined || setting === null) return undefined
|
|
177
|
+
// 베어 `--port`(=`true`)·빈 값(`''`) 등 문자열 아닌/빈 입력은 "값 필요" — 숫자 강등 전에 차단.
|
|
178
|
+
if (typeof setting !== 'string' || setting === '') {
|
|
179
|
+
throw new MegaConfigError('config.invalid_port', '--port requires a value (e.g. --port 3000).', { details: { setting } })
|
|
180
|
+
}
|
|
181
|
+
const n = Number(setting)
|
|
182
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
183
|
+
throw new MegaConfigError('config.invalid_port', `--port must be an integer in [0, 65535], got "${setting}".`, { details: { setting } })
|
|
184
|
+
}
|
|
185
|
+
return n
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `--host` 플래그를 검증된 listen 호스트로 해석한다(fail-closed). 미지정은 config/기본값에 위임하려
|
|
190
|
+
* `undefined`. 값 없는 베어 `--host`·빈 값 `--host=`는 throw — 빈 호스트를 `MegaServer` 로 흘리면
|
|
191
|
+
* Node 가 전 인터페이스(`::`)에 silent bind 해 운영자 의도와 달라진다(P4). 비어 있지 않은 문자열은 그대로.
|
|
192
|
+
*
|
|
193
|
+
* @param {string | boolean | undefined | null} setting - `--host` 플래그 값.
|
|
194
|
+
* @returns {string | undefined} 검증된 호스트, 또는 미지정이면 `undefined`(config/기본 위임).
|
|
195
|
+
* @throws {MegaConfigError} `config.invalid_host` — 값 없음/빈 값.
|
|
196
|
+
*/
|
|
197
|
+
export function resolveHost(setting) {
|
|
198
|
+
if (setting === undefined || setting === null) return undefined
|
|
199
|
+
if (typeof setting !== 'string' || setting === '') {
|
|
200
|
+
throw new MegaConfigError('config.invalid_host', '--host requires a value (e.g. --host 0.0.0.0).', { details: { setting } })
|
|
201
|
+
}
|
|
202
|
+
return setting
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `mega` CLI 진입점. 파싱 → 명령 분기. **이 함수는 process.exit 를 호출하지 않는다**(테스트 가능) —
|
|
207
|
+
* exit code 를 반환하고, bin 래퍼가 `process.exitCode` 로 반영한다.
|
|
208
|
+
*
|
|
209
|
+
* @param {string[]} argv - `process.argv.slice(2)`.
|
|
210
|
+
* @param {Object} [deps] - 테스트 주입용.
|
|
211
|
+
* @param {(msg: string) => void} [deps.out] - stdout writer(기본 console.log).
|
|
212
|
+
* @param {(msg: string) => void} [deps.err] - stderr writer(기본 console.error).
|
|
213
|
+
* @param {string} [deps.cwd] - 기본 projectRoot(기본 process.cwd()).
|
|
214
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [deps.logger]
|
|
215
|
+
* @returns {Promise<number>} exit code(0=성공, 1=실패/미지정 명령).
|
|
216
|
+
*/
|
|
217
|
+
export async function runCli(argv, { out = console.log, err = console.error, cwd, logger } = {}) {
|
|
218
|
+
const { _, flags } = parseArgs(argv)
|
|
219
|
+
const command = _[0]
|
|
220
|
+
const projectRoot = typeof flags.root === 'string' ? flags.root : (cwd ?? process.cwd())
|
|
221
|
+
|
|
222
|
+
// 런타임/부팅 명령은 config 로드 전에 프로젝트 `.env` 를 process.env 로 올린다(ADR-152). 스캐폴드
|
|
223
|
+
// 생성(new/g)은 대상 프로젝트가 아직 없거나 무관하므로 제외(cwd 의 무관한 .env 로드 방지).
|
|
224
|
+
if (command !== undefined && !NO_ENV_COMMANDS.has(command)) {
|
|
225
|
+
loadProjectEnv(projectRoot, logger)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (command === undefined || command === 'help' || flags.help === true) {
|
|
229
|
+
out(USAGE)
|
|
230
|
+
// 명령 없이 호출(`mega`)은 도움말 + 실패 코드(쉘 스크립트 친화), 명시적 help 는 0.
|
|
231
|
+
return command === 'help' || flags.help === true ? 0 : 1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (command === 'start' || command === 'serve') {
|
|
235
|
+
// listen 포트/호스트는 부팅(어댑터 connect) 전에 fail-closed 검증한다 — 잘못된 값을 늦게(listen 시점)
|
|
236
|
+
// cryptic 에러로 만나거나 silent 강등(특권/랜덤 포트)하지 않도록(per ADR-167 후속).
|
|
237
|
+
const port = resolvePort(flags.port)
|
|
238
|
+
const host = resolveHost(flags.host)
|
|
239
|
+
|
|
240
|
+
// cluster 워커 수 해석(ADR-154) — `--cluster` 플래그 > `MEGA_CLUSTER_WORKERS` env > `server.cluster`
|
|
241
|
+
// config 순. 앞선 소스가 정해지면 config 로드를 생략한다(env/플래그만으로 동작 — port 패턴과 동일).
|
|
242
|
+
let clusterSetting = flags.cluster ?? process.env.MEGA_CLUSTER_WORKERS
|
|
243
|
+
if (clusterSetting === undefined) {
|
|
244
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
245
|
+
clusterSetting = /** @type {any} */ (global).server?.cluster
|
|
246
|
+
}
|
|
247
|
+
const workers = resolveClusterWorkers(clusterSetting)
|
|
248
|
+
|
|
249
|
+
// 워커 부팅 함수 — 단일/클러스터 모드 공통. 클러스터에선 각 워커 프로세스가 이 함수를 실행한다.
|
|
250
|
+
const bootListen = async () => {
|
|
251
|
+
const { server } = await bootApp(projectRoot, { listen: true, port, host, logger })
|
|
252
|
+
MegaShutdown.register('mega-cli:server', async () => server.close())
|
|
253
|
+
MegaShutdown.setupSignals()
|
|
254
|
+
// 클러스터 워커면 메트릭 collect 응답기 설치(ADR-163) — 마스터의 집계 요청에 자기 메트릭 회신.
|
|
255
|
+
// 단일 프로세스면 no-op(cluster.isWorker=false).
|
|
256
|
+
installWorkerResponder()
|
|
257
|
+
return server
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (workers === null) {
|
|
261
|
+
// 단일 프로세스(클러스터 비활성) — 기존 경로 유지.
|
|
262
|
+
const server = await bootListen()
|
|
263
|
+
out(`mega: listening on [${server.hosts.join(', ')}]`)
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 클러스터 모드 — 마스터는 워커 N개 fork·respawn·graceful 협응(MegaCluster), 각 워커가 bootApp+listen.
|
|
268
|
+
// Node cluster 가 마스터의 공유 listen 소켓을 워커들에 분배하므로 SO_REUSEPORT 불필요(ADR-030/154).
|
|
269
|
+
const mega = new MegaCluster({ instances: workers })
|
|
270
|
+
await mega.start(async () => {
|
|
271
|
+
const server = await bootListen()
|
|
272
|
+
out(`mega: worker ${process.pid} listening on [${server.hosts.join(', ')}]`)
|
|
273
|
+
})
|
|
274
|
+
if (mega.isPrimary()) {
|
|
275
|
+
// 마스터에 메트릭 집계기 설치(ADR-163) — 워커의 /metrics 요청을 받아 전 워커 합산해 회신.
|
|
276
|
+
installPrimaryAggregator()
|
|
277
|
+
out(`mega: cluster master ${process.pid} forked ${workers} worker(s)`)
|
|
278
|
+
}
|
|
279
|
+
return 0
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (command === 'worker') {
|
|
283
|
+
await runWorkerHost(projectRoot, logger)
|
|
284
|
+
out('mega: worker started')
|
|
285
|
+
return 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (command === 'scheduler') {
|
|
289
|
+
await runSchedulerHost(projectRoot, logger)
|
|
290
|
+
out('mega: scheduler started')
|
|
291
|
+
return 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 마이그레이션 러너(ADR-149) — 일회성. `migrate:down`/`migrate:status` 는 콜론 포함이라 플러그인
|
|
295
|
+
// 명령 dispatch 보다 먼저 가로챈다.
|
|
296
|
+
if (command === 'migrate' || command === 'migrate:down' || command === 'migrate:status') {
|
|
297
|
+
const direction = command === 'migrate:down' ? 'down' : command === 'migrate:status' ? 'status' : 'up'
|
|
298
|
+
const dbKey = typeof flags.db === 'string' ? flags.db : undefined
|
|
299
|
+
await runMigrateHost(projectRoot, { direction, dbKey, logger, out })
|
|
300
|
+
return 0
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// scaffold/dev 명령군(new/g/routes/test/console)은 commander 로 파싱·디스패치(ADR-142). 런타임 명령
|
|
304
|
+
// (start/worker/scheduler)·플러그인 명령은 기존 zero-dep 경로(ADR-123) 유지.
|
|
305
|
+
if (SCAFFOLD_COMMANDS.has(command)) {
|
|
306
|
+
return runScaffoldCommand(argv, { out, err, projectRoot, logger })
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 플러그인 등록 CLI 명령(예: `mega sample:ping`). 명령 이름 컨벤션은 `<plugin>:<verb>` 이지만
|
|
310
|
+
// 강제는 아니다 — host 에 등록된 임의 이름을 그대로 조회한다.
|
|
311
|
+
const dispatched = await dispatchPluginCommand(projectRoot, command, { _: _.slice(1), flags }, logger)
|
|
312
|
+
if (dispatched.found) return dispatched.code
|
|
313
|
+
|
|
314
|
+
err(`mega: unknown command '${command}'.\n`)
|
|
315
|
+
err(USAGE)
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 플러그인 CLI 명령을 dispatch 한다 — config 로드 → 플러그인 install → `host.getCommand(name)` 조회.
|
|
321
|
+
* 어댑터 connect 는 하지 않는다(명령 핸들러가 필요하면 자체 처리 — 골격은 가볍게 유지, ADR-123).
|
|
322
|
+
*
|
|
323
|
+
* @param {string} projectRoot
|
|
324
|
+
* @param {string} name - 명령 이름.
|
|
325
|
+
* @param {{ _: string[], flags: Record<string, string | boolean> }} args - 핸들러에 그대로 전달.
|
|
326
|
+
* @param {{ debug?: Function }} [logger]
|
|
327
|
+
* @returns {Promise<{ found: boolean, code: number }>}
|
|
328
|
+
*/
|
|
329
|
+
export async function dispatchPluginCommand(projectRoot, name, args, logger) {
|
|
330
|
+
const { global } = await loadAndValidateConfig(projectRoot)
|
|
331
|
+
const host = new MegaPluginHost({ logger })
|
|
332
|
+
await loadPlugins(/** @type {any} */ (global).plugins, host, { projectRoot, logger })
|
|
333
|
+
const cmd = host.getCommand(name)
|
|
334
|
+
if (cmd === undefined) return { found: false, code: 1 }
|
|
335
|
+
// 핸들러 반환값을 exit code 로 해석(number 면 그대로, 그 외는 0). 에러는 호출자(runCli→bin)가 처리.
|
|
336
|
+
const result = await cmd.handler(args)
|
|
337
|
+
return { found: true, code: typeof result === 'number' ? result : 0 }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 잡/스케줄 **등록 소스 흡수**(ADR-123) — `config.<key>`(정적) + 플러그인 `host.list*()`(동적)을 합친다.
|
|
342
|
+
* 명시 등록만(auto-discovery X, ADR-079 정합). 순수 함수라 단위 검증 가능(register 부작용 분리).
|
|
343
|
+
* @param {Object} global - global config(`jobs`/`schedules` 배열 보유 가능).
|
|
344
|
+
* @param {import('../lib/mega-plugin.js').MegaPluginHost} host
|
|
345
|
+
* @param {'jobs' | 'schedules'} kind
|
|
346
|
+
* @returns {Function[]} 등록할 클래스 목록(config 먼저, 플러그인 등록분 뒤).
|
|
347
|
+
*/
|
|
348
|
+
export function collectRegistrations(global, host, kind) {
|
|
349
|
+
const fromConfig = /** @type {any} */ (global)?.[kind]
|
|
350
|
+
const staticPart = Array.isArray(fromConfig) ? fromConfig : []
|
|
351
|
+
const dynamicPart = kind === 'jobs' ? host.listJobs() : host.listSchedules()
|
|
352
|
+
return [...staticPart, ...dynamicPart]
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* `mega worker` 호스트 골격 — config + 어댑터 connect + ctx + `MegaJobWorker` 인스턴스 + graceful.
|
|
357
|
+
* 등록 소스 = `config.jobs` + 플러그인 `mega.jobs.register` 분(`collectRegistrations`, ADR-123).
|
|
358
|
+
* @param {string} projectRoot
|
|
359
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
360
|
+
* @returns {Promise<MegaJobWorker>}
|
|
361
|
+
*/
|
|
362
|
+
export async function runWorkerHost(projectRoot, logger) {
|
|
363
|
+
// bootApp 과 같은 토대(config → 플러그인 install → 어댑터 connect → ctx).
|
|
364
|
+
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
365
|
+
const worker = new MegaJobWorker({ ctx })
|
|
366
|
+
// 잡 메트릭 (ADR-132) — prepareRuntime 이 health.exposeMetrics 시 이미 MegaMetrics.init 했고,
|
|
367
|
+
// 옵트인 OFF 면 subscribeJobs 는 no-op() 을 반환한다. start 전에 구독해 enqueue(dispatch)부터 잡는다. 구독은
|
|
368
|
+
// MegaMetrics.shutdown(boot 가 'mega-metrics' MegaShutdown hook 으로 등록)에서 일괄 해제되므로 별도 등록 불필요.
|
|
369
|
+
MegaMetrics.subscribeJobs(worker)
|
|
370
|
+
// 등록 소스(ADR-123) = config.jobs(정적) + 플러그인 host.listJobs()(동적). register 가 subject/bus
|
|
371
|
+
// 미선언·중복을 부팅 시 fail-fast.
|
|
372
|
+
const jobs = collectRegistrations(global, host, 'jobs')
|
|
373
|
+
for (const JobClass of jobs) worker.register(/** @type {any} */ (JobClass))
|
|
374
|
+
logger?.info?.({ count: jobs.length }, 'worker.jobs registered')
|
|
375
|
+
await worker.start()
|
|
376
|
+
MegaShutdown.register('mega-worker', async () => {
|
|
377
|
+
await worker.stop()
|
|
378
|
+
})
|
|
379
|
+
MegaShutdown.setupSignals()
|
|
380
|
+
return worker
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* `mega scheduler` 호스트 골격 — config + 어댑터 connect + ctx + `MegaScheduler` 인스턴스 + graceful.
|
|
385
|
+
* @param {string} projectRoot
|
|
386
|
+
* @param {{ debug?: Function, info?: Function, warn?: Function }} [logger]
|
|
387
|
+
* @returns {Promise<MegaScheduler>}
|
|
388
|
+
*/
|
|
389
|
+
export async function runSchedulerHost(projectRoot, logger) {
|
|
390
|
+
const { global, host, ctx } = await prepareRuntime(projectRoot, { ping: false, logger })
|
|
391
|
+
const scheduler = new MegaScheduler({ ctx })
|
|
392
|
+
// 등록 소스(ADR-123) = config.schedules(정적) + 플러그인 host.listSchedules()(동적). register 가
|
|
393
|
+
// cron 미선언·중복을 부팅 시 fail-fast.
|
|
394
|
+
const schedules = collectRegistrations(global, host, 'schedules')
|
|
395
|
+
for (const TaskClass of schedules) scheduler.register(/** @type {any} */ (TaskClass))
|
|
396
|
+
logger?.info?.({ count: schedules.length }, 'scheduler.schedules registered')
|
|
397
|
+
scheduler.start()
|
|
398
|
+
MegaShutdown.register('mega-scheduler', async () => {
|
|
399
|
+
await scheduler.stop()
|
|
400
|
+
})
|
|
401
|
+
MegaShutdown.setupSignals()
|
|
402
|
+
return scheduler
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* 마이그레이션 대상 DB 어댑터 해석 — `services.databases` 의 globalKey 로 조회한다. `--db <key>` 미지정
|
|
407
|
+
* 시 유일 선언 db, 그게 아니면 `primary`, 둘 다 아니면(다중·미선언) fail-fast 로 명시를 요구한다.
|
|
408
|
+
* @param {Object} global - global config.
|
|
409
|
+
* @param {string} [dbKey] - `--db` 플래그.
|
|
410
|
+
* @returns {import('../adapters/mega-adapter.js').MegaAdapter}
|
|
411
|
+
*/
|
|
412
|
+
export function resolveMigrationAdapter(global, dbKey) {
|
|
413
|
+
const dbs = /** @type {any} */ (global)?.services?.databases ?? {}
|
|
414
|
+
const keys = Object.keys(dbs)
|
|
415
|
+
let key = dbKey
|
|
416
|
+
if (!key) {
|
|
417
|
+
if (keys.length === 1) key = keys[0]
|
|
418
|
+
else if (Object.prototype.hasOwnProperty.call(dbs, 'primary')) key = 'primary'
|
|
419
|
+
else {
|
|
420
|
+
throw new MegaConfigError(
|
|
421
|
+
'migrate.db_ambiguous',
|
|
422
|
+
`여러 db 가 선언됨 [${keys.join(', ') || '(none)'}]. 대상은 --db <key> 로 지정하세요.`,
|
|
423
|
+
{ details: { declared: keys } },
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!keys.includes(key)) {
|
|
428
|
+
throw new MegaConfigError('migrate.db_not_declared', `db '${key}' 가 services.databases 에 없습니다. 선언됨: [${keys.join(', ') || '(none)'}].`, {
|
|
429
|
+
details: { db: key, declared: keys },
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
return getAdapter('db', key)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* `mega migrate` 일회성 호스트 — config 로드 → 플러그인 install → DB build/connect → 러너 실행 →
|
|
437
|
+
* disconnect. 워커/wsHub/메트릭은 띄우지 않는다(마이그레이션엔 불필요·이벤트루프 유지 회피).
|
|
438
|
+
* `MegaShutdown.now`(process.exit)를 쓰지 않고 직접 disconnect 해 반환값으로 결과를 돌려준다(테스트 가능).
|
|
439
|
+
*
|
|
440
|
+
* @param {string} projectRoot
|
|
441
|
+
* @param {{ direction?: 'up'|'down'|'status', dbKey?: string, logger?: { debug?: Function, info?: Function, warn?: Function }, out?: (msg: string) => void }} [opts]
|
|
442
|
+
* @returns {Promise<{ applied: string[] } | { rolledBack: string|null } | { applied: string[], pending: string[] }>}
|
|
443
|
+
*/
|
|
444
|
+
export async function runMigrateHost(projectRoot, { direction = 'up', dbKey, logger, out = console.log } = {}) {
|
|
445
|
+
const { global, apps } = await loadAndValidateConfig(projectRoot)
|
|
446
|
+
// 플러그인 install 을 어댑터 build 보다 먼저(config-driven driver 제약, boot.js 주석 참조).
|
|
447
|
+
const host = new MegaPluginHost({ logger })
|
|
448
|
+
await loadPlugins(/** @type {any} */ (global).plugins, host, { projectRoot, logger })
|
|
449
|
+
buildFromGlobalConfig(global)
|
|
450
|
+
await connectAll({ logger })
|
|
451
|
+
try {
|
|
452
|
+
const db = /** @type {any} */ (resolveMigrationAdapter(global, dbKey))
|
|
453
|
+
const appNames = apps.map((a) => a.name)
|
|
454
|
+
if (direction === 'status') {
|
|
455
|
+
const s = await migrateStatus({ db, projectRoot, appNames })
|
|
456
|
+
out(`migrations — applied: ${s.applied.length}, pending: ${s.pending.length}`)
|
|
457
|
+
for (const n of s.applied) out(` [x] ${n}`)
|
|
458
|
+
for (const n of s.pending) out(` [ ] ${n}`)
|
|
459
|
+
return s
|
|
460
|
+
}
|
|
461
|
+
if (direction === 'down') {
|
|
462
|
+
const r = await migrateDown({ db, projectRoot, appNames, log: logger })
|
|
463
|
+
out(r.rolledBack ? `rolled back: ${r.rolledBack}` : 'nothing to roll back')
|
|
464
|
+
return r
|
|
465
|
+
}
|
|
466
|
+
const r = await migrateUp({ db, projectRoot, appNames, log: logger })
|
|
467
|
+
out(r.applied.length ? `applied ${r.applied.length} migration(s): ${r.applied.join(', ')}` : 'no pending migrations')
|
|
468
|
+
return r
|
|
469
|
+
} finally {
|
|
470
|
+
await disconnectAll({ logger })
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* zero-dep 템플릿 엔진 + 이름 변형 유틸 (ADR-142).
|
|
4
|
+
*
|
|
5
|
+
* `templates/<kind>/*.tpl` 파일의 `{{token}}` 자리를 vars 로 치환한다. 외부 템플릿 dep(handlebars 등)
|
|
6
|
+
* 없이 `process.argv` 파싱(ADR-123)과 같은 정책으로 단순 토큰 치환만 한다 — generator 가 만드는 코드는
|
|
7
|
+
* 정적 골격이라 조건/루프 같은 복잡한 템플릿 기능이 필요 없다.
|
|
8
|
+
*
|
|
9
|
+
* 토큰 형식은 `{{word}}`(이중 중괄호)라 생성 코드의 JSDoc `@param {string}`·객체 리터럴 `{ x }`
|
|
10
|
+
* (단일 중괄호)와 충돌하지 않는다. 미정의 토큰은 즉시 throw(silent 치환 누락 방지, P4).
|
|
11
|
+
*
|
|
12
|
+
* @module cli/template-engine
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 입력 이름을 단어 배열로 분해한다. camelCase 경계와 구분자(`-`, `_`, 공백, `.`)를 모두 단어 경계로
|
|
17
|
+
* 본다. 예: `'UserProfile'`/`'user-profile'`/`'user_profile'` → `['user','profile']`.
|
|
18
|
+
* @param {string} raw
|
|
19
|
+
* @returns {string[]}
|
|
20
|
+
*/
|
|
21
|
+
export function toWords(raw) {
|
|
22
|
+
return String(raw)
|
|
23
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
24
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.map((w) => w.toLowerCase())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 한 단어의 첫 글자를 대문자로.
|
|
31
|
+
* @param {string} w @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function capitalize(w) {
|
|
34
|
+
return w.length === 0 ? w : w[0].toUpperCase() + w.slice(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 입력 이름에서 표준 변형 4종을 만든다.
|
|
39
|
+
* @param {string} raw
|
|
40
|
+
* @returns {{ kebab: string, pascal: string, camel: string, snake: string, words: string[] }}
|
|
41
|
+
* @throws {Error} 영숫자 단어가 하나도 없으면(빈/특수문자만) fail-fast.
|
|
42
|
+
*/
|
|
43
|
+
export function nameVariants(raw) {
|
|
44
|
+
const words = toWords(raw)
|
|
45
|
+
if (words.length === 0) {
|
|
46
|
+
throw new Error(`Invalid name '${raw}': must contain at least one alphanumeric character.`)
|
|
47
|
+
}
|
|
48
|
+
const pascal = words.map(capitalize).join('')
|
|
49
|
+
return {
|
|
50
|
+
words,
|
|
51
|
+
kebab: words.join('-'),
|
|
52
|
+
snake: words.join('_'),
|
|
53
|
+
pascal,
|
|
54
|
+
camel: pascal[0].toLowerCase() + pascal.slice(1),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 템플릿 문자열의 `{{token}}` 을 vars 값으로 치환한다.
|
|
60
|
+
* @param {string} tpl - 템플릿 본문.
|
|
61
|
+
* @param {Record<string, string>} vars - 토큰 → 값.
|
|
62
|
+
* @returns {string}
|
|
63
|
+
* @throws {Error} 템플릿에 vars 에 없는 토큰이 있으면(오타·누락 방지).
|
|
64
|
+
*/
|
|
65
|
+
export function renderTemplate(tpl, vars) {
|
|
66
|
+
return tpl.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
|
|
67
|
+
if (!(key in vars)) {
|
|
68
|
+
throw new Error(`Template token '{{${key}}}' has no value. Provided: [${Object.keys(vars).join(', ')}].`)
|
|
69
|
+
}
|
|
70
|
+
return vars[key]
|
|
71
|
+
})
|
|
72
|
+
}
|