@gaonjs/cli 0.47.0 → 0.55.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 +3 -1
- package/dist/commands/check.js +44 -2
- package/dist/commands/db.js +9 -0
- package/dist/commands/gen.d.ts +2 -0
- package/dist/commands/gen.js +3 -1
- package/dist/commands/new.js +13 -0
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +8 -4
- package/dist/db/journal.js +58 -14
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +14 -14
- package/dist/db/replay.js +3 -3
- package/dist/db/resolve.d.ts +11 -1
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +17 -4
- package/dist/db.js +26 -5
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +9 -4
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/locale-parity.js +4 -1
- package/dist/doctor/render-return.d.ts +11 -0
- package/dist/doctor/render-return.js +143 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.js +16 -5
- package/dist/generate.d.ts +20 -1
- package/dist/generate.js +120 -21
- package/dist/hub.js +2 -0
- package/dist/i18n-config.d.ts +32 -0
- package/dist/i18n-config.js +170 -0
- package/dist/index.js +141 -29
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +13 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +16 -5
- package/dist/templates/auth/Dashboard.vue.tpl +3 -2
- package/dist/templates/auth/Login.vue.tpl +3 -5
- package/dist/templates/auth/Signup.vue.tpl +3 -5
- package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
- package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
- package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
- package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/Dockerfile.tpl +11 -1
- package/dist/templates/project/agents/async.md.tpl +70 -14
- package/dist/templates/project/agents/data.md.tpl +173 -52
- package/dist/templates/project/agents/frontend.md.tpl +19 -4
- package/dist/templates/project/agents/i18n.md.tpl +20 -2
- package/dist/templates/project/agents/mail.md.tpl +8 -1
- package/dist/templates/project/agents/realtime.md.tpl +25 -6
- package/dist/templates/project/agents/seal.md.tpl +8 -3
- package/dist/templates/project/agents/security.md.tpl +50 -19
- package/dist/templates/project/agents/storage.md.tpl +39 -6
- package/dist/templates/project/agents/web.md.tpl +54 -22
- package/dist/work.d.ts +3 -0
- package/dist/work.js +4 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -109,13 +109,15 @@ function renderHelp(version = VERSION) {
|
|
|
109
109
|
" gaon gen .gaon 타입 브리지 + api() 런타임 매니페스트만 재생성 (서버·검사 없이 · build 전제 · --json)",
|
|
110
110
|
" gaon build 멀티 앱 프론트 프로덕션 빌드 (gaon gen + apps/* 순회 · 앱별 dist/<앱>·base=/<앱>/ · --json)",
|
|
111
111
|
" gaon console 프로젝트 컨텍스트 REPL (--no-config)",
|
|
112
|
-
" gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all
|
|
113
|
-
|
|
112
|
+
" gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all)",
|
|
113
|
+
" gaon test -- <인자> `--` 뒤는 vitest 로 그대로 전달 (gaon 이 안 가로챔 · 예: gaon test -- --json --reporter=json)",
|
|
114
|
+
` gaon doctor 정적 검사 (${ALL_RULES.length} 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트·Link>Button 중첩·seal 클라 배선·보안 역전·§4.5 관계·import.meta.env·로케일 커버리지·render return 누락)`,
|
|
114
115
|
" gaon doctor --json 자동화용 JSON 출력",
|
|
115
116
|
" gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
|
|
116
117
|
" gaon doctor --fix 기계 정정 가능한 위반 계획(dry-run · v0.16 §7.5.3)",
|
|
117
118
|
" gaon doctor --fix --yes 실제 편집 적용(원본은 .bak-<타임스탬프> 로 자동 백업)",
|
|
118
|
-
" gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
|
|
119
|
+
" gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트 · --jwt --app <api> = API 앱 토큰 변형 · --public = 비-web 앱 공개 가입)",
|
|
120
|
+
" gaon g ui-kit UI 킷 스캐폴드 (Button·Input 등 원자 프리미티브 · --app <이름>)",
|
|
119
121
|
" gaon g controller <name> 컨트롤러 스캐폴드 (Rails 관례 · 페이지+JSON 액션)",
|
|
120
122
|
" gaon g model <Name> 모델 스캐폴드 (스키마+모델 · E-4 컬럼 예시)",
|
|
121
123
|
" gaon g page <Path/Name> Vue 페이지 (Inertia SPA · pageProps 브리지)",
|
|
@@ -147,19 +149,34 @@ function renderHelp(version = VERSION) {
|
|
|
147
149
|
* 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
|
|
148
150
|
*/
|
|
149
151
|
export function parseDoctorChecks(argv) {
|
|
150
|
-
// 인정 집합은 doctor.ts 의 ALL_RULES(정본
|
|
152
|
+
// 인정 집합은 doctor.ts 의 ALL_RULES(정본 28종)를 단일 출처로 쓴다 — 과거
|
|
151
153
|
// 손유지 9종 리스트가 뒤처져 --check=seal-security 같은 16종이 조용히 무시되고
|
|
152
154
|
// 전체 검사로 되돌아가던 표류를 근본 차단한다(결정 168).
|
|
153
155
|
const isKnown = (s) => ALL_RULES.includes(s);
|
|
154
156
|
const out = [];
|
|
157
|
+
const unknown = [];
|
|
155
158
|
for (const a of argv) {
|
|
156
159
|
if (a.startsWith("--check=")) {
|
|
157
160
|
for (const nm of a.slice("--check=".length).split(",")) {
|
|
158
|
-
if (isKnown(nm)
|
|
159
|
-
out.
|
|
161
|
+
if (isKnown(nm)) {
|
|
162
|
+
if (!out.includes(nm))
|
|
163
|
+
out.push(nm);
|
|
164
|
+
}
|
|
165
|
+
else if (nm !== "" && !unknown.includes(nm)) {
|
|
166
|
+
unknown.push(nm);
|
|
167
|
+
}
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
}
|
|
171
|
+
// 결정 411: 모르는 이름은 여전히 무시하되(안전 방향 — 전체 검사로 넓어짐) **조용히**
|
|
172
|
+
// 넘기지 않는다. 오타 하나가 "그 검사만 돌렸다" 는 착각으로 이어지고, 전부 오타면
|
|
173
|
+
// 28종 전체가 돌아가 선택 실행 의도가 통째로 사라진다.
|
|
174
|
+
if (unknown.length > 0) {
|
|
175
|
+
process.stderr.write(` ! 알 수 없는 검사 이름 무시: ${unknown.join(", ")}\n` +
|
|
176
|
+
` → 지원 이름은 gaon doctor --json 의 rule 값 또는 gaon help 참고` +
|
|
177
|
+
(out.length === 0 ? " (인정된 이름이 없어 전체 검사를 실행합니다)" : "") +
|
|
178
|
+
"\n");
|
|
179
|
+
}
|
|
163
180
|
return out.length ? out : undefined;
|
|
164
181
|
}
|
|
165
182
|
/**
|
|
@@ -225,6 +242,24 @@ export function parseDbArgs(sub, argv) {
|
|
|
225
242
|
const dbIdx = argv.indexOf("--db");
|
|
226
243
|
const cfgIdx = argv.indexOf("--config");
|
|
227
244
|
const valueFlags = new Set(["--db", "--config"]);
|
|
245
|
+
// 결정 404: 값 플래그의 **값 부재**도 fail-loud. `gaon db migrate --db`(값 없이 끝)는
|
|
246
|
+
// db=undefined 가 돼 "커넥션 하나만" 이던 의도가 조용히 **전 커넥션 적용**으로 확장됐다
|
|
247
|
+
// (결정 139 순회 기본값과 결합해 파괴 반경이 커진다). readPortFlag(결정 240)와 같은 규약.
|
|
248
|
+
for (const f of valueFlags) {
|
|
249
|
+
const i = argv.indexOf(f);
|
|
250
|
+
if (i < 0)
|
|
251
|
+
continue;
|
|
252
|
+
const v = argv[i + 1];
|
|
253
|
+
if (v === undefined || v.startsWith("-")) {
|
|
254
|
+
throw new Error(`gaon db ${sub}: ${f} 값이 없습니다.\n` +
|
|
255
|
+
` → 예: gaon db ${sub} ${f} ${f === "--db" ? "main" : "gaon.config.ts"}\n` +
|
|
256
|
+
` → 값 없이 두면 ${f === "--db" ? "전 커넥션에 적용" : "기본 설정 파일 사용"}으로 조용히 넓어져 멈춥니다.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 결정 358: db 는 파괴 방향 오동작이 가능한 명령군이라 미지 플래그를 조용히
|
|
260
|
+
// 무시하지 않는다 — `--dryrun`(오타)이 무시되면 dry-run 의도가 **실제 마이그레이션
|
|
261
|
+
// 적용**으로 반전된다(결정 267 down-위치 문제와 동형). 인정 집합 밖 `--*` 는 throw.
|
|
262
|
+
const knownFlags = new Set(["--db", "--config", "--json", "--yes", "--dry-run"]);
|
|
228
263
|
const positionals = [];
|
|
229
264
|
for (let i = 2; i < argv.length; i++) {
|
|
230
265
|
const a = argv[i];
|
|
@@ -234,10 +269,29 @@ export function parseDbArgs(sub, argv) {
|
|
|
234
269
|
i++; // 플래그 값 스킵
|
|
235
270
|
continue;
|
|
236
271
|
}
|
|
237
|
-
if (a.startsWith("--"))
|
|
238
|
-
|
|
272
|
+
if (a.startsWith("--")) {
|
|
273
|
+
if (!knownFlags.has(a)) {
|
|
274
|
+
throw new Error(`gaon db ${sub}: 알 수 없는 플래그 '${a}'.\n` +
|
|
275
|
+
` → 지원 플래그: --db <키> · --config <경로> · --dry-run · --yes · --json\n` +
|
|
276
|
+
` → 오타(예: --dryrun)가 조용히 무시되면 의도와 반대로 실행될 수 있어 멈춥니다.`);
|
|
277
|
+
}
|
|
278
|
+
continue; // 인정된 불리언 플래그
|
|
279
|
+
}
|
|
239
280
|
positionals.push(a);
|
|
240
281
|
}
|
|
282
|
+
// 결정 404: 미지 **위치 인자**도 막는다. 종전엔 positionals 가 'down' 판정에만 쓰이고
|
|
283
|
+
// 나머지는 버려져서 `gaon db migrate donw`(오타)·`rollback`(타 프레임웍 관례)이 무경고로
|
|
284
|
+
// **정방향 migrate 실 적용**으로 반전됐다 — 결정 358 이 플래그에서 막은 것과 같은 파괴
|
|
285
|
+
// 방향인데 위치 인자만 뚫려 있었다.
|
|
286
|
+
const allowedPositionals = sub === "migrate" ? ["down"] : [];
|
|
287
|
+
const unknown = positionals.find((p) => !allowedPositionals.includes(p));
|
|
288
|
+
if (unknown !== undefined) {
|
|
289
|
+
throw new Error(`gaon db ${sub}: 알 수 없는 인자 '${unknown}'.\n` +
|
|
290
|
+
(sub === "migrate"
|
|
291
|
+
? ` → 지원 인자: down(롤백) 하나뿐입니다. 예: gaon db migrate down\n`
|
|
292
|
+
: ` → gaon db ${sub} 는 위치 인자를 받지 않습니다.\n`) +
|
|
293
|
+
` → 오타가 조용히 무시되면 의도와 반대로(예: 롤백 대신 정방향 적용) 실행될 수 있어 멈춥니다.`);
|
|
294
|
+
}
|
|
241
295
|
return {
|
|
242
296
|
json: argv.includes("--json"),
|
|
243
297
|
db: dbIdx >= 0 ? argv[dbIdx + 1] : undefined,
|
|
@@ -362,9 +416,17 @@ export function runCli(argv, opts = {}) {
|
|
|
362
416
|
const knownSteps = ["typecheck", "vue-tsc", "build", "doctor"];
|
|
363
417
|
const onlyIdx = argv.indexOf("--only");
|
|
364
418
|
const onlyRaw = onlyIdx >= 0 ? argv[onlyIdx + 1] : undefined;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
419
|
+
// 결정 411: `--only` 오타를 조용히 전체 실행으로 폴백하지 않는다 — "typecheck 만
|
|
420
|
+
// 돌렸다" 는 착각으로 CI 시간이 늘거나(안전 방향이라 더 안 보인다), 반대로
|
|
421
|
+
// 단일 단계만 돌린 줄 알고 넘어간다. 값 부재도 같이 막는다(--port 규약 · 결정 240).
|
|
422
|
+
if (onlyIdx >= 0 && (onlyRaw === undefined || !knownSteps.includes(onlyRaw))) {
|
|
423
|
+
process.stderr.write(` ✗ gaon check: --only 값이 ${onlyRaw === undefined ? "없습니다" : `잘못됐습니다('${onlyRaw}')`}.\n` +
|
|
424
|
+
` → 지원 단계: ${knownSteps.join(" · ")}\n` +
|
|
425
|
+
` → 예: gaon check --only typecheck\n`);
|
|
426
|
+
process.exitCode = 2;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const only = onlyRaw !== undefined ? onlyRaw : undefined;
|
|
368
430
|
void runCheckCommand({
|
|
369
431
|
json: argv.includes("--json"),
|
|
370
432
|
only,
|
|
@@ -408,7 +470,7 @@ export function runCli(argv, opts = {}) {
|
|
|
408
470
|
});
|
|
409
471
|
return;
|
|
410
472
|
}
|
|
411
|
-
// `gaon doctor` — 정적 검사(M9-E ·
|
|
473
|
+
// `gaon doctor` — 정적 검사(M9-E · 28 검사 · ALL_RULES 단일 출처). --check=<이름>[,<이름>...] 로
|
|
412
474
|
// 선택 실행, --json 은 자동화 파싱용.
|
|
413
475
|
// exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
|
|
414
476
|
if (argv[0] === "doctor") {
|
|
@@ -421,11 +483,20 @@ export function runCli(argv, opts = {}) {
|
|
|
421
483
|
process.exitCode = 2;
|
|
422
484
|
return;
|
|
423
485
|
}
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
486
|
+
// 결정 359: 종료 코드 판정.
|
|
487
|
+
// --fix --yes: runDoctorCommand 가 적용 후 **재검사** 결과를 돌려주므로
|
|
488
|
+
// errors = 잔여 위반 — 전부 자동 수정됐으면 0(자동화 정합).
|
|
489
|
+
// --fix (dry-run): errors > 0 이면 1 · errors 0 이어도 **계획이 있으면 1**
|
|
490
|
+
// (고칠 게 있다는 신호 — 이전엔 warning-only 계획이 0 으로 삼켜짐).
|
|
427
491
|
// 기본: errors > 0 이면 1.
|
|
428
|
-
|
|
492
|
+
// 결정 410: `--fix --yes` 인데 **적용에 성공한 게 하나도 없으면** 비-0.
|
|
493
|
+
// 종전엔 잔여 errors 만 봐서, 위반이 warning 레벨이고 fixer 가 전부 실패하면
|
|
494
|
+
// (fixer 예외·대상 파일 선점 등) "고친 것 0" 인데 exit 0 이라 자동화가
|
|
495
|
+
// "고쳐졌다" 로 오판했다 — 시도했는데 아무것도 못 고친 건 성공이 아니다.
|
|
496
|
+
const fix = result.fix;
|
|
497
|
+
const dryPlans = fix !== undefined && !fix.applied && fix.outcomes.length > 0;
|
|
498
|
+
const nothingApplied = fix !== undefined && fix.applied && fix.outcomes.length > 0 && !fix.outcomes.some((o) => o.applied);
|
|
499
|
+
process.exitCode = result.errors.length > 0 || dryPlans || nothingApplied ? 1 : 0;
|
|
429
500
|
})
|
|
430
501
|
.catch((err) => {
|
|
431
502
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -491,14 +562,35 @@ export function runCli(argv, opts = {}) {
|
|
|
491
562
|
if (argv[0] === "db") {
|
|
492
563
|
const sub = argv[1];
|
|
493
564
|
const known = ["diff", "migrate", "reset", "seed", "status"];
|
|
565
|
+
// 결정 411: --json 모드면 실패 사유도 **구조화**해서 낸다 — runDbCommand 는 이미
|
|
566
|
+
// JSON 에러를 방출하는데(commands/db.ts) 라우팅 단계 실패만 텍스트라 자동화가
|
|
567
|
+
// 사유를 못 받았다(비대칭).
|
|
568
|
+
const dbJson = argv.includes("--json");
|
|
569
|
+
const failDb = (message) => {
|
|
570
|
+
if (dbJson) {
|
|
571
|
+
process.stdout.write(JSON.stringify({ ok: false, kind: "usage", command: "db", message }) + "\n");
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
process.stderr.write(` ✗ ${message}\n`);
|
|
575
|
+
}
|
|
576
|
+
process.exitCode = 1;
|
|
577
|
+
};
|
|
494
578
|
if (!sub || !known.includes(sub)) {
|
|
495
|
-
|
|
579
|
+
failDb(`알 수 없는 db 서브커맨드: ${sub ?? "(없음)"}\n` +
|
|
496
580
|
` → 지원: gaon db diff | migrate | reset | seed | status\n` +
|
|
497
|
-
` → 옵션: --json · --db <키>(생략 = 전 커넥션 순회) · --config <path> · --yes · --dry-run
|
|
498
|
-
|
|
581
|
+
` → 옵션: --json · --db <키>(생략 = 전 커넥션 순회) · --config <path> · --yes · --dry-run`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// 결정 358·404: 미지 플래그·미지 위치 인자·값 부재는 파싱 경계에서 fail-loud
|
|
585
|
+
// (오타 dry-run 이 실 적용되는 반전 방지).
|
|
586
|
+
let dbOpts;
|
|
587
|
+
try {
|
|
588
|
+
dbOpts = parseDbArgs(sub, argv);
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
failDb(err instanceof Error ? err.message : String(err));
|
|
499
592
|
return;
|
|
500
593
|
}
|
|
501
|
-
const dbOpts = parseDbArgs(sub, argv);
|
|
502
594
|
void runDbCommand(sub, dbOpts)
|
|
503
595
|
.then((code) => {
|
|
504
596
|
process.exitCode = code;
|
|
@@ -518,7 +610,13 @@ export function runCli(argv, opts = {}) {
|
|
|
518
610
|
const appIdx = argv.indexOf("--app");
|
|
519
611
|
const app = appIdx >= 0 ? argv[appIdx + 1] : undefined;
|
|
520
612
|
// 결정 155: --public 은 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱 탈출구).
|
|
521
|
-
|
|
613
|
+
// 결정 338: --jwt 는 API 앱 토큰 변형(--app 필수 · --public 배타).
|
|
614
|
+
const code = runGenerateAuthCommand({
|
|
615
|
+
app,
|
|
616
|
+
json: argv.includes("--json"),
|
|
617
|
+
public: argv.includes("--public"),
|
|
618
|
+
jwt: argv.includes("--jwt"),
|
|
619
|
+
});
|
|
522
620
|
process.exitCode = code;
|
|
523
621
|
return;
|
|
524
622
|
}
|
|
@@ -630,14 +728,27 @@ export function runCli(argv, opts = {}) {
|
|
|
630
728
|
// `gaon test [--scope unit|integration|all] [-- vitest 인자]` — 테스트 러너(M9-G).
|
|
631
729
|
if (argv[0] === "test") {
|
|
632
730
|
const rest = argv.slice(1);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
731
|
+
// 결정 411: `--` 뒤는 **전부 vitest 몫**이다. 종전엔 gaon 이 위치와 무관하게
|
|
732
|
+
// --json·--scope 를 가로채 `gaon test -- --json`(vitest 자체 JSON 리포터)이
|
|
733
|
+
// 불가능했다 — 탈출구를 연다(help 에 적힌 계약과 코드를 일치시킨다).
|
|
734
|
+
const sepIdx = rest.indexOf("--");
|
|
735
|
+
const own = sepIdx >= 0 ? rest.slice(0, sepIdx) : rest;
|
|
736
|
+
const forwarded = sepIdx >= 0 ? rest.slice(sepIdx + 1) : [];
|
|
737
|
+
const scopeIdx = own.indexOf("--scope");
|
|
738
|
+
const scopeRaw = scopeIdx >= 0 ? own[scopeIdx + 1] : undefined;
|
|
739
|
+
// 결정 411: scope 오타를 조용히 전체 실행으로 폴백하지 않는다(--pm 오타를 막은
|
|
740
|
+
// 결정 167 과 대칭) — 통합만 돌리려던 CI 가 전체를 돌리며 실 인프라까지 띄운다.
|
|
741
|
+
if (scopeIdx >= 0 && scopeRaw !== "unit" && scopeRaw !== "integration" && scopeRaw !== "all") {
|
|
742
|
+
process.stderr.write(` ✗ gaon test: --scope 값이 ${scopeRaw === undefined ? "없습니다" : `잘못됐습니다('${scopeRaw}')`}.\n` +
|
|
743
|
+
` → 지원 값: unit · integration · all\n` +
|
|
744
|
+
` → 예: gaon test --scope integration\n`);
|
|
745
|
+
process.exitCode = 2;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const scope = scopeRaw !== undefined ? scopeRaw : undefined;
|
|
638
749
|
const passthrough = [];
|
|
639
|
-
for (let i = 0; i <
|
|
640
|
-
const a =
|
|
750
|
+
for (let i = 0; i < own.length; i++) {
|
|
751
|
+
const a = own[i];
|
|
641
752
|
if (a === "--scope") {
|
|
642
753
|
i++;
|
|
643
754
|
continue;
|
|
@@ -647,7 +758,8 @@ export function runCli(argv, opts = {}) {
|
|
|
647
758
|
if (a !== undefined)
|
|
648
759
|
passthrough.push(a);
|
|
649
760
|
}
|
|
650
|
-
|
|
761
|
+
passthrough.push(...forwarded);
|
|
762
|
+
void runTestCommand(passthrough, { json: own.includes("--json"), scope })
|
|
651
763
|
.then((code) => {
|
|
652
764
|
process.exitCode = code;
|
|
653
765
|
})
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export declare function readAgentDocTool(args: ToolArgs, cwd: string): Promise<T
|
|
|
93
93
|
* AI 에이전트가 자기 산출물을 스스로 확증하는 표준 경로.
|
|
94
94
|
*
|
|
95
95
|
* 인자:
|
|
96
|
-
* { only?: 'typecheck'|'vue-tsc'|'build'|'doctor',
|
|
96
|
+
* { only?: 'typecheck'|'vue-tsc'|'build'|'doctor', noDoctor?: boolean }
|
|
97
97
|
*/
|
|
98
98
|
export declare function runCheckTool(args: ToolArgs, cwd: string): Promise<ToolResult>;
|
|
99
99
|
/**
|
package/dist/mcp/tools.js
CHANGED
|
@@ -434,17 +434,20 @@ function parseJsonTail(output) {
|
|
|
434
434
|
* AI 에이전트가 자기 산출물을 스스로 확증하는 표준 경로.
|
|
435
435
|
*
|
|
436
436
|
* 인자:
|
|
437
|
-
* { only?: 'typecheck'|'vue-tsc'|'build'|'doctor',
|
|
437
|
+
* { only?: 'typecheck'|'vue-tsc'|'build'|'doctor', noDoctor?: boolean }
|
|
438
438
|
*/
|
|
439
439
|
export async function runCheckTool(args, cwd) {
|
|
440
440
|
const only = stringOpt(args, 'only');
|
|
441
|
-
|
|
441
|
+
// 결정 362: doctor 는 check 에 **기본 포함**(결정 157). 구 파라미터 includeDoctor 는
|
|
442
|
+
// no-op CLI 플래그(--include-doctor)로 흘러 "false 여도 doctor 가 도는" 유령이었다 —
|
|
443
|
+
// 실 토글은 noDoctor(--no-doctor)다. includeDoctor 는 하위 호환으로 받되 무시한다.
|
|
444
|
+
const noDoctor = boolOpt(args, 'noDoctor');
|
|
442
445
|
const known = ['typecheck', 'vue-tsc', 'build', 'doctor'];
|
|
443
446
|
if (only && !known.includes(only)) {
|
|
444
447
|
return errorResult(`only 는 ${known.map((k) => `'${k}'`).join(' | ')} 중 하나여야 합니다. 입력값: '${only}'\n` +
|
|
445
448
|
`→ 인자를 고치거나 생략해 전체 검사를 실행하세요.`);
|
|
446
449
|
}
|
|
447
|
-
const cliArgs = ['check', '--json', ...(only ? ['--only', only] : []), ...(
|
|
450
|
+
const cliArgs = ['check', '--json', ...(only ? ['--only', only] : []), ...(noDoctor ? ['--no-doctor'] : [])];
|
|
448
451
|
const r = await spawnGaon(cwd, cliArgs);
|
|
449
452
|
if ('error' in r)
|
|
450
453
|
return errorResult(r.error);
|
|
@@ -454,7 +457,7 @@ export async function runCheckTool(args, cwd) {
|
|
|
454
457
|
ok: r.exitCode === 0,
|
|
455
458
|
exitCode: r.exitCode,
|
|
456
459
|
only: only ?? null,
|
|
457
|
-
|
|
460
|
+
noDoctor: noDoctor ?? false,
|
|
458
461
|
report: parseJsonTail(r.output) ?? null,
|
|
459
462
|
output: r.output,
|
|
460
463
|
},
|
|
@@ -590,7 +593,7 @@ export const TOOLS = [
|
|
|
590
593
|
name: 'run_check',
|
|
591
594
|
description: '`gaon check` 를 실 실행한다 — 검사 전에 .gaon 타입 브리지를 재생성(규칙 3)한 뒤 ' +
|
|
592
595
|
'typecheck·vue-tsc·build 를 순서대로 돌린다. 산출물을 고친 뒤 스스로 확증하는 표준 경로 ' +
|
|
593
|
-
'(CLAUDE.md §3 검증 루프). only 로 한 단계만,
|
|
596
|
+
'(CLAUDE.md §3 검증 루프). doctor 는 기본 포함(결정 157) — only 로 한 단계만, noDoctor 로 doctor 제외.',
|
|
594
597
|
inputSchema: {
|
|
595
598
|
type: 'object',
|
|
596
599
|
properties: {
|
|
@@ -599,7 +602,11 @@ export const TOOLS = [
|
|
|
599
602
|
enum: ['typecheck', 'vue-tsc', 'build', 'doctor'],
|
|
600
603
|
description: '지정 시 그 검사 하나만 실행. 생략 시 전체.',
|
|
601
604
|
},
|
|
602
|
-
|
|
605
|
+
noDoctor: { type: 'boolean', description: 'true 면 doctor 를 제외(기본 false — doctor 는 기본 포함 · 결정 157).' },
|
|
606
|
+
// 결정 411: 폐기된 includeDoctor 를 스키마에 되살리지 **않는다**. 자리를 남기면
|
|
607
|
+
// strict 클라이언트의 구 호출이 "성공했는데 조용히 무시" 로 끝나 결정 362 가
|
|
608
|
+
// 없애려던 유령 파라미터 오도가 되살아난다 — 스키마 부재로 인한 검증 거부가
|
|
609
|
+
// 오히려 fail-loud(어느 파라미터를 쓰라는 신호)라 결정 362 계약을 유지한다.
|
|
603
610
|
},
|
|
604
611
|
additionalProperties: false,
|
|
605
612
|
},
|
package/dist/messages-gen.d.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
|
|
4
4
|
* 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
|
|
5
5
|
*/
|
|
6
|
-
export declare function generateMessagesDts(localesDir: string, out: string): boolean;
|
|
6
|
+
export declare function generateMessagesDts(localesDir: string, out: string, baseLng?: string): boolean;
|
package/dist/messages-gen.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 카탈로그의 키를 유니온 타입으로 물성화해, t('key') 의 존재하지 않는 키를 컴파일
|
|
5
5
|
// 타임에 잡는다(현재는 GaonMessages 가 비어 있어 키가 string 으로 열림). 생성 파일은
|
|
6
6
|
// 타입만 담는다(규칙 3). @gaonjs/i18n 의 공개 API(loadLocales·renderMessagesDts)만 쓴다.
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { dirname } from 'node:path';
|
|
9
9
|
import { loadLocales, renderMessagesDts } from '@gaonjs/i18n';
|
|
10
10
|
/**
|
|
@@ -12,13 +12,24 @@ import { loadLocales, renderMessagesDts } from '@gaonjs/i18n';
|
|
|
12
12
|
* 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
|
|
13
13
|
* 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
|
|
14
14
|
*/
|
|
15
|
-
export function generateMessagesDts(localesDir, out) {
|
|
16
|
-
|
|
15
|
+
export function generateMessagesDts(localesDir, out, baseLng) {
|
|
16
|
+
// 결정 414: 카탈로그가 사라졌으면 **옛 생성물을 지운다** — 남겨두면 없어진 키의
|
|
17
|
+
// 유니온이 그대로 살아 t('없어진키') 가 계속 컴파일된다(제거를 못 잡는 사각).
|
|
18
|
+
if (!existsSync(localesDir)) {
|
|
19
|
+
if (existsSync(out))
|
|
20
|
+
rmSync(out, { force: true });
|
|
17
21
|
return false;
|
|
22
|
+
}
|
|
18
23
|
const resources = loadLocales(localesDir);
|
|
19
|
-
if (Object.keys(resources).length === 0)
|
|
24
|
+
if (Object.keys(resources).length === 0) {
|
|
25
|
+
if (existsSync(out))
|
|
26
|
+
rmSync(out, { force: true });
|
|
20
27
|
return false;
|
|
28
|
+
}
|
|
21
29
|
mkdirSync(dirname(out), { recursive: true });
|
|
22
|
-
|
|
30
|
+
// 결정 352: 기준 로케일 = config i18n.fallbackLng(호출자가 정적 분석으로 전달).
|
|
31
|
+
// 이전엔 항상 알파벳순 첫 로케일이라 컴파일 보증이 fallback 체인과 다른 로케일에
|
|
32
|
+
// 정박했다(en/ko + fallbackLng:'ko' 프로젝트에서 기준이 en).
|
|
33
|
+
writeFileSync(out, renderMessagesDts(resources, baseLng), 'utf8');
|
|
23
34
|
return true;
|
|
24
35
|
}
|
|
@@ -8,15 +8,16 @@ import CardContent from '@shared/components/ui/CardContent.vue'
|
|
|
8
8
|
import CardFooter from '@shared/components/ui/CardFooter.vue'
|
|
9
9
|
import Button from '@shared/components/ui/Button.vue'
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// 사용자는 자동 주입 공유 prop 이다(결정 116) — 컨트롤러가 넘기지 않고
|
|
12
12
|
// useShared() 로 읽는다. currentUser 는 직렬화됨(passwordDigest 없음 · §4.2).
|
|
13
13
|
// 이 페이지는 requireAuth 로 보호되므로 런타임엔 항상 로그인 상태(타입은 nullable).
|
|
14
14
|
const shared = useShared()
|
|
15
15
|
|
|
16
16
|
// 로그아웃 = DELETE {{URL_PREFIX}}/session (r.resource('session') 의 destroy).
|
|
17
17
|
// HTML <form> 은 DELETE 를 못 보내므로 Inertia 라우터로 실제 메서드를 보낸다(결정 64).
|
|
18
|
+
// CSRF 토큰은 프레임웍이 자동 부착한다(결정 342) — 헤더를 손으로 싣지 않는다.
|
|
18
19
|
function logout(): void {
|
|
19
|
-
router.delete('{{URL_PREFIX}}/session'
|
|
20
|
+
router.delete('{{URL_PREFIX}}/session')
|
|
20
21
|
}
|
|
21
22
|
</script>
|
|
22
23
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps, useForm,
|
|
2
|
+
import { pageProps, useForm, Link } from 'gaonjs/vue'
|
|
3
3
|
import Card from '@shared/components/ui/Card.vue'
|
|
4
4
|
import CardHeader from '@shared/components/ui/CardHeader.vue'
|
|
5
5
|
import CardTitle from '@shared/components/ui/CardTitle.vue'
|
|
@@ -17,12 +17,10 @@ import AlertDescription from '@shared/components/ui/AlertDescription.vue'
|
|
|
17
17
|
// 실패로 서버가 같은 페이지를 다시 render 하면 props.error 가 즉시 갱신된다.
|
|
18
18
|
const props = pageProps<'{{APP_NAME}}:session#new'>()
|
|
19
19
|
|
|
20
|
-
// csrf 는 자동 주입 공유 prop 이다(결정 116) — useShared() 로 읽는다.
|
|
21
|
-
const shared = useShared()
|
|
22
|
-
|
|
23
20
|
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
24
21
|
// 서버는 redirect(Inertia 응답)로 답하고, 실패 시 같은 페이지를 다시 render 한다.
|
|
25
|
-
|
|
22
|
+
// CSRF 토큰은 프레임웍이 자동 부착한다(결정 342) — 손으로 싣지 않는다.
|
|
23
|
+
const form = useForm({ email: '', password: '' })
|
|
26
24
|
</script>
|
|
27
25
|
|
|
28
26
|
<template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { pageProps, useForm,
|
|
2
|
+
import { pageProps, useForm, Link } from 'gaonjs/vue'
|
|
3
3
|
import Card from '@shared/components/ui/Card.vue'
|
|
4
4
|
import CardHeader from '@shared/components/ui/CardHeader.vue'
|
|
5
5
|
import CardTitle from '@shared/components/ui/CardTitle.vue'
|
|
@@ -15,11 +15,9 @@ import AlertDescription from '@shared/components/ui/AlertDescription.vue'
|
|
|
15
15
|
// pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
|
|
16
16
|
const props = pageProps<'{{APP_NAME}}:registration#new'>()
|
|
17
17
|
|
|
18
|
-
// csrf 는 자동 주입 공유 prop 이다(결정 116) — useShared() 로 읽는다.
|
|
19
|
-
const shared = useShared()
|
|
20
|
-
|
|
21
18
|
// 세션 앱 폼 = Inertia SPA 제출(결정 64) — fetch() 로 만들지 않는다.
|
|
22
|
-
|
|
19
|
+
// CSRF 토큰은 프레임웍이 자동 부착한다(결정 342) — 손으로 싣지 않는다.
|
|
20
|
+
const form = useForm({ name: '', email: '', password: '' })
|
|
23
21
|
</script>
|
|
24
22
|
|
|
25
23
|
<template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// API 앱 설정 — gaon g auth --jwt 스캐폴드. JWT(토큰) 인증을 표준 부팅(gaon dev / gaon serve)에 배선한다.
|
|
2
|
+
// JWT 는 API 앱 전용이다(§7) — 세션·CSRF 없이 Authorization: Bearer 로 인증한다.
|
|
3
|
+
import { defineAppConfig } from 'gaonjs/config'
|
|
4
|
+
import { loadUser } from './auth.js'
|
|
5
|
+
|
|
6
|
+
export default defineAppConfig({
|
|
7
|
+
// JWT secret 은 32자 이상 — .env 의 {{JWT_SECRET_ENV}} 로 주입한다(앱별 분리 · 결정 337).
|
|
8
|
+
// 운영(NODE_ENV=production)에서 아래 dev 폴백이 남아 있으면 부팅이 확정 종료된다(fail-loud).
|
|
9
|
+
auth: {
|
|
10
|
+
strategy: 'jwt',
|
|
11
|
+
secret: process.env.{{JWT_SECRET_ENV}} ?? 'dev-only-jwt-secret-{{APP_NAME}}-change-me-now!!',
|
|
12
|
+
loadUser,
|
|
13
|
+
// 액세스는 짧게, 리프레시는 길게(기본 15m / 7d). 토큰은 stateless 라 서버측
|
|
14
|
+
// 폐기 수단이 없다 — 민감한 앱은 refreshTtl 을 짧게 잡는다(agents/web.md §6).
|
|
15
|
+
accessTtl: '15m',
|
|
16
|
+
refreshTtl: '7d',
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 인증 배선 — gaon g auth --jwt 스캐폴드 (API 앱 · 토큰).
|
|
2
|
+
import type { JwtAuthOptions } from 'gaonjs/web'
|
|
3
|
+
import { User } from '../../domain/models/User.js'
|
|
4
|
+
|
|
5
|
+
// 액세스 토큰의 sub(사용자 id)로 사용자를 로드한다(§7 · JWT 는 API 앱 전용).
|
|
6
|
+
export const loadUser: JwtAuthOptions['loadUser'] = async (id) =>
|
|
7
|
+
await User.where('id', '=', BigInt(String(id))).first()
|
|
8
|
+
|
|
9
|
+
// this.currentUser 에 User 필드 타입을 얹는다(GaonRouteMap 과 동일 관례).
|
|
10
|
+
declare module 'gaonjs/web' {
|
|
11
|
+
interface GaonCurrentUser {
|
|
12
|
+
id: bigint
|
|
13
|
+
name: string
|
|
14
|
+
email: string
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { routes } from 'gaonjs/web'
|
|
2
|
+
|
|
3
|
+
export default routes((r) => {
|
|
4
|
+
r.post('/session', 'session#create') // 로그인 → 토큰 발급 (gaon g auth --jwt)
|
|
5
|
+
r.post('/session/refresh', 'session#refresh') // 액세스 토큰 재발급 (gaon g auth --jwt)
|
|
6
|
+
r.get('/session', 'session#show') // 현재 사용자 · Bearer (gaon g auth --jwt)
|
|
7
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 토큰 컨트롤러(발급/재발급/내 정보) — gaon g auth --jwt 스캐폴드 (API 앱 · JSON 전용).
|
|
2
|
+
// API 앱은 프론트엔드가 없다 — 모든 응답이 JSON 이고 페이지·useForm 을 쓰지 않는다.
|
|
3
|
+
import { controller, verifyPassword } from 'gaonjs/web'
|
|
4
|
+
import { User } from '../../../domain/models/User.js'
|
|
5
|
+
|
|
6
|
+
export default controller({
|
|
7
|
+
// POST {{URL_PREFIX}}/session — 로그인: 자격 검증 후 액세스+리프레시 토큰 발급.
|
|
8
|
+
// curl -X POST -H 'Content-Type: application/json' \
|
|
9
|
+
// -d '{"email":"a@x.com","password":"..."}' http://127.0.0.1:3000{{URL_PREFIX}}/session
|
|
10
|
+
async create() {
|
|
11
|
+
const { email, password } = this.params({ _row: {} as { email: string; password: string } })
|
|
12
|
+
const user = await User.where('email', '=', email).first()
|
|
13
|
+
if (!user || !(await verifyPassword(password, user.passwordDigest))) {
|
|
14
|
+
return this.json({ error: 'invalid_credentials', message: '이메일 또는 비밀번호가 올바르지 않습니다.' }, 401)
|
|
15
|
+
}
|
|
16
|
+
// this.jwt 는 JWT 앱에서만 존재한다(app.config 의 auth.strategy:'jwt' 배선).
|
|
17
|
+
const tokens = await this.jwt!.issue(user)
|
|
18
|
+
return this.json(tokens) // { accessToken, refreshToken }
|
|
19
|
+
},
|
|
20
|
+
// POST {{URL_PREFIX}}/session/refresh — 리프레시 토큰으로 액세스 토큰 재발급.
|
|
21
|
+
// 주의: 토큰은 stateless — 서버측 폐기(로그아웃·강제 무효화) 수단이 없다(결정 337).
|
|
22
|
+
// 유출된 리프레시 토큰은 만료까지 유효하므로 민감한 앱은 refreshTtl 을 짧게 잡는다.
|
|
23
|
+
async refresh() {
|
|
24
|
+
const { refreshToken } = this.params({ _row: {} as { refreshToken: string } })
|
|
25
|
+
const next = await this.jwt!.refresh(refreshToken)
|
|
26
|
+
if (!next) {
|
|
27
|
+
return this.json({ error: 'invalid_refresh_token', message: '리프레시 토큰이 유효하지 않거나 만료됐습니다.' }, 401)
|
|
28
|
+
}
|
|
29
|
+
return this.json(next) // { accessToken }
|
|
30
|
+
},
|
|
31
|
+
// GET {{URL_PREFIX}}/session — 현재 사용자 확인 (Authorization: Bearer <accessToken>).
|
|
32
|
+
async show() {
|
|
33
|
+
const user = this.requireAuth() // 토큰이 없거나 무효면 401
|
|
34
|
+
return this.json({ user }) // hidden 컬럼(passwordDigest)은 응답 경계에서 제외된다(§4.2)
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -111,7 +111,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
111
111
|
컬럼명 · 스키마 파일 ↔ 테이블 ↔ `tables.d.ts` 키 변환 규칙)은
|
|
112
112
|
`agents/data.md` "DB 네이밍" 표가 정본이다 — 먼저 읽는다.
|
|
113
113
|
|
|
114
|
-
### 2.2 `gaon doctor` 검사
|
|
114
|
+
### 2.2 `gaon doctor` 검사 28종
|
|
115
115
|
|
|
116
116
|
1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
|
|
117
117
|
2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
|
|
@@ -140,6 +140,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
140
140
|
25. `schema-relations` — 커넥션을 가로지르는 belongsTo·역방향 관계(SQL 조인이 커넥션을 못 넘음)와 존재하지 않는 관계 대상 = **에러**(§4.5). **파티션 키 컬럼이 실제 컬럼인지도 검사**(결정 277 · `checkPartitions` — 오타·유령 컬럼). data 패키지 검사(`checkCrossConnectionRelations`·`checkRelationTargets`·`checkPartitions`)를 CLI 러너가 배선 — 배포 후 raw postgres 에러 대신 doctor 가 잡는다 (결정 134·277 · `agents/data.md`)
|
|
141
141
|
26. `no-import-meta-env` — `.vue`(SFC) `<script>` 에서 `import.meta.env` 직접 사용 = **에러**. SFC 는 nodenext 아래 CommonJS 출력으로 분류돼 vue-tsc 가 TS1470 로 거부한다(`gaon check` red). 클라 공개 환경변수는 `import { env } from 'gaonjs/vue'` 로 읽으라(VITE_* 접두 제거·타입드 · `.gaon/env.d.ts` 는 `.env` 스캔 생성) — 템플릿 프로즈·주석의 언급은 오탐 제외 (결정 198 · `agents/frontend.md` §9)
|
|
142
142
|
27. `locale-parity` — `locales/` 의 로케일 간 키 부분 누락 = **경고**. 어떤 키가 특정 로케일에만 빠지면 `messages.d.ts`(기준 로케일 기준)는 컴파일을 통과하고, 런타임에 그 로케일 사용자는 fallback(대개 다른 언어) 번역을 조용히 본다. 검사가 로케일 간 키 diff 를 계산해 빠진 파일·키를 짚는다(`--json` 은 `detail.missing` 으로 구조화). 로케일이 0·1개면 무소음 (결정 216 · `agents/i18n.md`)
|
|
143
|
+
28. `render-return` — 액션이 `this.render`/`this.redirect`/`this.json` 을 호출만 하고 `return` 하지 않음 = 응답이 버려져 조용히 204(백지) — `return this.render(...)` 로 고치라 (결정 340 · 경고)
|
|
143
144
|
|
|
144
145
|
## 3. 로직 배치 One Way 판단표
|
|
145
146
|
|
|
@@ -200,7 +201,7 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
200
201
|
```bash
|
|
201
202
|
gaon check # .gaon 재생성 → typecheck + vue-tsc + build + doctor (기본 포함 · --no-doctor 로 뺌 · 결정 157)
|
|
202
203
|
gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
|
|
203
|
-
gaon doctor # 정적 검사
|
|
204
|
+
gaon doctor # 정적 검사 28종 (§2.2)
|
|
204
205
|
```
|
|
205
206
|
|
|
206
207
|
### 4.1 CLI 명령 (전 명령 `--json` 지원)
|
|
@@ -86,7 +86,7 @@ Gaon 프레임웍 문서: https://gaonjs.dev
|
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
gaon check # .gaon 재생성 → 타입검사+build+doctor (CI 한 번에 · --no-doctor 로 doctor 뺌)
|
|
89
|
-
gaon doctor # 정적 검사
|
|
89
|
+
gaon doctor # 정적 검사 28종 (상세 AGENTS §2.2)
|
|
90
90
|
npm test # Vitest · DB 테스트는 실 Docker 필수 (§9)
|
|
91
91
|
```
|
|
92
92
|
|
|
@@ -18,12 +18,22 @@ RUN pnpm install --frozen-lockfile
|
|
|
18
18
|
FROM base AS build
|
|
19
19
|
COPY --from=deps /app/node_modules ./node_modules
|
|
20
20
|
COPY . .
|
|
21
|
+
# 결정 408: 브라우저에 실리는 공개 변수(VITE_*)는 **빌드 시점에 번들로 각인**된다 —
|
|
22
|
+
# 런타임 env(compose.prod.yaml)로는 못 바꾼다. 값이 필요하면 빌드 인자로 넘긴다:
|
|
23
|
+
# docker build --build-arg VITE_API_BASE=https://api.example.com .
|
|
24
|
+
# 넘기지 않으면 아래 .env.example 의 placeholder 가 그대로 각인되므로, 공개 변수를
|
|
25
|
+
# 쓰는 앱은 반드시 build-arg 로 주거나 .env.example 에 **공개 가능한 실값**을 둔다.
|
|
26
|
+
# (비밀은 VITE_ 로 만들지 말 것 — 번들은 누구나 읽는다.)
|
|
27
|
+
ARG VITE_VARS=""
|
|
21
28
|
# gaon build 는 .gaon/env.d.ts 생성에 .env 가 필수(결정 198)인데 .env 는 이미지에
|
|
22
29
|
# 넣지 않는다(.dockerignore) — .env.example 을 임시 복제해 빌드하고 즉시 지운다.
|
|
23
30
|
# 지우는 이유: example 의 placeholder(SESSION_SECRET 등)가 런타임에 남으면 compose
|
|
24
31
|
# 가 env 를 안 넘겼을 때 fail-loud 검증을 조용히 통과시킨다(결정 313). 런타임 env
|
|
25
32
|
# 는 compose.prod.yaml 의 environment 가 단일 소스다.
|
|
26
|
-
RUN cp .env.example .env
|
|
33
|
+
RUN cp .env.example .env \
|
|
34
|
+
&& for kv in $VITE_VARS; do printf '%s\n' "$kv" >> .env; done \
|
|
35
|
+
&& pnpm build \
|
|
36
|
+
&& rm -f .env
|
|
27
37
|
|
|
28
38
|
# 3) 런타임 — 소스 + 번들 + 의존을 그대로 실행.
|
|
29
39
|
FROM base AS runtime
|