@gaonjs/cli 0.43.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.
- package/README.md +1 -1
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/db.js +29 -7
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +8 -3
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +4 -3
- package/dist/db/journal.js +21 -10
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +3 -3
- package/dist/db/replay.js +2 -2
- package/dist/db/resolve.d.ts +15 -0
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +13 -3
- 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/pageprops-destructure.d.ts +2 -2
- package/dist/doctor/pageprops-destructure.js +29 -23
- 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 +17 -6
- 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 +12 -0
- package/dist/i18n-config.js +95 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +59 -14
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +5 -2
- package/dist/scaffold/job.js +3 -1
- 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/.dockerignore.tpl +3 -0
- package/dist/templates/project/.env.example.tpl +1 -1
- package/dist/templates/project/AGENTS.md.tpl +5 -3
- package/dist/templates/project/CLAUDE.md.tpl +5 -4
- package/dist/templates/project/Dockerfile.tpl +6 -1
- package/dist/templates/project/agents/async.md.tpl +75 -10
- package/dist/templates/project/agents/data.md.tpl +159 -22
- package/dist/templates/project/agents/frontend.md.tpl +51 -12
- package/dist/templates/project/agents/i18n.md.tpl +5 -2
- package/dist/templates/project/agents/mail.md.tpl +4 -2
- package/dist/templates/project/agents/realtime.md.tpl +37 -3
- package/dist/templates/project/agents/seal.md.tpl +14 -3
- package/dist/templates/project/agents/security.md.tpl +47 -11
- package/dist/templates/project/agents/storage.md.tpl +16 -8
- package/dist/templates/project/agents/web.md.tpl +78 -24
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +4 -3
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +1 -1
- package/dist/templates/project/apps/web/main.ts.tpl +1 -1
- package/dist/templates/project/apps/web/routes.ts.tpl +1 -1
- package/dist/templates/project/docker-compose.yaml.tpl +1 -1
- package/dist/templates/project/pnpm-workspace.yaml.tpl +1 -1
- package/dist/templates/project/vite.config.ts.tpl +1 -1
- package/dist/work.d.ts +21 -0
- package/dist/work.js +45 -1
- package/package.json +12 -7
- package/dist/templates/index.ts +0 -109
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
|
-
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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): 앱별
|
|
290
|
-
// 있어 멱등, 비-web 앱은 여기서 키가 심어져
|
|
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface ParsedNewArgs {
|
|
|
64
64
|
readonly packageManager?: "pnpm" | "npm" | "yarn";
|
|
65
65
|
/** pm 값이 주어졌으나 pnpm·npm·yarn 이 아닐 때 그 원문(에러 안내용). */
|
|
66
66
|
readonly unknownPm?: string;
|
|
67
|
+
/**
|
|
68
|
+
* 결정 314: 파사드 버전 스펙(예 `^1.21.0`). create-gaon 이 자기 발행 메타에서
|
|
69
|
+
* 해석해 주입한다 — npx 설치 트리엔 gaonjs 패키지가 없어 상향 탐색(결정 242)이
|
|
70
|
+
* fail-loud 로 죽기 때문. 미지정이면 기존 자동 감지 경로 그대로.
|
|
71
|
+
*/
|
|
72
|
+
readonly gaonjsVersion?: string;
|
|
67
73
|
}
|
|
68
74
|
/**
|
|
69
75
|
* `gaon new <name>` 의 인자를 파싱한다.
|
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(정본
|
|
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);
|
|
@@ -172,12 +172,22 @@ export function parseDoctorChecks(argv) {
|
|
|
172
172
|
export function parseNewArgs(rest) {
|
|
173
173
|
let name;
|
|
174
174
|
let pmRaw;
|
|
175
|
+
let gaonjsVersion;
|
|
175
176
|
for (let i = 0; i < rest.length; i++) {
|
|
176
177
|
const a = rest[i];
|
|
177
178
|
if (a === "--package-manager" || a === "--pm") {
|
|
178
179
|
pmRaw = rest[++i];
|
|
179
180
|
continue;
|
|
180
181
|
}
|
|
182
|
+
// 결정 314: 값을 취하는 플래그라 pm 과 같은 이유로 값을 건너뛴다(이름 오인 방지).
|
|
183
|
+
if (a === "--gaonjs-version") {
|
|
184
|
+
gaonjsVersion = rest[++i];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (a.startsWith("--gaonjs-version=")) {
|
|
188
|
+
gaonjsVersion = a.slice("--gaonjs-version=".length);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
181
191
|
const eqPrefix = a.startsWith("--package-manager=")
|
|
182
192
|
? "--package-manager="
|
|
183
193
|
: a.startsWith("--pm=")
|
|
@@ -192,10 +202,15 @@ export function parseNewArgs(rest) {
|
|
|
192
202
|
if (name === undefined)
|
|
193
203
|
name = a;
|
|
194
204
|
}
|
|
205
|
+
const version = gaonjsVersion === undefined || gaonjsVersion === "" ? undefined : gaonjsVersion;
|
|
195
206
|
if (pmRaw === "pnpm" || pmRaw === "npm" || pmRaw === "yarn") {
|
|
196
|
-
return { name, packageManager: pmRaw };
|
|
207
|
+
return { name, packageManager: pmRaw, gaonjsVersion: version };
|
|
197
208
|
}
|
|
198
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
name,
|
|
211
|
+
unknownPm: pmRaw === undefined || pmRaw === "" ? undefined : pmRaw,
|
|
212
|
+
gaonjsVersion: version,
|
|
213
|
+
};
|
|
199
214
|
}
|
|
200
215
|
/**
|
|
201
216
|
* `gaon db <sub>` 의 인자를 파싱한다(argv = 전체 · argv[0]='db', argv[1]=sub).
|
|
@@ -210,6 +225,10 @@ export function parseDbArgs(sub, argv) {
|
|
|
210
225
|
const dbIdx = argv.indexOf("--db");
|
|
211
226
|
const cfgIdx = argv.indexOf("--config");
|
|
212
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"]);
|
|
213
232
|
const positionals = [];
|
|
214
233
|
for (let i = 2; i < argv.length; i++) {
|
|
215
234
|
const a = argv[i];
|
|
@@ -219,8 +238,14 @@ export function parseDbArgs(sub, argv) {
|
|
|
219
238
|
i++; // 플래그 값 스킵
|
|
220
239
|
continue;
|
|
221
240
|
}
|
|
222
|
-
if (a.startsWith("--"))
|
|
223
|
-
|
|
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
|
+
}
|
|
224
249
|
positionals.push(a);
|
|
225
250
|
}
|
|
226
251
|
return {
|
|
@@ -393,7 +418,7 @@ export function runCli(argv, opts = {}) {
|
|
|
393
418
|
});
|
|
394
419
|
return;
|
|
395
420
|
}
|
|
396
|
-
// `gaon doctor` — 정적 검사(M9-E ·
|
|
421
|
+
// `gaon doctor` — 정적 검사(M9-E · 28 검사 · ALL_RULES 단일 출처). --check=<이름>[,<이름>...] 로
|
|
397
422
|
// 선택 실행, --json 은 자동화 파싱용.
|
|
398
423
|
// exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
|
|
399
424
|
if (argv[0] === "doctor") {
|
|
@@ -406,11 +431,15 @@ export function runCli(argv, opts = {}) {
|
|
|
406
431
|
process.exitCode = 2;
|
|
407
432
|
return;
|
|
408
433
|
}
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
434
|
+
// 결정 359: 종료 코드 판정.
|
|
435
|
+
// --fix --yes: runDoctorCommand 가 적용 후 **재검사** 결과를 돌려주므로
|
|
436
|
+
// errors = 잔여 위반 — 전부 자동 수정됐으면 0(자동화 정합).
|
|
437
|
+
// --fix (dry-run): errors > 0 이면 1 · errors 0 이어도 **계획이 있으면 1**
|
|
438
|
+
// (고칠 게 있다는 신호 — 이전엔 warning-only 계획이 0 으로 삼켜짐).
|
|
412
439
|
// 기본: errors > 0 이면 1.
|
|
413
|
-
|
|
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;
|
|
414
443
|
})
|
|
415
444
|
.catch((err) => {
|
|
416
445
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -483,7 +512,16 @@ export function runCli(argv, opts = {}) {
|
|
|
483
512
|
process.exitCode = 1;
|
|
484
513
|
return;
|
|
485
514
|
}
|
|
486
|
-
|
|
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
|
+
}
|
|
487
525
|
void runDbCommand(sub, dbOpts)
|
|
488
526
|
.then((code) => {
|
|
489
527
|
process.exitCode = code;
|
|
@@ -503,7 +541,13 @@ export function runCli(argv, opts = {}) {
|
|
|
503
541
|
const appIdx = argv.indexOf("--app");
|
|
504
542
|
const app = appIdx >= 0 ? argv[appIdx + 1] : undefined;
|
|
505
543
|
// 결정 155: --public 은 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱 탈출구).
|
|
506
|
-
|
|
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
|
+
});
|
|
507
551
|
process.exitCode = code;
|
|
508
552
|
return;
|
|
509
553
|
}
|
|
@@ -588,6 +632,7 @@ export function runCli(argv, opts = {}) {
|
|
|
588
632
|
skipInstall: argv.includes("--skip-install"),
|
|
589
633
|
skipGit: argv.includes("--skip-git"),
|
|
590
634
|
packageManager,
|
|
635
|
+
gaonjsVersion: parsed.gaonjsVersion,
|
|
591
636
|
})
|
|
592
637
|
.then((code) => {
|
|
593
638
|
process.exitCode = code;
|
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,7 @@ 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).' },
|
|
603
606
|
},
|
|
604
607
|
additionalProperties: false,
|
|
605
608
|
},
|
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
|
@@ -12,13 +12,16 @@ import { loadLocales, renderMessagesDts } from '@gaonjs/i18n';
|
|
|
12
12
|
* 있으면 생성하지 않는다(GaonMessages 를 비운 채로 둬 t() 키가 string 폴백 — i18n 을
|
|
13
13
|
* 안 쓰는 프로젝트가 never 로 깨지지 않게). 생성 여부를 돌려준다.
|
|
14
14
|
*/
|
|
15
|
-
export function generateMessagesDts(localesDir, out) {
|
|
15
|
+
export function generateMessagesDts(localesDir, out, baseLng) {
|
|
16
16
|
if (!existsSync(localesDir))
|
|
17
17
|
return false;
|
|
18
18
|
const resources = loadLocales(localesDir);
|
|
19
19
|
if (Object.keys(resources).length === 0)
|
|
20
20
|
return false;
|
|
21
21
|
mkdirSync(dirname(out), { recursive: true });
|
|
22
|
-
|
|
22
|
+
// 결정 352: 기준 로케일 = config i18n.fallbackLng(호출자가 정적 분석으로 전달).
|
|
23
|
+
// 이전엔 항상 알파벳순 첫 로케일이라 컴파일 보증이 fallback 체인과 다른 로케일에
|
|
24
|
+
// 정박했다(en/ko + fallbackLng:'ko' 프로젝트에서 기준이 en).
|
|
25
|
+
writeFileSync(out, renderMessagesDts(resources, baseLng), 'utf8');
|
|
23
26
|
return true;
|
|
24
27
|
}
|