@gaonjs/cli 0.47.0 → 0.52.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.
Files changed (49) hide show
  1. package/dist/commands/check.d.ts +1 -1
  2. package/dist/commands/check.js +1 -1
  3. package/dist/commands/test.js +16 -3
  4. package/dist/db/journal.d.ts +4 -3
  5. package/dist/db/journal.js +21 -10
  6. package/dist/db/migrate.d.ts +3 -1
  7. package/dist/db/migrate.js +3 -3
  8. package/dist/db/replay.js +2 -2
  9. package/dist/db/status.js +13 -3
  10. package/dist/dev.d.ts +6 -4
  11. package/dist/dev.js +9 -4
  12. package/dist/doctor/fixers/index.d.ts +1 -1
  13. package/dist/doctor/fixers/index.js +6 -1
  14. package/dist/doctor/locale-parity.js +4 -1
  15. package/dist/doctor/render-return.d.ts +11 -0
  16. package/dist/doctor/render-return.js +143 -0
  17. package/dist/doctor/types.d.ts +1 -1
  18. package/dist/doctor.d.ts +3 -2
  19. package/dist/doctor.js +16 -5
  20. package/dist/generate.d.ts +20 -1
  21. package/dist/generate.js +120 -21
  22. package/dist/hub.js +2 -0
  23. package/dist/i18n-config.d.ts +12 -0
  24. package/dist/i18n-config.js +95 -0
  25. package/dist/index.js +41 -12
  26. package/dist/mcp/tools.d.ts +1 -1
  27. package/dist/mcp/tools.js +9 -6
  28. package/dist/messages-gen.d.ts +1 -1
  29. package/dist/messages-gen.js +5 -2
  30. package/dist/templates/auth/Dashboard.vue.tpl +3 -2
  31. package/dist/templates/auth/Login.vue.tpl +3 -5
  32. package/dist/templates/auth/Signup.vue.tpl +3 -5
  33. package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
  34. package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
  35. package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
  36. package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
  37. package/dist/templates/project/AGENTS.md.tpl +3 -2
  38. package/dist/templates/project/CLAUDE.md.tpl +1 -1
  39. package/dist/templates/project/agents/async.md.tpl +35 -8
  40. package/dist/templates/project/agents/data.md.tpl +113 -49
  41. package/dist/templates/project/agents/frontend.md.tpl +15 -4
  42. package/dist/templates/project/agents/i18n.md.tpl +5 -2
  43. package/dist/templates/project/agents/mail.md.tpl +2 -1
  44. package/dist/templates/project/agents/realtime.md.tpl +16 -6
  45. package/dist/templates/project/agents/seal.md.tpl +6 -3
  46. package/dist/templates/project/agents/security.md.tpl +37 -19
  47. package/dist/templates/project/agents/storage.md.tpl +6 -5
  48. package/dist/templates/project/agents/web.md.tpl +40 -22
  49. package/package.json +6 -6
package/dist/doctor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성 · E-5 확장)
3
3
  *
4
- * 27 검사를 조립한다:
4
+ * 28 검사를 조립한다:
5
5
  * 1) response-mixing (errata E-3 §C · 라이브)
6
6
  * 2) n-plus-one (errata E-4 (e))
7
7
  * 3) dependency-direction (CLAUDE.md §5 · 4 규칙)
@@ -29,6 +29,7 @@
29
29
  * 25) schema-relations (§4.5 · 결정 134 · 커넥션 가로지르는 belongsTo·관계 · 대상 부재 error)
30
30
  * 26) no-import-meta-env (결정 198 · F-9 ② · `.vue` 의 import.meta.env = TS1470 → env 접근자 안내 error)
31
31
  * 27) locale-parity (결정 216 · 13차 W4 · 로케일 간 키 부분 누락 = fallback 조용 노출 경고)
32
+ * 28) render-return (결정 340 · this.render/redirect/json 호출만 하고 return 누락 = 무신호 204 경고)
32
33
  *
33
34
  * 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
34
35
  * DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
@@ -42,6 +43,7 @@ import { existsSync } from 'node:fs';
42
43
  import { join, resolve } from 'node:path';
43
44
  import ts from 'typescript';
44
45
  import { checkResponseMixing } from './doctor/response-mixing.js';
46
+ import { checkRenderReturn } from './doctor/render-return.js';
45
47
  import { checkNPlusOne } from './doctor/n-plus-one.js';
46
48
  import { checkDependencyDirection } from './doctor/dependency-direction.js';
47
49
  import { checkConnections } from './doctor/connections.js';
@@ -73,6 +75,7 @@ import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, }
73
75
  import { makeResult, } from './doctor/types.js';
74
76
  import { FIXERS, FIXER_CAPABILITIES } from './doctor/fixers/index.js';
75
77
  export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
78
+ export { inspectDiscardedResponses, checkRenderReturn } from './doctor/render-return.js';
76
79
  export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
77
80
  export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
78
81
  export { extractConfigDbKeys, analyzeConfigDb, extractKeyUses, checkConnections, } from './doctor/connections.js';
@@ -97,10 +100,10 @@ export { checkLocaleParity } from './doctor/locale-parity.js';
97
100
  export { renderHuman, renderJson } from './doctor/reporter.js';
98
101
  export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
99
102
  /**
100
- * 실행할 검사 이름. 지정 없음(undefined) = 27개 모두.
103
+ * 실행할 검사 이름. 지정 없음(undefined) = 28개 모두.
101
104
  */
102
105
  /**
103
- * doctor 정적 검사 27종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
106
+ * doctor 정적 검사 28종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
104
107
  * 이 배열을 단일 출처로 삼는다(parseDoctorChecks) — 새 규칙 추가 시 여기만
105
108
  * 늘리면 실행·필터·타입이 함께 정합된다(손유지 중복 리스트 표류 방지).
106
109
  */
@@ -132,6 +135,7 @@ export const ALL_RULES = [
132
135
  'schema-relations',
133
136
  'no-import-meta-env',
134
137
  'locale-parity',
138
+ 'render-return',
135
139
  ];
