@gaonjs/cli 0.13.1 → 0.14.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/dev.d.ts +2 -0
- package/dist/commands/dev.js +11 -0
- package/dist/dev/console.d.ts +1 -1
- package/dist/dev/console.js +1 -0
- package/dist/dev/frontend-build.d.ts +14 -0
- package/dist/dev/frontend-build.js +56 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/templates/project/agents/data.md.tpl +27 -0
- package/dist/templates/project/agents/web.md.tpl +29 -0
- package/dist/templates/project/package.json.tpl +1 -0
- package/package.json +4 -4
- package/dist/templates/auth/app.ts.tpl +0 -29
- package/dist/templates/auth/server.ts.tpl +0 -14
package/dist/commands/dev.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface DevCommandOptions {
|
|
|
36
36
|
readonly noVueTsc?: boolean;
|
|
37
37
|
/** Docker 자동 기동 끄기(개별 debug · 사용자가 이미 띄운 경우). */
|
|
38
38
|
readonly noDocker?: boolean;
|
|
39
|
+
/** 프론트 watch 빌드(vite build --watch) 끄기(개별 debug). */
|
|
40
|
+
readonly noVite?: boolean;
|
|
39
41
|
/** 통합 콘솔 타임스탬프 표시. */
|
|
40
42
|
readonly timestamp?: boolean;
|
|
41
43
|
/** 시그널(테스트 주입 · 기본 process). */
|
package/dist/commands/dev.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createDevConsole } from '../dev/console.js';
|
|
|
30
30
|
import { ensureInfra, composeDown } from '../dev/docker.js';
|
|
31
31
|
import { startTscWatchers, killChild } from '../dev/tsc.js';
|
|
32
32
|
import { startRestartWatcher } from '../dev/watcher.js';
|
|
33
|
+
import { startFrontendBuild } from '../dev/frontend-build.js';
|
|
33
34
|
/**
|
|
34
35
|
* gaon 셀프 경로를 찾는다. 부모가 gaon 으로 실행됐다면 argv[1] 이
|
|
35
36
|
* gaonjs/dist/cli.js (또는 개발 시 packages/gaonjs/src/cli.ts). 자식
|
|
@@ -148,6 +149,15 @@ export async function runDevCommand(opts = {}) {
|
|
|
148
149
|
}
|
|
149
150
|
// ── 2) .gaon 재생성 워처 ─────────────────────────────────────────
|
|
150
151
|
const regen = await startGaonRegen({ cwd, console: consoleOut });
|
|
152
|
+
// ── 2') 프론트 번들 watch 빌드(결정 67) ─────────────────────────
|
|
153
|
+
// serve 가 dist/<앱>/index.html 을 문서 셸로 읽으므로 dev 에서 dist 를
|
|
154
|
+
// 최신으로 유지한다. 프론트 없는 프로젝트면 no-op.
|
|
155
|
+
const frontend = opts.noVite
|
|
156
|
+
? undefined
|
|
157
|
+
: startFrontendBuild({
|
|
158
|
+
cwd,
|
|
159
|
+
onLog: (line, level) => consoleOut.log('vite', line, level),
|
|
160
|
+
});
|
|
151
161
|
// ── 3) tsc / vue-tsc 워치 ────────────────────────────────────────
|
|
152
162
|
const tsc = opts.noTsc && opts.noVueTsc
|
|
153
163
|
? undefined
|
|
@@ -243,6 +253,7 @@ export async function runDevCommand(opts = {}) {
|
|
|
243
253
|
if (tsc) {
|
|
244
254
|
await tsc.stop();
|
|
245
255
|
}
|
|
256
|
+
frontend?.close();
|
|
246
257
|
regen.close();
|
|
247
258
|
if (stopDocker && composeFile) {
|
|
248
259
|
consoleOut.log('docker', '--stop-docker · compose down 실행');
|
package/dist/dev/console.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 정책상 chalk 는 쓰지 않는다 — 필요한 코드만 직접 쓴다.
|
|
14
14
|
*/
|
|
15
15
|
/** 콘솔이 구분하는 로그 소스. */
|
|
16
|
-
export type DevSource = 'dev' | 'docker' | 'serve' | 'watcher' | 'tsc' | 'vue-tsc';
|
|
16
|
+
export type DevSource = 'dev' | 'docker' | 'serve' | 'watcher' | 'tsc' | 'vue-tsc' | 'vite';
|
|
17
17
|
/** 로그 레벨 — human 은 색상 강조, json 은 필드로 실린다. */
|
|
18
18
|
export type DevLevel = 'info' | 'warn' | 'error';
|
|
19
19
|
export interface DevConsoleOptions {
|
package/dist/dev/console.js
CHANGED
|
@@ -26,6 +26,7 @@ const SOURCE_COLOR = {
|
|
|
26
26
|
watcher: '\x1b[36m', // cyan — 파일 감시
|
|
27
27
|
tsc: '\x1b[33m', // yellow — 타입 검사
|
|
28
28
|
'vue-tsc': '\x1b[95m', // bright magenta — vue 전용
|
|
29
|
+
vite: '\x1b[92m', // bright green — 프론트 빌드
|
|
29
30
|
};
|
|
30
31
|
/** stdout.isTTY 여부 자동 감지(테스트에서는 stream.isTTY 를 봄). */
|
|
31
32
|
function detectTty(stream) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FrontendBuildHandle {
|
|
2
|
+
/** watch 빌드 프로세스를 종료한다. */
|
|
3
|
+
close(): void;
|
|
4
|
+
}
|
|
5
|
+
export interface StartFrontendBuildOptions {
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
/** 통합 콘솔 로그 훅. */
|
|
8
|
+
readonly onLog: (line: string, level: 'info' | 'warn' | 'error') => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* vite build --watch 를 띄운다. 프론트가 없거나 vite 가 없으면 아무 것도 하지
|
|
12
|
+
* 않고 no-op 핸들을 돌려준다(dev 는 계속 진행).
|
|
13
|
+
*/
|
|
14
|
+
export declare function startFrontendBuild(opts: StartFrontendBuildOptions): FrontendBuildHandle;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · dev/frontend-build — 프론트 번들 watch 빌드 (결정 67 · M9-C)
|
|
3
|
+
*
|
|
4
|
+
* `gaon dev` 는 서버(serve)를 재시작 방식으로 띄운다(§dev.ts). 프론트 번들은
|
|
5
|
+
* serve 가 dist/<앱>/index.html 을 매 요청 읽어 문서 셸에 이어 붙이므로
|
|
6
|
+
* (packages/web frontend.ts), dev 에서 그 dist 를 최신으로 유지하려면 vite 를
|
|
7
|
+
* watch 모드로 함께 돌린다. 진짜 HMR(모듈 교체)은 v1 어댑터 범위 밖이라(§6.4)
|
|
8
|
+
* "저장 → 재빌드 → 새로고침" 루프를 제공한다.
|
|
9
|
+
*
|
|
10
|
+
* fail-open: apps/web/index.html·vite 가 없으면 조용히 건너뛴다(프론트 없는
|
|
11
|
+
* API 전용 프로젝트도 gaon dev 가 동작해야 한다). vite 실행 실패는 통합
|
|
12
|
+
* 콘솔에 알리되 dev 전체를 막지 않는다.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
17
|
+
/** 프로젝트에 프론트 진입 HTML(apps/web/index.html)이 있는가. */
|
|
18
|
+
function hasFrontend(cwd) {
|
|
19
|
+
return existsSync(join(cwd, 'apps', 'web', 'index.html'));
|
|
20
|
+
}
|
|
21
|
+
/** cwd 의 로컬 vite bin 을 찾는다(없으면 undefined — 건너뜀). */
|
|
22
|
+
function resolveViteBin(cwd) {
|
|
23
|
+
const bin = join(cwd, 'node_modules', '.bin', 'vite');
|
|
24
|
+
return existsSync(bin) ? bin : undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* vite build --watch 를 띄운다. 프론트가 없거나 vite 가 없으면 아무 것도 하지
|
|
28
|
+
* 않고 no-op 핸들을 돌려준다(dev 는 계속 진행).
|
|
29
|
+
*/
|
|
30
|
+
export function startFrontendBuild(opts) {
|
|
31
|
+
const cwd = resolve(opts.cwd);
|
|
32
|
+
if (!hasFrontend(cwd)) {
|
|
33
|
+
opts.onLog('프론트 진입 HTML(apps/web/index.html)이 없어 vite 빌드를 건너뜁니다.', 'info');
|
|
34
|
+
return { close() { } };
|
|
35
|
+
}
|
|
36
|
+
const bin = resolveViteBin(cwd);
|
|
37
|
+
if (!bin) {
|
|
38
|
+
opts.onLog('vite 를 찾지 못해 프론트 watch 빌드를 건너뜁니다 → pnpm add -D vite @vitejs/plugin-vue', 'warn');
|
|
39
|
+
return { close() { } };
|
|
40
|
+
}
|
|
41
|
+
const child = spawn(bin, ['build', '--watch'], {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
env: { ...process.env },
|
|
45
|
+
});
|
|
46
|
+
child.stdout?.on('data', (b) => opts.onLog(b.toString().trimEnd(), 'info'));
|
|
47
|
+
child.stderr?.on('data', (b) => opts.onLog(b.toString().trimEnd(), 'error'));
|
|
48
|
+
child.on('error', (err) => opts.onLog(`vite 실행 실패: ${err.message}`, 'error'));
|
|
49
|
+
opts.onLog('▶ vite build --watch — 프론트 번들 감시 (dist/web)', 'info');
|
|
50
|
+
return {
|
|
51
|
+
close() {
|
|
52
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
53
|
+
child.kill('SIGTERM');
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/dev/index.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export { startTscWatchers, killChild } from './tsc.js';
|
|
|
12
12
|
export type { TscWatcherOptions, TscWatcherHandle } from './tsc.js';
|
|
13
13
|
export { startRestartWatcher, isRestartChange, resolveWatchRoots } from './watcher.js';
|
|
14
14
|
export type { RestartWatcherOptions, RestartWatcherHandle } from './watcher.js';
|
|
15
|
+
export { startFrontendBuild } from './frontend-build.js';
|
|
16
|
+
export type { FrontendBuildHandle, StartFrontendBuildOptions } from './frontend-build.js';
|
|
15
17
|
export { createGaonViteServer } from './vite.js';
|
|
16
18
|
export type { GaonViteServer, CreateGaonViteServerOptions } from './vite.js';
|
package/dist/dev/index.js
CHANGED
|
@@ -8,5 +8,7 @@ export { createDevConsole } from './console.js';
|
|
|
8
8
|
export { findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, } from './docker.js';
|
|
9
9
|
export { startTscWatchers, killChild } from './tsc.js';
|
|
10
10
|
export { startRestartWatcher, isRestartChange, resolveWatchRoots } from './watcher.js';
|
|
11
|
+
// 프론트 번들 watch 빌드 (결정 67)
|
|
12
|
+
export { startFrontendBuild } from './frontend-build.js';
|
|
11
13
|
// Vite dev server 프로그램적 기동 (M3-runtime) — dev.ts 배선은 M1 이 통합.
|
|
12
14
|
export { createGaonViteServer } from './vite.js';
|
|
@@ -434,6 +434,33 @@ gaon db seed # domain/seed.ts 실행
|
|
|
434
434
|
**파일명 순으로 실제 실행**(replay)하고, ② 그 뒤 스키마 diff 로 나머지를
|
|
435
435
|
적용한다.
|
|
436
436
|
|
|
437
|
+
- **replay 가 diff 보다 먼저다 — 손작성 마이그는 "아직 diff 가 안 만든 테이블"을
|
|
438
|
+
참조할 수 없다** (결정 65). 스키마에만 선언돼 있고 아직 DB 에 없는 테이블을
|
|
439
|
+
FK 로 거는 마이그레이션은 **빈 DB 에서만** `relation "posts" does not exist` 로
|
|
440
|
+
실패한다(테이블이 이미 있는 개발 DB 에서는 통과 — 그래서 CI·새 클론에서만
|
|
441
|
+
터진다). 조인 테이블처럼 남의 테이블을 참조해야 하면 **참조 대상도 같은/앞선
|
|
442
|
+
마이그 파일에서 함께 만든다**:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// 20260728_add_tags.ts — posts 를 FK 로 걸려면 posts 가 먼저 존재해야 한다.
|
|
446
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
447
|
+
await db.schema.createTable('tags')/* … */.execute()
|
|
448
|
+
await db.schema.createTable('posts_tags')
|
|
449
|
+
.addColumn('postId', 'bigint', (c) => c.notNull().references('posts.id').onDelete('cascade'))
|
|
450
|
+
// ↑ posts 가 스키마에만 있고 DB 에 없으면 빈 DB 에서 실패한다.
|
|
451
|
+
.execute()
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
전부 스키마 우선으로 두면(손작성 마이그 없이) diff 가 의존 순서를 알아서
|
|
456
|
+
잡으므로 이 함정 자체가 없다 — **손작성 마이그는 diff 로 표현 못 하는 것에만**
|
|
457
|
+
쓴다(rename·백필·수동 DDL).
|
|
458
|
+
|
|
459
|
+
**단, 요구받은 마이그레이션을 생략하라는 뜻이 아니다** (결정 66). 지시나 리뷰가
|
|
460
|
+
마이그레이션 파일을 명시적으로 요구하면 만들되, 위 방식(참조 대상을 같은
|
|
461
|
+
파일에서 `ifNotExists()` 로 함께 보장)으로 함정을 피한다. 이 규칙은 마이그를
|
|
462
|
+
**쓸지 말지**가 아니라 **어떻게 쓸지**를 정한다.
|
|
463
|
+
|
|
437
464
|
- **손작성 마이그는 `db/migrations/<파일명>.ts`** 에 두고 `up(db)`·`down(db)`
|
|
438
465
|
(Kysely) 를 export 한다. **파일명이 곧 버전 키** — `0001_add_posts.ts`
|
|
439
466
|
시퀀스든 `20260724_add_posts.ts` 타임스탬프든 사전순 정렬이 안정적이면 된다
|
|
@@ -172,6 +172,35 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
- **세션을 확정·해제하는 것은 `this.auth`** (결정 65). `requireAuth()` 는 이미
|
|
176
|
+
로그인된 요청을 **지키는** 쪽이고, 로그인 자체를 **성립시키는** 호출은 따로다:
|
|
177
|
+
|
|
178
|
+
| 호출 | 하는 일 |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `this.auth.user` | 현재 사용자 (`GaonCurrentUser \| null`) |
|
|
181
|
+
| `this.auth.login(user)` | 세션에 사용자 id 를 심어 로그인 상태로 만든다 |
|
|
182
|
+
| `this.auth.logout()` | 세션에서 사용자를 지운다 |
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// 로그인 — 폼 액션의 유일한 render+redirect 혼용 예외(결정 57 보완)
|
|
186
|
+
async create() {
|
|
187
|
+
const { email, password } = this.params({ _row: {} as { email: string; password: string } })
|
|
188
|
+
const user = await User.where('email', '=', email).first()
|
|
189
|
+
if (!user || !(await verifyPassword(password, user.passwordDigest))) {
|
|
190
|
+
return this.render('Auth/Login', { error: '이메일 또는 비밀번호가 올바르지 않습니다.', csrf: this.csrfToken() })
|
|
191
|
+
}
|
|
192
|
+
this.auth.login(user) // 세션 확정
|
|
193
|
+
return this.redirect('/dashboard')
|
|
194
|
+
}
|
|
195
|
+
// 로그아웃 — DELETE /session (페이지에서 router.delete 로 호출 · 결정 64)
|
|
196
|
+
async destroy() {
|
|
197
|
+
this.auth.logout()
|
|
198
|
+
return this.redirect('/session/new')
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
API 앱(JWT)은 세션 대신 `this.jwt.issue(user)` / `this.jwt.refresh(token)` 를 쓴다.
|
|
203
|
+
|
|
175
204
|
- **`this.auth.user`/`requireAuth()` 사용자 타입은 앱이 증강한다** (결정 58).
|
|
176
205
|
`GaonCurrentUser` 는 빈 인터페이스라 증강 없이는 `user.id` 접근이 타입에러다.
|
|
177
206
|
`gaon g auth` 가 `apps/<app>/auth.ts` 에 심는다:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
30
|
"@gaonjs/async": "0.3.1",
|
|
31
|
-
"@gaonjs/config": "0.
|
|
32
|
-
"@gaonjs/
|
|
31
|
+
"@gaonjs/config": "0.4.0",
|
|
32
|
+
"@gaonjs/web": "0.6.0",
|
|
33
33
|
"@gaonjs/data": "0.8.4",
|
|
34
|
-
"@gaonjs/
|
|
34
|
+
"@gaonjs/core": "0.2.0",
|
|
35
35
|
"@gaonjs/mail": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// 웹 앱 팩토리 — gaon g auth 스캐폴드. createWebApp 으로 세션·인증을 배선한다.
|
|
2
|
-
import { createApp, type AppSessionOptions } from 'gaonjs/web'
|
|
3
|
-
import appRoutes from './routes.js'
|
|
4
|
-
import session from './controllers/session.js'
|
|
5
|
-
import registration from './controllers/registration.js'
|
|
6
|
-
import dashboard from './controllers/dashboard.js'
|
|
7
|
-
import { loadUser } from './auth.js'
|
|
8
|
-
|
|
9
|
-
export interface WebAppDeps {
|
|
10
|
-
/** 세션 설정 — { redisUrl, secret } (또는 redis 인스턴스). */
|
|
11
|
-
readonly session: AppSessionOptions
|
|
12
|
-
/** 서명 쿠키/CSRF 용 비밀. */
|
|
13
|
-
readonly cookieSecret?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function createWebApp(deps: WebAppDeps) {
|
|
17
|
-
return createApp({
|
|
18
|
-
apps: [
|
|
19
|
-
{
|
|
20
|
-
name: '{{APP_NAME}}',
|
|
21
|
-
routes: appRoutes,
|
|
22
|
-
controllers: { session, registration, dashboard },
|
|
23
|
-
session: deps.session,
|
|
24
|
-
auth: { loadUser, loginRedirect: '/session/new' },
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
cookieSecret: deps.cookieSecret,
|
|
28
|
-
})
|
|
29
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// 서버 진입점 — gaon g auth 스캐폴드. `node dist/server.js` 로 실행.
|
|
2
|
-
import { createWebApp } from './apps/{{APP_NAME}}/app.js'
|
|
3
|
-
|
|
4
|
-
const app = await createWebApp({
|
|
5
|
-
session: {
|
|
6
|
-
redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379',
|
|
7
|
-
secret: process.env.SESSION_SECRET ?? 'change-me-to-a-32+char-random-secret!!',
|
|
8
|
-
},
|
|
9
|
-
cookieSecret: process.env.COOKIE_SECRET,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
const port = Number(process.env.PORT ?? 3000)
|
|
13
|
-
await app.listen({ port, host: '0.0.0.0' })
|
|
14
|
-
console.log(`web 앱이 http://localhost:${port} 에서 실행 중입니다.`)
|