@gaonjs/cli 0.2.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.
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
- package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
- package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
- package/dist/commands/db.d.ts +20 -0
- package/dist/commands/db.js +74 -0
- package/dist/commands/dev.d.ts +68 -0
- package/dist/commands/dev.js +287 -0
- package/dist/commands/g.d.ts +26 -0
- package/dist/commands/g.js +124 -0
- package/dist/db/diff.d.ts +17 -0
- package/dist/db/diff.js +57 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrate.d.ts +16 -0
- package/dist/db/migrate.js +173 -0
- package/dist/db/reset.d.ts +18 -0
- package/dist/db/reset.js +150 -0
- package/dist/db/resolve.d.ts +32 -0
- package/dist/db/resolve.js +130 -0
- package/dist/dev/console.d.ts +39 -0
- package/dist/dev/console.js +100 -0
- package/dist/dev/docker.d.ts +52 -0
- package/dist/dev/docker.js +163 -0
- package/dist/dev/index.d.ts +14 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/tsc.d.ts +41 -0
- package/dist/dev/tsc.js +127 -0
- package/dist/dev/watcher.d.ts +50 -0
- package/dist/dev/watcher.js +95 -0
- package/dist/dev.d.ts +1 -16
- package/dist/dev.js +10 -66
- package/dist/doctor/connections.d.ts +14 -0
- package/dist/doctor/connections.js +168 -0
- package/dist/doctor/dependency-direction.d.ts +11 -0
- package/dist/doctor/dependency-direction.js +186 -0
- package/dist/doctor/migration-diff.d.ts +11 -0
- package/dist/doctor/migration-diff.js +109 -0
- package/dist/doctor/n-plus-one.d.ts +5 -0
- package/dist/doctor/n-plus-one.js +242 -0
- package/dist/doctor/reporter.d.ts +5 -0
- package/dist/doctor/reporter.js +41 -0
- package/dist/doctor/response-mixing.d.ts +12 -0
- package/dist/doctor/response-mixing.js +158 -0
- package/dist/doctor/setup.d.ts +26 -0
- package/dist/doctor/setup.js +52 -0
- package/dist/doctor/types.d.ts +37 -0
- package/dist/doctor/types.js +34 -0
- package/dist/doctor.d.ts +40 -23
- package/dist/doctor.js +131 -202
- package/dist/index.d.ts +14 -2
- package/dist/index.js +171 -28
- package/dist/scaffold/controller.d.ts +12 -0
- package/dist/scaffold/controller.js +50 -0
- package/dist/scaffold/index.d.ts +20 -0
- package/dist/scaffold/index.js +41 -0
- package/dist/scaffold/inflect.d.ts +19 -0
- package/dist/scaffold/inflect.js +50 -0
- package/dist/scaffold/job.d.ts +3 -0
- package/dist/scaffold/job.js +46 -0
- package/dist/scaffold/model.d.ts +8 -0
- package/dist/scaffold/model.js +66 -0
- package/dist/scaffold/page.d.ts +7 -0
- package/dist/scaffold/page.js +46 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +79 -0
- package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
- package/dist/templates/auth/session.controller.ts.tpl +1 -1
- package/package.json +4 -3
|
@@ -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,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,26 @@
|
|
|
1
|
+
import { type ScaffoldFile, type WriteResult } from '../scaffold/index.js';
|
|
2
|
+
export type GenerateType = 'controller' | 'model' | 'page' | 'job';
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
readonly cwd?: string;
|
|
5
|
+
readonly app?: string;
|
|
6
|
+
readonly overwrite?: boolean;
|
|
7
|
+
readonly json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface GenerateResult {
|
|
10
|
+
readonly type: GenerateType;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly app: string | null;
|
|
13
|
+
readonly write: WriteResult;
|
|
14
|
+
}
|
|
15
|
+
/** argv 에서 옵션을 뽑는다(간단 파서 · runCli 관례와 일치). */
|
|
16
|
+
export declare function parseGenerateArgs(argv: readonly string[]): {
|
|
17
|
+
type: string | undefined;
|
|
18
|
+
name: string | undefined;
|
|
19
|
+
app: string | undefined;
|
|
20
|
+
overwrite: boolean;
|
|
21
|
+
json: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** 타입에 따라 스캐폴드 파일 목록을 만든다. */
|
|
24
|
+
export declare function planScaffold(type: GenerateType, name: string, app: string): ScaffoldFile[];
|
|
25
|
+
/** `gaon g <type> <name>` 실행. exitCode 를 반환한다(0=성공, 1=실패). */
|
|
26
|
+
export declare function runGenerateCommand(type: GenerateType, name: string, opts?: GenerateOptions): number;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon g <type> <name>` — M9-B 스캐폴드 라우터.
|
|
2
|
+
//
|
|
3
|
+
// 기존 `gaon g auth` (generate.ts · M5) 는 그대로 두고, controller/model/
|
|
4
|
+
// page/job 4종을 이 파일이 받는다. runCli 는 argv[1] 이 'auth' 면
|
|
5
|
+
// runGenerateAuthCommand, 그 외는 runGenerateCommand 로 위임한다.
|
|
6
|
+
//
|
|
7
|
+
// 옵션(공통):
|
|
8
|
+
// --json 결과를 JSON 으로 출력(자동화 · v0.15 §13.5 M9)
|
|
9
|
+
// --app <name> 대상 앱(controller/page 전용 · 기본 'web')
|
|
10
|
+
// --overwrite 기존 파일 덮어쓰기(기본 실패)
|
|
11
|
+
//
|
|
12
|
+
// 파일 위치는 프로젝트 관습(CLAUDE.md §2)을 따른다:
|
|
13
|
+
// controller → apps/<app>/controllers/<plural>.ts
|
|
14
|
+
// model → domain/schema/<camel>.ts + domain/models/<camel>.ts
|
|
15
|
+
// page → apps/<app>/pages/<path>.vue
|
|
16
|
+
// job → domain/jobs/<camel>.ts
|
|
17
|
+
import { controllerScaffold, inflectModel, jobScaffold, modelScaffoldFiles, pageScaffold, writeScaffold, } from '../scaffold/index.js';
|
|
18
|
+
/** argv 에서 옵션을 뽑는다(간단 파서 · runCli 관례와 일치). */
|
|
19
|
+
export function parseGenerateArgs(argv) {
|
|
20
|
+
const type = argv[0];
|
|
21
|
+
// name = 첫 위치 인자(옵션 플래그 제외). --app <v> 도 건너뛴다.
|
|
22
|
+
let name;
|
|
23
|
+
let app;
|
|
24
|
+
let overwrite = false;
|
|
25
|
+
let json = false;
|
|
26
|
+
for (let i = 1; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === '--json') {
|
|
29
|
+
json = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (a === '--overwrite') {
|
|
33
|
+
overwrite = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (a === '--app') {
|
|
37
|
+
app = argv[++i];
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (a?.startsWith('--'))
|
|
41
|
+
continue;
|
|
42
|
+
if (name === undefined)
|
|
43
|
+
name = a;
|
|
44
|
+
}
|
|
45
|
+
return { type, name, app, overwrite, json };
|
|
46
|
+
}
|
|
47
|
+
/** 이름 검증 — 위험 문자(경로 탈출 등) 차단. */
|
|
48
|
+
function validateName(name) {
|
|
49
|
+
if (!name) {
|
|
50
|
+
throw new Error(`이름이 비어 있습니다.`);
|
|
51
|
+
}
|
|
52
|
+
if (name.includes('..') || name.includes('\\')) {
|
|
53
|
+
throw new Error(`이름에 '..' 또는 '\\\\' 는 사용할 수 없습니다: ${name}`);
|
|
54
|
+
}
|
|
55
|
+
// page 는 '/' 를 허용, 나머지는 슬래시 금지(g.ts 가 타입별로 재검사)
|
|
56
|
+
}
|
|
57
|
+
/** 타입에 따라 스캐폴드 파일 목록을 만든다. */
|
|
58
|
+
export function planScaffold(type, name, app) {
|
|
59
|
+
validateName(name);
|
|
60
|
+
if (type === 'page') {
|
|
61
|
+
return [pageScaffold(name, app)];
|
|
62
|
+
}
|
|
63
|
+
if (name.includes('/')) {
|
|
64
|
+
throw new Error(`${type} 이름에 '/' 는 사용할 수 없습니다: ${name}`);
|
|
65
|
+
}
|
|
66
|
+
const names = inflectModel(name);
|
|
67
|
+
if (type === 'controller')
|
|
68
|
+
return [controllerScaffold(names, app)];
|
|
69
|
+
if (type === 'model')
|
|
70
|
+
return modelScaffoldFiles(names);
|
|
71
|
+
if (type === 'job')
|
|
72
|
+
return [jobScaffold(names.pascal)];
|
|
73
|
+
throw new Error(`알 수 없는 제너레이터: ${type}`);
|
|
74
|
+
}
|
|
75
|
+
/** `gaon g <type> <name>` 실행. exitCode 를 반환한다(0=성공, 1=실패). */
|
|
76
|
+
export function runGenerateCommand(type, name, opts = {}) {
|
|
77
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
78
|
+
const app = opts.app ?? 'web';
|
|
79
|
+
let files;
|
|
80
|
+
try {
|
|
81
|
+
files = planScaffold(type, name, app);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
process.stderr.write(` ✗ gaon g ${type}: ${msg}\n`);
|
|
90
|
+
}
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
const write = writeScaffold(cwd, files, { overwrite: opts.overwrite });
|
|
94
|
+
// overwrite 없이 기존 파일이 있으면 실패(사고 방지 · Rails 관례).
|
|
95
|
+
const failed = write.skipped.length > 0;
|
|
96
|
+
const result = {
|
|
97
|
+
type,
|
|
98
|
+
name,
|
|
99
|
+
app: type === 'controller' || type === 'page' ? app : null,
|
|
100
|
+
write,
|
|
101
|
+
};
|
|
102
|
+
if (opts.json) {
|
|
103
|
+
const ok = !failed;
|
|
104
|
+
process.stdout.write(JSON.stringify({ ok, ...result, error: failed ? '기존 파일 존재 (--overwrite 로 덮어쓰기)' : null }, null, 2) +
|
|
105
|
+
'\n');
|
|
106
|
+
return ok ? 0 : 1;
|
|
107
|
+
}
|
|
108
|
+
const lines = [''];
|
|
109
|
+
lines.push(` gaon g ${type} ${name}${result.app ? ` (${result.app} 앱)` : ''}`);
|
|
110
|
+
lines.push('');
|
|
111
|
+
for (const f of write.created)
|
|
112
|
+
lines.push(` + ${f}`);
|
|
113
|
+
for (const f of write.overwritten)
|
|
114
|
+
lines.push(` ~ ${f} (덮어씀)`);
|
|
115
|
+
for (const f of write.skipped)
|
|
116
|
+
lines.push(` ✗ ${f} (이미 있음 — --overwrite 로 덮어쓰기)`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
if (failed) {
|
|
119
|
+
lines.push(' → --overwrite 를 붙이면 기존 파일을 덮어씁니다.');
|
|
120
|
+
lines.push('');
|
|
121
|
+
}
|
|
122
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
123
|
+
return failed ? 1 : 0;
|
|
124
|
+
}
|
|
@@ -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>;
|