@gaonjs/cli 0.4.0 → 0.10.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 (120) hide show
  1. package/dist/commands/check.d.ts +50 -0
  2. package/dist/commands/check.js +286 -0
  3. package/dist/commands/console.d.ts +46 -0
  4. package/dist/commands/console.js +129 -0
  5. package/dist/commands/db.d.ts +3 -1
  6. package/dist/commands/db.js +8 -2
  7. package/dist/commands/g.d.ts +1 -1
  8. package/dist/commands/g.js +27 -3
  9. package/dist/commands/mcp.d.ts +15 -0
  10. package/dist/commands/mcp.js +78 -0
  11. package/dist/commands/new.d.ts +45 -0
  12. package/dist/commands/new.js +274 -0
  13. package/dist/commands/test.d.ts +11 -0
  14. package/dist/commands/test.js +119 -0
  15. package/dist/db/diff.js +5 -0
  16. package/dist/db/journal.d.ts +34 -0
  17. package/dist/db/journal.js +71 -0
  18. package/dist/db/migrate.d.ts +6 -1
  19. package/dist/db/migrate.js +120 -102
  20. package/dist/db/replay.d.ts +49 -0
  21. package/dist/db/replay.js +148 -0
  22. package/dist/db/status.d.ts +12 -0
  23. package/dist/db/status.js +61 -0
  24. package/dist/dev/index.d.ts +2 -0
  25. package/dist/dev/index.js +2 -0
  26. package/dist/dev/vite.d.ts +67 -0
  27. package/dist/dev/vite.js +126 -0
  28. package/dist/dev.d.ts +18 -0
  29. package/dist/dev.js +15 -0
  30. package/dist/doctor/agents-doc-index.d.ts +4 -0
  31. package/dist/doctor/agents-doc-index.js +80 -0
  32. package/dist/doctor/fixers/dependency-direction.d.ts +9 -0
  33. package/dist/doctor/fixers/dependency-direction.js +98 -0
  34. package/dist/doctor/fixers/index.d.ts +15 -0
  35. package/dist/doctor/fixers/index.js +66 -0
  36. package/dist/doctor/fixers/schema-filename.d.ts +14 -0
  37. package/dist/doctor/fixers/schema-filename.js +104 -0
  38. package/dist/doctor/fixers/types.d.ts +59 -0
  39. package/dist/doctor/fixers/types.js +15 -0
  40. package/dist/doctor/no-auto-import.d.ts +10 -0
  41. package/dist/doctor/no-auto-import.js +158 -0
  42. package/dist/doctor/schema-filename.d.ts +6 -0
  43. package/dist/doctor/schema-filename.js +81 -0
  44. package/dist/doctor/shared-composable-purity.d.ts +8 -0
  45. package/dist/doctor/shared-composable-purity.js +164 -0
  46. package/dist/doctor/types.d.ts +1 -1
  47. package/dist/doctor/types.js +6 -5
  48. package/dist/doctor.d.ts +51 -0
  49. package/dist/doctor.js +191 -7
  50. package/dist/generate.js +2 -2
  51. package/dist/hub.d.ts +1 -1
  52. package/dist/index.d.ts +6 -2
  53. package/dist/index.js +154 -16
  54. package/dist/mcp/index.d.ts +7 -0
  55. package/dist/mcp/index.js +7 -0
  56. package/dist/mcp/server.d.ts +50 -0
  57. package/dist/mcp/server.js +102 -0
  58. package/dist/mcp/tools.d.ts +109 -0
  59. package/dist/mcp/tools.js +485 -0
  60. package/dist/scaffold/app.d.ts +5 -0
  61. package/dist/scaffold/app.js +172 -0
  62. package/dist/scaffold/controller.js +2 -2
  63. package/dist/scaffold/index.d.ts +2 -1
  64. package/dist/scaffold/index.js +2 -1
  65. package/dist/scaffold/job.d.ts +5 -0
  66. package/dist/scaffold/job.js +35 -0
  67. package/dist/scaffold/model.js +8 -8
  68. package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
  69. package/dist/templates/auth/registration.controller.ts.tpl +1 -1
  70. package/dist/templates/auth/session.controller.ts.tpl +1 -1
  71. package/dist/templates/auth/user.model.ts.tpl +1 -1
  72. package/dist/templates/index.d.ts +23 -0
  73. package/dist/templates/index.js +66 -0
  74. package/dist/templates/index.ts +85 -0
  75. package/dist/templates/project/.env.example.tpl +18 -0
  76. package/dist/templates/project/.gitignore.tpl +24 -0
  77. package/dist/templates/project/.npmrc.tpl +4 -0
  78. package/dist/templates/project/AGENTS.md.tpl +210 -0
  79. package/dist/templates/project/CLAUDE.md.tpl +119 -0
  80. package/dist/templates/project/agents/async.md.tpl +218 -0
  81. package/dist/templates/project/agents/data.md.tpl +532 -0
  82. package/dist/templates/project/agents/frontend.md.tpl +201 -0
  83. package/dist/templates/project/agents/realtime.md.tpl +157 -0
  84. package/dist/templates/project/agents/security.md.tpl +92 -0
  85. package/dist/templates/project/agents/testing.md.tpl +101 -0
  86. package/dist/templates/project/agents/web.md.tpl +177 -0
  87. package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
  88. package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
  89. package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
  90. package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
  91. package/dist/templates/project/apps/web/index.html.tpl +18 -0
  92. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
  93. package/dist/templates/project/apps/web/main.ts.tpl +24 -0
  94. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
  95. package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
  96. package/dist/templates/project/docker-compose.yaml.tpl +73 -0
  97. package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
  98. package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
  99. package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
  100. package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
  101. package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
  102. package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
  103. package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
  104. package/dist/templates/project/gaon.config.ts.tpl +27 -0
  105. package/dist/templates/project/package.json.tpl +30 -0
  106. package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
  107. package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
  108. package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
  109. package/dist/templates/project/tsconfig.json.tpl +25 -0
  110. package/dist/templates/project/vite.config.ts.tpl +23 -0
  111. package/dist/tsResolve.js +1 -1
  112. package/dist/work.d.ts +2 -2
  113. package/dist/work.js +3 -1
  114. package/package.json +13 -11
  115. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +0 -12
  116. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +0 -7
  117. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +0 -2
  118. package/dist/__fixtures__/db-minimal/gaon.config.js +0 -11
  119. package/dist/check.d.ts +0 -29
  120. package/dist/check.js +0 -92