136
140
  const CHECKERS = {
137
141
  'response-mixing': checkResponseMixing,
@@ -161,6 +165,7 @@ const CHECKERS = {
161
165
  'schema-relations': checkSchemaRelations,
162
166
  'no-import-meta-env': checkNoImportMetaEnv,
163
167
  'locale-parity': checkLocaleParity,
168
+ 'render-return': checkRenderReturn,
164
169
  };
165
170
  /**
166
171
  * 규칙을 순서대로 실행해 RuleReport[] 를 낸다. 규칙 하나가 크래시해도 나머지는
@@ -269,10 +274,16 @@ export async function runDoctorCommand(opts = {}) {
269
274
  // --fix: fixer 를 계획하고, --yes 이면 실제 편집한다. reports 를 그대로
270
275
  // 넘겨 규칙별 위반을 fixer 가 판단한다(passed 는 건너뜀).
271
276
  const fixReport = await runDoctorFix(reports, root, opts.yes === true);
272
- const resultWithFix = { ...baseResult, fix: fixReport };
277
+ // 결정 359: 편집이 실제로 일어났으면 **재검사 결과**를 리포트·종료 코드의 기준으로
278
+ // 삼는다 — 적용 전 리포트로 판정하면 "전부 자동 수정됐는데 exit 1" 로 역전돼
279
+ // CI 자동화(--fix --yes 후 green 기대)가 깨졌다. errors/warnings = 적용 후 잔여.
280
+ const finalResult = fixReport.applied && fixReport.outcomes.some((o) => o.applied)
281
+ ? makeResult(await runRules(root, rules))
282
+ : baseResult;
283
+ const resultWithFix = { ...finalResult, fix: fixReport };
273
284
  const out = json
274
285
  ? JSON.stringify(resultWithFix)
275
- : renderHuman(baseResult) + '\n\n' + renderFixHuman(fixReport);
286
+ : renderHuman(finalResult) + '\n\n' + renderFixHuman(fixReport);
276
287
  process.stdout.write(out + '\n');
277
288
  return resultWithFix;
278
289
  }
@@ -7,6 +7,12 @@ export interface AuthScaffoldOptions {
7
7
  * `--public` 로 비-web 앱도 공개 가입을 opt-in 한다(공개 비-web 앱용 탈출구).
8
8
  */
9
9
  readonly public?: boolean;
10
+ /**
11
+ * 결정 338: JWT(API 앱) 변형 — 세션·페이지 대신 토큰 발급/재발급/내 정보
12
+ * 컨트롤러(JSON 전용)와 `auth: { strategy:'jwt' }` 배선을 깐다. API 앱 전용이라
13
+ * `--app <이름>`(비-web) 이 필수다(web 은 세션이 정본 · §7).
14
+ */
15
+ readonly jwt?: boolean;
10
16
  }
11
17
  /** 생성할 파일 하나 — 경로는 프로젝트 루트 기준. */
