@gaonjs/cli 0.5.0 → 0.10.1

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 (82) hide show
  1. package/dist/commands/check.d.ts +21 -2
  2. package/dist/commands/check.js +70 -7
  3. package/dist/commands/db.d.ts +3 -1
  4. package/dist/commands/db.js +8 -2
  5. package/dist/commands/g.d.ts +1 -1
  6. package/dist/commands/g.js +27 -3
  7. package/dist/commands/mcp.d.ts +15 -0
  8. package/dist/commands/mcp.js +78 -0
  9. package/dist/db/diff.js +5 -0
  10. package/dist/db/journal.d.ts +34 -0
  11. package/dist/db/journal.js +71 -0
  12. package/dist/db/migrate.d.ts +6 -1
  13. package/dist/db/migrate.js +120 -102
  14. package/dist/db/replay.d.ts +49 -0
  15. package/dist/db/replay.js +148 -0
  16. package/dist/db/status.d.ts +12 -0
  17. package/dist/db/status.js +61 -0
  18. package/dist/dev/index.d.ts +2 -0
  19. package/dist/dev/index.js +2 -0
  20. package/dist/dev/vite.d.ts +67 -0
  21. package/dist/dev/vite.js +126 -0
  22. package/dist/dev.d.ts +18 -0
  23. package/dist/dev.js +15 -0
  24. package/dist/doctor/agents-doc-index.d.ts +4 -0
  25. package/dist/doctor/agents-doc-index.js +80 -0
  26. package/dist/doctor/fixers/dependency-direction.d.ts +9 -0
  27. package/dist/doctor/fixers/dependency-direction.js +98 -0
  28. package/dist/doctor/fixers/index.d.ts +15 -0
  29. package/dist/doctor/fixers/index.js +66 -0
  30. package/dist/doctor/fixers/schema-filename.d.ts +14 -0
  31. package/dist/doctor/fixers/schema-filename.js +104 -0
  32. package/dist/doctor/fixers/types.d.ts +59 -0
  33. package/dist/doctor/fixers/types.js +15 -0
  34. package/dist/doctor/schema-filename.d.ts +6 -0
  35. package/dist/doctor/schema-filename.js +81 -0
  36. package/dist/doctor/types.d.ts +1 -1
  37. package/dist/doctor.d.ts +49 -0
  38. package/dist/doctor.js +179 -5
  39. package/dist/generate.js +2 -2
  40. package/dist/hub.d.ts +1 -1
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.js +50 -10
  43. package/dist/mcp/index.d.ts +7 -0
  44. package/dist/mcp/index.js +7 -0
  45. package/dist/mcp/server.d.ts +50 -0
  46. package/dist/mcp/server.js +102 -0
  47. package/dist/mcp/tools.d.ts +109 -0
  48. package/dist/mcp/tools.js +485 -0
  49. package/dist/scaffold/app.d.ts +5 -0
  50. package/dist/scaffold/app.js +172 -0
  51. package/dist/scaffold/controller.js +2 -2
  52. package/dist/scaffold/index.d.ts +2 -1
  53. package/dist/scaffold/index.js +2 -1
  54. package/dist/scaffold/job.d.ts +5 -0
  55. package/dist/scaffold/job.js +35 -0
  56. package/dist/scaffold/model.js +8 -8
  57. package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
  58. package/dist/templates/auth/registration.controller.ts.tpl +1 -1
  59. package/dist/templates/auth/session.controller.ts.tpl +1 -1
  60. package/dist/templates/auth/user.model.ts.tpl +1 -1
  61. package/dist/templates/project/AGENTS.md.tpl +214 -0
  62. package/dist/templates/project/agents/async.md.tpl +218 -0
  63. package/dist/templates/project/agents/data.md.tpl +556 -0
  64. package/dist/templates/project/agents/frontend.md.tpl +201 -0
  65. package/dist/templates/project/agents/realtime.md.tpl +157 -0
  66. package/dist/templates/project/agents/security.md.tpl +92 -0
  67. package/dist/templates/project/agents/testing.md.tpl +101 -0
  68. package/dist/templates/project/agents/web.md.tpl +177 -0
  69. package/dist/templates/project/apps/web/index.html.tpl +18 -0
  70. package/dist/templates/project/apps/web/main.ts.tpl +24 -0
  71. package/dist/templates/project/package.json.tpl +5 -2
  72. package/dist/templates/project/vite.config.ts.tpl +23 -0
  73. package/dist/tsResolve.js +1 -1
  74. package/dist/work.d.ts +2 -2
  75. package/dist/work.js +3 -1
  76. package/package.json +13 -11
  77. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +0 -12
  78. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +0 -7
  79. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +0 -2
  80. package/dist/__fixtures__/db-minimal/gaon.config.js +0 -11
  81. package/dist/check.d.ts +0 -29
  82. package/dist/check.js +0 -92
