@gaonjs/cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check.d.ts +29 -0
- package/dist/check.js +92 -0
- package/dist/dev.d.ts +59 -0
- package/dist/dev.js +153 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -3
- package/dist/tsResolve.d.ts +7 -0
- package/dist/tsResolve.js +35 -0
- package/package.json +4 -2
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type DevLayout } from './dev.js';
|
|
2
|
+
export interface TypecheckResult {
|
|
3
|
+
readonly ok: boolean;
|
|
4
|
+
readonly output: string;
|
|
5
|
+
/** 실행한 체커 이름(vue-tsc | tsc | none). */
|
|
6
|
+
readonly checker: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CheckDeps {
|
|
9
|
+
readonly layout: DevLayout;
|
|
10
|
+
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
11
|
+
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
12
|
+
typecheck(): Promise<TypecheckResult>;
|
|
13
|
+
}
|
|
14
|
+
export interface CheckResult {
|
|
15
|
+
readonly ok: boolean;
|
|
16
|
+
readonly regenerated: {
|
|
17
|
+
readonly tables: boolean;
|
|
18
|
+
readonly routes: string[];
|
|
19
|
+
};
|
|
20
|
+
readonly typecheck: TypecheckResult;
|
|
21
|
+
}
|
|
22
|
+
/** .gaon 재생성 → 타입 체크. 재생성이 검사보다 먼저다(§13.4-5). */
|
|
23
|
+
export declare function runCheck(deps: CheckDeps): Promise<CheckResult>;
|
|
24
|
+
export interface CheckCommandOptions {
|
|
25
|
+
readonly cwd?: string;
|
|
26
|
+
readonly json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** `gaon check` 진입점. */
|
|
29
|
+
export declare function runCheckCommand(opts?: CheckCommandOptions): Promise<number>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon check` — 검사 전 .gaon 재생성 (M3, §13.4-5)
|
|
3
|
+
*
|
|
4
|
+
* 개발 서버(gaon dev)가 떠 있지 않은 CI·AI 에이전트 환경에서도 타입
|
|
5
|
+
* 체인이 항상 최신으로 검증되게, 타입 검사 **전에** .gaon(tables·routes)
|
|
6
|
+
* 을 먼저 재생성한 뒤 tsc/vue-tsc 를 돌린다.
|
|
7
|
+
*
|
|
8
|
+
* 오케스트레이션(runCheck)은 주입으로 순수하게 유지하고, 진입점이 cwd
|
|
9
|
+
* 관례로 실제 재생성기·타입 체커를 엮는다(dev.ts 와 동일 패턴).
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { generateTablesDts } from '@gaonjs/data';
|
|
15
|
+
import { generateRoutesDts } from '@gaonjs/web';
|
|
16
|
+
import { resolveDevLayout } from './dev.js';
|
|
17
|
+
import { registerTsResolve } from './tsResolve.js';
|
|
18
|
+
/** .gaon 재생성 → 타입 체크. 재생성이 검사보다 먼저다(§13.4-5). */
|
|
19
|
+
export async function runCheck(deps) {
|
|
20
|
+
const { layout } = deps;
|
|
21
|
+
const routes = [];
|
|
22
|
+
if (layout.schemaDir)
|
|
23
|
+
await deps.regenerateTables(layout.schemaDir, layout.tablesOut);
|
|
24
|
+
for (const app of layout.apps) {
|
|
25
|
+
await deps.regenerateRoutes(app.appDir, app.routesOut);
|
|
26
|
+
routes.push(app.name);
|
|
27
|
+
}
|
|
28
|
+
const typecheck = await deps.typecheck();
|
|
29
|
+
return {
|
|
30
|
+
ok: typecheck.ok,
|
|
31
|
+
regenerated: { tables: !!layout.schemaDir, routes },
|
|
32
|
+
typecheck,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 프로젝트에 맞는 타입 체커를 골라 실행한다. .vue 가 있으면 vue-tsc. */
|
|
36
|
+
function makeProjectTypecheck(cwd) {
|
|
37
|
+
return () => new Promise((resolve) => {
|
|
38
|
+
const tsconfig = join(cwd, 'tsconfig.json');
|
|
39
|
+
if (!existsSync(tsconfig)) {
|
|
40
|
+
resolve({
|
|
41
|
+
ok: false,
|
|
42
|
+
checker: 'none',
|
|
43
|
+
output: `[gaon check] tsconfig.json 이 없습니다: ${tsconfig}\n` +
|
|
44
|
+
`→ 프로젝트 루트에 tsconfig.json 을 만들고 다시 실행하세요.`,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const vueTsc = join(cwd, 'node_modules', '.bin', 'vue-tsc');
|
|
49
|
+
const useVue = existsSync(vueTsc);
|
|
50
|
+
const checker = useVue ? 'vue-tsc' : 'tsc';
|
|
51
|
+
const cmd = useVue ? vueTsc : process.execPath;
|
|
52
|
+
const args = useVue
|
|
53
|
+
? ['--noEmit', '-p', tsconfig]
|
|
54
|
+
: [join(cwd, 'node_modules', 'typescript', 'bin', 'tsc'), '--noEmit', '-p', tsconfig];
|
|
55
|
+
const child = spawn(cmd, args, { cwd });
|
|
56
|
+
let output = '';
|
|
57
|
+
child.stdout.on('data', (b) => (output += b.toString()));
|
|
58
|
+
child.stderr.on('data', (b) => (output += b.toString()));
|
|
59
|
+
child.on('error', (e) => resolve({ ok: false, checker, output: String(e) }));
|
|
60
|
+
child.on('close', (code) => resolve({ ok: code === 0, checker, output }));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** `gaon check` 진입점. */
|
|
64
|
+
export async function runCheckCommand(opts = {}) {
|
|
65
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
66
|
+
registerTsResolve(); // 사용자 .ts 의 `.js` 상대 import 를 런타임에 해석
|
|
67
|
+
const deps = {
|
|
68
|
+
layout: resolveDevLayout(cwd),
|
|
69
|
+
regenerateTables: generateTablesDts,
|
|
70
|
+
regenerateRoutes: generateRoutesDts,
|
|
71
|
+
typecheck: makeProjectTypecheck(cwd),
|
|
72
|
+
};
|
|
73
|
+
const result = await runCheck(deps);
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const r = result.regenerated;
|
|
79
|
+
const gen = [r.tables ? 'tables.d.ts' : null, ...r.routes.map((a) => `${a}/routes.d.ts`)]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(', ');
|
|
82
|
+
process.stdout.write(` 재생성: ${gen || '(대상 없음)'}\n`);
|
|
83
|
+
if (result.ok) {
|
|
84
|
+
process.stdout.write(` ✓ 타입 검사 통과 (${result.typecheck.checker})\n`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
process.stdout.write(` ✗ 타입 검사 실패 (${result.typecheck.checker})\n`);
|
|
88
|
+
process.stdout.write(result.typecheck.output);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result.ok ? 0 : 1;
|
|
92
|
+
}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type WatchHandle, type WatchOptions } from '@gaonjs/data';
|
|
2
|
+
export interface DevApp {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly appDir: string;
|
|
5
|
+
/** apps/<app>/.gaon/routes.d.ts */
|
|
6
|
+
readonly routesOut: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DevLayout {
|
|
9
|
+
/** domain/schema (없으면 undefined — tables 워치 생략). */
|
|
10
|
+
readonly schemaDir?: string;
|
|
11
|
+
/** .gaon/tables.d.ts */
|
|
12
|
+
readonly tablesOut: string;
|
|
13
|
+
readonly apps: readonly DevApp[];
|
|
14
|
+
}
|
|
15
|
+
export interface DevDeps {
|
|
16
|
+
readonly layout: DevLayout;
|
|
17
|
+
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
18
|
+
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
19
|
+
watch(dir: string, opts: WatchOptions): WatchHandle;
|
|
20
|
+
log(event: DevEvent): void;
|
|
21
|
+
onError(err: Error): void;
|
|
22
|
+
}
|
|
23
|
+
export type DevEvent = {
|
|
24
|
+
readonly kind: 'ready';
|
|
25
|
+
readonly schema: boolean;
|
|
26
|
+
readonly apps: string[];
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'regen';
|
|
29
|
+
readonly target: 'tables' | 'routes';
|
|
30
|
+
readonly app?: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: 'stopped';
|
|
33
|
+
};
|
|
34
|
+
export interface DevHandle {
|
|
35
|
+
/** 모든 워처를 닫는다. */
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 초기 1회 전체 재생성 후, 스키마·앱 라우트를 감시해 변경 시 해당
|
|
40
|
+
* .gaon 파일을 재생성한다. 반환 핸들의 close() 로 멈춘다.
|
|
41
|
+
*/
|
|
42
|
+
export declare function startDev(deps: DevDeps): Promise<DevHandle>;
|
|
43
|
+
/** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
|
|
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>;
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon dev` — .gaon 갱신 파이프라인 (M3)
|
|
3
|
+
*
|
|
4
|
+
* 파일 워처(@gaonjs/data watchDir) → 재생성(tables.d.ts·routes.d.ts) →
|
|
5
|
+
* 디스크 mtime 갱신 → 편집기 tsserver 자동 반영(§6.3, 열린 질문 3).
|
|
6
|
+
*
|
|
7
|
+
* 오케스트레이션은 주입(deps)으로 순수하게 유지하고(테스트 용이·dbCli
|
|
8
|
+
* 패턴과 동일), CLI 진입점이 cwd 관례로 실제 경로·재생성기를 엮는다.
|
|
9
|
+
* 실제 웹 서버·Vite·Docker 인프라 기동은 후속 마일스톤이다 — M3 는
|
|
10
|
+
* 타입 브리지 갱신 루프만 담당한다.
|
|
11
|
+
*/
|
|
12
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join, resolve } from 'node:path';
|
|
14
|
+
import { generateTablesDts, watchDir } from '@gaonjs/data';
|
|
15
|
+
import { generateRoutesDts } from '@gaonjs/web';
|
|
16
|
+
import { registerTsResolve } from './tsResolve.js';
|
|
17
|
+
const isSchemaChange = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts') && !f.includes('.gaon');
|
|
18
|
+
const isRoutesChange = (f) => !f.includes('.gaon') &&
|
|
19
|
+
f.endsWith('.ts') &&
|
|
20
|
+
!f.endsWith('.d.ts') &&
|
|
21
|
+
!f.endsWith('.test.ts') &&
|
|
22
|
+
(f === 'routes.ts' || f.startsWith('controllers'));
|
|
23
|
+
/**
|
|
24
|
+
* 초기 1회 전체 재생성 후, 스키마·앱 라우트를 감시해 변경 시 해당
|
|
25
|
+
* .gaon 파일을 재생성한다. 반환 핸들의 close() 로 멈춘다.
|
|
26
|
+
*/
|
|
27
|
+
export async function startDev(deps) {
|
|
28
|
+
const { layout } = deps;
|
|
29
|
+
const handles = [];
|
|
30
|
+
// 1) 초기 전체 재생성 — 개발 서버가 뜨자마자 타입 체인이 최신이 되게.
|
|
31
|
+
if (layout.schemaDir) {
|
|
32
|
+
await deps.regenerateTables(layout.schemaDir, layout.tablesOut);
|
|
33
|
+
deps.log({ kind: 'regen', target: 'tables' });
|
|
34
|
+
}
|
|
35
|
+
for (const app of layout.apps) {
|
|
36
|
+
await deps.regenerateRoutes(app.appDir, app.routesOut);
|
|
37
|
+
deps.log({ kind: 'regen', target: 'routes', app: app.name });
|
|
38
|
+
}
|
|
39
|
+
// 2) 스키마 워치 → tables.d.ts 재생성.
|
|
40
|
+
if (layout.schemaDir) {
|
|
41
|
+
const schemaDir = layout.schemaDir;
|
|
42
|
+
handles.push(deps.watch(schemaDir, {
|
|
43
|
+
filter: isSchemaChange,
|
|
44
|
+
onChange: async () => {
|
|
45
|
+
await deps.regenerateTables(schemaDir, layout.tablesOut);
|
|
46
|
+
deps.log({ kind: 'regen', target: 'tables' });
|
|
47
|
+
},
|
|
48
|
+
onError: deps.onError,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
// 3) 앱별 워치 → routes.d.ts 재생성(자기 앱만).
|
|
52
|
+
for (const app of layout.apps) {
|
|
53
|
+
handles.push(deps.watch(app.appDir, {
|
|
54
|
+
filter: isRoutesChange,
|
|
55
|
+
onChange: async () => {
|
|
56
|
+
await deps.regenerateRoutes(app.appDir, app.routesOut);
|
|
57
|
+
deps.log({ kind: 'regen', target: 'routes', app: app.name });
|
|
58
|
+
},
|
|
59
|
+
onError: deps.onError,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
deps.log({ kind: 'ready', schema: !!layout.schemaDir, apps: layout.apps.map((a) => a.name) });
|
|
63
|
+
return {
|
|
64
|
+
close() {
|
|
65
|
+
for (const h of handles)
|
|
66
|
+
h.close();
|
|
67
|
+
deps.log({ kind: 'stopped' });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
|
|
72
|
+
export function resolveDevLayout(cwd) {
|
|
73
|
+
const root = resolve(cwd);
|
|
74
|
+
const schemaDirPath = join(root, 'domain', 'schema');
|
|
75
|
+
const appsDir = join(root, 'apps');
|
|
76
|
+
const apps = [];
|
|
77
|
+
if (existsSync(appsDir)) {
|
|
78
|
+
for (const entry of readdirSync(appsDir, { withFileTypes: true })) {
|
|
79
|
+
if (!entry.isDirectory())
|
|
80
|
+
continue;
|
|
81
|
+
const appDir = join(appsDir, entry.name);
|
|
82
|
+
if (!existsSync(join(appDir, 'routes.ts')))
|
|
83
|
+
continue;
|
|
84
|
+
apps.push({
|
|
85
|
+
name: entry.name,
|
|
86
|
+
appDir,
|
|
87
|
+
routesOut: join(appDir, '.gaon', 'routes.d.ts'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
schemaDir: existsSync(schemaDirPath) ? schemaDirPath : undefined,
|
|
93
|
+
tablesOut: join(root, '.gaon', 'tables.d.ts'),
|
|
94
|
+
apps: apps.sort((a, b) => a.name.localeCompare(b.name)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** 사람이 읽는 이벤트 텍스트. */
|
|
98
|
+
function humanEvent(e) {
|
|
99
|
+
switch (e.kind) {
|
|
100
|
+
case 'ready': {
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (e.schema)
|
|
103
|
+
parts.push('schema→tables.d.ts');
|
|
104
|
+
for (const a of e.apps)
|
|
105
|
+
parts.push(`${a}→routes.d.ts`);
|
|
106
|
+
const what = parts.length ? parts.join(', ') : '(감시 대상 없음 — domain/schema·apps/* 확인)';
|
|
107
|
+
return ` gaon dev · 워치 시작 — ${what}\n 파일을 저장하면 .gaon 이 재생성되고 편집기가 자동 반영합니다. (Ctrl+C 종료)`;
|
|
108
|
+
}
|
|
109
|
+
case 'regen':
|
|
110
|
+
return e.target === 'tables'
|
|
111
|
+
? ' ↻ tables.d.ts 재생성'
|
|
112
|
+
: ` ↻ ${e.app}/.gaon/routes.d.ts 재생성`;
|
|
113
|
+
case 'stopped':
|
|
114
|
+
return ' gaon dev · 종료';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* `gaon dev` 진입점. cwd 관례로 레이아웃을 엮고 워치 루프를 띄운 뒤
|
|
119
|
+
* SIGINT/SIGTERM 에 graceful stop. 프로세스는 워처가 살아 있는 동안
|
|
120
|
+
* 유지된다(활성 핸들). 반환 프라미스는 종료 시 resolve.
|
|
121
|
+
*/
|
|
122
|
+
export async function runDevCommand(opts = {}) {
|
|
123
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
124
|
+
const json = opts.json ?? false;
|
|
125
|
+
const signals = opts.signals ?? process;
|
|
126
|
+
registerTsResolve(); // 사용자 .ts 의 `.js` 상대 import 를 런타임에 해석
|
|
127
|
+
const layout = resolveDevLayout(cwd);
|
|
128
|
+
const emit = (e) => {
|
|
129
|
+
if (json)
|
|
130
|
+
process.stdout.write(JSON.stringify(e) + '\n');
|
|
131
|
+
else
|
|
132
|
+
process.stdout.write(humanEvent(e) + '\n');
|
|
133
|
+
};
|
|
134
|
+
const deps = {
|
|
135
|
+
layout,
|
|
136
|
+
regenerateTables: generateTablesDts,
|
|
137
|
+
regenerateRoutes: generateRoutesDts,
|
|
138
|
+
watch: watchDir,
|
|
139
|
+
log: emit,
|
|
140
|
+
onError: (err) => process.stderr.write(` ✗ ${err.message}\n`),
|
|
141
|
+
};
|
|
142
|
+
const handle = await startDev(deps);
|
|
143
|
+
await new Promise((resolvePromise) => {
|
|
144
|
+
const stop = () => {
|
|
145
|
+
signals.off('SIGINT', stop);
|
|
146
|
+
signals.off('SIGTERM', stop);
|
|
147
|
+
handle.close();
|
|
148
|
+
resolvePromise();
|
|
149
|
+
};
|
|
150
|
+
signals.on('SIGINT', stop);
|
|
151
|
+
signals.on('SIGTERM', stop);
|
|
152
|
+
});
|
|
153
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { runDevCommand, startDev, resolveDevLayout, type DevDeps, type DevLayout, type DevApp, type DevEvent, type DevHandle, type DevCommandOptions, } from "./dev.js";
|
|
2
|
+
export { runCheck, runCheckCommand, type CheckDeps, type CheckResult, type TypecheckResult, type CheckCommandOptions, } from "./check.js";
|
|
1
3
|
export interface RoadmapReport {
|
|
2
4
|
readonly name: "gaon";
|
|
3
5
|
readonly version: string;
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @gaonjs/cli — Gaon CLI 구현.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 인자 없이 실행하면 로드맵·개발 상태를 출력하는 스텁이다. M3 부터
|
|
5
|
+
* `gaon dev` 가 실제로 동작한다 — .gaon 타입 브리지(tables·routes)를
|
|
6
|
+
* 감시·재생성한다(dev.ts). 모든 명령은 `--json` 출력을 함께 제공한다
|
|
7
|
+
* (CLAUDE.md §4). 스캐폴딩·나머지 제너레이터는 이후 마일스톤이다.
|
|
7
8
|
*
|
|
8
9
|
* 표시 버전은 호출자(파사드)가 주입한다 — 사용자가 설치한 패키지
|
|
9
10
|
* (`gaonjs`) 버전을 그대로 보여주기 위함. 미주입 시 core 버전을 쓴다.
|
|
10
11
|
*/
|
|
11
12
|
import { MILESTONES, VERSION, HOMEPAGE } from "@gaonjs/core";
|
|
13
|
+
import { runDevCommand } from "./dev.js";
|
|
14
|
+
import { runCheckCommand } from "./check.js";
|
|
15
|
+
export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
|
|
16
|
+
export { runCheck, runCheckCommand, } from "./check.js";
|
|
12
17
|
/** `--json` 출력용 구조화 리포트. */
|
|
13
18
|
export function roadmapReport(version = VERSION) {
|
|
14
19
|
return {
|
|
@@ -52,6 +57,9 @@ function renderHelp(version = VERSION) {
|
|
|
52
57
|
"",
|
|
53
58
|
" 사용법:",
|
|
54
59
|
" gaon 로드맵과 개발 상태를 출력",
|
|
60
|
+
" gaon dev .gaon 타입 브리지를 감시·재생성 (스키마·라우트)",
|
|
61
|
+
" gaon dev --json 재생성 이벤트를 JSON 으로 출력",
|
|
62
|
+
" gaon check .gaon 재생성 후 타입 검사 (CI·AI 정합)",
|
|
55
63
|
" gaon --json 같은 정보를 JSON 으로 출력",
|
|
56
64
|
" gaon --version 버전 출력",
|
|
57
65
|
" gaon --help 이 도움말",
|
|
@@ -63,6 +71,29 @@ function renderHelp(version = VERSION) {
|
|
|
63
71
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
64
72
|
export function runCli(argv, opts = {}) {
|
|
65
73
|
const version = opts.version ?? VERSION;
|
|
74
|
+
// `gaon dev` — .gaon 갱신 파이프라인(장기 실행). 워처가 활성 핸들로
|
|
75
|
+
// 프로세스를 살려 두고, SIGINT 로 종료된다. 에러는 stderr + exit 1.
|
|
76
|
+
if (argv[0] === "dev") {
|
|
77
|
+
void runDevCommand({ json: argv.includes("--json") }).catch((err) => {
|
|
78
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
79
|
+
process.stderr.write(` ✗ gaon dev 실패: ${msg}\n`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// `gaon check` — .gaon 재생성 후 타입 검사(§13.4-5). 종료 코드로 결과 전달.
|
|
85
|
+
if (argv[0] === "check") {
|
|
86
|
+
void runCheckCommand({ json: argv.includes("--json") })
|
|
87
|
+
.then((code) => {
|
|
88
|
+
process.exitCode = code;
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
process.stderr.write(` ✗ gaon check 실패: ${msg}\n`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
66
97
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
67
98
|
process.stdout.write(renderHelp(version) + "\n");
|
|
68
99
|
return;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type NextResolve = (specifier: string, context: unknown) => unknown | Promise<unknown>;
|
|
2
|
+
export declare function resolve(specifier: string, context: {
|
|
3
|
+
parentURL?: string;
|
|
4
|
+
}, nextResolve: NextResolve): Promise<unknown>;
|
|
5
|
+
/** 이 모듈을 ESM resolve 훅으로 등록한다(한 프로세스에서 1회). */
|
|
6
|
+
export declare function registerTsResolve(): void;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · TS 상대경로 해석 훅 (M3)
|
|
3
|
+
*
|
|
4
|
+
* 생성기(tables·routes)는 사용자 스키마·컨트롤러 .ts 를 런타임 동적
|
|
5
|
+
* import 해 구조를 읽는다. TS-for-ESM 관례상 상대 import 는 `.js` 로
|
|
6
|
+
* 쓰지만(`import { Post } from '../models/post.js'`), 개발 중 소스는
|
|
7
|
+
* `.ts` 다 — Node 는 `.js`→`.ts` 재매핑을 하지 않아 그대로면 모듈을
|
|
8
|
+
* 못 찾는다(vitest 는 Vite 가 재작성해 문제 없음, 실 gaon dev/check 만
|
|
9
|
+
* 해당). 이 훅이 `.js` 가 없고 형제 `.ts` 가 있으면 `.ts` 로 넘긴다.
|
|
10
|
+
*
|
|
11
|
+
* bare 지정자(@gaonjs/data 등)와 실재하는 `.js`(컴파일된 프로덕션)는
|
|
12
|
+
* 건드리지 않는다 — 그 경우 훅은 무동작이다.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { register } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
18
|
+
if (/\.js$/.test(specifier) &&
|
|
19
|
+
(specifier.startsWith('./') || specifier.startsWith('../')) &&
|
|
20
|
+
context.parentURL) {
|
|
21
|
+
const jsPath = fileURLToPath(new URL(specifier, context.parentURL));
|
|
22
|
+
if (!existsSync(jsPath) && existsSync(jsPath.replace(/\.js$/, '.ts'))) {
|
|
23
|
+
return nextResolve(specifier.replace(/\.js$/, '.ts'), context);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return nextResolve(specifier, context);
|
|
27
|
+
}
|
|
28
|
+
let registered = false;
|
|
29
|
+
/** 이 모듈을 ESM resolve 훅으로 등록한다(한 프로세스에서 1회). */
|
|
30
|
+
export function registerTsResolve() {
|
|
31
|
+
if (registered)
|
|
32
|
+
return;
|
|
33
|
+
registered = true;
|
|
34
|
+
register(import.meta.url);
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@gaonjs/core": "0.1.2"
|
|
27
|
+
"@gaonjs/core": "0.1.2",
|
|
28
|
+
"@gaonjs/data": "0.2.0",
|
|
29
|
+
"@gaonjs/web": "0.1.3"
|
|
28
30
|
},
|
|
29
31
|
"scripts": {
|
|
30
32
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|