12
18
  export interface ScaffoldFile {
@@ -19,6 +25,12 @@ export interface ScaffoldResult {
19
25
  readonly patched: string[];
20
26
  /** 손 수리가 필요한 지점 — 수리 안내 문장(§7.5.3)을 그대로 담는다. */
21
27
  readonly warnings: string[];
28
+ /**
29
+ * 결정 361: 인증 **배선이 미완**인가(app.config auth/routes 자동 패치 실패 —
30
+ * 이대로면 currentUser 영구 null / 로그인 라우트 부재). 안내성 경고(JWT stateless
31
+ * 한계 등)와 구분해, 이 값만 종료 코드(비-0)·JSON ok:false 로 이어진다.
32
+ */
33
+ readonly incomplete: boolean;
22
34
  }
23
35
  /**
24
36
  * 결정 155·142: `g auth --app <앱>` 이 앱별 세션 secret 환경변수를 .env·.env.example
@@ -32,7 +44,7 @@ export interface ScaffoldResult {
32
44
  * .env 를 새로 만드는 관례가 없어(사용자가 cp .env.example .env), 존재하는 파일만 패치한다.
33
45
  * 반환: 실제로 키를 추가한 상대 경로들.
34
46
  */
35
- export declare function patchEnvFiles(root: string, app: string): string[];
47
+ export declare function patchEnvFiles(root: string, app: string, kind?: 'session' | 'jwt'): string[];
36
48
  /** 인증 스캐폴드가 생성하는 파일 목록(라우트 제외). 템플릿을 읽어 렌더링한다.
37
49
  * 결정 155: 공개 가입 여부(includePublic)로 registration·Signup·dashboard 변형을 가른다. */
38
50
  export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldFile[];
@@ -40,6 +52,11 @@ export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldF
40
52
  * 기존 routes.ts 에 세션·회원가입 리소스를 끼워 넣는다. 이미 있으면 null.
41
53
  * `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
42
54
  */
55
+ /**
56
+ * 결정 338: 기존 routes.ts 에 JWT 토큰 라우트 3종을 끼워 넣는다. 이미 session#create
57
+ * 참조가 있으면 null(중복 방지). 삽입 지점 규칙은 patchRoutes 와 동일.
58
+ */
59
+ export declare function patchRoutesJwt(existing: string): string | null;
43
60
  export declare function patchRoutes(existing: string, includePublic?: boolean): string | null;
44
61
  /**
45
62
  * 기존 app.config.ts 에 auth 배선을 끼워 넣는다 (결정 93 · gaon new 기본 web 앱은
@@ -57,6 +74,8 @@ export interface GenerateAuthOptions {
57
74
  readonly json?: boolean;
58
75
  /** 결정 155: 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱용 탈출구). */
59
76
  readonly public?: boolean;
77
+ /** 결정 338: JWT(API 앱) 변형 — `--app <비-web 이름>` 필수 · `--public` 과 배타. */
78
+ readonly jwt?: boolean;
60
79
  }
61
80
  /** `gaon g auth` 진입점. 스캐폴드를 쓰고 결과를 사람/JSON 으로 출력한다. */
62
81
  export declare function runGenerateAuthCommand(opts?: GenerateAuthOptions): number;
package/dist/generate.js CHANGED
@@ -65,6 +65,14 @@ function devSessionSecretFor(app) {
65
65
  function envSecretPlaceholderFor(app) {
66
66
  return `change-me-to-a-32-char-${app}-session-secret!!`;
67
67
  }
68
+ /** 결정 338: 앱별 JWT secret 환경변수명 — '<APP>_JWT_SECRET' (세션 secret 관례와 대칭). */
69
+ function jwtSecretEnvFor(app) {
70
+ return `${app.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_JWT_SECRET`;
71
+ }
72
+ /** .env(.example)에 시드할 앱별 JWT secret 플레이스홀더(32자 이상 · 운영은 교체 — 결정 337 가드 대상). */
73
+ function envJwtPlaceholderFor(app) {
74
+ return `change-me-to-a-32-char-${app}-jwt-secret!!!`;
75
+ }
68
76
  /**
69
77
  * 결정 155·142: `g auth --app <앱>` 이 앱별 세션 secret 환경변수를 .env·.env.example
70
78
  * 에도 시드한다. web 은 SESSION_SECRET 이 프로젝트 스캐폴드(.env.example.tpl)에 이미
@@ -77,8 +85,13 @@ function envSecretPlaceholderFor(app) {
77
85
  * .env 를 새로 만드는 관례가 없어(사용자가 cp .env.example .env), 존재하는 파일만 패치한다.
78
86
  * 반환: 실제로 키를 추가한 상대 경로들.
79
87
  */
80
- export function patchEnvFiles(root, app) {
81
- const secretEnv = sessionSecretEnvFor(app);
88
+ export function patchEnvFiles(root, app, kind = 'session') {
89
+ // 결정 338: JWT 변형은 세션 secret 대신 <APP>_JWT_SECRET 을 시드한다(동일 멱등 규칙).
90
+ const secretEnv = kind === 'jwt' ? jwtSecretEnvFor(app) : sessionSecretEnvFor(app);
91
+ const comment = kind === 'jwt'
92
+ ? `# ${app} 앱 JWT secret (32자 이상 · 앱별 분리 · 운영은 반드시 교체 — dev 폴백은 부팅 거부 · 결정 337).`
93
+ : `# ${app} 앱 세션 secret (32자 이상 · 앱별 세션 완전 분리 · 운영은 반드시 교체).`;
94
+ const placeholder = kind === 'jwt' ? envJwtPlaceholderFor(app) : envSecretPlaceholderFor(app);
82
95
  const patched = [];
83
96
  for (const rel of ['.env', '.env.example']) {
84
97
  const abs = join(root, rel);
@@ -88,8 +101,7 @@ export function patchEnvFiles(root, app) {
88
101
  // 라인 시작에서 `KEY=` 를 찾는다(주석·부분 일치 회피).
89
102
  if (new RegExp(`^${secretEnv}=`, 'm').test(existing))
90
103
  continue;
91
- const block = `\n# ${app} 앱 세션 secret (32자 이상 · 앱별 세션 완전 분리 · 운영은 반드시 교체).\n` +
92
- `${secretEnv}=${envSecretPlaceholderFor(app)}\n`;
104
+ const block = `\n${comment}\n${secretEnv}=${placeholder}\n`;
93
105
  const sep = existing.endsWith('\n') ? '' : '\n';
94
106
  writeFileSync(abs, existing + sep + block, 'utf8');
95
107
  patched.push(rel);
@@ -113,6 +125,7 @@ function renderTemplate(name, app, includePublic) {
113
125
  .replaceAll('{{APP_NAME}}', app)
114
126
  .replaceAll('{{URL_PREFIX}}', urlPrefixFor(app))
115
127
  .replaceAll('{{SESSION_SECRET_ENV}}', sessionSecretEnvFor(app))
128
+ .replaceAll('{{JWT_SECRET_ENV}}', jwtSecretEnvFor(app))
116
129
  // 결정 218: 시큐어 세션 컨트롤러의 로그인 역할 게이트 기준. 결정 155·145 의 대시보드
117
130
  // 역할 게이트(role === 'admin')와 같은 값으로 고정해 두 층이 어긋나지 않게 한다.
118
131
  .replaceAll('{{REQUIRED_ROLE}}', 'admin')
@@ -123,6 +136,18 @@ function renderTemplate(name, app, includePublic) {
123
136
  export function authScaffoldFiles(opts = {}) {
124
137
  const app = opts.app ?? 'web';
125
138
  const includePublic = includesPublicRegistration(opts);
139
+ // 결정 338: JWT(API 앱) 변형 — 페이지·UI 킷·회원가입 없이 스키마/모델(공유) +
140
+ // 토큰 컨트롤러(JSON 전용) + auth.strategy:'jwt' 배선만. API 앱은 프론트가 없다.
141
+ if (opts.jwt) {
142
+ const specs = [
143
+ { tpl: 'user.schema.ts.tpl', out: 'domain/schema/users.ts' },
144
+ { tpl: 'user.model.ts.tpl', out: 'domain/models/User.ts' },
145
+ { tpl: 'jwt.auth.wiring.ts.tpl', out: `apps/${app}/auth.ts` },
146
+ { tpl: 'jwt.session.controller.ts.tpl', out: `apps/${app}/controllers/session.ts` },
147
+ { tpl: 'jwt.app.config.ts.tpl', out: `apps/${app}/app.config.ts` },
148
+ ];
149
+ return specs.map(({ tpl, out }) => ({ path: out, contents: renderTemplate(tpl, app, false) }));
150
+ }
126
151
  const specs = [
127
152
  { tpl: 'user.schema.ts.tpl', out: 'domain/schema/users.ts' },
128
153
  { tpl: 'user.model.ts.tpl', out: 'domain/models/User.ts' },
@@ -157,6 +182,22 @@ export function authScaffoldFiles(opts = {}) {
157
182
  * 기존 routes.ts 에 세션·회원가입 리소스를 끼워 넣는다. 이미 있으면 null.
158
183
  * `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
159
184
  */
185
+ /**
186
+ * 결정 338: 기존 routes.ts 에 JWT 토큰 라우트 3종을 끼워 넣는다. 이미 session#create
187
+ * 참조가 있으면 null(중복 방지). 삽입 지점 규칙은 patchRoutes 와 동일.
188
+ */
189
+ export function patchRoutesJwt(existing) {
190
+ if (existing.includes("'session#create'") || existing.includes("resource('session')"))
191
+ return null;
192
+ const m = existing.match(/routes\(\s*\(\s*\w+\s*\)\s*=>\s*\{/);
193
+ if (!m || m.index === undefined)
194
+ return null;
195
+ const insertAt = m.index + m[0].length;
196
+ const inject = "\n r.post('/session', 'session#create') // 로그인 → 토큰 발급 (gaon g auth --jwt)" +
197
+ "\n r.post('/session/refresh', 'session#refresh') // 액세스 토큰 재발급 (gaon g auth --jwt)" +
198
+ "\n r.get('/session', 'session#show') // 현재 사용자 · Bearer (gaon g auth --jwt)";
199
+ return existing.slice(0, insertAt) + inject + existing.slice(insertAt);
200
+ }
160
201
  export function patchRoutes(existing, includePublic = true) {
161
202
  if (existing.includes("resource('session')"))
162
203
  return null;
@@ -225,17 +266,35 @@ export function writeAuthScaffold(cwd, opts = {}) {
225
266
  const skipped = [];
226
267
  const patched = [];
227
268
  const warnings = [];
269
+ let incomplete = false;
228
270
  // 결정 75: auth 페이지(Login/Signup/Dashboard)는 UI 킷 컴포넌트를 쓴다.
229
271
  // 필요한 최소 세트를 먼저 보장한다 — 이미 있으면(gaon g ui-kit 를 먼저 돌린
230
272
  // 경우) skip, 없으면 생성. 이 보장이 없으면 스캐폴드 직후 페이지가 컴포넌트를
231
273
  // 해상하지 못해 vue-tsc 가 깨진다(gaon new → g auth 단독 경로 · 결정 60 게이트).
232
274
  const includePublic = includesPublicRegistration(opts);
233
- const ui = writeUiKitFiles(root, authUiKitFiles(app));
234
- created.push(...ui.created);
235
- skipped.push(...ui.skipped);
275
+ // 결정 338: JWT 변형은 프론트가 없다(API 앱 · JSON 전용) — UI 킷을 깔지 않는다.
276
+ if (!opts.jwt) {
277
+ const ui = writeUiKitFiles(root, authUiKitFiles(app));
278
+ created.push(...ui.created);
279
+ skipped.push(...ui.skipped);
280
+ }
236
281
  for (const file of authScaffoldFiles(opts)) {
237
282
  const abs = join(root, file.path);
238
283
  if (existsSync(abs)) {
284
+ // 결정 338: JWT 변형의 app.config 자동 패치는 지원하지 않는다(세션 병존 등
285
+ // 판단이 필요) — 이미 있으면 skip + 수리 안내로 폴백한다.
286
+ if (opts.jwt && file.path === `apps/${app}/app.config.ts`) {
287
+ const existing = readFileSync(abs, 'utf8');
288
+ skipped.push(file.path);
289
+ if (!/\bauth\s*:/.test(existing)) {
290
+ incomplete = true;
291
+ warnings.push(`apps/${app}/app.config.ts 가 이미 있어 JWT 인증 배선을 자동 추가하지 못했습니다.\n` +
292
+ `→ defineAppConfig({...}) 에 다음을 추가하세요 (import { loadUser } from './auth.js'):\n` +
293
+ ` auth: { strategy: 'jwt', secret: process.env.${jwtSecretEnvFor(app)} ?? '32자 이상 dev 비밀', loadUser },\n` +
294
+ `→ secret 은 32자 이상 · 운영은 .env 의 ${jwtSecretEnvFor(app)} 로 주입해야 부팅합니다(결정 337).`);
295
+ }
296
+ continue;
297
+ }
239
298
  // 결정 93: gaon new 기본 web 앱은 app.config.ts 에 세션만 배선돼 있다.
240
299
  // g auth 는 이를 skip 하지 않고 **auth 배선을 패치로 추가**해 완성한다 —
241
300
  // 결정 59: auth 배선이 없으면 로그인 후에도 currentUser 가 영구 null 이다.
@@ -249,6 +308,7 @@ export function writeAuthScaffold(cwd, opts = {}) {
249
308
  else if (!/\bauth\s*:/.test(existing)) {
250
309
  // 패치 불가(비관례 config) — skip + 수리 안내로 폴백.
251
310
  skipped.push(file.path);
311
+ incomplete = true;
252
312
  warnings.push(`apps/${app}/app.config.ts 가 이미 있어 인증 배선을 자동 추가하지 못했습니다.\n` +
253
313
  `→ apps/${app}/app.config.ts 의 defineAppConfig({...}) 에 다음을 추가하세요:\n` +
254
314
  ` session: { secret: process.env.${sessionSecretEnvFor(app)} ?? '32자 이상 비밀' },\n` +
@@ -268,31 +328,45 @@ export function writeAuthScaffold(cwd, opts = {}) {
268
328
  writeFileSync(abs, file.contents, 'utf8');
269
329
  created.push(file.path);
270
330
  }
271
- // routes.ts — 있으면 패치, 없으면 템플릿에서 생성.
331
+ // routes.ts — 있으면 패치, 없으면 템플릿에서 생성. JWT 변형은 토큰 라우트 3종(결정 338).
272
332
  const routesPath = join(root, 'apps', app, 'routes.ts');
273
333
  const routesRel = `apps/${app}/routes.ts`;
274
334
  if (existsSync(routesPath)) {
275
- const patchedContent = patchRoutes(readFileSync(routesPath, 'utf8'), includePublic);
335
+ const existing = readFileSync(routesPath, 'utf8');
336
+ const patchedContent = opts.jwt ? patchRoutesJwt(existing) : patchRoutes(existing, includePublic);
276
337
  if (patchedContent) {
277
338
  writeFileSync(routesPath, patchedContent, 'utf8');
278
339
  patched.push(routesRel);
279
340
  }
341
+ else if (existing.includes("resource('session')") || existing.includes("'session#create'")) {
342
+ skipped.push(routesRel); // 이미 배선 — 정상 멱등.
343
+ }
280
344
  else {
345
+ // 결정 361: routes( ... ) 골격을 못 찾아 패치 불가 — 인증 라우트가 등록되지 않는다.
281
346
  skipped.push(routesRel);
347
+ incomplete = true;
348
+ warnings.push(`apps/${app}/routes.ts 가 관례 골격(routes((r) => { ... }))이 아니어서 인증 라우트를 자동 추가하지 못했습니다.\n` +
349
+ `→ apps/${app}/routes.ts 에 ${opts.jwt ? "r.post('/session', 'session#create') 등 토큰 라우트 3종" : "r.resource('session') 및 r.get('/dashboard', 'dashboard#show')"} 를 직접 추가하세요.`);
282
350
  }
283
351
  }
284
352
  else {
285
353
  mkdirSync(dirname(routesPath), { recursive: true });
286
- writeFileSync(routesPath, renderTemplate('routes.ts.tpl', app, includePublic), 'utf8');
354
+ writeFileSync(routesPath, renderTemplate(opts.jwt ? 'jwt.routes.ts.tpl' : 'routes.ts.tpl', app, includePublic), 'utf8');
287
355
  created.push(routesRel);
288
356
  }
289
- // 결정 155·142(W3): 앱별 세션 secret 을 .env·.env.example 에 시드한다. web 은 이미
290
- // 있어 멱등, 비-web 앱은 여기서 키가 심어져 "공개 고정 dev secret 폴백" 표면을 막는다.
291
- patched.push(...patchEnvFiles(root, app));
357
+ // 결정 155·142(W3)·338: 앱별 secret 을 .env·.env.example 에 시드한다(세션 또는 JWT).
358
+ // web 은 이미 있어 멱등, 비-web 앱은 여기서 키가 심어져 dev 폴백 표면을 드러낸다.
359
+ patched.push(...patchEnvFiles(root, app, opts.jwt ? 'jwt' : 'session'));
360
+ // 결정 338: JWT 변형의 stateless 한계를 스캐폴드 시점에 명시한다(§7.5.3).
361
+ if (opts.jwt) {
362
+ warnings.push(`JWT 토큰은 stateless 입니다 — 서버측 폐기(로그아웃·강제 무효화) 수단이 없습니다(결정 337).\n` +
363
+ `→ 유출된 리프레시 토큰은 만료(기본 7d)까지 유효합니다. 민감한 앱은 app.config 의 refreshTtl 을 짧게 잡으세요.\n` +
364
+ `→ 사용자 계정은 이 API 앱에 회원가입이 없습니다 — web 앱 가입(gaon g auth) 또는 seed 로 만드세요.`);
365
+ }
292
366
  // 결정 155·218: 시큐어 앱(공개 가입 미생성)은 로그인 시점 역할 게이트 세션 컨트롤러 +
293
367
  // 역할 게이트 대시보드를 깔았다 — 두 층 다 role 컬럼이 있어야 동작하므로 추가를 안내한다
294
368
  // (§7.5.3 · authorize·로그인 게이트 예시가 실제 역할 규칙이 되도록).
295
- if (!includePublic) {
369
+ if (!includePublic && !opts.jwt) {
296
370
  warnings.push(`apps/${app}/ 는 시큐어 스캐폴드입니다(공개 회원가입 미생성 · 결정 155·218).\n` +
297
371
  `→ 관리자는 직접 만들거나 승격하세요(공개 가입 라우트 없음). 공개 가입이 필요하면 --public 로 다시 생성.\n` +
298
372
  `→ 역할 인가를 완성하려면 domain/schema/users.ts 에 role 컬럼을 추가하세요:\n` +
@@ -300,20 +374,33 @@ export function writeAuthScaffold(cwd, opts = {}) {
300
374
  ` 그러면 로그인(controllers/session.ts)이 역할 부족 시 세션을 만들지 않고,\n` +
301
375
  ` apps/${app}/controllers/dashboard.ts 의 authorize(역할) 게이트도 실제 역할로 동작합니다.`);
302
376
  }
303
- return { created: created.sort(), skipped: skipped.sort(), patched: patched.sort(), warnings };
377
+ return { created: created.sort(), skipped: skipped.sort(), patched: patched.sort(), warnings, incomplete };
304
378
  }
305
379
  /** `gaon g auth` 진입점. 스캐폴드를 쓰고 결과를 사람/JSON 으로 출력한다. */
306
380
  export function runGenerateAuthCommand(opts = {}) {
307
381
  const cwd = opts.cwd ?? process.cwd();
308
382
  const app = opts.app ?? 'web';
309
- const result = writeAuthScaffold(cwd, { app, public: opts.public });
383
+ // 결정 338: JWT 는 API 앱 전용(§7 · web 세션이 정본). 형식 불량은 즉시 수리 안내.
384
+ if (opts.jwt && (opts.app === undefined || app === 'web')) {
385
+ process.stderr.write(` ✗ gaon g auth --jwt: JWT 는 API 앱 전용입니다 — 대상 API 앱을 --app 으로 지정하세요(web 불가 · 세션이 정본).\n` +
386
+ ` → 예: gaon g app api && gaon g auth --jwt --app api\n`);
387
+ return 1;
388
+ }
389
+ if (opts.jwt && opts.public) {
390
+ process.stderr.write(` ✗ gaon g auth --jwt: --public 은 세션 스캐폴드 전용입니다 — JWT 변형은 회원가입을 깔지 않습니다(계정은 web 가입 또는 seed).\n`);
391
+ return 1;
392
+ }
393
+ const result = writeAuthScaffold(cwd, { app, public: opts.public, jwt: opts.jwt });
394
+ // 결정 361: 배선 미완(incomplete)은 성공이 아니다 — 이전엔 무조건 exit 0 이라
395
+ // 자동화가 "currentUser 영구 null" 스캐폴드를 성공으로 처리했다(g controller 의
396
+ // skip=1 과도 비대칭). JSON 은 ok 필드로, 종료 코드는 1 로 신호한다.
310
397
  if (opts.json) {
311
- process.stdout.write(JSON.stringify({ command: 'g auth', app, public: opts.public ?? app === 'web', ...result }, null, 2) + '\n');
312
- return 0;
398
+ process.stdout.write(JSON.stringify({ command: 'g auth', ok: !result.incomplete, app, jwt: opts.jwt ?? false, public: opts.jwt ? false : (opts.public ?? app === 'web'), ...result }, null, 2) + '\n');
399
+ return result.incomplete ? 1 : 0;
313
400
  }
314
- const includePublic = includesPublicRegistration({ app, public: opts.public });
401
+ const includePublic = !opts.jwt && includesPublicRegistration({ app, public: opts.public });
315
402
  const lines = [''];
316
- lines.push(` gaon g auth — 인증 스캐폴드 (${app} )`);
403
+ lines.push(` gaon g auth — 인증 스캐폴드 (${app} 앱${opts.jwt ? ' · JWT/API 변형' : ''})`);
317
404
  lines.push('');
318
405
  for (const f of result.created)
319
406
  lines.push(` + ${f}`);
@@ -327,6 +414,18 @@ export function runGenerateAuthCommand(opts = {}) {
327
414
  lines.push('', ` ⚠ ${w.split('\n').join('\n ')}`);
328
415
  lines.push('');
329
416
  const prefix = urlPrefixFor(app);
417
+ // 결정 338: JWT 변형은 다음 단계가 토큰 흐름이다(세션·Redis 불요 — 토큰 stateless).
418
+ if (opts.jwt) {
419
+ const jwtEnv = jwtSecretEnvFor(app);
420
+ lines.push(' 다음 단계:');
421
+ lines.push(` 1) .env 에 ${jwtEnv}(32자 이상 무작위)를 설정한다 — 운영은 dev 폴백이면 부팅 거부(결정 337).`);
422
+ lines.push(' 2) gaon db diff && gaon db migrate 로 users 테이블을 만든다.');
423
+ lines.push(' 3) gaon dev 로 실행한다 (.gaon 타입 브리지 생성 + JWT 인증 자동 배선).');
424
+ lines.push(` → POST ${prefix}/session 로그인(토큰 발급) · POST ${prefix}/session/refresh 재발급 · GET ${prefix}/session 현재 사용자(Bearer).`);
425
+ lines.push('');
426
+ process.stdout.write(lines.join('\n') + '\n');
427
+ return result.incomplete ? 1 : 0; // 결정 361
428
+ }
330
429
  const secretEnv = sessionSecretEnvFor(app);
331
430
  lines.push(' 다음 단계:');
332
431
  lines.push(` 1) .env 에 REDIS_URL·${secretEnv}(32자 이상)·COOKIE_SECRET 을 설정한다.`);
@@ -345,5 +444,5 @@ export function runGenerateAuthCommand(opts = {}) {
345
444
  }
346
445
  lines.push('');
347
446
  process.stdout.write(lines.join('\n') + '\n');
348
- return 0;
447
+ return result.incomplete ? 1 : 0; // 결정 361
349
448
  }
package/dist/hub.js CHANGED
@@ -62,6 +62,8 @@ export async function runHubCommand(opts = {}) {
62
62
  pingTimeoutMs: opts.pingTimeoutMs ?? envInt('GAON_HUB_PING_TIMEOUT_MS'),
63
63
  sweepMs: opts.sweepMs ?? envInt('GAON_HUB_SWEEP_MS'),
64
64
  reclaimGraceMs: envInt('GAON_HUB_RECLAIM_GRACE_MS'),
65
+ // 결정 350: 공유 토큰(선택). 설정 시 웹서버(serve)도 같은 GAON_HUB_TOKEN 필요.
66
+ authToken: process.env.GAON_HUB_TOKEN,
65
67
  onState: (s) => emit({ kind: 'leader', leader: s.leader, id }),
66
68
  });
67
69
  }
@@ -0,0 +1,12 @@
1
+ export interface ConfigI18nAnalysis {
2
+ /** defineConfig 인자에 i18n 프로퍼티가 존재하는가. */
3
+ readonly declared: boolean;
4
+ /** 정적 리터럴로 읽힌 dir(상대 경로 원문). 못 읽으면 undefined. */
5
+ readonly dir?: string;
6
+ /** 정적 리터럴로 읽힌 fallbackLng. 못 읽으면 undefined. */
7
+ readonly fallbackLng?: string;
8
+ }
9
+ /** gaon.config.ts 소스에서 i18n 블록의 dir·fallbackLng 리터럴을 뽑는다. */
10
+ export declare function analyzeConfigI18n(source: string): ConfigI18nAnalysis;
11
+ /** cwd 의 gaon.config.ts 를 읽어 i18n 블록을 분석한다. 파일이 없으면 미선언. */
12
+ export declare function analyzeProjectI18n(cwd: string): ConfigI18nAnalysis;
@@ -0,0 +1,95 @@
1
+ // @gaonjs/cli · gaon.config.ts 의 i18n 블록 정적 분석 (결정 352)
2
+ //
3
+ // messages.d.ts 축(생성기·워처)과 doctor(locale-parity)는 런타임 config 로드 없이
4
+ // 돌아야 한다(.env 부재·부분 프로젝트에서도) — connections.ts(결정 135)와 같은
5
+ // 정적 AST 분석으로 `i18n.dir`(카탈로그 위치)·`i18n.fallbackLng`(기준 로케일)를
6
+ // 뽑는다. 이전엔 dir 이 'locales' 로 하드코딩돼 커스텀 dir 프로젝트에서 타입 축과
7
+ // doctor 가 무소음으로 죽었고, 기준 로케일은 fallbackLng 가 아니라 알파벳순 첫
8
+ // 로케일이었다(컴파일 보증이 fallback 체인과 다른 로케일에 정박).
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import ts from 'typescript';
12
+ /** gaon.config.ts 소스에서 i18n 블록의 dir·fallbackLng 리터럴을 뽑는다. */
13
+ export function analyzeConfigI18n(source) {
14
+ const sf = ts.createSourceFile('gaon.config.ts', source, ts.ScriptTarget.ES2022, true);
15
+ let declared = false;
16
+ let dir;
17
+ let fallbackLng;
18
+ // i18n 초기화식에서 객체 리터럴을 찾는다 — 삼항·??·&&·괄호는 양변을 훑는다
19
+ // (connections.ts 와 동일 규약 · env 조건부 블록 대응). 두 분기가 서로 다른
20
+ // 리터럴을 주는 병리적 케이스는 먼저 읽힌 값을 쓴다(실사용 형태 아님).
21
+ const collect = (node) => {
22
+ if (ts.isParenthesizedExpression(node))
23
+ return collect(node.expression);
24
+ if (ts.isObjectLiteralExpression(node)) {
25
+ for (const p of node.properties) {
26
+ if (!ts.isPropertyAssignment(p))
27
+ continue;
28
+ const name = propNameText(p.name);
29
+ if (name === 'dir' && ts.isStringLiteral(p.initializer) && dir === undefined) {
30
+ dir = p.initializer.text;
31
+ }
32
+ if (name === 'fallbackLng' && ts.isStringLiteral(p.initializer) && fallbackLng === undefined) {
33
+ fallbackLng = p.initializer.text;
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ if (ts.isConditionalExpression(node)) {
39
+ collect(node.whenTrue);
40
+ collect(node.whenFalse);
41
+ return;
42
+ }
43
+ if (ts.isBinaryExpression(node)) {
44
+ const op = node.operatorToken.kind;
45
+ if (op === ts.SyntaxKind.QuestionQuestionToken ||
46
+ op === ts.SyntaxKind.BarBarToken ||
47
+ op === ts.SyntaxKind.AmpersandAmpersandToken) {
48
+ collect(node.left);
49
+ collect(node.right);
50
+ }
51
+ }
52
+ };
53
+ const visit = (node) => {
54
+ if (ts.isCallExpression(node) && isDefineConfig(node.expression)) {
55
+ const arg = node.arguments[0];
56
+ if (arg && ts.isObjectLiteralExpression(arg)) {
57
+ for (const p of arg.properties) {
58
+ if (ts.isPropertyAssignment(p) && propNameText(p.name) === 'i18n') {
59
+ declared = true;
60
+ collect(p.initializer);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ts.forEachChild(node, visit);
66
+ };
67
+ visit(sf);
68
+ return { declared, dir, fallbackLng };
69
+ }
70
+ /** cwd 의 gaon.config.ts 를 읽어 i18n 블록을 분석한다. 파일이 없으면 미선언. */
71
+ export function analyzeProjectI18n(cwd) {
72
+ const configPath = join(cwd, 'gaon.config.ts');
73
+ if (!existsSync(configPath))
74
+ return { declared: false };
75
+ try {
76
+ return analyzeConfigI18n(readFileSync(configPath, 'utf8'));
77
+ }
78
+ catch {
79
+ return { declared: false };
80
+ }
81
+ }
82
+ function isDefineConfig(e) {
83
+ if (ts.isIdentifier(e) && e.text === 'defineConfig')
84
+ return true;
85
+ if (ts.isPropertyAccessExpression(e) && e.name.text === 'defineConfig')
86
+ return true;
87
+ return false;
88
+ }
89
+ function propNameText(n) {
90
+ if (ts.isIdentifier(n))
91
+ return n.text;
92
+ if (ts.isStringLiteral(n))
93
+ return n.text;
94
+ return undefined;
95
+ }
package/dist/index.js CHANGED
@@ -110,12 +110,12 @@ function renderHelp(version = VERSION) {
110
110
  " gaon build 멀티 앱 프론트 프로덕션 빌드 (gaon gen + apps/* 순회 · 앱별 dist/<앱>·base=/<앱>/ · --json)",
111
111
  " gaon console 프로젝트 컨텍스트 REPL (--no-config)",
112
112
  " gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all · -- vitest 인자)",
113
- ` gaon doctor 정적 검사 (${ALL_RULES.length} 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트·Link>Button 중첩·seal 클라 배선·보안 역전·§4.5 관계·import.meta.env·로케일 커버리지)`,
113
+ ` gaon doctor 정적 검사 (${ALL_RULES.length} 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트·Link>Button 중첩·seal 클라 배선·보안 역전·§4.5 관계·import.meta.env·로케일 커버리지·render return 누락)`,
114
114
  " gaon doctor --json 자동화용 JSON 출력",
115
115
  " gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
116
116
  " gaon doctor --fix 기계 정정 가능한 위반 계획(dry-run · v0.16 §7.5.3)",
117
117
  " gaon doctor --fix --yes 실제 편집 적용(원본은 .bak-<타임스탬프> 로 자동 백업)",
118
- " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
118
+ " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트 · --jwt --app <api> = API 앱 토큰 변형)",
119
119
  " gaon g controller <name> 컨트롤러 스캐폴드 (Rails 관례 · 페이지+JSON 액션)",
120
120
  " gaon g model <Name> 모델 스캐폴드 (스키마+모델 · E-4 컬럼 예시)",
121
121
  " gaon g page <Path/Name> Vue 페이지 (Inertia SPA · pageProps 브리지)",
@@ -147,7 +147,7 @@ function renderHelp(version = VERSION) {
147
147
  * 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
148
148
  */
149
149
  export function parseDoctorChecks(argv) {
150
- // 인정 집합은 doctor.ts 의 ALL_RULES(정본 27종)를 단일 출처로 쓴다 — 과거
150
+ // 인정 집합은 doctor.ts 의 ALL_RULES(정본 28종)를 단일 출처로 쓴다 — 과거
151
151
  // 손유지 9종 리스트가 뒤처져 --check=seal-security 같은 16종이 조용히 무시되고
152
152
  // 전체 검사로 되돌아가던 표류를 근본 차단한다(결정 168).
153
153
  const isKnown = (s) => ALL_RULES.includes(s);
@@ -225,6 +225,10 @@ export function parseDbArgs(sub, argv) {
225
225
  const dbIdx = argv.indexOf("--db");
226
226
  const cfgIdx = argv.indexOf("--config");
227
227
  const valueFlags = new Set(["--db", "--config"]);
228
+ // 결정 358: db 는 파괴 방향 오동작이 가능한 명령군이라 미지 플래그를 조용히
229
+ // 무시하지 않는다 — `--dryrun`(오타)이 무시되면 dry-run 의도가 **실제 마이그레이션
230
+ // 적용**으로 반전된다(결정 267 down-위치 문제와 동형). 인정 집합 밖 `--*` 는 throw.
231
+ const knownFlags = new Set(["--db", "--config", "--json", "--yes", "--dry-run"]);
228
232
  const positionals = [];
229
233
  for (let i = 2; i < argv.length; i++) {
230
234
  const a = argv[i];
@@ -234,8 +238,14 @@ export function parseDbArgs(sub, argv) {
234
238
  i++; // 플래그 값 스킵
235
239
  continue;
236
240
  }
237
- if (a.startsWith("--"))
238
- continue; // 불리언 플래그
241
+ if (a.startsWith("--")) {
242
+ if (!knownFlags.has(a)) {
243
+ throw new Error(`gaon db ${sub}: 알 수 없는 플래그 '${a}'.\n` +
244
+ ` → 지원 플래그: --db <키> · --config <경로> · --dry-run · --yes · --json\n` +
245
+ ` → 오타(예: --dryrun)가 조용히 무시되면 의도와 반대로 실행될 수 있어 멈춥니다.`);
246
+ }
247
+ continue; // 인정된 불리언 플래그
248
+ }
239
249
  positionals.push(a);
240
250
  }
241
251
  return {
@@ -408,7 +418,7 @@ export function runCli(argv, opts = {}) {
408
418
  });
409
419
  return;
410
420
  }
411
- // `gaon doctor` — 정적 검사(M9-E · 27 검사 · ALL_RULES 단일 출처). --check=<이름>[,<이름>...] 로
421
+ // `gaon doctor` — 정적 검사(M9-E · 28 검사 · ALL_RULES 단일 출처). --check=<이름>[,<이름>...] 로
412
422
  // 선택 실행, --json 은 자동화 파싱용.
413
423
  // exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
414
424
  if (argv[0] === "doctor") {
@@ -421,11 +431,15 @@ export function runCli(argv, opts = {}) {
421
431
  process.exitCode = 2;
422
432
  return;
423
433
  }
424
- // --fix --yes: 편집이 실제로 일어났고 나머지 위반이 없으면 0,
425
- // 아직 수동 수정이 남아 있으면 1(자동화 정합).
426
- // --fix (dry-run): errors > 0 이면 1( 고침) · 계획만 있어도 1.
434
+ // 결정 359: 종료 코드 판정.
435
+ // --fix --yes: runDoctorCommand 적용 후 **재검사** 결과를 돌려주므로
436
+ // errors = 잔여 위반 전부 자동 수정됐으면 0(자동화 정합).
437
+ // --fix (dry-run): errors > 0 이면 1 · errors 0 이어도 **계획이 있으면 1**
438
+ // (고칠 게 있다는 신호 — 이전엔 warning-only 계획이 0 으로 삼켜짐).
427
439
  // 기본: errors > 0 이면 1.
428
- process.exitCode = result.errors.length > 0 ? 1 : 0;
440
+ const fix = result.fix;
441
+ const dryPlans = fix !== undefined && !fix.applied && fix.outcomes.length > 0;
442
+ process.exitCode = result.errors.length > 0 || dryPlans ? 1 : 0;
429
443
  })
430
444
  .catch((err) => {
431
445
  const msg = err instanceof Error ? err.message : String(err);
@@ -498,7 +512,16 @@ export function runCli(argv, opts = {}) {
498
512
  process.exitCode = 1;
499
513
  return;
500
514
  }
501
- const dbOpts = parseDbArgs(sub, argv);
515
+ // 결정 358: 미지 플래그는 파싱 경계에서 fail-loud(오타 dry-run 이 실 적용되는 반전 방지).
516
+ let dbOpts;
517
+ try {
518
+ dbOpts = parseDbArgs(sub, argv);
519
+ }
520
+ catch (err) {
521
+ process.stderr.write(` ✗ ${err instanceof Error ? err.message : String(err)}\n`);
522
+ process.exitCode = 1;
523
+ return;
524
+ }
502
525
  void runDbCommand(sub, dbOpts)
503
526
  .then((code) => {
504
527
  process.exitCode = code;
@@ -518,7 +541,13 @@ export function runCli(argv, opts = {}) {
518
541
  const appIdx = argv.indexOf("--app");
519
542
  const app = appIdx >= 0 ? argv[appIdx + 1] : undefined;
520
543
  // 결정 155: --public 은 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱 탈출구).
521
- const code = runGenerateAuthCommand({ app, json: argv.includes("--json"), public: argv.includes("--public") });
544
+ // 결정 338: --jwt API 앱 토큰 변형(--app 필수 · --public 배타).
545
+ const code = runGenerateAuthCommand({
546
+ app,
547
+ json: argv.includes("--json"),
548
+ public: argv.includes("--public"),
549
+ jwt: argv.includes("--jwt"),
550
+ });
522
551
  process.exitCode = code;
523
552
  return;
524
553
  }
@@ -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', includeDoctor?: boolean }
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
  /**