@gaonjs/cli 0.41.1 → 0.41.3

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.
@@ -33,9 +33,11 @@ function humanEvent(e) {
33
33
  return ` ▶ 로드됨 — ${cfg} · ${apps} · ${d}`;
34
34
  }
35
35
  case 'ready':
36
+ // 결정 251: 컨텍스트 문구는 실제 주입된 키(contextKeys)에서 파생한다 — 배너가
37
+ // 현실과 어긋날 수 없게(과거엔 빈 스텁 models·services 를 약속해 오도).
36
38
  return e.noConfig
37
39
  ? ' ▶ REPL 준비 (--no-config · 프로젝트 컨텍스트 없음). .exit 로 종료.'
38
- : ' ▶ REPL 준비. 컨텍스트: gaon, config, apps, models, services, domain. .exit 로 종료.';
40
+ : ` ▶ REPL 준비. 컨텍스트: ${e.contextKeys.join(', ')}. .exit 로 종료.`;
39
41
  case 'exited':
40
42
  return ' gaon console · 종료';
41
43
  }
@@ -76,11 +78,10 @@ export async function runConsoleCommand(opts = {}) {
76
78
  context.config = config;
77
79
  context.apps = wired.apps;
78
80
  context.domain = domain;
79
- // 모델·서비스 자동 노출은 사용자 코드 스캔이 필요해 v1 domain 요약만
80
- // 노출한다(§13.5 M9-G 최소 스코프). 사용자는 컨텍스트에서 필요 파일을
81
- // `await import(...)` 가져올 있다(REPL top-level await 허용).
82
- context.models = {};
83
- context.services = {};
81
+ // 결정 251: 모델·서비스 자동 노출은 사용자 코드 스캔이 필요해 v1 M9-G 최소 스코프
82
+ // 밖이다(§13.5). 스텁(context.models={} ) 심어 배너로 약속하면 오도이므로
83
+ // 심지 않는다 배너도 실제 주입된 키만 나열한다(위 humanEvent). 사용자는 컨텍스트에서
84
+ // 필요 파일을 `await import(...)` 로 가져온다(REPL 은 top-level await 허용).
84
85
  emit({
85
86
  kind: 'loaded',
86
87
  configPath,
@@ -92,7 +93,7 @@ export async function runConsoleCommand(opts = {}) {
92
93
  },
93
94
  });
94
95
  }
95
- emit({ kind: 'ready', noConfig });
96
+ emit({ kind: 'ready', noConfig, contextKeys: Object.keys(context) });
96
97
  // REPL 시작 — 테스트는 replFactory 로 stdin/stdout 을 갈아 끼운다.
97
98
  const repl = opts.replFactory
98
99
  ? opts.replFactory()