@@ -0,0 +1,81 @@
1
+ // @gaonjs/cli · doctor · 스키마 파일명 관례 (결정 38 · 2026-07-25)
2
+ //
3
+ // 정본 §1.1: 스키마 파일명 = 테이블명을 **camelCase** 로 바꾼 것. 테이블명에
4
+ // `_` 가 있어도 파일명엔 `_` 를 쓰지 않는다 — 조인 테이블 `posts_tags` →
5
+ // `domain/schema/postsTags.ts` (파일 안 `table('posts_tags', …)` 는 스네이크
6
+ // 유지). 소형 모델(haiku)이 다단어 테이블에서 파일명을 스네이크로 쓰는
7
+ // 실측(feature-slice T4)이 있어, 잘못된 파일명을 검출해 §7.5.3 수리 안내를
8
+ // 준다. `The One Way` — 두 형태 허용 없이 camelCase 하나로 못박는다.
9
+ //
10
+ // 판정 범위: **파일명이 camelCase 가 아닌 경우만**(스네이크·케밥·Pascal) 잡는다.
11
+ // 단수/복수(`post.ts` vs `posts.ts`)는 이 결정의 대상이 아니라 건드리지 않는다
12
+ // — 기존 fixture 가 단수 파일명(`post.ts`)을 쓰므로 그것까지 실격시키면 결정
13
+ // 범위를 넘는다. 그래서 기대 파일명은 **실제 파일명 stem 을 camelCase 로 정규화**
14
+ // 한 값이다(테이블명 단수화가 아님). 이미 camelCase 면 정규화해도 그대로라 통과.
15
+ // table() 선언이 있는 파일만 대상으로 삼아 배럴·비스키마 파일 오탐을 막는다.
16
+ import { readdir, readFile, stat } from 'node:fs/promises';
17
+ import { basename, join, relative } from 'node:path';
18
+ import { toCamel, toPascal } from '../scaffold/inflect.js';
19
+ /** 파일 stem 을 camelCase 로 정규화(`posts_tags`→`postsTags` · `Posts`→`posts` · `posts`→`posts`). */
20
+ export function expectedSchemaStem(stem) {
21
+ return toCamel(toPascal(stem));
22
+ }
23
+ /** 소스에서 첫 `table('<이름>', …)` 의 테이블명을 뽑는다(없으면 undefined). */
24
+ export function extractTableName(source) {
25
+ const m = source.match(/\btable\(\s*['"`]([^'"`]+)['"`]/);
26
+ return m ? m[1] : undefined;
27
+ }
28
+ export async function checkSchemaFilename(cwd) {
29
+ const schemaDir = join(cwd, 'domain', 'schema');
30
+ const files = [];
31
+ await collectTsFiles(schemaDir, files);
32
+ const issues = [];
33
+ for (const abs of files) {
34
+ const source = await readFile(abs, 'utf8');
35
+ const tableName = extractTableName(source);
36
+ if (!tableName)
37
+ continue; // table() 없음 = 스키마 테이블 파일 아님 · 건너뜀
38
+ const actualStem = basename(abs).replace(/\.ts$/, '');
39
+ const expected = expectedSchemaStem(actualStem);
40
+ if (actualStem === expected)
41
+ continue; // 이미 camelCase(단수/복수 무관) · 통과
42
+ const rel = relative(cwd, abs).split('\\').join('/');
43
+ const expectedRel = rel.replace(/[^/]+\.ts$/, `${expected}.ts`);
44
+ issues.push({
45
+ rule: 'schema-filename',
46
+ level: 'error',
47
+ file: rel,
48
+ message: `스키마 파일명이 관례(§1.1 · camelCase)와 어긋납니다: '${actualStem}.ts' ` +
49
+ `(테이블 '${tableName}' · 기대 파일명 '${expected}.ts').\n` +
50
+ `→ '${rel}' 를 '${expectedRel}' 로 이름을 바꾸고, 이 스키마를 import 하는 ` +
51
+ `모델(예: '../schema/${expected}.js')의 경로도 함께 고치세요. ` +
52
+ `테이블명 문자열('${tableName}')은 그대로 둡니다.`,
53
+ detail: { tableName, actual: `${actualStem}.ts`, expected: `${expected}.ts` },
54
+ });
55
+ }
56
+ return { rule: 'schema-filename', issues };
57
+ }
58
+ /** domain/schema/ 아래 .ts(선언·테스트 제외)를 재귀 수집. migration-diff 와 동일 패턴. */
59
+ async function collectTsFiles(root, out) {
60
+ let entries;
61
+ try {
62
+ entries = (await readdir(root, { withFileTypes: true }));
63
+ }
64
+ catch {
65
+ return; // domain/schema/ 없음 = 검사할 것 없음
66
+ }
67
+ for (const e of entries) {
68
+ const p = join(root, e.name);
69
+ if (e.isDirectory()) {
70
+ await collectTsFiles(p, out);
71
+ }
72
+ else if (e.isFile() &&
73
+ e.name.endsWith('.ts') &&
74
+ !e.name.endsWith('.d.ts') &&
75
+ !e.name.endsWith('.test.ts')) {
76
+ const s = await stat(p);
77
+ if (s.isFile())
78
+ out.push(p);
79
+ }
80
+ }
81
+ }
@@ -1,4 +1,4 @@
1
- export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-composable-purity' | 'no-auto-import';
1
+ export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-composable-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index';
2
2
  export type DoctorLevel = 'passed' | 'warning' | 'error';
3
3
  export interface DoctorCheck {
4
4
  readonly rule: DoctorRule;
package/dist/doctor.d.ts CHANGED
@@ -8,12 +8,45 @@ export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/
8
8
  export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
9
9
  export { inspectSharedComposable, checkSharedComposablePurity, } from './doctor/shared-composable-purity.js';
10
10
  export { inspectConfigForAutoImport, inspectPackageJson, checkNoAutoImport, } from './doctor/no-auto-import.js';
11
+ export { expectedSchemaStem, extractTableName, checkSchemaFilename, } from './doctor/schema-filename.js';
12
+ export { extractAgentDocRefs, checkAgentsDocIndex } from './doctor/agents-doc-index.js';
11
13
  export { renderHuman, renderJson } from './doctor/reporter.js';
12
14
  export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
13
15
  export interface DoctorCommandOptions {
14
16
  readonly cwd?: string;
15
17
  readonly json?: boolean;
16
18
  readonly checks?: readonly DoctorRule[];
19
+ /**
20
+ * 자동 정정 모드(v0.16 §7.5.3). true 면 각 규칙의 fixer 를 계획한다.
21
+ * `yes` 가 없으면 dry-run(계획만 보고 · 파일 편집 X), 있으면 실제 편집.
22
+ * 편집 전 원본은 `<파일>.bak-<타임스탬프>` 로 백업된다(--yes 여도 안전망).
23
+ */
24
+ readonly fix?: boolean;
25
+ /** 실제 파일 편집 승인. `fix` 와 함께 줘야 편집이 일어난다. */
26
+ readonly yes?: boolean;
27
+ }
28
+ /** --fix 리포트의 파일별 결과. FixerPlan 위에 dry/applied 상태를 얹는다. */
29
+ export interface FixOutcome {
30
+ readonly rule: DoctorRule;
31
+ readonly file: string;
32
+ readonly summary: string;
33
+ /** true 이면 실제 편집됨, false 이면 dry-run(계획만 · --yes 없음). */
34
+ readonly applied: boolean;
35
+ /** 실제 편집 시 원본 백업 경로(프로젝트 상대). dry-run 은 undefined. */
36
+ readonly backup?: string;
37
+ }
38
+ /** --fix 실행 결과. runCli 는 fixesApplied · fixesPlanned 로 종료 코드 결정. */
39
+ export interface DoctorFixReport {
40
+ /** fixer 가 있는 규칙별 계획·적용 결과. */
41
+ readonly outcomes: readonly FixOutcome[];
42
+ /** fixer 가 없어서 수동 수정이 필요한 규칙별 위반 수(요약). */
43
+ readonly manualRequired: readonly {
44
+ rule: DoctorRule;
45
+ issueCount: number;
46
+ note: string;
47
+ }[];
48
+ /** 실제로 편집이 일어났는지(--yes 여부). */
49
+ readonly applied: boolean;
17
50
  }
18
51
  /**
19
52
  * `gaon doctor` 진입점 (M9-E · 확장 · M9-E-Fix 하드닝).
@@ -35,6 +68,22 @@ export interface DoctorCommandOptions {
35
68
  * 나머지 규칙은 계속 실행(부분 결과 확보).
36
69
  */
37
70
  export declare function runDoctorCommand(opts?: DoctorCommandOptions): Promise<DoctorResult>;
71
+ /**
72
+ * fixer 를 계획·(승인 시)실행한다. 파일 편집은 여기서만 일어난다 —
73
+ * fixer 자체는 순수 계산 함수.
74
+ *
75
+ * 안전:
76
+ * 1) apply=false(=--yes 없음) 면 dry-run — 파일을 만지지 않는다.
77
+ * 2) apply=true 면 원본을 `<파일>.bak-<타임스탬프>` 로 백업 후 편집.
78
+ * 3) 편집 중 예외가 나면 그 파일만 실패로 리포트하고 다음 파일을 계속.
79
+ */
80
+ export declare function runDoctorFix(reports: readonly RuleReport[], cwd: string, apply: boolean): Promise<DoctorFixReport>;
81
+ /** 리포트에 함께 실려 나가는 확장 결과. runCli 가 종료 코드를 결정하는 근거. */
82
+ export interface DoctorResultWithFix extends DoctorResult {
83
+ readonly fix: DoctorFixReport;
84
+ }
85
+ /** 사람 친화 fix 리포트. json 은 상위 renderJson 이 그대로 직렬화. */
86
+ export declare function renderFixHuman(report: DoctorFixReport): string;
38
87
  export interface DoctorIssue {
39
88
  readonly file: string;
40
89
  readonly line: number;
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
- * 7 검사를 조립한다:
4
+ * 8 검사를 조립한다:
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 규칙)
@@ -9,6 +9,7 @@
9
9
  * 5) migration-diff (최소 감지 · 상세는 M9-D)
10
10
  * 6) shared-composable-purity (errata E-5 §2.2 · 결정 25)
11
11
  * 7) no-auto-import (errata E-5 §2.4 · v0.15 §1.2)
12
+ * 8) schema-filename (§1.1 · 결정 38 · 스키마 파일명 camelCase)
12
13
  *
13
14
  * 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
14
15
  * DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
@@ -17,7 +18,9 @@
17
18
  * 하위 호환: 기존 응답 혼용 API(`inspectControllerSource`·`runDoctor`)는
18
19
  * 그대로 export — 기존 테스트가 계속 동작한다.
19
20
  */
20
- import { resolve } from 'node:path';
21
+ import { rename, writeFile } from 'node:fs/promises';
22
+ import { existsSync } from 'node:fs';
23
+ import { join, resolve } from 'node:path';
21
24
  import ts from 'typescript';
22
25
  import { checkResponseMixing } from './doctor/response-mixing.js';
23
26
  import { checkNPlusOne } from './doctor/n-plus-one.js';
@@ -26,9 +29,12 @@ import { checkConnections } from './doctor/connections.js';
26
29
  import { checkMigrationDiff } from './doctor/migration-diff.js';
27
30
  import { checkSharedComposablePurity } from './doctor/shared-composable-purity.js';
28
31
  import { checkNoAutoImport } from './doctor/no-auto-import.js';
32
+ import { checkSchemaFilename } from './doctor/schema-filename.js';
33
+ import { checkAgentsDocIndex } from './doctor/agents-doc-index.js';
29
34
  import { renderHuman, renderJson } from './doctor/reporter.js';
30
35
  import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
31
36
  import { makeResult, } from './doctor/types.js';
37
+ import { FIXERS, FIXER_CAPABILITIES } from './doctor/fixers/index.js';
32
38
  export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
33
39
  export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
34
40
  export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
@@ -36,6 +42,8 @@ export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/
36
42
  export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
37
43
  export { inspectSharedComposable, checkSharedComposablePurity, } from './doctor/shared-composable-purity.js';
38
44
  export { inspectConfigForAutoImport, inspectPackageJson, checkNoAutoImport, } from './doctor/no-auto-import.js';
45
+ export { expectedSchemaStem, extractTableName, checkSchemaFilename, } from './doctor/schema-filename.js';
46
+ export { extractAgentDocRefs, checkAgentsDocIndex } from './doctor/agents-doc-index.js';
39
47
  export { renderHuman, renderJson } from './doctor/reporter.js';
40
48
  export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
41
49
  /**
@@ -49,6 +57,8 @@ const ALL_RULES = [
49
57
  'migration-diff',
50
58
  'shared-composable-purity',
51
59
  'no-auto-import',
60
+ 'schema-filename',
61
+ 'agents-doc-index',
52
62
  ];
53
63
  const CHECKERS = {
54
64
  'response-mixing': checkResponseMixing,
@@ -58,6 +68,8 @@ const CHECKERS = {
58
68
  'migration-diff': checkMigrationDiff,
59
69
  'shared-composable-purity': checkSharedComposablePurity,
60
70
  'no-auto-import': checkNoAutoImport,
71
+ 'schema-filename': checkSchemaFilename,
72
+ 'agents-doc-index': checkAgentsDocIndex,
61
73
  };
62
74
  /**
63
75
  * `gaon doctor` 진입점 (M9-E · 확장 · M9-E-Fix 하드닝).
@@ -130,10 +142,172 @@ export async function runDoctorCommand(opts = {}) {
130
142
  });
131
143
  }
132
144
  }
133
- const result = makeResult(reports);
134
- const out = json ? renderJson(result) : renderHuman(result);
145
+ const baseResult = makeResult(reports);
146
+ // --fix 없으면 종전 동작 그대로.
147
+ if (!opts.fix) {
148
+ const out = json ? renderJson(baseResult) : renderHuman(baseResult);
149
+ process.stdout.write(out + '\n');
150
+ return baseResult;
151
+ }
152
+ // --fix: fixer 를 계획하고, --yes 이면 실제 편집한다. reports 를 그대로
153
+ // 넘겨 규칙별 위반을 fixer 가 판단한다(passed 는 건너뜀).
154
+ const fixReport = await runDoctorFix(reports, root, opts.yes === true);
155
+ const resultWithFix = { ...baseResult, fix: fixReport };
156
+ const out = json
157
+ ? JSON.stringify(resultWithFix)
158
+ : renderHuman(baseResult) + '\n\n' + renderFixHuman(fixReport);
135
159
  process.stdout.write(out + '\n');
136
- return result;
160
+ return resultWithFix;
161
+ }
162
+ /**
163
+ * fixer 를 계획·(승인 시)실행한다. 파일 편집은 여기서만 일어난다 —
164
+ * fixer 자체는 순수 계산 함수.
165
+ *
166
+ * 안전:
167
+ * 1) apply=false(=--yes 없음) 면 dry-run — 파일을 만지지 않는다.
168
+ * 2) apply=true 면 원본을 `<파일>.bak-<타임스탬프>` 로 백업 후 편집.
169
+ * 3) 편집 중 예외가 나면 그 파일만 실패로 리포트하고 다음 파일을 계속.
170
+ */
171
+ export async function runDoctorFix(reports, cwd, apply) {
172
+ const outcomes = [];
173
+ const manualRequired = [];
174
+ const ts = timestampSuffix(new Date());
175
+ for (const report of reports) {
176
+ const issues = report.issues.filter((i) => i.level !== 'passed');
177
+ if (issues.length === 0)
178
+ continue;
179
+ const fixer = FIXERS[report.rule];
180
+ if (!fixer) {
181
+ const cap = FIXER_CAPABILITIES.find((c) => c.rule === report.rule);
182
+ manualRequired.push({
183
+ rule: report.rule,
184
+ issueCount: issues.length,
185
+ note: cap?.note ?? '수동 수정 필요',
186
+ });
187
+ continue;
188
+ }
189
+ let plans;
190
+ try {
191
+ plans = await fixer(issues, cwd);
192
+ }
193
+ catch (err) {
194
+ const msg = err instanceof Error ? err.message : String(err);
195
+ outcomes.push({
196
+ rule: report.rule,
197
+ file: '(fixer error)',
198
+ summary: `fixer 실행 실패: ${msg}`,
199
+ applied: false,
200
+ });
201
+ continue;
202
+ }
203
+ for (const plan of plans) {
204
+ const label = plan.kind === 'rename' ? `${plan.file} → ${plan.to}` : plan.file;
205
+ if (!apply) {
206
+ outcomes.push({
207
+ rule: report.rule,
208
+ file: label,
209
+ summary: plan.summary,
210
+ applied: false,
211
+ });
212
+ continue;
213
+ }
214
+ try {
215
+ if (plan.kind === 'rename') {
216
+ // 이동 대상이 이미 있으면 데이터를 덮지 않고 건너뛴다(수동 확인).
217
+ if (existsSync(join(cwd, plan.to))) {
218
+ outcomes.push({
219
+ rule: report.rule,
220
+ file: label,
221
+ summary: `이동 대상이 이미 존재해 건너뜀(수동 확인 필요): ${plan.to}`,
222
+ applied: false,
223
+ });
224
+ continue;
225
+ }
226
+ // importer 참조를 먼저 갱신(각각 백업), 그다음 파일 이동(내용 불변).
227
+ const backups = [];
228
+ for (const ref of plan.refEdits) {
229
+ const bak = `${ref.file}.bak-${ts}`;
230
+ await writeFile(join(cwd, bak), ref.before, 'utf8');
231
+ await writeFile(join(cwd, ref.file), ref.after, 'utf8');
232
+ backups.push(bak);
233
+ }
234
+ await rename(join(cwd, plan.file), join(cwd, plan.to));
235
+ outcomes.push({
236
+ rule: report.rule,
237
+ file: label,
238
+ summary: plan.summary + (backups.length ? `\n ↳ importer 백업: ${backups.join(', ')}` : ''),
239
+ applied: true,
240
+ backup: backups[0],
241
+ });
242
+ continue;
243
+ }
244
+ // 내용 재작성 — 원본 백업 먼저.
245
+ const abs = join(cwd, plan.file);
246
+ const backupRel = `${plan.file}.bak-${ts}`;
247
+ await writeFile(join(cwd, backupRel), plan.before, 'utf8');
248
+ await writeFile(abs, plan.after, 'utf8');
249
+ outcomes.push({
250
+ rule: report.rule,
251
+ file: plan.file,
252
+ summary: plan.summary,
253
+ applied: true,
254
+ backup: backupRel,
255
+ });
256
+ }
257
+ catch (err) {
258
+ const msg = err instanceof Error ? err.message : String(err);
259
+ outcomes.push({
260
+ rule: report.rule,
261
+ file: label,
262
+ summary: `편집 실패(원본 유지): ${msg}`,
263
+ applied: false,
264
+ });
265
+ }
266
+ }
267
+ }
268
+ return { outcomes, manualRequired, applied: apply };
269
+ }
270
+ /** 사람 친화 fix 리포트. json 은 상위 renderJson 이 그대로 직렬화. */
271
+ export function renderFixHuman(report) {
272
+ const lines = [];
273
+ const mode = report.applied ? '적용' : 'dry-run(--yes 없음 · 계획만)';
274
+ lines.push(` gaon doctor --fix · ${mode}`);
275
+ if (report.outcomes.length === 0 && report.manualRequired.length === 0) {
276
+ lines.push(' (자동 정정할 대상이 없습니다.)');
277
+ return lines.join('\n');
278
+ }
279
+ for (const o of report.outcomes) {
280
+ const mark = o.applied ? '✓' : '·';
281
+ lines.push('');
282
+ lines.push(` ${mark} [${o.rule}] ${o.file}`);
283
+ for (const ln of o.summary.split('\n'))
284
+ lines.push(` ${ln}`);
285
+ if (o.backup)
286
+ lines.push(` ↳ 원본 백업: ${o.backup}`);
287
+ }
288
+ if (report.manualRequired.length > 0) {
289
+ lines.push('');
290
+ lines.push(' 수동 수정 필요:');
291
+ for (const m of report.manualRequired) {
292
+ lines.push(` · [${m.rule}] 위반 ${m.issueCount}건 · ${m.note}`);
293
+ }
294
+ }
295
+ if (!report.applied && report.outcomes.some((o) => !o.applied)) {
296
+ lines.push('');
297
+ lines.push(' → 실제 편집은 --fix --yes 로 진행하세요(원본은 자동 백업).');
298
+ }
299
+ return lines.join('\n');
300
+ }
301
+ /** 파일명 안전 문자만으로 만든 로컬 타임스탬프(YYYYMMDD-HHMMSS). */
302
+ function timestampSuffix(d) {
303
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
304
+ const y = d.getFullYear();
305
+ const M = pad(d.getMonth() + 1);
306
+ const D = pad(d.getDate());
307
+ const h = pad(d.getHours());
308
+ const m = pad(d.getMinutes());
309
+ const s = pad(d.getSeconds());
310
+ return `${y}${M}${D}-${h}${m}${s}`;
137
311
  }
138
312
  /**
139
313
  * 하위 호환: 응답 혼용만 검사해 legacy shape 을 낸다.
package/dist/generate.js CHANGED
@@ -26,8 +26,8 @@ function renderTemplate(name, app) {
26
26
  }
27
27
  /** 템플릿 파일 → 생성 경로 매핑(라우트 제외 — 라우트는 패치로 처리). */
28
28
  const TEMPLATES = [
29
- { tpl: 'user.schema.ts.tpl', out: () => 'domain/schema/user.ts' },
30
- { tpl: 'user.model.ts.tpl', out: () => 'domain/models/user.ts' },
29
+ { tpl: 'user.schema.ts.tpl', out: () => 'domain/schema/users.ts' },
30
+ { tpl: 'user.model.ts.tpl', out: () => 'domain/models/User.ts' },
31
31
  { tpl: 'auth.wiring.ts.tpl', out: (a) => `apps/${a}/auth.ts` },
32
32
  { tpl: 'session.controller.ts.tpl', out: (a) => `apps/${a}/controllers/session.ts` },
33
33
  { tpl: 'registration.controller.ts.tpl', out: (a) => `apps/${a}/controllers/registration.ts` },
package/dist/hub.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export interface HubCommandOptions {
2
2
  readonly json?: boolean;
3
- /** NATS 접속지. 생략 시 GAON_NATS_URL, 그다음 기본(4222). */
3
+ /** NATS 접속지. 생략 시 NATS_URL(그다음 하위호환 GAON_NATS_URL), 기본(4222). */
4
4
  readonly natsUrl?: string;
5
5
  /** 리스 TTL(ms). 생략 시 env GAON_HUB_TTL_MS, 그다음 허브 기본(5000). */
6
6
  readonly ttlMs?: number;
package/dist/index.d.ts CHANGED
@@ -15,7 +15,8 @@ export { runJobsCommand, type JobsCommandOptions } from "./jobs.js";
15
15
  export { runDbSeedCommand, loadSeed, type DbSeedOptions, type DbSeedResult } from "./db.js";
16
16
  export { runDbCommand, type DbSubcommand, type DbCommandOptions, } from "./commands/db.js";
17
17
  export { runDbDiff, runDbMigrate, runDbReset, resolveDbTarget, type DbDiffOptions, type DbDiffResult, type DbMigrateOptions, type DbMigrateResult, type DbResetOptions, type DbResetResult, type ResolveDbOptions, type ResolvedDbTarget, } from "./db/index.js";
18
- export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, type DoctorResult, type DoctorCheck, type DoctorLevel, type DoctorRule, type RuleReport, type DoctorIssue, type LegacyDoctorResult, type ActionUsage, type ResponseKind, type DoctorCommandOptions, } from "./doctor.js";
18
+ export { runDoctorCommand, runDoctor, runDoctorFix, renderFixHuman, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, type DoctorResult, type DoctorResultWithFix, type DoctorCheck, type DoctorLevel, type DoctorRule, type RuleReport, type DoctorIssue, type LegacyDoctorResult, type ActionUsage, type ResponseKind, type DoctorCommandOptions, type FixOutcome, type DoctorFixReport, } from "./doctor.js";
19
+ export { FIXERS, FIXER_CAPABILITIES, fixDependencyDirection, fixDomainToSharedTypeOnly, type Fixer, type FixerCapability, type FixerPlan, } from "./doctor/fixers/index.js";
19
20
  export { loadDomain, type LoadedDomain } from "./domain.js";
20
21
  export interface RoadmapReport {
21
22
  readonly name: "gaon";
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { runWorkCommand } from "./work.js";
24
24
  import { runJobsCommand } from "./jobs.js";
25
25
  import { runDbCommand } from "./commands/db.js";
26
26
  import { runDoctorCommand } from "./doctor.js";
27
+ import { runMcpCommand } from "./commands/mcp.js";
27
28
  export { startDev, resolveDevLayout, } from "./dev.js";
28
29
  export { runDevCommand } from "./commands/dev.js";
29
30
  export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, } from "./dev/index.js";
@@ -40,7 +41,8 @@ export { runJobsCommand } from "./jobs.js";
40
41
  export { runDbSeedCommand, loadSeed } from "./db.js";
41
42
  export { runDbCommand, } from "./commands/db.js";
42
43
  export { runDbDiff, runDbMigrate, runDbReset, resolveDbTarget, } from "./db/index.js";
43
- export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, } from "./doctor.js";
44
+ export { runDoctorCommand, runDoctor, runDoctorFix, renderFixHuman, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, } from "./doctor.js";
45
+ export { FIXERS, FIXER_CAPABILITIES, fixDependencyDirection, fixDomainToSharedTypeOnly, } from "./doctor/fixers/index.js";
44
46
  export { loadDomain } from "./domain.js";
45
47
  /** `--json` 출력용 구조화 리포트. */
46
48
  export function roadmapReport(version = VERSION) {
@@ -99,19 +101,25 @@ function renderHelp(version = VERSION) {
99
101
  " gaon doctor 정적 검사 (7 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import)",
100
102
  " gaon doctor --json 자동화용 JSON 출력",
101
103
  " gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
104
+ " gaon doctor --fix 기계 정정 가능한 위반 계획(dry-run · v0.16 §7.5.3)",
105
+ " gaon doctor --fix --yes 실제 편집 적용(원본은 .bak-<타임스탬프> 로 자동 백업)",
102
106
  " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
103
107
  " gaon g controller <name> 컨트롤러 스캐폴드 (Rails 관례 · 페이지+JSON 액션)",
104
108
  " gaon g model <Name> 모델 스캐폴드 (스키마+모델 · E-4 컬럼 예시)",
105
109
  " gaon g page <Path/Name> Vue 페이지 (Inertia SPA · pageProps 브리지)",
106
110
  " gaon g job <Name> 비동기 잡 (domain/jobs · later/in/at)",
111
+ " gaon g app <name> 앱 스캐폴드 (apps/<name>/ · routes·controllers·pages·layouts)",
107
112
  " gaon g <type> --overwrite 기존 파일 덮어쓰기 · --app <이름> · --json",
113
+ " gaon mcp 내장 MCP 서버 · AI 도구 4종 (list_routes·get_schema·run_migration·run_tests · --http)",
108
114
  " gaon hub 실시간 허브 프로세스 (프레즌스 권위·중계 · 리더 선출 HA)",
109
115
  " gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
110
116
  " gaon jobs list --failed DLQ(실패 잡) 목록",
111
117
  " gaon jobs retry <id> DLQ 잡 재적재",
112
- " gaon db diff 스키마 ↔ DB 차이 계산 (적용 X · --json · --db <키>)",
113
- " gaon db migrate 스키마 변경을 실제 적용 + _gaon_migrations 이력",
114
- " gaon db migrate --dry-run 적용 없이 up SQL 만 출력",
118
+ " gaon db diff 스키마 ↔ DB 차이 미리보기 (적용 X · --json · --db <키>)",
119
+ " gaon db migrate db/migrations/*.ts replay + 스키마 diff 적용 + 이력",
120
+ " gaon db migrate down 가장 최근 이력 롤백",
121
+ " gaon db migrate --dry-run 적용 없이 실행 예정 파일 + up SQL 만 출력",
122
+ " gaon db status 마이그레이션 파일 적용/대기 + 스키마 drift",
115
123
  " gaon db reset --yes DROP ALL → 재마이그 → seed (--yes 필수 · production 거부)",
116
124
  " gaon db seed domain/seed.ts 실행 (M8)",
117
125
  " gaon --json 같은 정보를 JSON 으로 출력",
@@ -220,12 +228,18 @@ export function runCli(argv, opts = {}) {
220
228
  // exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
221
229
  if (argv[0] === "doctor") {
222
230
  const checks = parseDoctorChecks(argv);
223
- void runDoctorCommand({ json: argv.includes("--json"), checks })
231
+ const fix = argv.includes("--fix");
232
+ const yes = argv.includes("--yes");
233
+ void runDoctorCommand({ json: argv.includes("--json"), checks, fix, yes })
224
234
  .then((result) => {
225
235
  if (result.fatal) {
226
236
  process.exitCode = 2;
227
237
  return;
228
238
  }
239
+ // --fix --yes: 편집이 실제로 일어났고 나머지 위반이 없으면 0,
240
+ // 아직 수동 수정이 남아 있으면 1(자동화 정합).
241
+ // --fix (dry-run): errors > 0 이면 1(안 고침) · 계획만 있어도 1.
242
+ // 기본: errors > 0 이면 1.
229
243
  process.exitCode = result.errors.length > 0 ? 1 : 0;
230
244
  })
231
245
  .catch((err) => {
@@ -235,6 +249,25 @@ export function runCli(argv, opts = {}) {
235
249
  });
236
250
  return;
237
251
  }
252
+ // `gaon mcp` — 내장 MCP 서버(M10-B · 정본 §12 질문 8). AI 에이전트에게 4종
253
+ // 도구(list_routes·get_schema·run_migration·run_tests)를 노출한다. stdio(기본)
254
+ // 또는 --http 전송. 표시 버전은 파사드 버전을 주입한다.
255
+ if (argv[0] === "mcp") {
256
+ void runMcpCommand({
257
+ json: argv.includes("--json"),
258
+ transport: argv.includes("--http") ? "http" : "stdio",
259
+ version,
260
+ })
261
+ .then((code) => {
262
+ process.exitCode = code;
263
+ })
264
+ .catch((err) => {
265
+ const msg = err instanceof Error ? err.message : String(err);
266
+ process.stderr.write(` ✗ gaon mcp 실패: ${msg}\n`);
267
+ process.exitCode = 1;
268
+ });
269
+ return;
270
+ }
238
271
  // `gaon hub` — 실시간 허브 프로세스(§7 M6). 운영 프로세스 3종 중 하나.
239
272
  // NATS·리더 선출 대기로 프로세스를 살려 두고, SIGINT/SIGTERM 에 그레이스풀 종료.
240
273
  if (argv[0] === "hub") {
@@ -271,10 +304,10 @@ export function runCli(argv, opts = {}) {
271
304
  // `gaon db <sub>` — diff · migrate · reset · seed (§7 M8/M9-D).
272
305
  if (argv[0] === "db") {
273
306
  const sub = argv[1];
274
- const known = ["diff", "migrate", "reset", "seed"];
307
+ const known = ["diff", "migrate", "reset", "seed", "status"];
275
308
  if (!sub || !known.includes(sub)) {
276
309
  process.stderr.write(` ✗ 알 수 없는 db 서브커맨드: ${sub ?? "(없음)"}\n` +
277
- ` → 지원: gaon db diff | migrate | reset | seed\n` +
310
+ ` → 지원: gaon db diff | migrate | reset | seed | status\n` +
278
311
  ` → 옵션: --json · --db <키> · --config <path> · --yes · --dry-run\n`);
279
312
  process.exitCode = 1;
280
313
  return;
@@ -287,6 +320,8 @@ export function runCli(argv, opts = {}) {
287
320
  config: cfgIdx >= 0 ? argv[cfgIdx + 1] : undefined,
288
321
  yes: argv.includes("--yes"),
289
322
  dryRun: argv.includes("--dry-run"),
323
+ // `gaon db migrate down` — 위치 인자로 롤백 지시.
324
+ down: sub === "migrate" && argv[2] === "down",
290
325
  };
291
326
  void runDbCommand(sub, dbOpts)
292
327
  .then((code) => {
@@ -310,7 +345,7 @@ export function runCli(argv, opts = {}) {
310
345
  process.exitCode = code;
311
346
  return;
312
347
  }
313
- const known = ["controller", "model", "page", "job"];
348
+ const known = ["controller", "model", "page", "job", "app"];
314
349
  const type = argv[1];
315
350
  if (type && known.includes(type)) {
316
351
  const rest = argv.slice(2);
@@ -339,8 +374,13 @@ export function runCli(argv, opts = {}) {
339
374
  name = a;
340
375
  }
341
376
  if (!name) {
377
+ const example = type === "page"
378
+ ? "Posts/Index"
379
+ : type === "app"
380
+ ? "admin"
381
+ : "Post";
342
382
  process.stderr.write(` ✗ gaon g ${type}: 이름이 없습니다.\n` +
343
- ` → 예: gaon g ${type} ${type === "page" ? "Posts/Index" : "Post"}\n`);
383
+ ` → 예: gaon g ${type} ${example}\n`);
344
384
  process.exitCode = 1;
345
385
  return;
346
386
  }
@@ -349,7 +389,7 @@ export function runCli(argv, opts = {}) {
349
389
  return;
350
390
  }
351
391
  process.stderr.write(` ✗ 알 수 없는 제너레이터: ${argv[1] ?? "(없음)"}\n` +
352
- ` → 현재 지원: gaon g auth | controller | model | page | job\n` +
392
+ ` → 현재 지원: gaon g auth | controller | model | page | job | app\n` +
353
393
  ` → 옵션: --app <이름> · --overwrite · --json\n`);
354
394
  process.exitCode = 1;
355
395
  return;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @gaonjs/cli · MCP 모듈 public exports — v0.15 §13.5 M10.
3
+ *
4
+ * `gaon mcp` 명령이 소비하는 조립 API. tools.ts 의 TOOLS 카탈로그는 SSOT.
5
+ */
6
+ export { createMcpServer, startMcpServer, SERVER_NAME, type McpServerOptions, type McpServerHandle, type McpServerVersion, } from './server.js';
7
+ export { TOOLS, findTool, listRoutesTool, getSchemaTool, runMigrationTool, runTestsTool, projectSummary, type McpToolSpec, type ToolResult, type ToolArgs, } from './tools.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @gaonjs/cli · MCP 모듈 public exports — v0.15 §13.5 M10.
3
+ *
4
+ * `gaon mcp` 명령이 소비하는 조립 API. tools.ts 의 TOOLS 카탈로그는 SSOT.
5
+ */
6
+ export { createMcpServer, startMcpServer, SERVER_NAME, } from './server.js';
7
+ export { TOOLS, findTool, listRoutesTool, getSchemaTool, runMigrationTool, runTestsTool, projectSummary, } from './tools.js';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @gaonjs/cli · MCP 서버 배선 — v0.15 §13.5 M10 · 질문 8.
3
+ *
4
+ * `gaon mcp` 가 이 함수를 stdio 트랜스포트로 부트업한다. Node.js MCP SDK
5
+ * (@modelcontextprotocol/sdk) 의 Server + StdioServerTransport 를 사용해
6
+ * JSON-RPC 2.0 over stdio 로 4 도구를 노출한다(§9 실 프로토콜).
7
+ *
8
+ * 도구 목록/호출은 tools.ts 의 TOOLS 카탈로그를 그대로 소비 — 카탈로그가
9
+ * 단일 진실 원천(SSOT). 여기서 도구를 추가하지 않는다.
10
+ *
11
+ * transport:
12
+ * - stdio (기본 · MCP 표준)
13
+ * - http 는 v1 범위 외(§13.5 M10 명시 X · 필요 시 후속 마일스톤에서 추가)
14
+ *
15
+ * 종료: transport 가 닫히면(SIGINT / stdin 종료) close() 로 정리한다.
16
+ */
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ /** 서버 이름·버전 — MCP initialize 응답의 serverInfo. */
19
+ export declare const SERVER_NAME = "gaon";
20
+ /**
21
+ * 서버 버전 — CLI 가 주입한다(호출자가 넘긴 값 · 미주입 시 'dev').
22
+ * 실제 값은 packages/cli/package.json 의 version(파사드 gaonjs 로 재수출됨).
23
+ */
24
+ export type McpServerVersion = string;
25
+ export interface McpServerOptions {
26
+ /** 프로젝트 루트 — 각 도구가 이 cwd 기준으로 관례를 해석. */
27
+ readonly cwd: string;
28
+ /** 서버 버전 — 미주입 시 'dev'. */
29
+ readonly version?: McpServerVersion;
30
+ }
31
+ /** 서버 핸들 — 종료 대기·강제 종료용. */
32
+ export interface McpServerHandle {
33
+ /** transport 가 종료될 때까지 대기. stdin close / SIGINT 시 resolve. */
34
+ wait(): Promise<void>;
35
+ /** 강제 종료 — server + transport 정리. */
36
+ close(): Promise<void>;
37
+ }
38
+ /**
39
+ * MCP 서버 인스턴스 만들기 + tool handlers 등록.
40
+ * transport 연결은 startMcpServer 에서 한다(테스트 격리를 위해 분리).
41
+ */
42
+ export declare function createMcpServer(opts: McpServerOptions): Server;
43
+ /**
44
+ * `gaon mcp` 본체 — stdio transport 로 서버를 부트업하고 stdin close 를 기다린다.
45
+ * 반환 핸들의 wait() 는 transport 가 종료될 때 resolve.
46
+ *
47
+ * 테스트에서는 stdio 를 파이프로 감싼 자식 프로세스로 실행하고, 이 함수는
48
+ * 자식 프로세스 내부에서 호출된다(§9 실 프로토콜 왕복).
49
+ */
50
+ export declare function startMcpServer(opts: McpServerOptions): Promise<McpServerHandle>;