@gaonjs/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
  2. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
  3. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
  4. package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
  5. package/dist/commands/db.d.ts +20 -0
  6. package/dist/commands/db.js +74 -0
  7. package/dist/commands/dev.d.ts +68 -0
  8. package/dist/commands/dev.js +287 -0
  9. package/dist/db/diff.d.ts +17 -0
  10. package/dist/db/diff.js +57 -0
  11. package/dist/db/index.d.ts +4 -0
  12. package/dist/db/index.js +8 -0
  13. package/dist/db/migrate.d.ts +16 -0
  14. package/dist/db/migrate.js +173 -0
  15. package/dist/db/reset.d.ts +18 -0
  16. package/dist/db/reset.js +150 -0
  17. package/dist/db/resolve.d.ts +32 -0
  18. package/dist/db/resolve.js +130 -0
  19. package/dist/dev/console.d.ts +39 -0
  20. package/dist/dev/console.js +100 -0
  21. package/dist/dev/docker.d.ts +52 -0
  22. package/dist/dev/docker.js +163 -0
  23. package/dist/dev/index.d.ts +14 -0
  24. package/dist/dev/index.js +10 -0
  25. package/dist/dev/tsc.d.ts +41 -0
  26. package/dist/dev/tsc.js +127 -0
  27. package/dist/dev/watcher.d.ts +50 -0
  28. package/dist/dev/watcher.js +95 -0
  29. package/dist/dev.d.ts +1 -16
  30. package/dist/dev.js +10 -66
  31. package/dist/doctor/reporter.d.ts +1 -1
  32. package/dist/doctor/reporter.js +16 -3
  33. package/dist/doctor/setup.d.ts +26 -0
  34. package/dist/doctor/setup.js +52 -0
  35. package/dist/doctor/types.d.ts +13 -0
  36. package/dist/doctor/types.js +5 -1
  37. package/dist/doctor.d.ts +19 -5
  38. package/dist/doctor.js +64 -5
  39. package/dist/index.d.ts +5 -1
  40. package/dist/index.js +71 -21
  41. package/package.json +5 -5
