@gaonjs/cli 0.3.0 → 0.5.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 (81) 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/check.d.ts +31 -0
  6. package/dist/commands/check.js +223 -0
  7. package/dist/commands/console.d.ts +46 -0
  8. package/dist/commands/console.js +129 -0
  9. package/dist/commands/db.d.ts +20 -0
  10. package/dist/commands/db.js +74 -0
  11. package/dist/commands/dev.d.ts +68 -0
  12. package/dist/commands/dev.js +287 -0
  13. package/dist/commands/new.d.ts +45 -0
  14. package/dist/commands/new.js +274 -0
  15. package/dist/commands/test.d.ts +11 -0
  16. package/dist/commands/test.js +119 -0
  17. package/dist/db/diff.d.ts +17 -0
  18. package/dist/db/diff.js +57 -0
  19. package/dist/db/index.d.ts +4 -0
  20. package/dist/db/index.js +8 -0
  21. package/dist/db/migrate.d.ts +16 -0
  22. package/dist/db/migrate.js +173 -0
  23. package/dist/db/reset.d.ts +18 -0
  24. package/dist/db/reset.js +150 -0
  25. package/dist/db/resolve.d.ts +32 -0
  26. package/dist/db/resolve.js +130 -0
  27. package/dist/dev/console.d.ts +39 -0
  28. package/dist/dev/console.js +100 -0
  29. package/dist/dev/docker.d.ts +52 -0
  30. package/dist/dev/docker.js +163 -0
  31. package/dist/dev/index.d.ts +14 -0
  32. package/dist/dev/index.js +10 -0
  33. package/dist/dev/tsc.d.ts +41 -0
  34. package/dist/dev/tsc.js +127 -0
  35. package/dist/dev/watcher.d.ts +50 -0
  36. package/dist/dev/watcher.js +95 -0
  37. package/dist/dev.d.ts +1 -16
  38. package/dist/dev.js +10 -66
  39. package/dist/doctor/no-auto-import.d.ts +10 -0
  40. package/dist/doctor/no-auto-import.js +158 -0
  41. package/dist/doctor/reporter.d.ts +1 -1
  42. package/dist/doctor/reporter.js +16 -3
  43. package/dist/doctor/setup.d.ts +26 -0
  44. package/dist/doctor/setup.js +52 -0
  45. package/dist/doctor/shared-composable-purity.d.ts +8 -0
  46. package/dist/doctor/shared-composable-purity.js +164 -0
  47. package/dist/doctor/types.d.ts +14 -1
  48. package/dist/doctor/types.js +10 -5
  49. package/dist/doctor.d.ts +21 -5
  50. package/dist/doctor.js +77 -8
  51. package/dist/index.d.ts +9 -2
  52. package/dist/index.js +175 -27
  53. package/dist/templates/index.d.ts +23 -0
  54. package/dist/templates/index.js +66 -0
  55. package/dist/templates/index.ts +85 -0
  56. package/dist/templates/project/.env.example.tpl +18 -0
  57. package/dist/templates/project/.gitignore.tpl +24 -0
  58. package/dist/templates/project/.npmrc.tpl +4 -0
  59. package/dist/templates/project/CLAUDE.md.tpl +119 -0
  60. package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
  61. package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
  62. package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
  63. package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
  64. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
  65. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
  66. package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
  67. package/dist/templates/project/docker-compose.yaml.tpl +73 -0
  68. package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
  69. package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
  70. package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
  71. package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
  72. package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
  73. package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
  74. package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
  75. package/dist/templates/project/gaon.config.ts.tpl +27 -0
  76. package/dist/templates/project/package.json.tpl +27 -0
  77. package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
  78. package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
  79. package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
  80. package/dist/templates/project/tsconfig.json.tpl +25 -0
  81. package/package.json +5 -5
