@gaonjs/cli 0.5.0 → 0.10.1
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/commands/check.d.ts +21 -2
- package/dist/commands/check.js +70 -7
- package/dist/commands/db.d.ts +3 -1
- package/dist/commands/db.js +8 -2
- package/dist/commands/g.d.ts +1 -1
- package/dist/commands/g.js +27 -3
- package/dist/commands/mcp.d.ts +15 -0
- package/dist/commands/mcp.js +78 -0
- package/dist/db/diff.js +5 -0
- package/dist/db/journal.d.ts +34 -0
- package/dist/db/journal.js +71 -0
- package/dist/db/migrate.d.ts +6 -1
- package/dist/db/migrate.js +120 -102
- package/dist/db/replay.d.ts +49 -0
- package/dist/db/replay.js +148 -0
- package/dist/db/status.d.ts +12 -0
- package/dist/db/status.js +61 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/dev/vite.d.ts +67 -0
- package/dist/dev/vite.js +126 -0
- package/dist/dev.d.ts +18 -0
- package/dist/dev.js +15 -0
- package/dist/doctor/agents-doc-index.d.ts +4 -0
- package/dist/doctor/agents-doc-index.js +80 -0
- package/dist/doctor/fixers/dependency-direction.d.ts +9 -0
- package/dist/doctor/fixers/dependency-direction.js +98 -0
- package/dist/doctor/fixers/index.d.ts +15 -0
- package/dist/doctor/fixers/index.js +66 -0
- package/dist/doctor/fixers/schema-filename.d.ts +14 -0
- package/dist/doctor/fixers/schema-filename.js +104 -0
- package/dist/doctor/fixers/types.d.ts +59 -0
- package/dist/doctor/fixers/types.js +15 -0
- package/dist/doctor/schema-filename.d.ts +6 -0
- package/dist/doctor/schema-filename.js +81 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +49 -0
- package/dist/doctor.js +179 -5
- package/dist/generate.js +2 -2
- package/dist/hub.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +50 -10
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/server.d.ts +50 -0
- package/dist/mcp/server.js +102 -0
- package/dist/mcp/tools.d.ts +109 -0
- package/dist/mcp/tools.js +485 -0
- package/dist/scaffold/app.d.ts +5 -0
- package/dist/scaffold/app.js +172 -0
- package/dist/scaffold/controller.js +2 -2
- package/dist/scaffold/index.d.ts +2 -1
- package/dist/scaffold/index.js +2 -1
- package/dist/scaffold/job.d.ts +5 -0
- package/dist/scaffold/job.js +35 -0
- package/dist/scaffold/model.js +8 -8
- package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
- package/dist/templates/auth/registration.controller.ts.tpl +1 -1
- package/dist/templates/auth/session.controller.ts.tpl +1 -1
- package/dist/templates/auth/user.model.ts.tpl +1 -1
- package/dist/templates/project/AGENTS.md.tpl +214 -0
- package/dist/templates/project/agents/async.md.tpl +218 -0
- package/dist/templates/project/agents/data.md.tpl +556 -0
- package/dist/templates/project/agents/frontend.md.tpl +201 -0
- package/dist/templates/project/agents/realtime.md.tpl +157 -0
- package/dist/templates/project/agents/security.md.tpl +92 -0
- package/dist/templates/project/agents/testing.md.tpl +101 -0
- package/dist/templates/project/agents/web.md.tpl +177 -0
- package/dist/templates/project/apps/web/index.html.tpl +18 -0
- package/dist/templates/project/apps/web/main.ts.tpl +24 -0
- package/dist/templates/project/package.json.tpl +5 -2
- package/dist/templates/project/vite.config.ts.tpl +23 -0
- package/dist/tsResolve.js +1 -1
- package/dist/work.d.ts +2 -2
- package/dist/work.js +3 -1
- package/package.json +13 -11
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +0 -12
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +0 -7
- package/dist/__fixtures__/db-minimal/gaon.config.d.ts +0 -2
- package/dist/__fixtures__/db-minimal/gaon.config.js +0 -11
- package/dist/check.d.ts +0 -29
- package/dist/check.js +0 -92
package/dist/commands/check.d.ts
CHANGED
|
@@ -18,13 +18,32 @@ export interface CheckStepResult {
|
|
|
18
18
|
/** stdout+stderr 합본(사람 UI 에 그대로 출력). skipped 는 짧은 사유. */
|
|
19
19
|
readonly output?: string;
|
|
20
20
|
}
|
|
21
|
+
export type RegenStatus = 'done' | 'skipped' | 'failed';
|
|
22
|
+
/**
|
|
23
|
+
* 검사 직전 .gaon 재생성 결과. 규칙 3(§13.4-5) — check 는 최신 타입 브리지
|
|
24
|
+
* 위에서 검증해야 한다. done = 재생성함 · skipped = 재생성할 대상 없음
|
|
25
|
+
* (domain/schema · apps/* 없음) · failed = 스키마/라우트 구문 오류로 재생성
|
|
26
|
+
* 실패(이 경우 검사를 진행하지 않고 실패로 종료).
|
|
27
|
+
*/
|
|
28
|
+
export interface CheckRegen {
|
|
29
|
+
readonly status: RegenStatus;
|
|
30
|
+
/** tables.d.ts 를 재생성했는가. */
|
|
31
|
+
readonly tables: boolean;
|
|
32
|
+
/** routes.d.ts 를 재생성한 앱 이름. */
|
|
33
|
+
readonly apps: readonly string[];
|
|
34
|
+
/** 실패 시 에러 + 수리 안내. done/skipped 는 undefined. */
|
|
35
|
+
readonly output?: string;
|
|
36
|
+
}
|
|
21
37
|
export interface CheckReport {
|
|
22
38
|
readonly ok: boolean;
|
|
39
|
+
/** 검사 직전 .gaon 재생성 단계(규칙 3). */
|
|
40
|
+
readonly regen: CheckRegen;
|
|
23
41
|
readonly steps: readonly CheckStepResult[];
|
|
24
42
|
}
|
|
25
43
|
/**
|
|
26
|
-
* `gaon check` 진입점.
|
|
27
|
-
* exit 1. --only 지정 시 그 단계만
|
|
44
|
+
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
45
|
+
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
46
|
+
* 실행. --include-doctor 시 doctor 추가.
|
|
28
47
|
*
|
|
29
48
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
30
49
|
*/
|
package/dist/commands/check.js
CHANGED
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
import { spawn } from 'node:child_process';
|
|
29
29
|
import { existsSync, readFileSync } from 'node:fs';
|
|
30
30
|
import { join } from 'node:path';
|
|
31
|
+
import { generateTablesDts } from '@gaonjs/data';
|
|
32
|
+
import { generateRoutesDts } from '@gaonjs/web';
|
|
31
33
|
import { runDoctorCommand } from '../doctor.js';
|
|
34
|
+
import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
|
|
35
|
+
import { registerTsResolve } from '../tsResolve.js';
|
|
32
36
|
/**
|
|
33
37
|
* 프로젝트 스크립트 존재 여부. pnpm/npm 어느 쪽이든 `scripts.<name>` 을
|
|
34
38
|
* 정의해 두면 우선 사용한다.
|
|
@@ -179,8 +183,42 @@ async function runDoctorStep(cwd) {
|
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
186
|
+
* 검사 직전 .gaon 타입 브리지를 1회 재생성한다(규칙 3 · §13.4-5). 개발
|
|
187
|
+
* 서버 없이 부르는 check 가 stale/누락된 .gaon 으로 검증해 잘못된 통과·
|
|
188
|
+
* 실패를 내지 않게 한다. --only 여부와 무관하게 항상 먼저 돈다 —
|
|
189
|
+
* typecheck·vue-tsc·build 가 모두 .gaon 을 소비하기 때문.
|
|
190
|
+
*/
|
|
191
|
+
async function regenerateGaon(cwd) {
|
|
192
|
+
const layout = resolveDevLayout(cwd);
|
|
193
|
+
if (!layout.schemaDir && layout.apps.length === 0) {
|
|
194
|
+
return { status: 'skipped', tables: false, apps: [] };
|
|
195
|
+
}
|
|
196
|
+
// 생성기는 사용자 스키마·컨트롤러 .ts 를 동적 import 한다. TS-for-ESM
|
|
197
|
+
// 관례상 상대 import 가 `.js` 라 Node 가 못 찾으므로 `.js`→`.ts` 해석
|
|
198
|
+
// 훅을 등록한다(gaon dev·serve 와 동일 · tsResolve).
|
|
199
|
+
registerTsResolve();
|
|
200
|
+
try {
|
|
201
|
+
const result = await regenerateGaonOnce(layout, {
|
|
202
|
+
regenerateTables: generateTablesDts,
|
|
203
|
+
regenerateRoutes: generateRoutesDts,
|
|
204
|
+
});
|
|
205
|
+
return { status: 'done', tables: result.tables, apps: result.apps };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
return {
|
|
210
|
+
status: 'failed',
|
|
211
|
+
tables: false,
|
|
212
|
+
apps: [],
|
|
213
|
+
output: `.gaon 타입 브리지 재생성에 실패해 검사를 중단합니다: ${msg}\n` +
|
|
214
|
+
`→ domain/schema/*.ts 와 apps/*/routes.ts·controllers/*.ts 의 구문 오류를 고친 뒤 다시 실행하세요.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
220
|
+
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
221
|
+
* 실행. --include-doctor 시 doctor 추가.
|
|
184
222
|
*
|
|
185
223
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
186
224
|
*/
|
|
@@ -189,9 +227,14 @@ export async function runCheckCommand(opts = {}) {
|
|
|
189
227
|
const json = opts.json ?? false;
|
|
190
228
|
const only = opts.only;
|
|
191
229
|
const includeDoctor = opts.includeDoctor ?? false;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
230
|
+
// 규칙 3 — 검사는 최신 타입 브리지 위에서만 신뢰할 수 있다. 재생성이
|
|
231
|
+
// 실패하면 stale .gaon 으로 검사하지 않고 즉시 중단한다.
|
|
232
|
+
const regen = await regenerateGaon(cwd);
|
|
233
|
+
const steps = regen.status === 'failed'
|
|
234
|
+
? []
|
|
235
|
+
: only
|
|
236
|
+
? [only]
|
|
237
|
+
: ['typecheck', 'vue-tsc', 'build', ...(includeDoctor ? ['doctor'] : [])];
|
|
195
238
|
const results = [];
|
|
196
239
|
for (const step of steps) {
|
|
197
240
|
if (step === 'doctor') {
|
|
@@ -201,12 +244,13 @@ export async function runCheckCommand(opts = {}) {
|
|
|
201
244
|
results.push(await runStep(step, cwd));
|
|
202
245
|
}
|
|
203
246
|
}
|
|
204
|
-
const ok = results.every((r) => r.status !== 'failed');
|
|
205
|
-
const report = { ok, steps: results };
|
|
247
|
+
const ok = regen.status !== 'failed' && results.every((r) => r.status !== 'failed');
|
|
248
|
+
const report = { ok, regen, steps: results };
|
|
206
249
|
if (json) {
|
|
207
250
|
process.stdout.write(JSON.stringify(report) + '\n');
|
|
208
251
|
}
|
|
209
252
|
else {
|
|
253
|
+
writeRegenLine(regen);
|
|
210
254
|
for (const r of results) {
|
|
211
255
|
const mark = r.status === 'passed' ? '✓' : r.status === 'failed' ? '✗' : '·';
|
|
212
256
|
process.stdout.write(` ${mark} ${r.step} — ${r.status}${r.command ? ` (${r.command})` : ''}\n`);
|
|
@@ -221,3 +265,22 @@ export async function runCheckCommand(opts = {}) {
|
|
|
221
265
|
}
|
|
222
266
|
return ok ? 0 : 1;
|
|
223
267
|
}
|
|
268
|
+
/** 사람 UI 에 재생성 단계를 한 줄로 보고한다. */
|
|
269
|
+
function writeRegenLine(regen) {
|
|
270
|
+
if (regen.status === 'failed') {
|
|
271
|
+
process.stdout.write(' ✗ .gaon 재생성 — failed\n');
|
|
272
|
+
if (regen.output)
|
|
273
|
+
process.stdout.write(` ${regen.output.split('\n').join('\n ')}\n`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (regen.status === 'skipped') {
|
|
277
|
+
process.stdout.write(' · .gaon 재생성 — skipped (domain/schema · apps/* 없음)\n');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (regen.tables)
|
|
282
|
+
parts.push('tables.d.ts');
|
|
283
|
+
for (const app of regen.apps)
|
|
284
|
+
parts.push(`${app}/routes.d.ts`);
|
|
285
|
+
process.stdout.write(` ✓ .gaon 재생성 — ${parts.join(', ')}\n`);
|
|
286
|
+
}
|
package/dist/commands/db.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DbSubcommand = 'diff' | 'migrate' | 'reset' | 'seed';
|
|
1
|
+
export type DbSubcommand = 'diff' | 'migrate' | 'reset' | 'seed' | 'status';
|
|
2
2
|
export interface DbCommandOptions {
|
|
3
3
|
/** 프로젝트 루트 — gaon.config.ts·domain/schema 위치. 기본 process.cwd(). */
|
|
4
4
|
readonly cwd?: string;
|
|
@@ -12,6 +12,8 @@ export interface DbCommandOptions {
|
|
|
12
12
|
readonly yes?: boolean;
|
|
13
13
|
/** migrate/reset dry-run — 실행 없이 SQL 만 출력. */
|
|
14
14
|
readonly dryRun?: boolean;
|
|
15
|
+
/** `gaon db migrate down` — 가장 최근 이력 한 건 롤백(§4.8). */
|
|
16
|
+
readonly down?: boolean;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* `gaon db <sub>` 진입점. 사람/JSON 출력을 자체 처리하고 exitCode 를
|
package/dist/commands/db.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { runDbDiff } from '../db/diff.js';
|
|
11
11
|
import { runDbMigrate } from '../db/migrate.js';
|
|
12
12
|
import { runDbReset } from '../db/reset.js';
|
|
13
|
+
import { runDbStatus } from '../db/status.js';
|
|
13
14
|
import { runDbSeedCommand } from '../db.js';
|
|
14
15
|
/**
|
|
15
16
|
* `gaon db <sub>` 진입점. 사람/JSON 출력을 자체 처리하고 exitCode 를
|
|
@@ -34,7 +35,12 @@ export async function runDbCommand(subcommand, opts = {}) {
|
|
|
34
35
|
return r.exitCode;
|
|
35
36
|
}
|
|
36
37
|
if (subcommand === 'migrate') {
|
|
37
|
-
const r = await runDbMigrate({ cwd, dbKey, json, dryRun, configPath });
|
|
38
|
+
const r = await runDbMigrate({ cwd, dbKey, json, dryRun, down: opts.down ?? false, configPath });
|
|
39
|
+
emit(r.text, r.json);
|
|
40
|
+
return r.exitCode;
|
|
41
|
+
}
|
|
42
|
+
if (subcommand === 'status') {
|
|
43
|
+
const r = await runDbStatus({ cwd, dbKey, json, configPath });
|
|
38
44
|
emit(r.text, r.json);
|
|
39
45
|
return r.exitCode;
|
|
40
46
|
}
|
|
@@ -58,7 +64,7 @@ export async function runDbCommand(subcommand, opts = {}) {
|
|
|
58
64
|
}
|
|
59
65
|
// 방어 — dispatcher 라 도달할 수 없지만 컴파일러 만족용.
|
|
60
66
|
process.stderr.write(` ✗ 알 수 없는 db 서브커맨드: ${String(subcommand)}\n` +
|
|
61
|
-
` → 지원: diff | migrate | reset | seed\n`);
|
|
67
|
+
` → 지원: diff | migrate | reset | seed | status\n`);
|
|
62
68
|
return 1;
|
|
63
69
|
}
|
|
64
70
|
catch (err) {
|
package/dist/commands/g.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ScaffoldFile, type WriteResult } from '../scaffold/index.js';
|
|
2
|
-
export type GenerateType = 'controller' | 'model' | 'page' | 'job';
|
|
2
|
+
export type GenerateType = 'controller' | 'model' | 'page' | 'job' | 'app';
|
|
3
3
|
export interface GenerateOptions {
|
|
4
4
|
readonly cwd?: string;
|
|
5
5
|
readonly app?: string;
|
package/dist/commands/g.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
//
|
|
12
12
|
// 파일 위치는 프로젝트 관습(CLAUDE.md §2)을 따른다:
|
|
13
13
|
// controller → apps/<app>/controllers/<plural>.ts
|
|
14
|
-
// model → domain/schema/<
|
|
14
|
+
// model → domain/schema/<posts>.ts + domain/models/<Post>.ts (§3.4: 스키마=테이블명 · 모델=Pascal)
|
|
15
15
|
// page → apps/<app>/pages/<path>.vue
|
|
16
16
|
// job → domain/jobs/<camel>.ts
|
|
17
|
-
import {
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { appScaffoldFiles, controllerScaffold, inflectModel, jobScaffold, jobTestScaffold, modelScaffoldFiles, pageScaffold, writeScaffold, } from '../scaffold/index.js';
|
|
18
20
|
/** argv 에서 옵션을 뽑는다(간단 파서 · runCli 관례와 일치). */
|
|
19
21
|
export function parseGenerateArgs(argv) {
|
|
20
22
|
const type = argv[0];
|
|
@@ -60,6 +62,10 @@ export function planScaffold(type, name, app) {
|
|
|
60
62
|
if (type === 'page') {
|
|
61
63
|
return [pageScaffold(name, app)];
|
|
62
64
|
}
|
|
65
|
+
// app 은 이름 자체가 앱 폴더명이 되므로 '/' 는 허용하지 않는다(app.ts 재검증).
|
|
66
|
+
if (type === 'app') {
|
|
67
|
+
return appScaffoldFiles(name);
|
|
68
|
+
}
|
|
63
69
|
if (name.includes('/')) {
|
|
64
70
|
throw new Error(`${type} 이름에 '/' 는 사용할 수 없습니다: ${name}`);
|
|
65
71
|
}
|
|
@@ -68,8 +74,9 @@ export function planScaffold(type, name, app) {
|
|
|
68
74
|
return [controllerScaffold(names, app)];
|
|
69
75
|
if (type === 'model')
|
|
70
76
|
return modelScaffoldFiles(names);
|
|
77
|
+
// 잡은 통합 테스트 골격을 짝으로 생성한다 (결정 42 · expectJobProcessed).
|
|
71
78
|
if (type === 'job')
|
|
72
|
-
return [jobScaffold(names.pascal)];
|
|
79
|
+
return [jobScaffold(names.pascal), jobTestScaffold(names.pascal)];
|
|
73
80
|
throw new Error(`알 수 없는 제너레이터: ${type}`);
|
|
74
81
|
}
|
|
75
82
|
/** `gaon g <type> <name>` 실행. exitCode 를 반환한다(0=성공, 1=실패). */
|
|
@@ -90,6 +97,23 @@ export function runGenerateCommand(type, name, opts = {}) {
|
|
|
90
97
|
}
|
|
91
98
|
return 1;
|
|
92
99
|
}
|
|
100
|
+
// gaon g app <name>: 앱 폴더가 이미 있으면 사고를 막기 위해 즉시 실패한다
|
|
101
|
+
// (--overwrite 로도 되돌리지 않는다 — 앱 재생성은 파괴적이라 사용자 판단
|
|
102
|
+
// 필요). 다른 스캐폴드는 파일 단위 skip 관례(Rails)를 그대로 유지.
|
|
103
|
+
if (type === 'app') {
|
|
104
|
+
const appDir = join(cwd, 'apps', name);
|
|
105
|
+
if (existsSync(appDir)) {
|
|
106
|
+
const msg = `앱 폴더가 이미 존재합니다: apps/${name}\n` +
|
|
107
|
+
` → 다른 이름을 쓰거나 apps/${name} 을 수동으로 정리한 뒤 다시 실행하세요.`;
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
process.stderr.write(` ✗ gaon g app: ${msg}\n`);
|
|
113
|
+
}
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
93
117
|
const write = writeScaffold(cwd, files, { overwrite: opts.overwrite });
|
|
94
118
|
// overwrite 없이 기존 파일이 있으면 실패(사고 방지 · Rails 관례).
|
|
95
119
|
const failed = write.skipped.length > 0;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface McpCommandOptions {
|
|
2
|
+
readonly cwd?: string;
|
|
3
|
+
readonly json?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* 트랜스포트. v1 은 'stdio' 만. 미래 확장(http)을 위한 자리.
|
|
6
|
+
* 지금 'http' 를 주면 명확히 미지원 에러(§7.5.3 수리 안내).
|
|
7
|
+
*/
|
|
8
|
+
readonly transport?: 'stdio' | 'http';
|
|
9
|
+
/** 서버 버전 문자열(파사드 gaonjs 가 주입). 미주입 시 'dev'. */
|
|
10
|
+
readonly version?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `gaon mcp` 진입점. stdio transport 는 stdin close 까지 살아 있는다.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runMcpCommand(opts?: McpCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon mcp` — MCP 서버 부트업 명령 (M10-B · §13.5 M10 · 질문 8)
|
|
3
|
+
*
|
|
4
|
+
* 정본 §12 질문 8 확정 명세 4종(라우트 목록·스키마 조회·마이그레이션 실행·
|
|
5
|
+
* 테스트 실행)을 stdio JSON-RPC 로 노출한다. AI 에이전트(Claude Desktop ·
|
|
6
|
+
* Cursor · Codex 등)가 이 서버에 붙어 grep 대신 프레임웍에게 직접 묻는다.
|
|
7
|
+
*
|
|
8
|
+
* 트랜스포트:
|
|
9
|
+
* - stdio (기본 · MCP 표준) — Claude Desktop 등의 mcpServers 설정이
|
|
10
|
+
* 기본으로 stdio.
|
|
11
|
+
* - v1 범위: stdio 만. http/sse 는 후속(§13.5 M10 명시 X).
|
|
12
|
+
*
|
|
13
|
+
* 사용법:
|
|
14
|
+
* gaon mcp # stdio 로 서버 부트, stdin close 까지 대기
|
|
15
|
+
* gaon mcp --json # 자동화용 배너 · 이벤트를 JSON 라인으로 stderr 출력
|
|
16
|
+
*
|
|
17
|
+
* 관례:
|
|
18
|
+
* - 배너·상태 메시지는 stderr 로만 낸다(stdout 은 MCP 프로토콜 전용).
|
|
19
|
+
* - --json 이면 { kind: 'listening' | 'stopped' | 'error' } 라인을 stderr 로.
|
|
20
|
+
* - exit code: 정상 종료 0, transport 오류 1.
|
|
21
|
+
*/
|
|
22
|
+
import { startMcpServer } from '../mcp/server.js';
|
|
23
|
+
function humanEvent(e) {
|
|
24
|
+
switch (e.kind) {
|
|
25
|
+
case 'listening':
|
|
26
|
+
return ` gaon mcp · stdio 리슨 중 (cwd=${e.cwd}) · 도구 ${e.tools.length}종: ${e.tools.join(', ')}`;
|
|
27
|
+
case 'stopped':
|
|
28
|
+
return ` gaon mcp · 종료`;
|
|
29
|
+
case 'error':
|
|
30
|
+
return ` ✗ gaon mcp · ${e.message}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `gaon mcp` 진입점. stdio transport 는 stdin close 까지 살아 있는다.
|
|
35
|
+
*/
|
|
36
|
+
export async function runMcpCommand(opts = {}) {
|
|
37
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
38
|
+
const json = opts.json ?? false;
|
|
39
|
+
const transport = opts.transport ?? 'stdio';
|
|
40
|
+
const version = opts.version ?? 'dev';
|
|
41
|
+
// 배너·이벤트는 stderr 로 낸다 — stdout 은 MCP JSON-RPC 프로토콜 스트림.
|
|
42
|
+
const emit = (e) => {
|
|
43
|
+
const line = json ? JSON.stringify(e) : humanEvent(e);
|
|
44
|
+
process.stderr.write(line + '\n');
|
|
45
|
+
};
|
|
46
|
+
if (transport !== 'stdio') {
|
|
47
|
+
emit({
|
|
48
|
+
kind: 'error',
|
|
49
|
+
message: `v1 은 stdio 트랜스포트만 지원합니다(요청값: ${transport}).\n` +
|
|
50
|
+
` → gaon mcp (기본 stdio) 로 실행하세요.`,
|
|
51
|
+
});
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
// 도구 목록 스냅샷(배너용) — TOOLS SSOT 는 tools.ts.
|
|
55
|
+
const { TOOLS } = await import('../mcp/tools.js');
|
|
56
|
+
const toolNames = TOOLS.map((t) => t.name);
|
|
57
|
+
try {
|
|
58
|
+
const handle = await startMcpServer({ cwd, version });
|
|
59
|
+
emit({ kind: 'listening', transport: 'stdio', cwd, tools: toolNames });
|
|
60
|
+
// stdin close (=상대편 파이프 종료) 시 transport 가 닫히고 wait() 이 resolve.
|
|
61
|
+
// SIGINT/SIGTERM 을 받아도 명시적으로 서버를 닫는다.
|
|
62
|
+
const onSignal = () => {
|
|
63
|
+
void handle.close();
|
|
64
|
+
};
|
|
65
|
+
process.on('SIGINT', onSignal);
|
|
66
|
+
process.on('SIGTERM', onSignal);
|
|
67
|
+
await handle.wait();
|
|
68
|
+
process.off('SIGINT', onSignal);
|
|
69
|
+
process.off('SIGTERM', onSignal);
|
|
70
|
+
emit({ kind: 'stopped' });
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
emit({ kind: 'error', message });
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/db/diff.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 방언별 up/down 을 이미 만든다(@gaonjs/data). 이 함수는 배선·표현 담당.
|
|
5
5
|
import { computeMigration } from '@gaonjs/data';
|
|
6
6
|
import { resolveDbTarget } from './resolve.js';
|
|
7
|
+
import { listMigrationFiles } from './replay.js';
|
|
7
8
|
function opSummary(op) {
|
|
8
9
|
switch (op.kind) {
|
|
9
10
|
case 'createTable':
|
|
@@ -30,6 +31,9 @@ export async function runDbDiff(opts) {
|
|
|
30
31
|
try {
|
|
31
32
|
const plan = await computeMigration(target.db, target.tables, target.dialect);
|
|
32
33
|
const ops = plan.ops.map(opSummary);
|
|
34
|
+
// diff 는 스키마↔DB 차이만 미리 보여준다(적용 X). db/migrations/*.ts 는
|
|
35
|
+
// migrate 의 replay 단계가 실제로 실행한다(§4.8) — 여기선 목록만 참고로 싣는다.
|
|
36
|
+
const migrationFiles = listMigrationFiles(opts.cwd).map((f) => `db/migrations/${f}`);
|
|
33
37
|
const json = {
|
|
34
38
|
command: 'diff',
|
|
35
39
|
db: opts.dbKey,
|
|
@@ -37,6 +41,7 @@ export async function runDbDiff(opts) {
|
|
|
37
41
|
ops,
|
|
38
42
|
up: plan.up,
|
|
39
43
|
down: plan.down,
|
|
44
|
+
migrationFiles,
|
|
40
45
|
};
|
|
41
46
|
const text = ops.length === 0
|
|
42
47
|
? ` [${opts.dbKey}] 변경 없음 — 스키마와 DB 가 일치합니다.`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Kysely } from 'kysely';
|
|
2
|
+
export type MigrationKind = 'file' | 'schema';
|
|
3
|
+
export interface JournalEntry {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly kind: MigrationKind;
|
|
6
|
+
readonly db_key: string;
|
|
7
|
+
readonly statements: number;
|
|
8
|
+
readonly summary: string;
|
|
9
|
+
readonly down_sql: string | null;
|
|
10
|
+
readonly applied_at: Date;
|
|
11
|
+
}
|
|
12
|
+
/** _gaon_migrations 존재 여부. information_schema 조회로 방언 무관. */
|
|
13
|
+
export declare function journalExists(db: Kysely<any>): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
16
|
+
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
17
|
+
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureJournal(db: Kysely<any>): Promise<void>;
|
|
20
|
+
export interface RecordArgs {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly kind: MigrationKind;
|
|
23
|
+
readonly dbKey: string;
|
|
24
|
+
readonly statements: number;
|
|
25
|
+
readonly summary: string;
|
|
26
|
+
readonly downSql: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
29
|
+
export declare function recordEntry(db: Kysely<any>, e: RecordArgs): Promise<void>;
|
|
30
|
+
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
31
|
+
export declare function latestEntry(db: Kysely<any>): Promise<JournalEntry | undefined>;
|
|
32
|
+
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
33
|
+
export declare function appliedFileMigrations(db: Kysely<any>): Promise<Set<string>>;
|
|
34
|
+
export declare function deleteEntry(db: Kysely<any>, id: string): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @gaonjs/cli · _gaon_migrations 이력 원장 (결정 39 · v0.16 §4.8)
|
|
2
|
+
//
|
|
3
|
+
// 한 원장에 두 종류를 기록한다:
|
|
4
|
+
// · kind='file' — db/migrations/*.ts 파일 리플레이. id=파일명(커넥션 DB
|
|
5
|
+
// 안에서 유일). down 은 파일 안 down() 을 쓰므로 down_sql=null.
|
|
6
|
+
// · kind='schema' — schema-diff 배치. id=`<epochMs>-<문수>`. down_sql 에
|
|
7
|
+
// 롤백 SQL(JSON 배열)을 담아 `migrate down` 이 되돌릴 수 있다.
|
|
8
|
+
//
|
|
9
|
+
// 마이그레이션은 커넥션별로 돈다(§4.5) — 원장도 커넥션 DB 마다 따로 존재한다.
|
|
10
|
+
import { sql } from 'kysely';
|
|
11
|
+
import { MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
12
|
+
/** _gaon_migrations 존재 여부. information_schema 조회로 방언 무관. */
|
|
13
|
+
export async function journalExists(db) {
|
|
14
|
+
const rows = await sql `
|
|
15
|
+
select count(*)::text as n
|
|
16
|
+
from information_schema.tables
|
|
17
|
+
where table_name = ${MIGRATIONS_TABLE}
|
|
18
|
+
`.execute(db);
|
|
19
|
+
const first = rows.rows[0];
|
|
20
|
+
return first ? Number(first.n) > 0 : false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
24
|
+
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
25
|
+
* mysql 공통 지원 타입. kind 기본값 'schema' 는 구버전 스키마 호환용.
|
|
26
|
+
*/
|
|
27
|
+
export async function ensureJournal(db) {
|
|
28
|
+
await sql
|
|
29
|
+
.raw(`create table if not exists ${MIGRATIONS_TABLE} (` +
|
|
30
|
+
` id varchar(255) not null primary key,` +
|
|
31
|
+
` kind varchar(16) not null default 'schema',` +
|
|
32
|
+
` applied_at timestamp not null,` +
|
|
33
|
+
` db_key varchar(64) not null,` +
|
|
34
|
+
` statements integer not null,` +
|
|
35
|
+
` summary text not null,` +
|
|
36
|
+
` down_sql text` +
|
|
37
|
+
`)`)
|
|
38
|
+
.execute(db);
|
|
39
|
+
}
|
|
40
|
+
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
41
|
+
export async function recordEntry(db, e) {
|
|
42
|
+
await sql `
|
|
43
|
+
insert into ${sql.ref(MIGRATIONS_TABLE)}
|
|
44
|
+
(id, kind, applied_at, db_key, statements, summary, down_sql)
|
|
45
|
+
values (${e.id}, ${e.kind}, ${new Date()}, ${e.dbKey}, ${e.statements}, ${e.summary}, ${e.downSql})
|
|
46
|
+
`.execute(db);
|
|
47
|
+
}
|
|
48
|
+
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
49
|
+
export async function latestEntry(db) {
|
|
50
|
+
if (!(await journalExists(db)))
|
|
51
|
+
return undefined;
|
|
52
|
+
const rows = await sql `
|
|
53
|
+
select id, kind, db_key, statements, summary, down_sql, applied_at
|
|
54
|
+
from ${sql.ref(MIGRATIONS_TABLE)}
|
|
55
|
+
order by applied_at desc, id desc
|
|
56
|
+
limit 1
|
|
57
|
+
`.execute(db);
|
|
58
|
+
return rows.rows[0];
|
|
59
|
+
}
|
|
60
|
+
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
61
|
+
export async function appliedFileMigrations(db) {
|
|
62
|
+
if (!(await journalExists(db)))
|
|
63
|
+
return new Set();
|
|
64
|
+
const rows = await sql `
|
|
65
|
+
select id from ${sql.ref(MIGRATIONS_TABLE)} where kind = 'file'
|
|
66
|
+
`.execute(db);
|
|
67
|
+
return new Set(rows.rows.map((r) => r.id));
|
|
68
|
+
}
|
|
69
|
+
export async function deleteEntry(db, id) {
|
|
70
|
+
await sql `delete from ${sql.ref(MIGRATIONS_TABLE)} where id = ${id}`.execute(db);
|
|
71
|
+
}
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { listMigrationFiles } from './replay.js';
|
|
1
2
|
export interface DbMigrateOptions {
|
|
2
3
|
readonly cwd: string;
|
|
3
4
|
readonly dbKey: string;
|
|
4
5
|
readonly json: boolean;
|
|
5
6
|
readonly dryRun: boolean;
|
|
7
|
+
/** `gaon db migrate down` — 가장 최근 이력 한 건 롤백. */
|
|
8
|
+
readonly down?: boolean;
|
|
6
9
|
readonly configPath?: string;
|
|
7
10
|
}
|
|
8
11
|
export interface DbMigrateResult {
|
|
@@ -11,6 +14,8 @@ export interface DbMigrateResult {
|
|
|
11
14
|
readonly json: unknown;
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
|
-
* `gaon db migrate` — 진짜 apply. 실 DB 필요(§9).
|
|
17
|
+
* `gaon db migrate` — 진짜 apply(합성형: replay → schema-diff). 실 DB 필요(§9).
|
|
15
18
|
*/
|
|
16
19
|
export declare function runDbMigrate(opts: DbMigrateOptions): Promise<DbMigrateResult>;
|
|
20
|
+
/** `gaon db status` 가 재사용하는 마이그레이션 파일 목록(파일명 순). */
|
|
21
|
+
export { listMigrationFiles };
|