@gaonjs/cli 0.4.0 → 0.10.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/commands/check.d.ts +50 -0
- package/dist/commands/check.js +286 -0
- package/dist/commands/console.d.ts +46 -0
- package/dist/commands/console.js +129 -0
- 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/commands/new.d.ts +45 -0
- package/dist/commands/new.js +274 -0
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +119 -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/no-auto-import.d.ts +10 -0
- package/dist/doctor/no-auto-import.js +158 -0
- package/dist/doctor/schema-filename.d.ts +6 -0
- package/dist/doctor/schema-filename.js +81 -0
- package/dist/doctor/shared-composable-purity.d.ts +8 -0
- package/dist/doctor/shared-composable-purity.js +164 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +6 -5
- package/dist/doctor.d.ts +51 -0
- package/dist/doctor.js +191 -7
- package/dist/generate.js +2 -2
- package/dist/hub.d.ts +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +154 -16
- 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/index.d.ts +23 -0
- package/dist/templates/index.js +66 -0
- package/dist/templates/index.ts +85 -0
- package/dist/templates/project/.env.example.tpl +18 -0
- package/dist/templates/project/.gitignore.tpl +24 -0
- package/dist/templates/project/.npmrc.tpl +4 -0
- package/dist/templates/project/AGENTS.md.tpl +210 -0
- package/dist/templates/project/CLAUDE.md.tpl +119 -0
- package/dist/templates/project/agents/async.md.tpl +218 -0
- package/dist/templates/project/agents/data.md.tpl +532 -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/channels/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
- package/dist/templates/project/apps/web/index.html.tpl +18 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
- package/dist/templates/project/apps/web/main.ts.tpl +24 -0
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
- package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
- package/dist/templates/project/docker-compose.yaml.tpl +73 -0
- package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
- package/dist/templates/project/gaon.config.ts.tpl +27 -0
- package/dist/templates/project/package.json.tpl +30 -0
- package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
- package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
- package/dist/templates/project/tsconfig.json.tpl +25 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** `gaon new` 옵션. 모두 선택적 — 기본값이 The One Way. */
|
|
2
|
+
export interface NewCommandOptions {
|
|
3
|
+
/** 프로젝트 생성 위치의 부모 폴더. 기본 process.cwd(). */
|
|
4
|
+
readonly cwd?: string;
|
|
5
|
+
/** 자동화용 구조화 출력. */
|
|
6
|
+
readonly json?: boolean;
|
|
7
|
+
/** 의존성 설치 스킵(테스트·오프라인). */
|
|
8
|
+
readonly skipInstall?: boolean;
|
|
9
|
+
/** git init · 첫 커밋 스킵(테스트·기존 git 저장소에 삽입). */
|
|
10
|
+
readonly skipGit?: boolean;
|
|
11
|
+
/** 패키지 매니저. 기본 pnpm(모노레포 관례 정합). */
|
|
12
|
+
readonly packageManager?: 'pnpm' | 'npm' | 'yarn';
|
|
13
|
+
/**
|
|
14
|
+
* (테스트 훅) 템플릿의 gaonjs 의존성 버전. 미지정 시 파사드(gaonjs)
|
|
15
|
+
* 패키지의 실 버전을 자동 조회한다(package.json).
|
|
16
|
+
*/
|
|
17
|
+
readonly gaonjsVersion?: string;
|
|
18
|
+
}
|
|
19
|
+
/** JSON 리포트(--json). 사람 모드는 콘솔에 사람이 읽는 요약을 출력한다. */
|
|
20
|
+
export interface NewCommandResult {
|
|
21
|
+
readonly ok: boolean;
|
|
22
|
+
readonly project: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly filesCreated: number;
|
|
25
|
+
readonly install: {
|
|
26
|
+
readonly ran: boolean;
|
|
27
|
+
readonly skipped: boolean;
|
|
28
|
+
readonly packageManager: string;
|
|
29
|
+
readonly durationMs: number;
|
|
30
|
+
readonly exitCode: number | null;
|
|
31
|
+
};
|
|
32
|
+
readonly git: {
|
|
33
|
+
readonly ran: boolean;
|
|
34
|
+
readonly skipped: boolean;
|
|
35
|
+
readonly initialized: boolean;
|
|
36
|
+
readonly firstCommit: boolean;
|
|
37
|
+
};
|
|
38
|
+
readonly totalMs: number;
|
|
39
|
+
readonly error?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `gaon new <name>` 실행. 파일 생성 → pnpm install → git init 순서.
|
|
43
|
+
* 각 단계는 옵션으로 스킵 가능. 반환값은 프로세스 종료 코드(0=성공).
|
|
44
|
+
*/
|
|
45
|
+
export declare function runNewCommand(name: string, opts?: NewCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon new <name>` — 프로젝트 스캐폴드 (M9-F).
|
|
2
|
+
//
|
|
3
|
+
// v0.15 §13.5 M9 완료 기준 "설치 후 첫 화면까지 60초" 를 만족시키기 위한
|
|
4
|
+
// One Way. `gaon new demo` → cd demo → `gaon serve` → http://localhost:3000
|
|
5
|
+
// 응답 이 전체 흐름의 목표. 60초 안에 이 왕복이 끝나야 한다.
|
|
6
|
+
//
|
|
7
|
+
// 관례(§2 · errata E-1 · E-3 · E-4 · E-5):
|
|
8
|
+
// · 폴더 배치: apps/<앱>/ + domain/ + shared/ (앱 밖에 domain)
|
|
9
|
+
// · 프론트: Vue + Inertia SPA (`gaonjs/vue`)
|
|
10
|
+
// · 인프라: docker-compose.yaml 5종(pg · redis · nats · mailpit · minio)
|
|
11
|
+
// · 컴포저블·레이아웃: E-5(apps/<앱>/composables + shared/composables, 대칭
|
|
12
|
+
// 구조 · layouts/Default.vue 존재 시 자동 적용)
|
|
13
|
+
// · 자동 import 금지 — 모든 import 는 명시적으로(E-5 §2.4).
|
|
14
|
+
//
|
|
15
|
+
// 옵션은 최소로: --skip-install · --skip-git · --json · --package-manager.
|
|
16
|
+
// The One Way — pnpm workspace monorepo · Vue · Inertia SPA · 5종 인프라.
|
|
17
|
+
//
|
|
18
|
+
// 이 파일은 파일 생성·설치·git init 만 담당하고, argv 파싱과 배선은
|
|
19
|
+
// index.ts 가 통합한다(M9-F 는 편집 X — M1 이 통합 커밋).
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { renderProjectFiles } from '../templates/index.js';
|
|
26
|
+
/** 이름 유효성 — npm 패키지명 규칙(단순 부분)만 검사. */
|
|
27
|
+
function validateProjectName(name) {
|
|
28
|
+
if (!name)
|
|
29
|
+
throw new Error('프로젝트 이름이 비어 있습니다.');
|
|
30
|
+
if (name.includes('/') || name.includes('\\')) {
|
|
31
|
+
throw new Error(`프로젝트 이름에 '/' 또는 '\\\\' 는 사용할 수 없습니다: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
if (name.includes('..')) {
|
|
34
|
+
throw new Error(`프로젝트 이름에 '..' 는 사용할 수 없습니다: ${name}`);
|
|
35
|
+
}
|
|
36
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) {
|
|
37
|
+
throw new Error(`프로젝트 이름은 영문/숫자/-/_ 로 시작해 같은 문자만 씁니다: ${name}\n` +
|
|
38
|
+
` → 예: gaon new my-app · gaon new demo`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 파사드(gaonjs) 패키지의 실 버전을 찾는다 — package.json 을 위로 훑는다. */
|
|
42
|
+
function detectGaonjsVersion() {
|
|
43
|
+
// 가장 안전한 방법: 이 파일 위치를 기준으로 packages/gaonjs/package.json 을
|
|
44
|
+
// 상향 탐색. 개발 트리(src)와 배포 트리(dist) 모두에서 동작한다.
|
|
45
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
for (let i = 0; i < 8; i++) {
|
|
47
|
+
const candidate = join(dir, '..', 'gaonjs', 'package.json');
|
|
48
|
+
if (existsSync(candidate)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(candidate, 'utf8');
|
|
51
|
+
const pkg = JSON.parse(raw);
|
|
52
|
+
if (typeof pkg.version === 'string')
|
|
53
|
+
return `^${pkg.version}`;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* fall through */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const parent = dirname(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
break;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
// 발견 못 하면 최근 스텁 배포 라인의 안전 하한.
|
|
65
|
+
return '^0.5.0';
|
|
66
|
+
}
|
|
67
|
+
/** 대상 폴더가 비어 있는지(생성 대상으로 안전한지) 확인. */
|
|
68
|
+
function isEmptyOrMissing(dir) {
|
|
69
|
+
if (!existsSync(dir))
|
|
70
|
+
return true;
|
|
71
|
+
try {
|
|
72
|
+
const entries = readdirSync(dir);
|
|
73
|
+
return entries.length === 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** 파일 전체를 디스크에 쓴다. 이미 있으면 예외. */
|
|
80
|
+
function writeAll(root, files) {
|
|
81
|
+
let count = 0;
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
const abs = join(root, f.path);
|
|
84
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
85
|
+
writeFileSync(abs, f.contents, 'utf8');
|
|
86
|
+
count += 1;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
/** 패키지 매니저 install 명령을 동기 실행. 반환값에 종료 코드·시간 포함.
|
|
91
|
+
* json 모드에서는 stdout 을 완전 격리해 JSON 라인이 pnpm progress 로
|
|
92
|
+
* 오염되지 않도록 한다 — 파이프라인 파서 안전 정합. */
|
|
93
|
+
function runInstall(root, pm, json) {
|
|
94
|
+
const args = ['install'];
|
|
95
|
+
const t0 = Date.now();
|
|
96
|
+
// stdio: json 모드에서는 stdout 을 ignore(우리 JSON 을 오염시키지 않게),
|
|
97
|
+
// stderr 는 사용자에게 원인 진단이 필요할 수 있어 inherit.
|
|
98
|
+
// human 모드에서는 모든 스트림을 inherit(사용자가 progress 를 본다).
|
|
99
|
+
const res = spawnSync(pm, args, {
|
|
100
|
+
cwd: root,
|
|
101
|
+
stdio: json ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
102
|
+
env: process.env,
|
|
103
|
+
});
|
|
104
|
+
const ms = Date.now() - t0;
|
|
105
|
+
if (res.error) {
|
|
106
|
+
process.stderr.write(` ✗ ${pm} 을 찾지 못했습니다. → ${pm} 을 설치하거나 --skip-install 로 건너뛰세요.\n`);
|
|
107
|
+
return { code: -1, ms };
|
|
108
|
+
}
|
|
109
|
+
return { code: res.status, ms };
|
|
110
|
+
}
|
|
111
|
+
/** git init + 첫 커밋. git 이 없으면 조용히 스킵(로컬에 git 없이도 스캐폴드는 성공). */
|
|
112
|
+
function runGitInit(root) {
|
|
113
|
+
const opts = { cwd: root, stdio: 'ignore', env: process.env };
|
|
114
|
+
const init = spawnSync('git', ['init', '--initial-branch=main'], opts);
|
|
115
|
+
if (init.error || init.status !== 0)
|
|
116
|
+
return { initialized: false, firstCommit: false };
|
|
117
|
+
spawnSync('git', ['add', '-A'], opts);
|
|
118
|
+
// 첫 커밋 — 사용자 git config 가 없어도 실패하지 않도록 -c 로 임시 identity 주입.
|
|
119
|
+
const commit = spawnSync('git', [
|
|
120
|
+
'-c',
|
|
121
|
+
'user.email=gaon@gaonjs.dev',
|
|
122
|
+
'-c',
|
|
123
|
+
'user.name=Gaon',
|
|
124
|
+
'commit',
|
|
125
|
+
'-m',
|
|
126
|
+
'chore: gaon new — initial scaffold',
|
|
127
|
+
'--no-gpg-sign',
|
|
128
|
+
], opts);
|
|
129
|
+
return { initialized: true, firstCommit: commit.status === 0 };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `gaon new <name>` 실행. 파일 생성 → pnpm install → git init 순서.
|
|
133
|
+
* 각 단계는 옵션으로 스킵 가능. 반환값은 프로세스 종료 코드(0=성공).
|
|
134
|
+
*/
|
|
135
|
+
export async function runNewCommand(name, opts = {}) {
|
|
136
|
+
const t0 = Date.now();
|
|
137
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
138
|
+
const pm = opts.packageManager ?? 'pnpm';
|
|
139
|
+
const json = opts.json ?? false;
|
|
140
|
+
const emitError = (msg, totalMs) => {
|
|
141
|
+
if (json) {
|
|
142
|
+
const result = {
|
|
143
|
+
ok: false,
|
|
144
|
+
project: name,
|
|
145
|
+
path: resolve(cwd, name),
|
|
146
|
+
filesCreated: 0,
|
|
147
|
+
install: { ran: false, skipped: true, packageManager: pm, durationMs: 0, exitCode: null },
|
|
148
|
+
git: { ran: false, skipped: true, initialized: false, firstCommit: false },
|
|
149
|
+
totalMs,
|
|
150
|
+
error: msg,
|
|
151
|
+
};
|
|
152
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
process.stderr.write(` ✗ gaon new: ${msg}\n`);
|
|
156
|
+
}
|
|
157
|
+
return 1;
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
validateProjectName(name);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
return emitError(err instanceof Error ? err.message : String(err), Date.now() - t0);
|
|
164
|
+
}
|
|
165
|
+
const root = resolve(cwd, name);
|
|
166
|
+
if (!isEmptyOrMissing(root)) {
|
|
167
|
+
return emitError(`대상 폴더가 비어 있지 않습니다: ${root}\n` +
|
|
168
|
+
` → 다른 이름을 쓰거나 폴더를 비운 뒤 다시 실행하세요.`, Date.now() - t0);
|
|
169
|
+
}
|
|
170
|
+
// 파일 생성 — 실패 시 부분 생성물이 남지 않도록 폴더를 정리하지는 않는다
|
|
171
|
+
// (사용자가 원인 파악 후 rm -rf 로 지우도록). 정상 흐름에서는 문제 없음.
|
|
172
|
+
const gaonjsVersion = opts.gaonjsVersion ?? detectGaonjsVersion();
|
|
173
|
+
const files = renderProjectFiles({ projectName: name, gaonjsVersion });
|
|
174
|
+
let filesCreated = 0;
|
|
175
|
+
try {
|
|
176
|
+
mkdirSync(root, { recursive: true });
|
|
177
|
+
filesCreated = writeAll(root, files);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
return emitError(`파일 생성 실패: ${err instanceof Error ? err.message : String(err)}`, Date.now() - t0);
|
|
181
|
+
}
|
|
182
|
+
// pnpm install (또는 npm/yarn) — 스킵 시 안내만.
|
|
183
|
+
const installReport = {
|
|
184
|
+
ran: false,
|
|
185
|
+
skipped: opts.skipInstall === true,
|
|
186
|
+
packageManager: pm,
|
|
187
|
+
durationMs: 0,
|
|
188
|
+
exitCode: null,
|
|
189
|
+
};
|
|
190
|
+
if (!opts.skipInstall) {
|
|
191
|
+
if (!json)
|
|
192
|
+
process.stdout.write(`\n · 의존성 설치 (${pm} install) ...\n`);
|
|
193
|
+
const { code, ms } = runInstall(root, pm, json);
|
|
194
|
+
installReport.ran = true;
|
|
195
|
+
installReport.durationMs = ms;
|
|
196
|
+
installReport.exitCode = code;
|
|
197
|
+
if (code !== 0) {
|
|
198
|
+
// install 실패는 스캐폴드 자체 실패로 취급하지 않는다 — 파일은 이미
|
|
199
|
+
// 만들어졌으므로 사용자가 원인(레지스트리·네트워크) 확인 후 재시도 가능.
|
|
200
|
+
// 종료 코드는 실패로 반환(자동화 파이프라인 정합).
|
|
201
|
+
const total = Date.now() - t0;
|
|
202
|
+
if (json) {
|
|
203
|
+
const result = {
|
|
204
|
+
ok: false,
|
|
205
|
+
project: name,
|
|
206
|
+
path: root,
|
|
207
|
+
filesCreated,
|
|
208
|
+
install: installReport,
|
|
209
|
+
git: { ran: false, skipped: true, initialized: false, firstCommit: false },
|
|
210
|
+
totalMs: total,
|
|
211
|
+
error: `${pm} install 실패 (exit ${code})`,
|
|
212
|
+
};
|
|
213
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
process.stderr.write(` ✗ ${pm} install 이 실패했습니다 (exit ${code}).\n` +
|
|
217
|
+
` → 네트워크·레지스트리 상태를 확인한 뒤 ${root} 에서 다시 ${pm} install 을 실행하세요.\n`);
|
|
218
|
+
}
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// git init + 첫 커밋 — 스킵 시 그대로 넘어감.
|
|
223
|
+
const gitReport = { ran: false, skipped: opts.skipGit === true, initialized: false, firstCommit: false };
|
|
224
|
+
if (!opts.skipGit) {
|
|
225
|
+
if (!json)
|
|
226
|
+
process.stdout.write(` · git 저장소 초기화 (git init + 첫 커밋) ...\n`);
|
|
227
|
+
const g = runGitInit(root);
|
|
228
|
+
gitReport.ran = true;
|
|
229
|
+
gitReport.initialized = g.initialized;
|
|
230
|
+
gitReport.firstCommit = g.firstCommit;
|
|
231
|
+
}
|
|
232
|
+
const totalMs = Date.now() - t0;
|
|
233
|
+
if (json) {
|
|
234
|
+
const result = {
|
|
235
|
+
ok: true,
|
|
236
|
+
project: name,
|
|
237
|
+
path: root,
|
|
238
|
+
filesCreated,
|
|
239
|
+
install: installReport,
|
|
240
|
+
git: gitReport,
|
|
241
|
+
totalMs,
|
|
242
|
+
};
|
|
243
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
// 사람 모드 요약 — 다음 단계를 § 방향과 함께 안내(§7.5.3 원칙 정합).
|
|
247
|
+
const lines = [''];
|
|
248
|
+
lines.push(` gaon new ${name} — 스캐폴드 완료 (${filesCreated}개 파일 · ${totalMs}ms)`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(` 폴더: ${root}`);
|
|
251
|
+
if (installReport.ran) {
|
|
252
|
+
lines.push(` ${pm} install: ${installReport.exitCode === 0 ? '성공' : '실패'} (${installReport.durationMs}ms)`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
lines.push(` ${pm} install: 스킵 (--skip-install)`);
|
|
256
|
+
}
|
|
257
|
+
if (gitReport.ran) {
|
|
258
|
+
lines.push(` git init: ${gitReport.initialized ? '완료' : '실패'} · 첫 커밋: ${gitReport.firstCommit ? '완료' : '실패(수동)'}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
lines.push(` git init: 스킵 (--skip-git)`);
|
|
262
|
+
}
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push(' 다음 단계:');
|
|
265
|
+
lines.push(` 1) cd ${name}`);
|
|
266
|
+
lines.push(' 2) cp .env.example .env # 환경 변수 편집');
|
|
267
|
+
lines.push(' 3) gaon dev # Docker + 타입 브리지 + serve 통합');
|
|
268
|
+
lines.push(' 4) http://localhost:3000 # 홈 페이지 확인 (60초 실측 · v0.15 §13.5 M9)');
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push(' 문서: https://gaonjs.dev');
|
|
271
|
+
lines.push('');
|
|
272
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TestScope = 'unit' | 'integration' | 'all';
|
|
2
|
+
export interface TestCommandOptions {
|
|
3
|
+
readonly cwd?: string;
|
|
4
|
+
readonly json?: boolean;
|
|
5
|
+
readonly scope?: TestScope;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `gaon test` 진입점. args 는 사용자가 넘긴 잔여 인자(필터 문자열 등).
|
|
9
|
+
* pnpm test 는 pnpm 관례상 `pnpm test -- <args>` 로 넘겨야 vitest 까지 도달.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runTestCommand(args?: readonly string[], opts?: TestCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon test` — vitest wrapper (M9-G · v0.15 §13.5)
|
|
3
|
+
*
|
|
4
|
+
* The One Way — 하나의 명령이 vitest 를 얇게 감싼다. 사용자는
|
|
5
|
+
* `gaon test` 로 전체를, `gaon test posts` 로 필터를, `gaon test --unit`
|
|
6
|
+
* 으로 단위만 실행한다. 나머지는 전부 vitest 에 그대로 위임 — 우리가
|
|
7
|
+
* 관례를 재발명하지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* 스코프 필터(§9 실 인프라 관례):
|
|
10
|
+
* --unit *.test.ts (통합 제외)
|
|
11
|
+
* --integration *.integration.test.ts 만
|
|
12
|
+
* (기본) 둘 다 실행
|
|
13
|
+
*
|
|
14
|
+
* 실행 경로 우선순위:
|
|
15
|
+
* 1) 사용자 package.json 의 `test` 스크립트가 있으면 `pnpm test -- <args>`
|
|
16
|
+
* 2) 로컬 node_modules/.bin/vitest 가 있으면 직접 spawn(`run` 모드)
|
|
17
|
+
* 3) 둘 다 없으면 exit 127 + 설치 안내
|
|
18
|
+
*
|
|
19
|
+
* §9 실 인프라 · 목업 X — 실 vitest 프로세스를 spawn 한다.
|
|
20
|
+
* exit code · signal 은 vitest 그대로 propagate.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
/** 사용자 프로젝트에 `test` 스크립트가 있는지. */
|
|
26
|
+
function hasTestScript(cwd) {
|
|
27
|
+
const pkgPath = join(cwd, 'package.json');
|
|
28
|
+
if (!existsSync(pkgPath))
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
32
|
+
return typeof pkg.scripts?.test === 'string' && pkg.scripts.test.length > 0;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 스코프 → vitest CLI 인자로 매핑. vitest 는 위치 인자를 파일 패턴으로
|
|
40
|
+
* 해석한다(맨 뒤에 붙는 값이 include 필터). --include 플래그는 vitest
|
|
41
|
+
* CLI 에 없다 — 그래서 unit 은 --exclude 로, integration 은 위치 인자
|
|
42
|
+
* 패턴으로 처리한다.
|
|
43
|
+
*/
|
|
44
|
+
function scopeArgs(scope) {
|
|
45
|
+
if (scope === 'unit') {
|
|
46
|
+
// 통합 테스트 제외 — vitest 의 --exclude 로 스킵.
|
|
47
|
+
return ['--exclude', '**/*.integration.test.ts'];
|
|
48
|
+
}
|
|
49
|
+
if (scope === 'integration') {
|
|
50
|
+
// integration 만 — 위치 인자 필터(파일 경로 substring 매칭).
|
|
51
|
+
return ['integration.test'];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `gaon test` 진입점. args 는 사용자가 넘긴 잔여 인자(필터 문자열 등).
|
|
57
|
+
* pnpm test 는 pnpm 관례상 `pnpm test -- <args>` 로 넘겨야 vitest 까지 도달.
|
|
58
|
+
*/
|
|
59
|
+
export async function runTestCommand(args = [], opts = {}) {
|
|
60
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
61
|
+
const scope = opts.scope ?? 'all';
|
|
62
|
+
const json = opts.json ?? false;
|
|
63
|
+
const scopeExtras = scopeArgs(scope);
|
|
64
|
+
const passthrough = [...scopeExtras, ...args];
|
|
65
|
+
let cmd;
|
|
66
|
+
let spawnArgs;
|
|
67
|
+
if (hasTestScript(cwd)) {
|
|
68
|
+
cmd = 'pnpm';
|
|
69
|
+
// pnpm test -- <extras> — `--` 로 뒤 인자를 스크립트에 전달.
|
|
70
|
+
spawnArgs = ['test', ...(passthrough.length ? ['--', ...passthrough] : [])];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const vitestBin = join(cwd, 'node_modules', '.bin', 'vitest');
|
|
74
|
+
if (!existsSync(vitestBin)) {
|
|
75
|
+
const msg = `vitest 가 설치돼 있지 않고 package.json 에 "test" 스크립트도 없습니다.\n` +
|
|
76
|
+
`→ pnpm add -D vitest 후 다시 실행하거나, package.json 에 "test": "vitest run" 을 추가하세요.`;
|
|
77
|
+
if (json) {
|
|
78
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
process.stderr.write(` ✗ ${msg}\n`);
|
|
82
|
+
}
|
|
83
|
+
return 127;
|
|
84
|
+
}
|
|
85
|
+
cmd = vitestBin;
|
|
86
|
+
spawnArgs = ['run', ...passthrough];
|
|
87
|
+
}
|
|
88
|
+
if (json) {
|
|
89
|
+
process.stdout.write(JSON.stringify({ kind: 'starting', cmd, args: spawnArgs, scope }) + '\n');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
process.stdout.write(` gaon test · ${cmd} ${spawnArgs.join(' ')} (scope=${scope})\n`);
|
|
93
|
+
}
|
|
94
|
+
const exitCode = await new Promise((resolvePromise) => {
|
|
95
|
+
const child = spawn(cmd, spawnArgs, {
|
|
96
|
+
cwd,
|
|
97
|
+
env: process.env,
|
|
98
|
+
// vitest 컬러 출력·리포터를 그대로 보여주기 위해 stdio 를 상속한다.
|
|
99
|
+
stdio: 'inherit',
|
|
100
|
+
});
|
|
101
|
+
child.on('error', (err) => {
|
|
102
|
+
process.stderr.write(` ✗ gaon test spawn 실패: ${String(err)}\n`);
|
|
103
|
+
resolvePromise(127);
|
|
104
|
+
});
|
|
105
|
+
child.on('close', (code, signal) => {
|
|
106
|
+
if (signal) {
|
|
107
|
+
// SIGINT 등으로 종료된 경우 — 130(SIGINT) 관례.
|
|
108
|
+
resolvePromise(signal === 'SIGINT' ? 130 : 1);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
resolvePromise(code ?? 1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
if (json) {
|
|
116
|
+
process.stdout.write(JSON.stringify({ kind: 'exited', exitCode }) + '\n');
|
|
117
|
+
}
|
|
118
|
+
return exitCode;
|
|
119
|
+
}
|
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
|
+
}
|