@@ -0,0 +1,130 @@
1
+ // @gaonjs/cli · gaon db 공통 커넥션·스키마 해석 (M9-D)
2
+ //
3
+ // gaon.config.ts → db.<키> 로 커넥션을 조립하고, domain/schema/*.ts 를
4
+ // 스캔해 이 커넥션의 TableDef[] 를 돌려준다. diff/migrate/reset 이 이 값을
5
+ // 공유한다.
6
+ //
7
+ // config 우선순위:
8
+ // 1) opts.config (사용자 명시 --config <path>)
9
+ // 2) cwd/gaon.config.ts (findConfigPath 관례)
10
+ // config 도 GAON_DATABASE_URL 도 없으면 수리 안내 에러(§7.5.3).
11
+ //
12
+ // domain/schema/ 가 없으면 tables=[] (diff 는 dropTable 만 있는 상태).
13
+ // 스키마는 { db: '<키>' } 로 커넥션 바인딩된 것만 이 대상에 포함한다(§4.5).
14
+ import { existsSync } from 'node:fs';
15
+ import { join, resolve as resolvePath } from 'node:path';
16
+ import { pathToFileURL } from 'node:url';
17
+ import { env, EnvError } from '@gaonjs/core';
18
+ import { findConfigPath, loadGaonConfig } from '@gaonjs/config';
19
+ import { createDb, registerConnection, destroyAllConnections, scanSchemaDir, } from '@gaonjs/data';
20
+ import { dialectFor } from '@gaonjs/data';
21
+ import { registerTsResolve } from '../tsResolve.js';
22
+ /** URL 에서 어댑터를 추정한다(seed·work 와 같은 관례). */
23
+ function dbConfigFromUrl(url) {
24
+ if (url.startsWith('mysql://') || url.startsWith('mariadb://')) {
25
+ return { adapter: 'mysql', url, poolMax: 4 };
26
+ }
27
+ return { adapter: 'postgres', url, poolMax: 4 };
28
+ }
29
+ /**
30
+ * TableDef 정의를 스캔한다. 스키마 디렉터리가 없으면 [] 를 돌려준다.
31
+ * 스캔 결과는 커넥션 키로 걸러 diff·migrate 대상만 남긴다(§4.5).
32
+ */
33
+ async function scanTables(cwd, dbKey) {
34
+ const schemaDir = join(cwd, 'domain', 'schema');
35
+ if (!existsSync(schemaDir))
36
+ return [];
37
+ // outDir 은 이 함수에서 실제로 파일을 쓰지 않으므로 관례 경로만 지정한다
38
+ // (scanSchemaDir 은 import 지정자 계산에만 사용). collectTables 는
39
+ // DiscoveredTable(이름·경로) 만 돌려주므로, 실제 TableDef 값은 모듈 네임스페이스에서
40
+ // 다시 꺼내야 한다 — 커넥션 키(§4.5)로 필터링해 이 db 것만 남긴다.
41
+ const outDir = join(cwd, '.gaon');
42
+ const modules = await scanSchemaDir(schemaDir, outDir);
43
+ const tables = [];
44
+ for (const mod of modules) {
45
+ for (const [, value] of Object.entries(mod.ns)) {
46
+ if (isTableDef(value) && value.db === dbKey)
47
+ tables.push(value);
48
+ }
49
+ }
50
+ return tables.sort((a, b) => a.name.localeCompare(b.name));
51
+ }
52
+ function isTableDef(v) {
53
+ return (typeof v === 'object' &&
54
+ v !== null &&
55
+ 'name' in v &&
56
+ 'defs' in v &&
57
+ 'db' in v &&
58
+ typeof v.name === 'string');
59
+ }
60
+ /**
61
+ * gaon.config.ts 를 로드해 target db 커넥션을 등록하고 스키마 테이블을 모아 온다.
62
+ *
63
+ * 커넥션 결정 순서:
64
+ * 1) config.db[dbKey] 가 있으면 그 어댑터·URL 사용
65
+ * 2) dbKey='main' 이고 GAON_DATABASE_URL 이 있으면 그 URL 로 main 을 세움
66
+ * 3) 그 외 → 수리 안내 에러
67
+ */
68
+ export async function resolveDbTarget(opts) {
69
+ const cwd = resolvePath(opts.cwd);
70
+ registerTsResolve();
71
+ // 사용자가 --config <path> 를 준 경우 그 경로에서 default export 를 로드한다.
72
+ // 없으면 cwd 관례.
73
+ let configPath;
74
+ let config;
75
+ if (opts.configPath) {
76
+ configPath = resolvePath(opts.configPath);
77
+ if (!existsSync(configPath)) {
78
+ throw new Error(`[gaon db] --config 로 지정한 파일이 없습니다: ${configPath}\n` +
79
+ `→ 경로를 확인하거나 --config 없이 실행해 cwd/gaon.config.ts 를 쓰세요.`);
80
+ }
81
+ const mod = (await import(pathToFileURL(configPath).href));
82
+ if (!mod.default || typeof mod.default !== 'object') {
83
+ throw new Error(`[gaon db] --config 파일의 default export 가 객체가 아닙니다: ${configPath}\n` +
84
+ `→ export default defineConfig({ db: { main: { adapter: 'postgres', url: '...' } } }) 형태로 두세요.`);
85
+ }
86
+ config = mod.default;
87
+ }
88
+ else {
89
+ configPath = findConfigPath(cwd);
90
+ config = await loadGaonConfig(cwd);
91
+ }
92
+ const dbKey = opts.dbKey;
93
+ const cfg = config.db?.[dbKey];
94
+ let connCfg;
95
+ if (cfg) {
96
+ connCfg = cfg;
97
+ }
98
+ else if (dbKey === 'main') {
99
+ // 폴백: GAON_DATABASE_URL (seed 와 같은 관례).
100
+ const url = env.optional('GAON_DATABASE_URL');
101
+ if (!url) {
102
+ throw new EnvError(`[gaon db] main 커넥션을 결정할 수 없습니다.\n` +
103
+ ` → gaon.config.ts 에 db.main 을 선언하거나, .env 에 다음 줄을 추가하세요:\n` +
104
+ ` GAON_DATABASE_URL=postgres://user:pass@localhost:5432/mydb`);
105
+ }
106
+ connCfg = dbConfigFromUrl(url);
107
+ }
108
+ else {
109
+ const known = Object.keys(config.db ?? {}).sort();
110
+ throw new Error(`[gaon db] 커넥션 '${dbKey}' 이(가) gaon.config.ts 에 없습니다.\n` +
111
+ ` 등록된 키: ${known.length ? known.join(', ') : '(없음)'}\n` +
112
+ ` → gaon.config.ts 의 db.${dbKey} 를 선언하고 다시 실행하세요.`);
113
+ }
114
+ const db = createDb(connCfg);
115
+ registerConnection(dbKey, db, connCfg.adapter);
116
+ const dialect = dialectFor(connCfg.adapter);
117
+ const tables = await scanTables(cwd, dbKey);
118
+ return {
119
+ dbKey,
120
+ adapter: connCfg.adapter,
121
+ db,
122
+ dialect,
123
+ tables,
124
+ configPath,
125
+ url: 'url' in connCfg ? connCfg.url : undefined,
126
+ async close() {
127
+ await destroyAllConnections();
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @gaonjs/cli · dev/console — `gaon dev` 통합 콘솔 (M9-C)
3
+ *
4
+ * `gaon dev` 는 여러 자식(Docker · serve · tsc · vue-tsc · 파일 워처)의
5
+ * 출력을 한 콘솔로 합친다. 어느 소스에서 왔는지 즉시 알아보게 [tag] 를
6
+ * 접두어로 붙이고, 소스별 색으로 시선을 잡는다.
7
+ *
8
+ * 두 가지 모드:
9
+ * - human: ` [serve] ▶ 리슨 중 — ...` (색상 · 사람 눈)
10
+ * - json : `{"source":"serve","level":"info","msg":"..."}` (자동화 파싱)
11
+ *
12
+ * ANSI 는 TTY 일 때만 켠다(파이프 · 리다이렉트에는 순수 텍스트). 무의존
13
+ * 정책상 chalk 는 쓰지 않는다 — 필요한 코드만 직접 쓴다.
14
+ */
15
+ /** 콘솔이 구분하는 로그 소스. */
16
+ export type DevSource = 'dev' | 'docker' | 'serve' | 'watcher' | 'tsc' | 'vue-tsc';
17
+ /** 로그 레벨 — human 은 색상 강조, json 은 필드로 실린다. */
18
+ export type DevLevel = 'info' | 'warn' | 'error';
19
+ export interface DevConsoleOptions {
20
+ readonly json?: boolean;
21
+ /** 색상 강제 on/off. 생략 시 stdout.isTTY 자동 감지. */
22
+ readonly color?: boolean;
23
+ /** 타임스탬프 표시(사람 모드). 생략 시 false. */
24
+ readonly timestamp?: boolean;
25
+ /** 출력 스트림(테스트 주입). 기본 process.stdout. */
26
+ readonly stdout?: NodeJS.WritableStream;
27
+ /** 에러 출력 스트림(테스트 주입). 기본 process.stderr. */
28
+ readonly stderr?: NodeJS.WritableStream;
29
+ }
30
+ export interface DevConsole {
31
+ log(source: DevSource, line: string, level?: DevLevel): void;
32
+ /** 여러 줄(개행 포함) 청크를 줄 단위로 나눠 로그. 자식 stdout 파이프용. */
33
+ pipe(source: DevSource, chunk: string | Buffer, level?: DevLevel): void;
34
+ }
35
+ /**
36
+ * 통합 콘솔을 만든다. json 모드면 소스별 JSON 라인, 그 외엔 색상 태그
37
+ * + 사람 텍스트로 출력한다. 에러 레벨은 stderr, 그 외는 stdout.
38
+ */
39
+ export declare function createDevConsole(opts?: DevConsoleOptions): DevConsole;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @gaonjs/cli · dev/console — `gaon dev` 통합 콘솔 (M9-C)
3
+ *
4
+ * `gaon dev` 는 여러 자식(Docker · serve · tsc · vue-tsc · 파일 워처)의
5
+ * 출력을 한 콘솔로 합친다. 어느 소스에서 왔는지 즉시 알아보게 [tag] 를
6
+ * 접두어로 붙이고, 소스별 색으로 시선을 잡는다.
7
+ *
8
+ * 두 가지 모드:
9
+ * - human: ` [serve] ▶ 리슨 중 — ...` (색상 · 사람 눈)
10
+ * - json : `{"source":"serve","level":"info","msg":"..."}` (자동화 파싱)
11
+ *
12
+ * ANSI 는 TTY 일 때만 켠다(파이프 · 리다이렉트에는 순수 텍스트). 무의존
13
+ * 정책상 chalk 는 쓰지 않는다 — 필요한 코드만 직접 쓴다.
14
+ */
15
+ // ── ANSI 색상표 ────────────────────────────────────────────────────
16
+ // 소스별로 태그가 눈에 밟히게 색을 다르게 잡는다. bold 로 두께를 준다.
17
+ const RESET = '\x1b[0m';
18
+ const BOLD = '\x1b[1m';
19
+ const DIM = '\x1b[2m';
20
+ const RED = '\x1b[31m';
21
+ const YELLOW = '\x1b[33m';
22
+ const SOURCE_COLOR = {
23
+ dev: '\x1b[35m', // magenta — 오케스트레이터
24
+ docker: '\x1b[34m', // blue — 인프라
25
+ serve: '\x1b[32m', // green — 웹 서버
26
+ watcher: '\x1b[36m', // cyan — 파일 감시
27
+ tsc: '\x1b[33m', // yellow — 타입 검사
28
+ 'vue-tsc': '\x1b[95m', // bright magenta — vue 전용
29
+ };
30
+ /** stdout.isTTY 여부 자동 감지(테스트에서는 stream.isTTY 를 봄). */
31
+ function detectTty(stream) {
32
+ const s = stream;
33
+ return s.isTTY === true;
34
+ }
35
+ /** 태그 폭을 고정해 정렬을 맞춘다(가장 긴 태그가 vue-tsc = 7 자). */
36
+ const TAG_WIDTH = 8;
37
+ function padTag(source) {
38
+ const tag = `[${source}]`;
39
+ return tag.length >= TAG_WIDTH ? tag + ' ' : tag + ' '.repeat(TAG_WIDTH - tag.length);
40
+ }
41
+ function timestampNow() {
42
+ const d = new Date();
43
+ const hh = String(d.getHours()).padStart(2, '0');
44
+ const mm = String(d.getMinutes()).padStart(2, '0');
45
+ const ss = String(d.getSeconds()).padStart(2, '0');
46
+ return `${hh}:${mm}:${ss}`;
47
+ }
48
+ /**
49
+ * 통합 콘솔을 만든다. json 모드면 소스별 JSON 라인, 그 외엔 색상 태그
50
+ * + 사람 텍스트로 출력한다. 에러 레벨은 stderr, 그 외는 stdout.
51
+ */
52
+ export function createDevConsole(opts = {}) {
53
+ const stdout = opts.stdout ?? process.stdout;
54
+ const stderr = opts.stderr ?? process.stderr;
55
+ const json = opts.json ?? false;
56
+ const useColor = opts.color ?? detectTty(stdout);
57
+ const showTimestamp = opts.timestamp ?? false;
58
+ const paint = (color, text) => useColor ? `${BOLD}${color}${text}${RESET}` : text;
59
+ const formatHuman = (source, line, level) => {
60
+ const rawTag = padTag(source);
61
+ const coloredTag = paint(SOURCE_COLOR[source], rawTag);
62
+ const ts = showTimestamp ? paint(DIM, timestampNow()) + ' ' : '';
63
+ const body = level === 'error'
64
+ ? useColor
65
+ ? `${RED}${line}${RESET}`
66
+ : line
67
+ : level === 'warn'
68
+ ? useColor
69
+ ? `${YELLOW}${line}${RESET}`
70
+ : line
71
+ : line;
72
+ return `${ts}${coloredTag} ${body}`;
73
+ };
74
+ const write = (level, text) => {
75
+ const stream = level === 'error' ? stderr : stdout;
76
+ stream.write(text.endsWith('\n') ? text : text + '\n');
77
+ };
78
+ const log = (source, line, level = 'info') => {
79
+ if (json) {
80
+ const rec = { source, level, msg: line, ts: new Date().toISOString() };
81
+ write(level, JSON.stringify(rec));
82
+ return;
83
+ }
84
+ // 원본 line 이 이미 개행을 포함하면 각 줄에 태그를 붙여야 정렬이 유지된다.
85
+ const lines = line.replace(/\n+$/, '').split('\n');
86
+ for (const l of lines)
87
+ write(level, formatHuman(source, l, level));
88
+ };
89
+ const pipe = (source, chunk, level = 'info') => {
90
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
91
+ // 개행 없는 부분 청크(TSC 워치 등 진행 스피너)도 표시하되, 마지막
92
+ // 개행이 없으면 다음 청크와 합쳐 정렬이 어긋나지 않도록 단순히 그대로 쓴다.
93
+ for (const raw of text.split('\n')) {
94
+ if (raw.length === 0)
95
+ continue;
96
+ log(source, raw, level);
97
+ }
98
+ };
99
+ return { log, pipe };
100
+ }
@@ -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>;