@@ -0,0 +1,158 @@
1
+ // @gaonjs/cli · doctor · 자동 import 감지 (M9-E 확장 · errata E-5 §2.4)
2
+ //
3
+ // Nuxt 식 자동 import(컴포넌트·컴포저블을 import 문 없이 사용)는 넣지
4
+ // 않는다. 출처가 코드에 보이지 않는 마법은 AI 가 "이 심볼이 어디서
5
+ // 왔는지"를 추측하게 만들어 v0.15 §1.2 (관례로 추측을 없앤다) 와 정면
6
+ // 충돌한다. 모든 컴포넌트·컴포저블은 명시적으로 import 한다.
7
+ //
8
+ // 검사 대상 (E-5 §2.4 · v0.15 §1.2):
9
+ // 1) 프로젝트 루트의 대표 config 파일 안의 import 선언에서
10
+ // 'unplugin-auto-import' · 'unplugin-vue-components' · 'unimport'
11
+ // 발견 시 error.
12
+ // 2) package.json 의 dependencies · devDependencies · peerDependencies
13
+ // · optionalDependencies 에 위 3종이 있으면 error.
14
+ //
15
+ // 방법: config 는 TS AST 로 import 선언만 훑고 · package.json 은 JSON
16
+ // 파싱 + 라인 검색(에러 메시지에 라인을 붙이기 위해 원문에서 재검색).
17
+ import { existsSync } from 'node:fs';
18
+ import { readFile } from 'node:fs/promises';
19
+ import { join, relative } from 'node:path';
20
+ import ts from 'typescript';
21
+ /** 금지 플러그인 목록 — 자동 import 를 도입하는 대표 3종. */
22
+ const FORBIDDEN_PLUGINS = [
23
+ 'unplugin-auto-import',
24
+ 'unplugin-vue-components',
25
+ 'unimport',
26
+ ];
27
+ /**
28
+ * 검사할 config 파일. Vite/Nuxt/Vue-CLI 관례 파일명을 모두 훑는다.
29
+ * 존재하지 않는 파일은 스킵.
30
+ */
31
+ const CONFIG_FILES = [
32
+ 'vite.config.ts',
33
+ 'vite.config.js',
34
+ 'vite.config.mjs',
35
+ 'nuxt.config.ts',
36
+ 'nuxt.config.js',
37
+ 'vue.config.js',
38
+ 'vue.config.ts',
39
+ ];
40
+ /**
41
+ * config 파일 하나의 소스에서 자동 import 플러그인 import 를 잡는다
42
+ * (단위 테스트 진입점).
43
+ */
44
+ export function inspectConfigForAutoImport(file, source, cwd) {
45
+ const issues = [];
46
+ const rel = relative(cwd, file);
47
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
48
+ const visit = (node) => {
49
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
50
+ const spec = node.moduleSpecifier.text;
51
+ const plugin = matchForbidden(spec);
52
+ if (plugin) {
53
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
54
+ issues.push({
55
+ rule: 'no-auto-import',
56
+ level: 'error',
57
+ file: rel,
58
+ line: line + 1,
59
+ message: `${rel} (line ${line + 1})\n` +
60
+ ` '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
61
+ `→ 자동 import 플러그인 제거 · 명시 import 사용`,
62
+ detail: { plugin, source: 'config-import', module: spec },
63
+ });
64
+ }
65
+ }
66
+ // require('unplugin-...') 형태(CJS config)도 잡는다.
67
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require') {
68
+ const arg = node.arguments[0];
69
+ if (arg && ts.isStringLiteral(arg)) {
70
+ const plugin = matchForbidden(arg.text);
71
+ if (plugin) {
72
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
73
+ issues.push({
74
+ rule: 'no-auto-import',
75
+ level: 'error',
76
+ file: rel,
77
+ line: line + 1,
78
+ message: `${rel} (line ${line + 1})\n` +
79
+ ` '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
80
+ `→ 자동 import 플러그인 제거 · 명시 import 사용`,
81
+ detail: { plugin, source: 'config-require', module: arg.text },
82
+ });
83
+ }
84
+ }
85
+ }
86
+ ts.forEachChild(node, visit);
87
+ };
88
+ visit(sf);
89
+ return issues;
90
+ }
91
+ /** 지정한 모듈 이름이 금지 목록에 걸리면 해당 플러그인 이름을 낸다. */
92
+ function matchForbidden(spec) {
93
+ return FORBIDDEN_PLUGINS.find((p) => spec === p || spec.startsWith(p + '/'));
94
+ }
95
+ /** package.json 안의 deps 4종을 훑어 자동 import 플러그인 유무를 낸다. */
96
+ export function inspectPackageJson(file, source, cwd) {
97
+ const issues = [];
98
+ const rel = relative(cwd, file);
99
+ let pkg;
100
+ try {
101
+ pkg = JSON.parse(source);
102
+ }
103
+ catch {
104
+ // JSON 파싱 실패 시 이 규칙은 아무 것도 하지 않는다 — 다른 규칙 도구가
105
+ // 잡을 문제(예: gaon check 의 tsconfig 파싱)이지 자동 import 검사의 몫이 아님.
106
+ return [];
107
+ }
108
+ const allDeps = {
109
+ ...(pkg.dependencies ?? {}),
110
+ ...(pkg.devDependencies ?? {}),
111
+ ...(pkg.peerDependencies ?? {}),
112
+ ...(pkg.optionalDependencies ?? {}),
113
+ };
114
+ for (const plugin of FORBIDDEN_PLUGINS) {
115
+ if (!(plugin in allDeps))
116
+ continue;
117
+ const line = findLineForKey(source, plugin);
118
+ const loc = line ? ` (line ${line})` : '';
119
+ issues.push({
120
+ rule: 'no-auto-import',
121
+ level: 'error',
122
+ file: rel,
123
+ line: line,
124
+ message: `${rel}${loc}\n` +
125
+ ` package.json 에 '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
126
+ `→ 자동 import 플러그인 제거 · 명시 import 사용 · 'pnpm remove ${plugin}' 후 vite/nuxt config 도 정리`,
127
+ detail: { plugin, source: 'package-json' },
128
+ });
129
+ }
130
+ return issues;
131
+ }
132
+ /** package.json 원문에서 `"<key>"` 가 처음 나오는 1-기반 라인 번호. */
133
+ function findLineForKey(source, key) {
134
+ const lines = source.split(/\r?\n/);
135
+ const needle = `"${key}"`;
136
+ for (let i = 0; i < lines.length; i++) {
137
+ if (lines[i].includes(needle))
138
+ return i + 1;
139
+ }
140
+ return undefined;
141
+ }
142
+ /** 프로젝트 루트를 훑어 자동 import 설정·의존을 잡는다. */
143
+ export async function checkNoAutoImport(cwd) {
144
+ const issues = [];
145
+ for (const cfg of CONFIG_FILES) {
146
+ const full = join(cwd, cfg);
147
+ if (!existsSync(full))
148
+ continue;
149
+ const src = await readFile(full, 'utf8');
150
+ issues.push(...inspectConfigForAutoImport(full, src, cwd));
151
+ }
152
+ const pkgPath = join(cwd, 'package.json');
153
+ if (existsSync(pkgPath)) {
154
+ const src = await readFile(pkgPath, 'utf8');
155
+ issues.push(...inspectPackageJson(pkgPath, src, cwd));
156
+ }
157
+ return { rule: 'no-auto-import', issues };
158
+ }
@@ -0,0 +1,6 @@
1
+ import type { RuleReport } from './types.js';
2
+ /** 파일 stem 을 camelCase 로 정규화(`posts_tags`→`postsTags` · `Posts`→`posts` · `posts`→`posts`). */
3
+ export declare function expectedSchemaStem(stem: string): string;
4
+ /** 소스에서 첫 `table('<이름>', …)` 의 테이블명을 뽑는다(없으면 undefined). */
5
+ export declare function extractTableName(source: string): string | undefined;
6
+ export declare function checkSchemaFilename(cwd: string): Promise<RuleReport>;
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import type { DoctorCheck, RuleReport } from './types.js';
2
+ /**
3
+ * 단일 shared 컴포저블 소스를 검사해 순수성 위반 목록을 낸다
4
+ * (단위 테스트 진입점).
5
+ */
6
+ export declare function inspectSharedComposable(file: string, source: string, cwd: string): DoctorCheck[];
7
+ /** shared/composables/ 를 훑어 순수성 위반을 모두 낸다. */
8
+ export declare function checkSharedComposablePurity(cwd: string): Promise<RuleReport>;
@@ -0,0 +1,164 @@
1
+ // @gaonjs/cli · doctor · shared 컴포저블 순수성 검사 (M9-E 확장 · errata E-5 §2.2)
2
+ //
3
+ // shared/composables/ 는 shared 컴포넌트의 "props 로만" 규칙과 정확히
4
+ // 같은 구도를 따른다. 라우트를 몰라야 하고(api·pageProps 금지) · domain
5
+ // 은 타입으로만 참조해야 한다. 필요한 데이터·호출 함수는 인자로 받는다.
6
+ // 정본 근거: errata E-5 §2.2 (컴포저블·레이아웃 관례 · 결정 25).
7
+ //
8
+ // 검사 대상:
9
+ // 1) 프레임웍 모듈(gaonjs · gaonjs/vue · @gaonjs/vue · @gaonjs/web) 에서
10
+ // 'api' · 'pageProps' 를 value import 하면 error.
11
+ // - type-only 는 무해(구조 참조뿐 · 라우트 지식 필요 없음).
12
+ // 2) domain 을 value import 하면 error.
13
+ // - type-only 는 허용(모델 Row 타입 등 · 순수 타입 참조).
14
+ //
15
+ // 방법: shared/composables/*.ts 를 TS AST 로 파싱해 import 선언만 훑는다.
16
+ // 상대 import 는 실 파일까지 해석하지 않고 경로 접두사(domain/) 로 판정 —
17
+ // 이 검사는 순수성 게이트라 정확도보다 재현성이 우선(false positive 는
18
+ // 오히려 안전).
19
+ import { readdir, readFile } from 'node:fs/promises';
20
+ import { join, relative, resolve, dirname } from 'node:path';
21
+ import ts from 'typescript';
22
+ /** 라우트 지식을 담은 심볼 — shared 는 참조할 수 없다. */
23
+ const FORBIDDEN_FRAMEWORK_NAMES = new Set(['api', 'pageProps']);
24
+ /**
25
+ * 프레임웍 모듈 패턴. 문서 표기(gaon/vue)는 실 패키지 이름의 짧은
26
+ * 별칭이며, 실 배포본은 gaonjs 파사드 subpath 와 @gaonjs 스코프를 쓴다.
27
+ * 하나만 잡으면 우회가 쉬우므로 알려진 표기 3종을 모두 매치한다.
28
+ */
29
+ const FRAMEWORK_MODULE_PATTERNS = [
30
+ /^gaonjs$/,
31
+ /^gaonjs\/vue$/,
32
+ /^@gaonjs\/vue$/,
33
+ /^@gaonjs\/web$/,
34
+ ];
35
+ /** import 선언에서 이름과 type-only 플래그를 추출한다. */
36
+ function collectImportedNames(node) {
37
+ const out = [];
38
+ const clause = node.importClause;
39
+ if (!clause)
40
+ return out;
41
+ const clauseTypeOnly = clause.isTypeOnly;
42
+ if (clause.name)
43
+ out.push({ name: clause.name.text, typeOnly: clauseTypeOnly });
44
+ const bindings = clause.namedBindings;
45
+ if (bindings) {
46
+ if (ts.isNamespaceImport(bindings)) {
47
+ out.push({ name: bindings.name.text, typeOnly: clauseTypeOnly });
48
+ }
49
+ else if (ts.isNamedImports(bindings)) {
50
+ for (const el of bindings.elements) {
51
+ out.push({ name: el.name.text, typeOnly: clauseTypeOnly || el.isTypeOnly });
52
+ }
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ function isRelativeSpecifier(s) {
58
+ return s.startsWith('./') || s.startsWith('../');
59
+ }
60
+ function matchesFrameworkModule(spec) {
61
+ return FRAMEWORK_MODULE_PATTERNS.some((re) => re.test(spec));
62
+ }
63
+ /**
64
+ * 단일 shared 컴포저블 소스를 검사해 순수성 위반 목록을 낸다
65
+ * (단위 테스트 진입점).
66
+ */
67
+ export function inspectSharedComposable(file, source, cwd) {
68
+ const issues = [];
69
+ const rel = relative(cwd, file);
70
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
71
+ const baseDir = dirname(file);
72
+ const visit = (node) => {
73
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
74
+ const spec = node.moduleSpecifier.text;
75
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
76
+ const names = collectImportedNames(node);
77
+ // 1) 프레임웍의 api/pageProps 를 value import → error
78
+ if (matchesFrameworkModule(spec)) {
79
+ for (const n of names) {
80
+ if (n.typeOnly)
81
+ continue; // type-only 는 라우트 실행 지식이 아님
82
+ if (!FORBIDDEN_FRAMEWORK_NAMES.has(n.name))
83
+ continue;
84
+ issues.push({
85
+ rule: 'shared-composable-purity',
86
+ level: 'error',
87
+ file: rel,
88
+ line: line + 1,
89
+ message: `${rel} (line ${line + 1})\n` +
90
+ ` '${spec}' 의 '${n.name}' import 발견 · shared 는 라우트를 몰라야 함\n` +
91
+ `→ 옵션: apps/<앱>/composables/ 로 옮기거나 · ${n.name} 호출을 인자로 받도록 바꾸라`,
92
+ detail: {
93
+ kind: 'framework-api',
94
+ module: spec,
95
+ name: n.name,
96
+ },
97
+ });
98
+ }
99
+ }
100
+ // 2) domain value import → error (type-only 는 허용)
101
+ if (isRelativeSpecifier(spec)) {
102
+ const abs = resolve(baseDir, spec);
103
+ const parts = relative(cwd, abs).split(/[\\/]/).filter(Boolean);
104
+ if (parts[0] === 'domain') {
105
+ const valueNames = names.filter((n) => !n.typeOnly);
106
+ if (valueNames.length > 0) {
107
+ issues.push({
108
+ rule: 'shared-composable-purity',
109
+ level: 'error',
110
+ file: rel,
111
+ line: line + 1,
112
+ message: `${rel} (line ${line + 1})\n` +
113
+ ` domain 값 import 발견 · shared 컴포저블은 domain 을 타입으로만 참조해야 함\n` +
114
+ `→ 'import type { ... } from ...' 형태로 바꾸거나 · 필요한 값은 인자로 받도록 바꾸라`,
115
+ detail: {
116
+ kind: 'domain-value',
117
+ module: spec,
118
+ names: valueNames.map((n) => n.name),
119
+ },
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+ ts.forEachChild(node, visit);
126
+ };
127
+ visit(sf);
128
+ return issues;
129
+ }
130
+ /** shared/composables/ 를 훑어 순수성 위반을 모두 낸다. */
131
+ export async function checkSharedComposablePurity(cwd) {
132
+ const composablesDir = join(cwd, 'shared', 'composables');
133
+ const files = [];
134
+ await collectTsFiles(composablesDir, files);
135
+ const issues = [];
136
+ for (const file of files) {
137
+ const src = await readFile(file, 'utf8');
138
+ issues.push(...inspectSharedComposable(file, src, cwd));
139
+ }
140
+ return { rule: 'shared-composable-purity', issues };
141
+ }
142
+ async function collectTsFiles(root, out) {
143
+ let entries;
144
+ try {
145
+ entries = (await readdir(root, { withFileTypes: true }));
146
+ }
147
+ catch {
148
+ return;
149
+ }
150
+ for (const e of entries) {
151
+ const name = e.name;
152
+ if (name === 'node_modules' || name === 'dist' || name === '.gaon')
153
+ continue;
154
+ const full = join(root, name);
155
+ if (e.isDirectory())
156
+ await collectTsFiles(full, out);
157
+ else if (e.isFile() &&
158
+ name.endsWith('.ts') &&
159
+ !name.endsWith('.d.ts') &&
160
+ !name.endsWith('.test.ts')) {
161
+ out.push(full);
162
+ }
163
+ }
164
+ }
@@ -1,4 +1,4 @@
1
- export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff';
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;
@@ -1,9 +1,10 @@
1
- // @gaonjs/cli · doctor 공용 타입 (M9-E · M9-E-Fix)
1
+ // @gaonjs/cli · doctor 공용 타입 (M9-E · M9-E-Fix · M9-E 확장 · E-5)
2
2
  //
3
- // 5 검사(response-mixing · n-plus-one · dependency-direction · connections
4
- // · migration-diff)가 모두 DoctorCheck 낸다. 상위(runDoctorCommand)는
5
- // level passed/warnings/errors 갈라 담는다. 자동화(CI)는 JSON
6
- // 파싱해 errors.length > 0 이면 fail 판단한다.
3
+ // 7 검사(response-mixing · n-plus-one · dependency-direction · connections
4
+ // · migration-diff · shared-composable-purity · no-auto-import)가 모두
5
+ // DoctorCheck 낸다. 상위(runDoctorCommand)는 level 로 passed/
6
+ // warnings/errors 갈라 담는다. 자동화(CI)는 JSON 을 파싱해
7
+ // errors.length > 0 이면 fail 로 판단한다.
7
8
  //
8
9
  // 사전 검사 실패(프로젝트 마커 없음 · TS API 미노출)는 규칙 실행 자체가
9
10
  // 불가능한 상황이므로 별도 `fatal` 필드로 표현한다 — runCli 는 이 경우
package/dist/doctor.d.ts CHANGED
@@ -6,12 +6,47 @@ export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one
6
6
  export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
7
7
  export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/connections.js';
8
8
  export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
9
+ export { inspectSharedComposable, checkSharedComposablePurity, } from './doctor/shared-composable-purity.js';
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';
9
13
  export { renderHuman, renderJson } from './doctor/reporter.js';
10
14
  export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
11
15
  export interface DoctorCommandOptions {
12
16
  readonly cwd?: string;
13
17
  readonly json?: boolean;
14
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;
15
50
  }
16
51
  /**
17
52
  * `gaon doctor` 진입점 (M9-E · 확장 · M9-E-Fix 하드닝).
@@ -33,6 +68,22 @@ export interface DoctorCommandOptions {
33
68
  * 나머지 규칙은 계속 실행(부분 결과 확보).
34
69
  */
35
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;
36
87
  export interface DoctorIssue {
37
88
  readonly file: string;
38
89
  readonly line: number;