@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,52 @@
1
+ /**
2
+ * @gaonjs/cli · dev/docker — Docker Compose 자동 오케스트레이션 (M9-C)
3
+ *
4
+ * `gaon dev` 는 개발 인프라(pg · redis · nats · mailpit · minio)를 실 컨테이너
5
+ * 로 띄운다 — CLAUDE.md §9(실 인프라 필수 · 목업 금지) 정합. 이미 떠 있으면
6
+ * 재사용하고, 죽었으면 up -d 한다. Ctrl+C 로 종료해도 컨테이너는 유지되어
7
+ * 다음 실행을 빨리 시작할 수 있다(--stop-docker 옵션 시만 down).
8
+ *
9
+ * fail-closed: Docker 가 없거나 compose.yaml 을 못 찾으면 명확한 수리
10
+ * 안내(§7.5.3 — 에러 = 다음 명령서)를 던진다.
11
+ */
12
+ export interface DockerLocateOptions {
13
+ /** 프로젝트 루트(기본 process.cwd). */
14
+ readonly cwd?: string;
15
+ }
16
+ /** Docker Compose 파일을 찾는 관례 — 프로젝트 루트 → packages/data 순. */
17
+ export declare function findComposeFile(opts?: DockerLocateOptions): string | undefined;
18
+ /** `docker` 바이너리가 PATH 에 있는지. */
19
+ export declare function isDockerAvailable(): Promise<boolean>;
20
+ /** compose 서비스 상태 요약(존재 · 실행 개수). */
21
+ export interface ComposeStatus {
22
+ /** 정의된 서비스 개수(compose config --services). */
23
+ readonly definedCount: number;
24
+ /** 이미 running 상태인 서비스 개수. */
25
+ readonly runningCount: number;
26
+ /** 이미 필요한 만큼 다 떠 있어 up 이 불필요한가. */
27
+ readonly allRunning: boolean;
28
+ }
29
+ /**
30
+ * `docker compose ps --status running --format json` 로 실행 중 서비스를
31
+ * 세고, `docker compose config --services` 로 정의된 서비스와 비교한다.
32
+ */
33
+ export declare function inspectCompose(composeFile: string): Promise<ComposeStatus>;
34
+ export interface ComposeUpOptions {
35
+ /** 진행 로그 훅(자식 stdout/stderr 를 그대로 스트림). */
36
+ readonly onLine?: (line: string, level: 'info' | 'error') => void;
37
+ }
38
+ /** `docker compose up -d` — 이미 running 이면 no-op 에 가깝다. */
39
+ export declare function composeUp(composeFile: string, opts?: ComposeUpOptions): Promise<void>;
40
+ /** `docker compose down` — --stop-docker 옵션 시만 부른다. */
41
+ export declare function composeDown(composeFile: string, opts?: ComposeUpOptions): Promise<void>;
42
+ /**
43
+ * `gaon dev` 진입점에서 부를 상위 오케스트레이터: compose 파일 감지 →
44
+ * Docker 가용 확인 → 상태 조회 → 필요 시 up. 반환값으로 후속 종료
45
+ * 로직(down 여부)에 필요한 정보를 넘긴다.
46
+ */
47
+ export interface EnsureInfraResult {
48
+ readonly composeFile: string;
49
+ readonly startedNow: boolean;
50
+ readonly status: ComposeStatus;
51
+ }
52
+ export declare function ensureInfra(opts?: DockerLocateOptions & ComposeUpOptions): Promise<EnsureInfraResult | undefined>;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @gaonjs/cli · dev/docker — Docker Compose 자동 오케스트레이션 (M9-C)
3
+ *
4
+ * `gaon dev` 는 개발 인프라(pg · redis · nats · mailpit · minio)를 실 컨테이너
5
+ * 로 띄운다 — CLAUDE.md §9(실 인프라 필수 · 목업 금지) 정합. 이미 떠 있으면
6
+ * 재사용하고, 죽었으면 up -d 한다. Ctrl+C 로 종료해도 컨테이너는 유지되어
7
+ * 다음 실행을 빨리 시작할 수 있다(--stop-docker 옵션 시만 down).
8
+ *
9
+ * fail-closed: Docker 가 없거나 compose.yaml 을 못 찾으면 명확한 수리
10
+ * 안내(§7.5.3 — 에러 = 다음 명령서)를 던진다.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { join, resolve } from 'node:path';
14
+ import { spawn } from 'node:child_process';
15
+ /** Docker Compose 파일을 찾는 관례 — 프로젝트 루트 → packages/data 순. */
16
+ export function findComposeFile(opts = {}) {
17
+ const cwd = resolve(opts.cwd ?? process.cwd());
18
+ const candidates = [
19
+ join(cwd, 'compose.yaml'),
20
+ join(cwd, 'compose.yml'),
21
+ join(cwd, 'docker-compose.yaml'),
22
+ join(cwd, 'docker-compose.yml'),
23
+ // 모노레포 관례(현 저장소) — 사용자 프로젝트에는 없을 수 있으니
24
+ // 있으면만 채택한다.
25
+ join(cwd, 'packages', 'data', 'compose.yaml'),
26
+ ];
27
+ for (const p of candidates)
28
+ if (existsSync(p))
29
+ return p;
30
+ return undefined;
31
+ }
32
+ /** `docker` 바이너리가 PATH 에 있는지. */
33
+ export async function isDockerAvailable() {
34
+ return runOnce('docker', ['--version']).then((r) => r.exitCode === 0, () => false);
35
+ }
36
+ /**
37
+ * `docker compose ps --status running --format json` 로 실행 중 서비스를
38
+ * 세고, `docker compose config --services` 로 정의된 서비스와 비교한다.
39
+ */
40
+ export async function inspectCompose(composeFile) {
41
+ const defined = await runOnce('docker', ['compose', '-f', composeFile, 'config', '--services']);
42
+ if (defined.exitCode !== 0) {
43
+ throw new Error(`Docker Compose 정의를 읽지 못했습니다 (${composeFile}).\n` +
44
+ `→ 이 파일이 유효한 compose 형식인지 확인하세요.\n` +
45
+ `→ 수동 확인: docker compose -f ${composeFile} config --services\n` +
46
+ (defined.stderr ? `stderr: ${defined.stderr.trim()}` : ''));
47
+ }
48
+ const definedList = defined.stdout
49
+ .split('\n')
50
+ .map((s) => s.trim())
51
+ .filter((s) => s.length > 0);
52
+ // ps --format json 은 라인 당 JSON — 서비스가 없으면 빈 출력.
53
+ const ps = await runOnce('docker', [
54
+ 'compose',
55
+ '-f',
56
+ composeFile,
57
+ 'ps',
58
+ '--status',
59
+ 'running',
60
+ '--format',
61
+ 'json',
62
+ ]);
63
+ const runningNames = new Set();
64
+ if (ps.exitCode === 0) {
65
+ for (const line of ps.stdout.split('\n')) {
66
+ const t = line.trim();
67
+ if (!t)
68
+ continue;
69
+ try {
70
+ const rec = JSON.parse(t);
71
+ if (rec.Service)
72
+ runningNames.add(rec.Service);
73
+ }
74
+ catch {
75
+ // ps 가 배열 형태로 오는 이전 버전 호환.
76
+ try {
77
+ const arr = JSON.parse(t);
78
+ for (const r of arr)
79
+ if (r.Service)
80
+ runningNames.add(r.Service);
81
+ }
82
+ catch {
83
+ // 파싱 실패한 줄은 무시(포맷 편차 방어).
84
+ }
85
+ }
86
+ }
87
+ }
88
+ const running = definedList.filter((n) => runningNames.has(n)).length;
89
+ return {
90
+ definedCount: definedList.length,
91
+ runningCount: running,
92
+ allRunning: definedList.length > 0 && running === definedList.length,
93
+ };
94
+ }
95
+ /** `docker compose up -d` — 이미 running 이면 no-op 에 가깝다. */
96
+ export async function composeUp(composeFile, opts = {}) {
97
+ const res = await runStreamed('docker', ['compose', '-f', composeFile, 'up', '-d'], opts.onLine);
98
+ if (res.exitCode !== 0) {
99
+ throw new Error(`Docker Compose up 이 실패했습니다 (${composeFile}, exit ${res.exitCode}).\n` +
100
+ `→ 로그를 확인하세요: docker compose -f ${composeFile} logs\n` +
101
+ `→ 포트 충돌이면 compose.yaml 의 포트를 조정하거나 기존 컨테이너를 정리하세요.`);
102
+ }
103
+ }
104
+ /** `docker compose down` — --stop-docker 옵션 시만 부른다. */
105
+ export async function composeDown(composeFile, opts = {}) {
106
+ const res = await runStreamed('docker', ['compose', '-f', composeFile, 'down'], opts.onLine);
107
+ if (res.exitCode !== 0) {
108
+ throw new Error(`Docker Compose down 이 실패했습니다 (${composeFile}, exit ${res.exitCode}).\n` +
109
+ `→ 수동 정리: docker compose -f ${composeFile} down`);
110
+ }
111
+ }
112
+ export async function ensureInfra(opts = {}) {
113
+ const composeFile = findComposeFile({ cwd: opts.cwd });
114
+ if (!composeFile) {
115
+ // compose 파일이 없는 프로젝트는 인프라 오케스트레이션 스킵.
116
+ // (사용자 프로젝트가 아직 인프라를 정의하지 않은 초기 상태 가능.)
117
+ return undefined;
118
+ }
119
+ const hasDocker = await isDockerAvailable();
120
+ if (!hasDocker) {
121
+ throw new Error(`Docker 를 찾을 수 없습니다.\n` +
122
+ `compose 파일이 있지만 (${composeFile}) docker 바이너리가 PATH 에 없습니다.\n` +
123
+ `→ Docker Desktop 을 설치하고 실행하세요: https://www.docker.com/products/docker-desktop/\n` +
124
+ `→ 이미 설치되어 있다면 실행 중인지 확인하세요 (docker ps).\n` +
125
+ `→ 인프라 없이 진행하려면 compose.yaml 을 이동/제거하세요 — gaon dev 는 감지 시 자동 기동합니다.`);
126
+ }
127
+ const status = await inspectCompose(composeFile);
128
+ if (status.allRunning) {
129
+ return { composeFile, startedNow: false, status };
130
+ }
131
+ await composeUp(composeFile, { onLine: opts.onLine });
132
+ return { composeFile, startedNow: true, status: await inspectCompose(composeFile) };
133
+ }
134
+ /** 한 번 실행하고 결과를 모아 반환(단순 조회용). */
135
+ function runOnce(cmd, args) {
136
+ return new Promise((resolvePromise, rejectPromise) => {
137
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
138
+ const out = [];
139
+ const err = [];
140
+ child.stdout.on('data', (b) => out.push(b.toString('utf8')));
141
+ child.stderr.on('data', (b) => err.push(b.toString('utf8')));
142
+ child.on('error', rejectPromise);
143
+ child.on('close', (code) => resolvePromise({ exitCode: code ?? -1, stdout: out.join(''), stderr: err.join('') }));
144
+ });
145
+ }
146
+ /** 실행하며 라인 단위로 스트림(진행 로그를 콘솔로 흘리기 위함). */
147
+ function runStreamed(cmd, args, onLine) {
148
+ return new Promise((resolvePromise, rejectPromise) => {
149
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
150
+ const emit = (chunk, level) => {
151
+ if (!onLine)
152
+ return;
153
+ for (const line of chunk.toString('utf8').split('\n')) {
154
+ if (line.trim().length > 0)
155
+ onLine(line, level);
156
+ }
157
+ };
158
+ child.stdout.on('data', (b) => emit(b, 'info'));
159
+ child.stderr.on('data', (b) => emit(b, 'error'));
160
+ child.on('error', rejectPromise);
161
+ child.on('close', (code) => resolvePromise({ exitCode: code ?? -1 }));
162
+ });
163
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @gaonjs/cli · dev — `gaon dev` 서브모듈 public export (M9-C)
3
+ *
4
+ * 상위 index.ts 는 이 모듈만 재수출한다 — 오케스트레이터 진입점은
5
+ * commands/dev.ts 의 runDevCommand.
6
+ */
7
+ export { createDevConsole } from './console.js';
8
+ export type { DevConsole, DevConsoleOptions, DevSource, DevLevel } from './console.js';
9
+ export { findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, } from './docker.js';
10
+ export type { ComposeStatus, ComposeUpOptions, EnsureInfraResult, DockerLocateOptions } from './docker.js';
11
+ export { startTscWatchers, killChild } from './tsc.js';
12
+ export type { TscWatcherOptions, TscWatcherHandle } from './tsc.js';
13
+ export { startRestartWatcher, isRestartChange, resolveWatchRoots } from './watcher.js';
14
+ export type { RestartWatcherOptions, RestartWatcherHandle } from './watcher.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @gaonjs/cli · dev — `gaon dev` 서브모듈 public export (M9-C)
3
+ *
4
+ * 상위 index.ts 는 이 모듈만 재수출한다 — 오케스트레이터 진입점은
5
+ * commands/dev.ts 의 runDevCommand.
6
+ */
7
+ export { createDevConsole } from './console.js';
8
+ export { findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, } from './docker.js';
9
+ export { startTscWatchers, killChild } from './tsc.js';
10
+ export { startRestartWatcher, isRestartChange, resolveWatchRoots } from './watcher.js';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @gaonjs/cli · dev/tsc — tsc / vue-tsc watch 스포너 (M9-C)
3
+ *
4
+ * `gaon dev` 는 백그라운드에서 두 개의 타입 검사기를 --watch 모드로 띄운다:
5
+ * - tsc : 프로젝트 전체 TS 소스 (packages/*, apps/*, domain/*)
6
+ * - vue-tsc : .vue 템플릿 포함 타입 검사
7
+ *
8
+ * 두 프로세스 모두 stdout/stderr 를 통합 콘솔로 파이프 — 타입 에러가 나는
9
+ * 순간 개발자가 즉시 알 수 있다. tsconfig.json 은 프로젝트 관례대로 cwd
10
+ * 의 것을 쓴다(없으면 스킵). SIGINT 시 두 자식을 확실히 종료한다.
11
+ *
12
+ * 무의존 원칙 — child_process.spawn 만 쓴다. node_modules/.bin 을 우선
13
+ * 탐색하고, 없으면 npx 로 폴백한다(사용자 환경 편차 방어).
14
+ */
15
+ import { type ChildProcess } from 'node:child_process';
16
+ export interface TscWatcherOptions {
17
+ /** 프로젝트 루트(tsconfig.json 위치). */
18
+ readonly cwd?: string;
19
+ /** tsconfig 경로(옵션). 생략 시 cwd/tsconfig.json. */
20
+ readonly project?: string;
21
+ /** stdout · stderr 라인 훅(통합 콘솔로 파이프). */
22
+ readonly onLine?: (source: 'tsc' | 'vue-tsc', line: string, level: 'info' | 'error') => void;
23
+ /** 자식 죽었을 때(비정상 종료 감시). */
24
+ readonly onExit?: (source: 'tsc' | 'vue-tsc', code: number | null) => void;
25
+ }
26
+ export interface TscWatcherHandle {
27
+ /** SIGTERM 후 잠깐 대기 · 안 죽으면 SIGKILL. */
28
+ stop(): Promise<void>;
29
+ /** 진단용 — 살아 있는 자식만 세운다. */
30
+ readonly children: readonly ChildProcess[];
31
+ }
32
+ /**
33
+ * tsc + vue-tsc 워처 두 개를 띄운다. project 파일이 없으면 스킵.
34
+ * 개별 워처를 끄는 옵션(no-tsc · no-vue-tsc)은 호출자가 결정한다.
35
+ */
36
+ export declare function startTscWatchers(opts: TscWatcherOptions & {
37
+ readonly enableTsc?: boolean;
38
+ readonly enableVueTsc?: boolean;
39
+ }): TscWatcherHandle;
40
+ /** SIGTERM 후 timeoutMs 안에 안 죽으면 SIGKILL. */
41
+ export declare function killChild(child: ChildProcess, timeoutMs: number): Promise<void>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @gaonjs/cli · dev/tsc — tsc / vue-tsc watch 스포너 (M9-C)
3
+ *
4
+ * `gaon dev` 는 백그라운드에서 두 개의 타입 검사기를 --watch 모드로 띄운다:
5
+ * - tsc : 프로젝트 전체 TS 소스 (packages/*, apps/*, domain/*)
6
+ * - vue-tsc : .vue 템플릿 포함 타입 검사
7
+ *
8
+ * 두 프로세스 모두 stdout/stderr 를 통합 콘솔로 파이프 — 타입 에러가 나는
9
+ * 순간 개발자가 즉시 알 수 있다. tsconfig.json 은 프로젝트 관례대로 cwd
10
+ * 의 것을 쓴다(없으면 스킵). SIGINT 시 두 자식을 확실히 종료한다.
11
+ *
12
+ * 무의존 원칙 — child_process.spawn 만 쓴다. node_modules/.bin 을 우선
13
+ * 탐색하고, 없으면 npx 로 폴백한다(사용자 환경 편차 방어).
14
+ */
15
+ import { existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { spawn } from 'node:child_process';
18
+ /** node_modules/.bin/<name> 이 있으면 그 경로, 없으면 undefined. */
19
+ function findLocalBin(cwd, name) {
20
+ const local = resolve(cwd, 'node_modules', '.bin', name);
21
+ if (existsSync(local))
22
+ return local;
23
+ // 모노레포 루트로 한 단계 위 탐색(사용자 프로젝트가 workspace 인 경우).
24
+ const parent = resolve(cwd, '..', '..', 'node_modules', '.bin', name);
25
+ if (existsSync(parent))
26
+ return parent;
27
+ return undefined;
28
+ }
29
+ /** tsc 또는 vue-tsc 를 --watch 로 띄운다. bin 을 못 찾으면 undefined. */
30
+ function spawnWatcher(source, cwd, project, onLine) {
31
+ const bin = findLocalBin(cwd, source);
32
+ if (!bin)
33
+ return undefined;
34
+ // --noEmit 은 워치도 지원(파일만 검사). --pretty false 로 색상 코드를
35
+ // 우리 콘솔에 맡긴다(중복 색상 방지).
36
+ const args = ['--noEmit', '--watch', '--preserveWatchOutput', '--pretty', 'false', '-p', project];
37
+ const child = spawn(bin, args, {
38
+ cwd,
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ env: { ...process.env, FORCE_COLOR: '0' },
41
+ });
42
+ const forward = (chunk, level) => {
43
+ if (!onLine)
44
+ return;
45
+ for (const line of chunk.toString('utf8').split('\n')) {
46
+ const t = line.trimEnd();
47
+ if (t.length === 0)
48
+ continue;
49
+ onLine(source, t, level);
50
+ }
51
+ };
52
+ child.stdout?.on('data', (b) => forward(b, 'info'));
53
+ child.stderr?.on('data', (b) => forward(b, 'error'));
54
+ return child;
55
+ }
56
+ /**
57
+ * tsc + vue-tsc 워처 두 개를 띄운다. project 파일이 없으면 스킵.
58
+ * 개별 워처를 끄는 옵션(no-tsc · no-vue-tsc)은 호출자가 결정한다.
59
+ */
60
+ export function startTscWatchers(opts) {
61
+ const cwd = resolve(opts.cwd ?? process.cwd());
62
+ const project = opts.project ?? join(cwd, 'tsconfig.json');
63
+ const children = [];
64
+ if (!existsSync(project)) {
65
+ // 사용자 프로젝트가 아직 tsconfig 를 갖고 있지 않으면 조용히 스킵.
66
+ // (신규 프로젝트 초기 상태 방어 — 에러가 아니라 no-op.)
67
+ return { stop: async () => { }, children };
68
+ }
69
+ if (opts.enableTsc !== false) {
70
+ const c = spawnWatcher('tsc', cwd, project, opts.onLine);
71
+ if (c) {
72
+ children.push(c);
73
+ c.on('exit', (code) => opts.onExit?.('tsc', code));
74
+ }
75
+ else {
76
+ opts.onLine?.('tsc', `tsc 를 찾지 못했습니다 (node_modules/.bin/tsc). → pnpm install 후 다시 시도하세요.`, 'error');
77
+ }
78
+ }
79
+ if (opts.enableVueTsc !== false) {
80
+ const c = spawnWatcher('vue-tsc', cwd, project, opts.onLine);
81
+ if (c) {
82
+ children.push(c);
83
+ c.on('exit', (code) => opts.onExit?.('vue-tsc', code));
84
+ }
85
+ else {
86
+ // vue-tsc 는 프론트가 Vue 가 아니면 없을 수 있음 — 정보성 로그.
87
+ opts.onLine?.('vue-tsc', `vue-tsc 를 찾지 못했습니다 (node_modules/.bin/vue-tsc). Vue 프로젝트라면 pnpm install 로 설치하세요.`, 'info');
88
+ }
89
+ }
90
+ return {
91
+ children,
92
+ async stop() {
93
+ // SIGTERM → 2초 유예 → SIGKILL. tsc/vue-tsc 는 SIGTERM 에 잘 반응한다.
94
+ await Promise.all(children.map((c) => killChild(c, 2000)));
95
+ },
96
+ };
97
+ }
98
+ /** SIGTERM 후 timeoutMs 안에 안 죽으면 SIGKILL. */
99
+ export function killChild(child, timeoutMs) {
100
+ return new Promise((resolvePromise) => {
101
+ if (child.exitCode !== null || child.signalCode !== null) {
102
+ resolvePromise();
103
+ return;
104
+ }
105
+ const to = setTimeout(() => {
106
+ if (child.exitCode === null && child.signalCode === null) {
107
+ try {
108
+ child.kill('SIGKILL');
109
+ }
110
+ catch {
111
+ /* 이미 죽었으면 무시 */
112
+ }
113
+ }
114
+ }, timeoutMs);
115
+ child.once('exit', () => {
116
+ clearTimeout(to);
117
+ resolvePromise();
118
+ });
119
+ try {
120
+ child.kill('SIGTERM');
121
+ }
122
+ catch {
123
+ clearTimeout(to);
124
+ resolvePromise();
125
+ }
126
+ });
127
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @gaonjs/cli · dev/watcher — 서버 재시작 트리거 워처 (M9-C)
3
+ *
4
+ * `gaon dev` 오케스트레이터는 서버 코드(apps/*, domain/*, gaon.config.ts,
5
+ * packages/*)의 변경을 감시해 서버 자식 프로세스를 재시작한다. 이 모듈은
6
+ * "무엇을 감시하고, 무엇을 무시할지"만 결정한다 — 재시작 정책은 호출자가
7
+ * 콜백에서 처리한다(관심사 분리).
8
+ *
9
+ * .gaon 파이프라인(tables.d.ts · routes.d.ts 재생성)은 별도(dev.ts 의
10
+ * startDev) 라 여기선 다루지 않는다. 그쪽 파일들은 이 워처의 무시 목록에
11
+ * 들어있어 재시작이 유발되지 않는다(무한 루프 방지).
12
+ *
13
+ * 무의존 — @gaonjs/data 의 watchDir 프리미티브(node:fs.watch 기반)를 재사용.
14
+ */
15
+ export interface RestartWatcherOptions {
16
+ /** 프로젝트 루트(기본 process.cwd). */
17
+ readonly cwd?: string;
18
+ /**
19
+ * 변경이 감지되면 호출. changed 는 감시 루트 기준 상대경로 목록이다.
20
+ * 호출자가 여기서 서버 자식을 재시작한다.
21
+ */
22
+ readonly onRestart: (changed: string[]) => void | Promise<void>;
23
+ /** 에러 훅. 생략 시 무시(런타임을 죽이지 않기 위함). */
24
+ readonly onError?: (err: Error) => void;
25
+ /** debounce(ms). 저장 폭풍을 한 배치로 묶기 위해. 기본 200ms. */
26
+ readonly debounceMs?: number;
27
+ }
28
+ export interface RestartWatcherHandle {
29
+ /** 감시 목록(진단 · 테스트용). */
30
+ readonly roots: readonly string[];
31
+ close(): void;
32
+ }
33
+ /**
34
+ * 재시작을 유발하는 변경인가.
35
+ * - 확장자가 RESTART_EXTS 중 하나
36
+ * - 경로 조각에 IGNORE_SEGMENTS 가 없음
37
+ * - .test.ts / .spec.ts 는 서버 실행에 영향 없으니 무시
38
+ * - .d.ts 는 생성물이므로 무시(스캐폴드/재생성이 만듦)
39
+ */
40
+ export declare function isRestartChange(rel: string): boolean;
41
+ /**
42
+ * 감시할 루트 후보를 프로젝트 관례로 해석한다. 존재하는 것만 담아 반환.
43
+ */
44
+ export declare function resolveWatchRoots(cwd: string): string[];
45
+ /**
46
+ * 서버 재시작 워처를 띄운다. 각 루트에 대해 recursive watcher 를 하나씩
47
+ * 걸고, 필터로 재시작 파일만 통과시킨다. 파일 하나(gaon.config.ts)를 감시할
48
+ * 때도 watchDir 은 부모 디렉터리를 잡고 filter 로 걸러낸다.
49
+ */
50
+ export declare function startRestartWatcher(opts: RestartWatcherOptions): RestartWatcherHandle;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @gaonjs/cli · dev/watcher — 서버 재시작 트리거 워처 (M9-C)
3
+ *
4
+ * `gaon dev` 오케스트레이터는 서버 코드(apps/*, domain/*, gaon.config.ts,
5
+ * packages/*)의 변경을 감시해 서버 자식 프로세스를 재시작한다. 이 모듈은
6
+ * "무엇을 감시하고, 무엇을 무시할지"만 결정한다 — 재시작 정책은 호출자가
7
+ * 콜백에서 처리한다(관심사 분리).
8
+ *
9
+ * .gaon 파이프라인(tables.d.ts · routes.d.ts 재생성)은 별도(dev.ts 의
10
+ * startDev) 라 여기선 다루지 않는다. 그쪽 파일들은 이 워처의 무시 목록에
11
+ * 들어있어 재시작이 유발되지 않는다(무한 루프 방지).
12
+ *
13
+ * 무의존 — @gaonjs/data 의 watchDir 프리미티브(node:fs.watch 기반)를 재사용.
14
+ */
15
+ import { existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { watchDir } from '@gaonjs/data';
18
+ // 재시작을 유발하는 확장자. 다른 것(이미지 · 마크다운 등)은 무시.
19
+ const RESTART_EXTS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json', '.vue'];
20
+ // 재시작을 유발하지 않는 경로 조각(생성물 · 캐시 · 테스트 산출물).
21
+ const IGNORE_SEGMENTS = ['.gaon', 'node_modules', 'dist', '.git', 'coverage', '.turbo'];
22
+ /**
23
+ * 재시작을 유발하는 변경인가.
24
+ * - 확장자가 RESTART_EXTS 중 하나
25
+ * - 경로 조각에 IGNORE_SEGMENTS 가 없음
26
+ * - .test.ts / .spec.ts 는 서버 실행에 영향 없으니 무시
27
+ * - .d.ts 는 생성물이므로 무시(스캐폴드/재생성이 만듦)
28
+ */
29
+ export function isRestartChange(rel) {
30
+ if (!rel)
31
+ return false;
32
+ const norm = rel.replace(/\\/g, '/');
33
+ for (const seg of IGNORE_SEGMENTS) {
34
+ if (norm.startsWith(`${seg}/`) || norm.includes(`/${seg}/`) || norm === seg)
35
+ return false;
36
+ }
37
+ if (norm.endsWith('.d.ts'))
38
+ return false;
39
+ if (/\.(test|spec)\.[cm]?[tj]sx?$/.test(norm))
40
+ return false;
41
+ return RESTART_EXTS.some((ext) => norm.endsWith(ext));
42
+ }
43
+ /**
44
+ * 감시할 루트 후보를 프로젝트 관례로 해석한다. 존재하는 것만 담아 반환.
45
+ */
46
+ export function resolveWatchRoots(cwd) {
47
+ const root = resolve(cwd);
48
+ const candidates = [
49
+ join(root, 'apps'),
50
+ join(root, 'domain'),
51
+ join(root, 'shared'),
52
+ join(root, 'packages'),
53
+ join(root, 'gaon.config.ts'),
54
+ join(root, 'gaon.config.js'),
55
+ ];
56
+ return candidates.filter((p) => existsSync(p));
57
+ }
58
+ /**
59
+ * 서버 재시작 워처를 띄운다. 각 루트에 대해 recursive watcher 를 하나씩
60
+ * 걸고, 필터로 재시작 파일만 통과시킨다. 파일 하나(gaon.config.ts)를 감시할
61
+ * 때도 watchDir 은 부모 디렉터리를 잡고 filter 로 걸러낸다.
62
+ */
63
+ export function startRestartWatcher(opts) {
64
+ const cwd = resolve(opts.cwd ?? process.cwd());
65
+ const debounceMs = opts.debounceMs ?? 200;
66
+ const onError = opts.onError ?? (() => { });
67
+ const roots = resolveWatchRoots(cwd);
68
+ const handles = [];
69
+ // 디렉터리 vs 단일 파일 구분 — 파일이면 부모 디렉터리를 감시하고
70
+ // filter 에서 정확 매칭한다.
71
+ for (const rootPath of roots) {
72
+ const isFile = rootPath.endsWith('.ts') || rootPath.endsWith('.js');
73
+ const watchTarget = isFile ? resolve(rootPath, '..') : rootPath;
74
+ const fileBaseName = isFile ? rootPath.slice(watchTarget.length + 1) : undefined;
75
+ handles.push(watchDir(watchTarget, {
76
+ debounceMs,
77
+ filter: (rel) => {
78
+ if (fileBaseName)
79
+ return rel === fileBaseName;
80
+ return isRestartChange(rel);
81
+ },
82
+ onChange: async (changed) => {
83
+ await opts.onRestart(changed);
84
+ },
85
+ onError,
86
+ }));
87
+ }
88
+ return {
89
+ roots,
90
+ close() {
91
+ for (const h of handles)
92
+ h.close();
93
+ },
94
+ };
95
+ }
package/dist/dev.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WatchHandle, type WatchOptions } from '@gaonjs/data';
1
+ import type { WatchHandle, WatchOptions } from '@gaonjs/data';
2
2
  export interface DevApp {
3
3
  readonly name: string;
4
4
  readonly appDir: string;
@@ -42,18 +42,3 @@ export interface DevHandle {
42
42
  export declare function startDev(deps: DevDeps): Promise<DevHandle>;
43
43
  /** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
44
44
  export declare function resolveDevLayout(cwd: string): DevLayout;
45
- export interface DevCommandOptions {
46
- readonly cwd?: string;
47
- readonly json?: boolean;
48
- /** 프로세스 시그널 등록·해제(테스트에서 주입 가능). 기본 process. */
49
- readonly signals?: {
50
- on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
51
- off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
52
- };
53
- }
54
- /**
55
- * `gaon dev` 진입점. cwd 관례로 레이아웃을 엮고 워치 루프를 띄운 뒤
56
- * SIGINT/SIGTERM 에 graceful stop. 프로세스는 워처가 살아 있는 동안
57
- * 유지된다(활성 핸들). 반환 프라미스는 종료 시 resolve.
58
- */
59
- export declare function runDevCommand(opts?: DevCommandOptions): Promise<void>;