package/dist/serve.js CHANGED
@@ -134,29 +134,44 @@ export async function runServeCommand(opts = {}) {
134
134
  const configPath = findConfigPath(cwd);
135
135
  const config = await loadGaonConfig(cwd);
136
136
  const wired = await wireGaon(config, cwd);
137
- emit({ kind: 'starting', configPath, apps: wired.apps.map((a) => a.name) });
138
- const host = opts.host ?? config.web?.host ?? '0.0.0.0';
139
- // opts.port(프로그램적)는 검증하지 않는다 port:0(임의 포트) 같은 정당한 값이 있다.
140
- // env PORT 사용자 입력이라 비숫자면 NaN 대신 fail-loud(결정 240 · §7.5.3).
141
- const envPort = process.env.PORT != null && process.env.PORT !== '' ? parsePort(process.env.PORT, 'env PORT') : undefined;
142
- const port = opts.port ?? config.web?.port ?? envPort ?? 3000;
143
- // dev 전용 진단 라우트(결정 69). listen 전에 등록한다 — 운영 serve 는
144
- // opts.dev 없어 등록되지 않으므로 /_gaon/health 는 production 에 없다.
145
- // wired.app 여기서 FastifyInstance 로 해상되므로(cli fastify 타입을
146
- // 직접 의존하지 않는다) 라우트 등록은 serve 층에서 인라인으로 한다.
147
- if (opts.dev) {
148
- const healthCtx = { cwd, config, apps: wired.apps, nats: wired.nats, port, host };
149
- wired.app.get(DEV_HEALTH_PATH, async () => computeHealth(healthCtx));
137
+ // 결정 250(샘플 C · P2): wireGaon NATS·Redis 소켓을 뒤의 부팅 단계(포트 파싱·listen
138
+ // 등)에서 실패하면, 상위 catch(index.ts)가 exitCode=1 세팅하고 끝나 열린 소켓이 이벤트
139
+ // 루프를 붙잡아 프로세스가 종료하지 못하고 매달린다(정확한 fail-loud 메시지는 찍히지만
140
+ // 행업). 부팅 실패는 자기가 핸들을 정리하고 명시 종료한다( 파일 헤더 "예외는 stderr +
141
+ // exit 1" 실제로 구현). 정상 부팅·정상 종료 경로엔 영향이 없다(process.exit 실패 시만).
142
+ try {
143
+ emit({ kind: 'starting', configPath, apps: wired.apps.map((a) => a.name) });
144
+ const host = opts.host ?? config.web?.host ?? '0.0.0.0';
145
+ // opts.port(프로그램적)는 검증하지 않는다 port:0(임의 포트) 같은 정당한 값이 있다.
146
+ // env PORT 사용자 입력이라 비숫자면 NaN 대신 fail-loud(결정 240 · §7.5.3).
147
+ const envPort = process.env.PORT != null && process.env.PORT !== '' ? parsePort(process.env.PORT, 'env PORT') : undefined;
148
+ const port = opts.port ?? config.web?.port ?? envPort ?? 3000;
149
+ // dev 전용 진단 라우트(결정 69). listen 전에 등록한다 — 운영 serve 는
150
+ // opts.dev 가 없어 등록되지 않으므로 /_gaon/health 는 production 에 없다.
151
+ // wired.app 은 여기서 FastifyInstance 로 해상되므로(cli 는 fastify 타입을
152
+ // 직접 의존하지 않는다) 라우트 등록은 serve 층에서 인라인으로 한다.
153
+ if (opts.dev) {
154
+ const healthCtx = { cwd, config, apps: wired.apps, nats: wired.nats, port, host };
155
+ wired.app.get(DEV_HEALTH_PATH, async () => computeHealth(healthCtx));
156
+ }
157
+ await wired.app.listen({ host, port });
158
+ const displayHost = host === '0.0.0.0' ? 'localhost' : host;
159
+ emit({ kind: 'listening', host, port, url: `http://${displayHost}:${port}` });
160
+ // 결정 98: serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않는다 —
161
+ // 수정이 조용히 반영 안 되는 혼란(첫 실사용 관측)을 막으려 한 줄 안내한다.
162
+ // dev 자식(gaon dev · opts.dev)은 이미 감시하므로 그때는 안내하지 않고,
163
+ // production 은 운영 로그 소음을 막으려 출력하지 않는다. json 은 파싱 안전상 제외.
164
+ if (!json && !opts.dev && process.env.NODE_ENV !== 'production') {
165
+ process.stdout.write(' ℹ serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않습니다 → 개발 중이면 `gaon dev` 를 쓰세요.\n');
166
+ }
150
167
  }
151
- await wired.app.listen({ host, port });
152
- const displayHost = host === '0.0.0.0' ? 'localhost' : host;
153
- emit({ kind: 'listening', host, port, url: `http://${displayHost}:${port}` });
154
- // 결정 98: serve 빌드된 번들을 서빙하며 코드 변경을 감시하지 않는다
155
- // 수정이 조용히 반영 되는 혼란(첫 실사용 관측)을 막으려 한 줄 안내한다.
156
- // dev 자식(gaon dev · opts.dev) 이미 감시하므로 그때는 안내하지 않고,
157
- // production 은 운영 로그 소음을 막으려 출력하지 않는다. json 은 파싱 안전상 제외.
158
- if (!json && !opts.dev && process.env.NODE_ENV !== 'production') {
159
- process.stdout.write(' ℹ serve 는 빌드된 번들을 서빙하며 코드 변경을 감시하지 않습니다 → 개발 중이면 `gaon dev` 를 쓰세요.\n');
168
+ catch (err) {
169
+ const msg = err instanceof Error ? err.message : String(err);
170
+ process.stderr.write(` ✗ gaon serve 부팅 실패: ${msg}\n`);
171
+ // 열린 핸들(NATS·Redis·DB) 정리 이게 없으면 아래 명시 종료 없이도, 있어도 소켓이
172
+ // 남아 다음 부팅/셧다운을 오염시킨다. close 실패는 삼켜 종료를 막지 않는다(best-effort).
173
+ await wired.close().catch(() => { });
174
+ process.exit(1);
160
175
  }
161
176
  await new Promise((resolvePromise) => {
162
177
  const stop = () => {
@@ -169,6 +169,12 @@ DB 테스트는 손으로 커넥션을 배선하지 않는다 — `gaon test`
169
169
  의 "롤백=미발행" 보장이 테스트에서도 참). 이 배선이 없던 때는 테스트의 emit 이 즉시발행
170
170
  경로로 새 그 보장이 **공허하게 통과**했다 — 아웃박스 롤백을 단언하는 테스트는 표준
171
171
  하네스로 그대로 돈다(수동 `setServiceTxWrapper` 불필요).
172
+ - `connectTestDatabase` 는 **락·캐시 백엔드도 배선**한다(결정 249) — serve·work 의
173
+ `wireDomain`(결정 202)과 대칭으로. 그래서 `lock()`·`cache()` 를 쓰는 잡·서비스가
174
+ 테스트에서도 그대로 돈다(이전엔 serve/work 에선 정상인데 `gaon test` 에서만
175
+ `LockBackendUnconfiguredError` 로 깨졌다). 단일 프로세스 테스트라 분산 락이 무의미하고
176
+ 락 키에 테스트 격리 prefix 가 없어, Redis 유무와 무관하게 in-memory 백엔드로 격리한다
177
+ (수동 `setLockBackend` 불필요).
172
178
 
173
179
  스캐폴드가 심어 주는 `test/setup.ts`(수정 불필요):
174
180
 
package/dist/work.js CHANGED
@@ -70,35 +70,41 @@ export async function runWorkCommand(opts = {}) {
70
70
  }
71
71
  };
72
72
  const nats = await connectNats({ servers: opts.natsUrl, name: `work@${id}` });
73
- // 도메인 배터리 배선(결정 129) 핸들러가 모델을 읽고(registerConnection)
74
- // 메일을 보내려면(configureMailer) work serve 같은 도메인 배선을 태워야
75
- // 한다. serve wireGaon wireDomain, work 여기서 직접 wireDomain 을
76
- // 호출한다. 배선이 없으면 잡에서 모델 조회·메일 발송이 운영 워커에서만
77
- // 조용히 깨진다(gaon dev serve 프로세스가 대신 배선해 가려짐).
78
- const config = await loadGaonConfig(root);
79
- const domainWiring = await wireDomain(config, root);
80
- // 아웃박스 릴레이·모델 읽기용 main 커넥션. gaon.config 에 db.main 이 있으면
81
- // wireDomain 이 이미 등록했다. config 없이 env(DATABASE_URL)만 있는 프로젝트는
82
- // 여기서 관례로 보강 등록해 잡의 모델 읽기와 릴레이가 함께 동작하게 한다.
83
- // 스캐폴드 .env 관례(DATABASE_URL)를 정본으로 · GAON_DATABASE_URL 은 하위호환.
84
- const dbUrl = opts.databaseUrl ?? process.env.DATABASE_URL ?? process.env.GAON_DATABASE_URL;
85
- if (!hasConnection('main') && dbUrl) {
86
- const cfg = dbConfigFromUrl(dbUrl);
87
- registerConnection('main', createDb(cfg), cfg.adapter);
88
- }
89
- const db = hasConnection('main') ? getConnection('main') : undefined;
90
- const domain = await loadDomain(root);
91
- // 로드된 도메인 자산 요약(파일=등록 관측용). 잡·리스너·메일 수를 노출한다.
92
- if (json) {
93
- process.stdout.write(JSON.stringify({
94
- kind: 'domain',
95
- jobs: domain.jobs,
96
- listeners: domain.listeners,
97
- mails: domain.mails,
98
- }) + '\n');
99
- }
73
+ // 결정 250(샘플 C · P2): connectNats 소켓을 뒤의 부팅 단계(config 로드·wireDomain·
74
+ // loadDomain·runWork) 어디서 실패해도 열린 NATS·Redis 소켓이 이벤트 루프를 붙잡아 상위
75
+ // catch(index.ts)가 exitCode=1 세팅한 프로세스가 매달린다. 부팅 실패는 그때까지
76
+ // 핸들을 정리하고 명시 종료한다(serve 대칭). 정상 부팅·정상 종료엔 영향 없다.
77
+ // (이전엔 runWork 실패만 정리했고, 앞의 wireDomain·loadDomain throw 는 nats 를 누출했다.)
78
+ let domainWiring;
100
79
  let work;
101
80
  try {
81
+ // 도메인 배터리 배선(결정 129) — 잡 핸들러가 모델을 읽고(registerConnection)
82
+ // 메일을 보내려면(configureMailer) work 도 serve 와 같은 도메인 배선을 태워야
83
+ // 한다. serve 는 wireGaon → wireDomain, work 은 여기서 직접 wireDomain 을
84
+ // 호출한다. 이 배선이 없으면 잡에서 모델 조회·메일 발송이 운영 워커에서만
85
+ // 조용히 깨진다(gaon dev 는 serve 프로세스가 대신 배선해 가려짐).
86
+ const config = await loadGaonConfig(root);
87
+ domainWiring = await wireDomain(config, root);
88
+ // 아웃박스 릴레이·모델 읽기용 main 커넥션. gaon.config 에 db.main 이 있으면
89
+ // wireDomain 이 이미 등록했다. config 없이 env(DATABASE_URL)만 있는 프로젝트는
90
+ // 여기서 관례로 보강 등록해 잡의 모델 읽기와 릴레이가 함께 동작하게 한다.
91
+ // 스캐폴드 .env 관례(DATABASE_URL)를 정본으로 · GAON_DATABASE_URL 은 하위호환.
92
+ const dbUrl = opts.databaseUrl ?? process.env.DATABASE_URL ?? process.env.GAON_DATABASE_URL;
93
+ if (!hasConnection('main') && dbUrl) {
94
+ const cfg = dbConfigFromUrl(dbUrl);
95
+ registerConnection('main', createDb(cfg), cfg.adapter);
96
+ }
97
+ const db = hasConnection('main') ? getConnection('main') : undefined;
98
+ const domain = await loadDomain(root);
99
+ // 로드된 도메인 자산 요약(파일=등록 관측용). 잡·리스너·메일 수를 노출한다.
100
+ if (json) {
101
+ process.stdout.write(JSON.stringify({
102
+ kind: 'domain',
103
+ jobs: domain.jobs,
104
+ listeners: domain.listeners,
105
+ mails: domain.mails,
106
+ }) + '\n');
107
+ }
102
108
  work = await runWork({
103
109
  nats,
104
110
  id,
@@ -114,9 +120,12 @@ export async function runWorkCommand(opts = {}) {
114
120
  });
115
121
  }
116
122
  catch (err) {
117
- await domainWiring.closeDomain().catch(() => { });
118
- await nats.close();
119
- throw err;
123
+ const msg = err instanceof Error ? err.message : String(err);
124
+ process.stderr.write(` ✗ gaon work 부팅 실패: ${msg}\n`);
125
+ if (domainWiring)
126
+ await domainWiring.closeDomain().catch(() => { });
127
+ await nats.close().catch(() => { });
128
+ process.exit(1);
120
129
  }
121
130
  await new Promise((resolve) => {
122
131
  const stop = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.41.1",
3
+ "version": "0.41.3",
4
4
  "description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,12 +28,12 @@
28
28
  "typescript": "^5.9.0",
29
29
  "vite": "^7.0.0",
30
30
  "@gaonjs/async": "0.15.0",
31
+ "@gaonjs/mail": "0.3.0",
32
+ "@gaonjs/data": "0.17.0",
31
33
  "@gaonjs/core": "0.2.3",
32
- "@gaonjs/config": "0.17.1",
33
34
  "@gaonjs/i18n": "0.2.2",
34
- "@gaonjs/web": "0.19.3",
35
- "@gaonjs/data": "0.17.0",
36
- "@gaonjs/mail": "0.3.0"
35
+ "@gaonjs/web": "0.19.4",
36
+ "@gaonjs/config": "0.17.3"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""