@@ -0,0 +1,12 @@
1
+ import { type RowOf } from '@gaonjs/data';
2
+ export declare const m9dWidgets: import("@gaonjs/data").TableDef<"m9d_widgets", {
3
+ id: import("@gaonjs/data").BigintCol<{
4
+ hasDefault: true;
5
+ }>;
6
+ name: import("@gaonjs/data").StringCol<{}>;
7
+ }>;
8
+ declare module '@gaonjs/data' {
9
+ interface GaonTables {
10
+ m9d_widgets: RowOf<typeof m9dWidgets>;
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ // db 통합 테스트 픽스처 — main 커넥션의 유일한 테이블.
2
+ // 이름은 m9d_widgets (M8 seed 픽스처의 widgets 와 GaonTables 병합 충돌 회피).
3
+ import { table, t } from '@gaonjs/data';
4
+ export const m9dWidgets = table('m9d_widgets', {
5
+ id: t.id(),
6
+ name: t.string().max(30),
7
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@gaonjs/config").GaonConfig;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ // db 통합 테스트 픽스처 — main 커넥션만 있는 최소 gaon.config.ts.
2
+ // url 은 GAON_TEST_PG_URL 우선 · 로컬 default 는 packages/data/compose.yaml 정합.
3
+ import { defineConfig } from '@gaonjs/config';
4
+ export default defineConfig({
5
+ db: {
6
+ main: {
7
+ adapter: 'postgres',
8
+ url: process.env.GAON_TEST_PG_URL ?? 'postgres://gaon:gaon@127.0.0.1:5433/gaon_test',
9
+ },
10
+ },
11
+ });
@@ -0,0 +1,20 @@
1
+ export type DbSubcommand = 'diff' | 'migrate' | 'reset' | 'seed';
2
+ export interface DbCommandOptions {
3
+ /** 프로젝트 루트 — gaon.config.ts·domain/schema 위치. 기본 process.cwd(). */
4
+ readonly cwd?: string;
5
+ /** 자동화 출력. 기본 false(사람 텍스트). */
6
+ readonly json?: boolean;
7
+ /** 커넥션 키(§4.5). 기본 'main'. */
8
+ readonly db?: string;
9
+ /** --config <path> 오버라이드. 없으면 cwd/gaon.config.ts 관례. */
10
+ readonly config?: string;
11
+ /** reset 확증 — 없으면 거부(--dry-run 은 예외). */
12
+ readonly yes?: boolean;
13
+ /** migrate/reset dry-run — 실행 없이 SQL 만 출력. */
14
+ readonly dryRun?: boolean;
15
+ }
16
+ /**
17
+ * `gaon db <sub>` 진입점. 사람/JSON 출력을 자체 처리하고 exitCode 를
18
+ * 돌려준다. seed 는 기존 M8 runDbSeedCommand 로 위임(신규 서명에 정합).
19
+ */
20
+ export declare function runDbCommand(subcommand: DbSubcommand, opts?: DbCommandOptions): Promise<number>;
@@ -0,0 +1,74 @@
1
+ // @gaonjs/cli · `gaon db <sub>` — 서브커맨드 디스패처 (M9-D)
2
+ //
3
+ // 4종을 한 곳에서 배선한다: diff · migrate · reset · seed.
4
+ // · diff/migrate/reset 은 db/*.ts 로 위임.
5
+ // · seed 는 기존 M8 로직(runDbSeedCommand · packages/cli/src/db.ts) 을
6
+ // 그대로 위임 — 이 세션은 seed 를 건드리지 않는다.
7
+ //
8
+ // runCli 는 argv 를 이 함수로 넘긴다. 사람/JSON 출력·exitCode 전달까지
9
+ // 여기서 마감해서 index.ts 는 dispatch 만 한다.
10
+ import { runDbDiff } from '../db/diff.js';
11
+ import { runDbMigrate } from '../db/migrate.js';
12
+ import { runDbReset } from '../db/reset.js';
13
+ import { runDbSeedCommand } from '../db.js';
14
+ /**
15
+ * `gaon db <sub>` 진입점. 사람/JSON 출력을 자체 처리하고 exitCode 를
16
+ * 돌려준다. seed 는 기존 M8 runDbSeedCommand 로 위임(신규 서명에 정합).
17
+ */
18
+ export async function runDbCommand(subcommand, opts = {}) {
19
+ const cwd = opts.cwd ?? process.cwd();
20
+ const json = opts.json ?? false;
21
+ const dbKey = opts.db ?? 'main';
22
+ const configPath = opts.config;
23
+ const dryRun = opts.dryRun ?? false;
24
+ const emit = (text, jsonValue) => {
25
+ if (json)
26
+ process.stdout.write(JSON.stringify(jsonValue) + '\n');
27
+ else
28
+ process.stdout.write(text + '\n');
29
+ };
30
+ try {
31
+ if (subcommand === 'diff') {
32
+ const r = await runDbDiff({ cwd, dbKey, json, configPath });
33
+ emit(r.text, r.json);
34
+ return r.exitCode;
35
+ }
36
+ if (subcommand === 'migrate') {
37
+ const r = await runDbMigrate({ cwd, dbKey, json, dryRun, configPath });
38
+ emit(r.text, r.json);
39
+ return r.exitCode;
40
+ }
41
+ if (subcommand === 'reset') {
42
+ const r = await runDbReset({
43
+ cwd,
44
+ dbKey,
45
+ json,
46
+ yes: opts.yes ?? false,
47
+ dryRun,
48
+ configPath,
49
+ });
50
+ emit(r.text, r.json);
51
+ return r.exitCode;
52
+ }
53
+ if (subcommand === 'seed') {
54
+ // 기존 M8 로직 — 옵션 shape 이 다르므로 필요한 것만 전달.
55
+ const r = await runDbSeedCommand({ root: cwd, json });
56
+ emit(r.text, r.json);
57
+ return r.exitCode;
58
+ }
59
+ // 방어 — dispatcher 라 도달할 수 없지만 컴파일러 만족용.
60
+ process.stderr.write(` ✗ 알 수 없는 db 서브커맨드: ${String(subcommand)}\n` +
61
+ ` → 지원: diff | migrate | reset | seed\n`);
62
+ return 1;
63
+ }
64
+ catch (err) {
65
+ const msg = err instanceof Error ? err.message : String(err);
66
+ if (json) {
67
+ process.stdout.write(JSON.stringify({ command: subcommand, ok: false, error: msg }) + '\n');
68
+ }
69
+ else {
70
+ process.stderr.write(` ✗ gaon db ${subcommand} 실패: ${msg}\n`);
71
+ }
72
+ return 1;
73
+ }
74
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon dev` — 개발 오케스트레이션 (M9-C · v0.15 §13.5)
3
+ *
4
+ * 하나의 명령이 개발 스택 전체를 띄운다:
5
+ * 1) Docker Compose — pg · redis · nats · mailpit · minio 자동 up -d
6
+ * 2) .gaon 워처 — tables.d.ts · routes.d.ts 재생성 (기존 M3)
7
+ * 3) serve 자식 — 실 웹 서버(Fastify) · 소스 변경 시 재시작
8
+ * 4) tsc / vue-tsc — --watch 모드 · 타입 에러 즉시 리포트
9
+ * 5) restart 워처 — apps/ · domain/ · packages/ 변경 → serve 재시작
10
+ * 6) 통합 콘솔 — [source] 태그·색상별 · JSON 모드 지원
11
+ *
12
+ * The One Way: `gaon dev` 하나로 전부 · 옵션은 개별 debug 용만 노출.
13
+ * fail-closed: Docker 없으면 명확 안내(§7.5.3), 목업 대체 절대 X (§9).
14
+ * ctrl-C: graceful — serve → tsc → 워처 순 정리 · Docker 는 유지(재사용)
15
+ * 또는 --stop-docker 시 down.
16
+ *
17
+ * HMR: v1 Vue 어댑터는 아직 Vite 통합이 없어(§6.4) 서버 재시작 방식만
18
+ * 지원한다. 진짜 Vue HMR 은 어댑터 통합 이후 확장 예정.
19
+ */
20
+ import { existsSync } from 'node:fs';
21
+ export interface DevCommandOptions {
22
+ readonly cwd?: string;
23
+ /** JSON 라인 모드(자동화 · CI 파싱). */
24
+ readonly json?: boolean;
25
+ /** Ctrl+C 시 Docker Compose 도 down 시킨다(기본 false — 재사용). */
26
+ readonly stopDocker?: boolean;
27
+ /** serve 리슨 포트. 생략 시 config 기본값. */
28
+ readonly port?: number;
29
+ /** serve 리슨 호스트. 생략 시 config 기본값. */
30
+ readonly host?: string;
31
+ /** 서버 재시작 워처 끄기(개별 debug). */
32
+ readonly noWatch?: boolean;
33
+ /** tsc 워치 끄기(개별 debug). */
34
+ readonly noTsc?: boolean;
35
+ /** vue-tsc 워치 끄기(개별 debug). */
36
+ readonly noVueTsc?: boolean;
37
+ /** Docker 자동 기동 끄기(개별 debug · 사용자가 이미 띄운 경우). */
38
+ readonly noDocker?: boolean;
39
+ /** 통합 콘솔 타임스탬프 표시. */
40
+ readonly timestamp?: boolean;
41
+ /** 시그널(테스트 주입 · 기본 process). */
42
+ readonly signals?: {
43
+ on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
44
+ off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
45
+ };
46
+ }
47
+ /**
48
+ * gaon 셀프 경로를 찾는다. 부모가 gaon 으로 실행됐다면 argv[1] 이
49
+ * gaonjs/dist/cli.js (또는 개발 시 packages/gaonjs/src/cli.ts). 자식
50
+ * serve 도 같은 진입점을 재사용한다 — 사용자 환경에서 강제 폴백 없이
51
+ * 동일 CLI 를 다시 부른다.
52
+ */
53
+ declare function resolveSelfCliEntry(): {
54
+ node: string;
55
+ args: string[];
56
+ };
57
+ /**
58
+ * `gaon dev` 진입점. 자식들을 순서대로 띄우고, SIGINT/SIGTERM 로 정리한다.
59
+ * 반환 프라미스는 완전 종료 시 resolve — 프로세스는 자식 워처가 살아 있는
60
+ * 동안 유지된다.
61
+ */
62
+ export declare function runDevCommand(opts?: DevCommandOptions): Promise<void>;
63
+ export declare const _internals: {
64
+ resolveSelfCliEntry: typeof resolveSelfCliEntry;
65
+ fileURLToPathSafe: (u: string) => string;
66
+ existsSync: typeof existsSync;
67
+ };
68
+ export {};
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon dev` — 개발 오케스트레이션 (M9-C · v0.15 §13.5)
3
+ *
4
+ * 하나의 명령이 개발 스택 전체를 띄운다:
5
+ * 1) Docker Compose — pg · redis · nats · mailpit · minio 자동 up -d
6
+ * 2) .gaon 워처 — tables.d.ts · routes.d.ts 재생성 (기존 M3)
7
+ * 3) serve 자식 — 실 웹 서버(Fastify) · 소스 변경 시 재시작
8
+ * 4) tsc / vue-tsc — --watch 모드 · 타입 에러 즉시 리포트
9
+ * 5) restart 워처 — apps/ · domain/ · packages/ 변경 → serve 재시작
10
+ * 6) 통합 콘솔 — [source] 태그·색상별 · JSON 모드 지원
11
+ *
12
+ * The One Way: `gaon dev` 하나로 전부 · 옵션은 개별 debug 용만 노출.
13
+ * fail-closed: Docker 없으면 명확 안내(§7.5.3), 목업 대체 절대 X (§9).
14
+ * ctrl-C: graceful — serve → tsc → 워처 순 정리 · Docker 는 유지(재사용)
15
+ * 또는 --stop-docker 시 down.
16
+ *
17
+ * HMR: v1 Vue 어댑터는 아직 Vite 통합이 없어(§6.4) 서버 재시작 방식만
18
+ * 지원한다. 진짜 Vue HMR 은 어댑터 통합 이후 확장 예정.
19
+ */
20
+ import { spawn } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { existsSync } from 'node:fs';
23
+ import { resolve } from 'node:path';
24
+ import { loadDotEnv, VERSION } from '@gaonjs/core';
25
+ import { registerTsResolve } from '../tsResolve.js';
26
+ import { startDev, resolveDevLayout } from '../dev.js';
27
+ import { generateTablesDts, watchDir } from '@gaonjs/data';
28
+ import { generateRoutesDts } from '@gaonjs/web';
29
+ import { createDevConsole } from '../dev/console.js';
30
+ import { ensureInfra, composeDown } from '../dev/docker.js';
31
+ import { startTscWatchers, killChild } from '../dev/tsc.js';
32
+ import { startRestartWatcher } from '../dev/watcher.js';
33
+ /**
34
+ * gaon 셀프 경로를 찾는다. 부모가 gaon 으로 실행됐다면 argv[1] 이
35
+ * gaonjs/dist/cli.js (또는 개발 시 packages/gaonjs/src/cli.ts). 자식
36
+ * serve 도 같은 진입점을 재사용한다 — 사용자 환경에서 강제 폴백 없이
37
+ * 동일 CLI 를 다시 부른다.
38
+ */
39
+ function resolveSelfCliEntry() {
40
+ const scriptPath = process.argv[1];
41
+ if (!scriptPath) {
42
+ throw new Error('gaon dev · CLI 자기참조 실패: process.argv[1] 이 비어 있습니다.\n' +
43
+ '→ gaon 을 npm 스크립트나 bin 을 통해 실행했는지 확인하세요.');
44
+ }
45
+ // .ts 진입점(모노레포 dev)은 --experimental-strip-types 필요.
46
+ const isTs = scriptPath.endsWith('.ts');
47
+ const flags = isTs ? ['--experimental-strip-types', '--no-warnings'] : [];
48
+ return { node: process.execPath, args: [...flags, scriptPath] };
49
+ }
50
+ /**
51
+ * serve 자식을 하나 띄운다. stdout/stderr 을 통합 콘솔로 파이프하고,
52
+ * exit 콜백에서 종료 코드를 알려준다(비정상 종료 감시용).
53
+ */
54
+ function spawnServe(args) {
55
+ const { node, args: nodeArgs } = resolveSelfCliEntry();
56
+ const serveArgs = ['serve'];
57
+ if (args.json)
58
+ serveArgs.push('--json');
59
+ if (args.port != null)
60
+ serveArgs.push('--port', String(args.port));
61
+ if (args.host != null)
62
+ serveArgs.push('--host', args.host);
63
+ const child = spawn(node, [...nodeArgs, ...serveArgs], {
64
+ cwd: args.cwd,
65
+ // detached=false + own signal handling: 부모가 명시적으로 kill 한다.
66
+ // 셸에서 Ctrl+C 를 누르면 자식도 같이 SIGINT 를 받는데(같은 프로세스
67
+ // 그룹), serve 는 자체 handler 로 graceful 종료하므로 안전.
68
+ stdio: ['ignore', 'pipe', 'pipe'],
69
+ env: { ...process.env },
70
+ });
71
+ child.stdout?.on('data', (b) => args.console.pipe('serve', b, 'info'));
72
+ child.stderr?.on('data', (b) => args.console.pipe('serve', b, 'error'));
73
+ child.on('exit', (code, signal) => args.onExit(code, signal));
74
+ return child;
75
+ }
76
+ /**
77
+ * .gaon 재생성 파이프라인(M3)을 띄운다. 로그는 통합 콘솔로 흘리고,
78
+ * 이 모듈은 층분리를 위해 startDev 를 그대로 재사용한다.
79
+ */
80
+ async function startGaonRegen(args) {
81
+ const layout = resolveDevLayout(args.cwd);
82
+ return await startDev({
83
+ layout,
84
+ regenerateTables: generateTablesDts,
85
+ regenerateRoutes: generateRoutesDts,
86
+ watch: watchDir,
87
+ log: (e) => {
88
+ if (e.kind === 'ready') {
89
+ const parts = [];
90
+ if (e.schema)
91
+ parts.push('schema→tables.d.ts');
92
+ for (const a of e.apps)
93
+ parts.push(`${a}→routes.d.ts`);
94
+ args.console.log('watcher', parts.length
95
+ ? `.gaon 재생성 준비 완료 — ${parts.join(', ')}`
96
+ : `.gaon 재생성 대상 없음 (domain/schema · apps/* 확인)`);
97
+ }
98
+ else if (e.kind === 'regen') {
99
+ args.console.log('watcher', e.target === 'tables' ? '↻ tables.d.ts 재생성' : `↻ ${e.app}/.gaon/routes.d.ts 재생성`);
100
+ }
101
+ else {
102
+ args.console.log('watcher', '.gaon 워처 종료');
103
+ }
104
+ },
105
+ onError: (err) => args.console.log('watcher', err.message, 'error'),
106
+ });
107
+ }
108
+ /**
109
+ * `gaon dev` 진입점. 자식들을 순서대로 띄우고, SIGINT/SIGTERM 로 정리한다.
110
+ * 반환 프라미스는 완전 종료 시 resolve — 프로세스는 자식 워처가 살아 있는
111
+ * 동안 유지된다.
112
+ */
113
+ export async function runDevCommand(opts = {}) {
114
+ const cwd = resolve(opts.cwd ?? process.cwd());
115
+ const json = opts.json ?? false;
116
+ const stopDocker = opts.stopDocker ?? false;
117
+ const signals = opts.signals ?? process;
118
+ registerTsResolve();
119
+ loadDotEnv(cwd);
120
+ const consoleOut = createDevConsole({ json, timestamp: opts.timestamp });
121
+ const startedAt = Date.now();
122
+ consoleOut.log('dev', `gaon dev · 시작 (v${VERSION}) — ${cwd}`);
123
+ // ── 1) Docker Compose 자동 기동 ─────────────────────────────────
124
+ let composeFile;
125
+ if (opts.noDocker !== true) {
126
+ try {
127
+ const infra = await ensureInfra({
128
+ cwd,
129
+ onLine: (line, level) => consoleOut.log('docker', line, level),
130
+ });
131
+ if (infra) {
132
+ composeFile = infra.composeFile;
133
+ consoleOut.log('docker', infra.startedNow
134
+ ? `▶ compose up 완료 — ${infra.status.runningCount}/${infra.status.definedCount} 서비스 · ${infra.composeFile}`
135
+ : `▶ 이미 실행 중(재사용) — ${infra.status.runningCount}/${infra.status.definedCount} 서비스 · ${infra.composeFile}`);
136
+ }
137
+ else {
138
+ consoleOut.log('docker', 'compose 파일이 없어 인프라 자동 기동을 건너뜁니다.');
139
+ }
140
+ }
141
+ catch (err) {
142
+ consoleOut.log('docker', err.message, 'error');
143
+ throw err;
144
+ }
145
+ }
146
+ else {
147
+ consoleOut.log('docker', '--no-docker · 인프라 자동 기동 비활성화');
148
+ }
149
+ // ── 2) .gaon 재생성 워처 ─────────────────────────────────────────
150
+ const regen = await startGaonRegen({ cwd, console: consoleOut });
151
+ // ── 3) tsc / vue-tsc 워치 ────────────────────────────────────────
152
+ const tsc = opts.noTsc && opts.noVueTsc
153
+ ? undefined
154
+ : startTscWatchers({
155
+ cwd,
156
+ enableTsc: opts.noTsc !== true,
157
+ enableVueTsc: opts.noVueTsc !== true,
158
+ onLine: (source, line, level) => consoleOut.log(source, line, level),
159
+ onExit: (source, code) => {
160
+ // 비정상 종료(code≠0) 는 알린다 · 정상(SIGTERM=null) 은 조용히.
161
+ if (code != null && code !== 0) {
162
+ consoleOut.log(source, `[${source}] 비정상 종료 (code=${code})`, 'warn');
163
+ }
164
+ },
165
+ });
166
+ // ── 4) serve 자식 · 재시작 스케줄러 ─────────────────────────────
167
+ let serveChild;
168
+ let shuttingDown = false;
169
+ let restartInFlight;
170
+ const startServe = () => {
171
+ consoleOut.log('serve', '▶ serve 자식 프로세스 spawn');
172
+ return spawnServe({
173
+ cwd,
174
+ json,
175
+ port: opts.port,
176
+ host: opts.host,
177
+ console: consoleOut,
178
+ onExit: (code, signal) => {
179
+ if (shuttingDown)
180
+ return;
181
+ // 사용자 편집으로 인한 재시작 중이면 무시 — restart 흐름이 다시 띄운다.
182
+ if (restartInFlight)
183
+ return;
184
+ if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT')
185
+ return;
186
+ consoleOut.log('serve', `serve 자식이 비정상 종료 (code=${code}, signal=${signal}) — 파일을 저장해 재시도하거나 로그를 확인하세요.`, 'error');
187
+ },
188
+ });
189
+ };
190
+ serveChild = startServe();
191
+ const restartServe = async (changed) => {
192
+ if (shuttingDown)
193
+ return;
194
+ // 이미 재시작 중이면 그 프라미스에 대기(중첩 방지).
195
+ if (restartInFlight)
196
+ return restartInFlight;
197
+ const summary = changed.slice(0, 3).join(', ') + (changed.length > 3 ? ` ... (+${changed.length - 3})` : '');
198
+ consoleOut.log('watcher', `↻ 재시작 — ${summary}`);
199
+ restartInFlight = (async () => {
200
+ try {
201
+ if (serveChild && serveChild.exitCode === null && serveChild.signalCode === null) {
202
+ await killChild(serveChild, 3000);
203
+ }
204
+ if (shuttingDown)
205
+ return;
206
+ serveChild = startServe();
207
+ }
208
+ finally {
209
+ restartInFlight = undefined;
210
+ }
211
+ })();
212
+ return restartInFlight;
213
+ };
214
+ // ── 5) 파일 워처(서버 재시작 트리거) ───────────────────────────
215
+ let restartWatcher;
216
+ if (opts.noWatch !== true) {
217
+ restartWatcher = startRestartWatcher({
218
+ cwd,
219
+ onRestart: (changed) => restartServe(changed),
220
+ onError: (err) => consoleOut.log('watcher', err.message, 'error'),
221
+ });
222
+ consoleOut.log('watcher', restartWatcher.roots.length > 0
223
+ ? `▶ 재시작 워치 — ${restartWatcher.roots.map((r) => shortenPath(r, cwd)).join(', ')}`
224
+ : `▶ 재시작 워치 — 감시 대상 없음(apps/ · domain/ 등 확인)`);
225
+ }
226
+ consoleOut.log('dev', `준비 완료 — 부팅 ${Date.now() - startedAt}ms (Ctrl+C 로 종료)`);
227
+ // ── 6) SIGINT/SIGTERM → graceful ────────────────────────────────
228
+ await new Promise((resolvePromise) => {
229
+ const stop = () => {
230
+ if (shuttingDown)
231
+ return;
232
+ shuttingDown = true;
233
+ signals.off('SIGINT', stop);
234
+ signals.off('SIGTERM', stop);
235
+ consoleOut.log('dev', '종료 신호 수신 — 정리 중...');
236
+ void (async () => {
237
+ try {
238
+ // 순서: 재시작 워처 → serve → tsc → .gaon 워처 → Docker(옵션)
239
+ restartWatcher?.close();
240
+ if (serveChild) {
241
+ await killChild(serveChild, 5000);
242
+ }
243
+ if (tsc) {
244
+ await tsc.stop();
245
+ }
246
+ regen.close();
247
+ if (stopDocker && composeFile) {
248
+ consoleOut.log('docker', '--stop-docker · compose down 실행');
249
+ try {
250
+ await composeDown(composeFile, {
251
+ onLine: (line, level) => consoleOut.log('docker', line, level),
252
+ });
253
+ }
254
+ catch (err) {
255
+ consoleOut.log('docker', err.message, 'error');
256
+ }
257
+ }
258
+ else if (composeFile) {
259
+ consoleOut.log('docker', 'compose 는 유지 · 다음 gaon dev 에서 재사용');
260
+ }
261
+ }
262
+ finally {
263
+ consoleOut.log('dev', 'gaon dev · 종료');
264
+ resolvePromise();
265
+ }
266
+ })();
267
+ };
268
+ signals.on('SIGINT', stop);
269
+ signals.on('SIGTERM', stop);
270
+ });
271
+ }
272
+ /** cwd 기준 상대경로로 짧게(로그 가독성). */
273
+ function shortenPath(p, cwd) {
274
+ const normP = resolve(p);
275
+ const normCwd = resolve(cwd);
276
+ if (normP === normCwd)
277
+ return '.';
278
+ if (normP.startsWith(normCwd + '/'))
279
+ return normP.slice(normCwd.length + 1);
280
+ return normP;
281
+ }
282
+ // (test·doc 목적으로 노출) — 파일이 실제 존재하는지 여부는 호출자 판단.
283
+ export const _internals = {
284
+ resolveSelfCliEntry,
285
+ fileURLToPathSafe: (u) => (u.startsWith('file:') ? fileURLToPath(u) : u),
286
+ existsSync,
287
+ };
@@ -0,0 +1,17 @@
1
+ export interface DbDiffOptions {
2
+ readonly cwd: string;
3
+ readonly dbKey: string;
4
+ readonly json: boolean;
5
+ readonly configPath?: string;
6
+ }
7
+ export interface DbDiffResult {
8
+ readonly exitCode: number;
9
+ readonly text: string;
10
+ readonly json: unknown;
11
+ }
12
+ /**
13
+ * `gaon db diff` — 커넥션 대비 스키마 diff 를 계산해 텍스트·JSON 을 낳는다.
14
+ * 아무것도 적용하지 않는다. exitCode 는 항상 0(변경 없음도 성공 · CI 에서
15
+ * 변경 존재 여부만 보고 싶으면 --json 의 changes 를 읽어라).
16
+ */
17
+ export declare function runDbDiff(opts: DbDiffOptions): Promise<DbDiffResult>;
@@ -0,0 +1,57 @@
1
+ // @gaonjs/cli · `gaon db diff` — 스키마(desired) ↔ DB(current) 차이 계산 (M9-D)
2
+ //
3
+ // 순수 diff — 아무것도 실행하지 않는다(SQL 만 출력). computeMigration 이
4
+ // 방언별 up/down 을 이미 만든다(@gaonjs/data). 이 함수는 배선·표현 담당.
5
+ import { computeMigration } from '@gaonjs/data';
6
+ import { resolveDbTarget } from './resolve.js';
7
+ function opSummary(op) {
8
+ switch (op.kind) {
9
+ case 'createTable':
10
+ return { kind: op.kind, table: op.table.name };
11
+ case 'dropTable':
12
+ return { kind: op.kind, table: op.name };
13
+ case 'addColumn':
14
+ case 'dropColumn':
15
+ case 'alterColumn':
16
+ return { kind: op.kind, table: op.table, column: op.column };
17
+ }
18
+ }
19
+ /**
20
+ * `gaon db diff` — 커넥션 대비 스키마 diff 를 계산해 텍스트·JSON 을 낳는다.
21
+ * 아무것도 적용하지 않는다. exitCode 는 항상 0(변경 없음도 성공 · CI 에서
22
+ * 변경 존재 여부만 보고 싶으면 --json 의 changes 를 읽어라).
23
+ */
24
+ export async function runDbDiff(opts) {
25
+ const target = await resolveDbTarget({
26
+ cwd: opts.cwd,
27
+ dbKey: opts.dbKey,
28
+ configPath: opts.configPath,
29
+ });
30
+ try {
31
+ const plan = await computeMigration(target.db, target.tables, target.dialect);
32
+ const ops = plan.ops.map(opSummary);
33
+ const json = {
34
+ command: 'diff',
35
+ db: opts.dbKey,
36
+ changes: ops.length,
37
+ ops,
38
+ up: plan.up,
39
+ down: plan.down,
40
+ };
41
+ const text = ops.length === 0
42
+ ? ` [${opts.dbKey}] 변경 없음 — 스키마와 DB 가 일치합니다.`
43
+ : ` [${opts.dbKey}] ${ops.length}개 변경\n` +
44
+ ` -- up --\n${indent(plan.up.join('\n'))}\n` +
45
+ ` -- down --\n${indent(plan.down.join('\n'))}`;
46
+ return { exitCode: 0, text, json };
47
+ }
48
+ finally {
49
+ await target.close();
50
+ }
51
+ }
52
+ function indent(text) {
53
+ return text
54
+ .split('\n')
55
+ .map((l) => (l ? ` ${l}` : l))
56
+ .join('\n');
57
+ }
@@ -0,0 +1,4 @@
1
+ export { runDbDiff, type DbDiffOptions, type DbDiffResult, } from './diff.js';
2
+ export { runDbMigrate, type DbMigrateOptions, type DbMigrateResult, } from './migrate.js';
3
+ export { runDbReset, type DbResetOptions, type DbResetResult, } from './reset.js';
4
+ export { resolveDbTarget, type ResolveDbOptions, type ResolvedDbTarget, } from './resolve.js';
@@ -0,0 +1,8 @@
1
+ // @gaonjs/cli · gaon db 서브커맨드 공용 모듈 (M9-D)
2
+ //
3
+ // dispatcher 는 commands/db.ts (runDbCommand) 가 담당하고, 여기서는 개별
4
+ // 서브커맨드 로직·타입만 노출한다. seed 는 기존 db.ts (M8) 를 그대로 위임.
5
+ export { runDbDiff, } from './diff.js';
6
+ export { runDbMigrate, } from './migrate.js';
7
+ export { runDbReset, } from './reset.js';
8
+ export { resolveDbTarget, } from './resolve.js';
@@ -0,0 +1,16 @@
1
+ export interface DbMigrateOptions {
2
+ readonly cwd: string;
3
+ readonly dbKey: string;
4
+ readonly json: boolean;
5
+ readonly dryRun: boolean;
6
+ readonly configPath?: string;
7
+ }
8
+ export interface DbMigrateResult {
9
+ readonly exitCode: number;
10
+ readonly text: string;
11
+ readonly json: unknown;
12
+ }
13
+ /**
14
+ * `gaon db migrate` — 진짜 apply. 실 DB 필요(§9).
15
+ */
16
+ export declare function runDbMigrate(opts: DbMigrateOptions): Promise<DbMigrateResult>;