@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,126 @@
1
+ /**
2
+ * @gaonjs/cli · dev/vite — Vite dev server 프로그램적 기동 (M3-runtime)
3
+ *
4
+ * 정본 정합:
5
+ * · §6.4 Vue 어댑터 인터페이스 — `vite: () => [vuePlugin()]` 은 서버 배선
6
+ * 시 프론트엔드 어댑터가 제공한다. 이 파일은 그 배선을 CLI 측에서
7
+ * 쉽게 부를 수 있는 최소 래퍼일 뿐이다.
8
+ * · The One Way (§CLAUDE.md 6) — Vite 를 middlewareMode 로 띄운다. Fastify
9
+ * 한 포트로 API 와 프론트가 함께 서빙된다 — 두 포트·프록시 규칙 X.
10
+ *
11
+ * 이 파일은 `dev.ts` 에 배선되지 않는다(사용자 지침) — M1 세션이 통합
12
+ * 커밋에서 배선한다. 여기서는 함수만 노출한다.
13
+ *
14
+ * 참고: vite 는 이 파일을 실행하는 환경에 이미 설치돼 있어야 한다
15
+ * (packages/cli/package.json 에 dep 등록). vite 가 없으면 명확한 에러를
16
+ * 던져 사용자에게 설치 안내를 준다(§7.5.3 에러=수리 안내서).
17
+ */
18
+ import { existsSync } from 'node:fs';
19
+ import { resolve, join } from 'node:path';
20
+ /**
21
+ * Vite dev server 를 middleware 모드로 띄운다. Fastify 에 붙여 하나의
22
+ * 포트로 API + 프론트를 함께 서빙한다(The One Way).
23
+ *
24
+ * 파일이 존재하지 않는 경우(초기 프로젝트에 apps/web/index.html 이 없는
25
+ * 순간)에는 명확한 에러를 던진다 — dev.ts 가 이 예외를 잡아 사용자에게
26
+ * "→ apps/<앱>/index.html 을 만들거나 gaon new 로 새 프로젝트를 만드세요"
27
+ * 라고 알린다.
28
+ */
29
+ export async function createGaonViteServer(opts) {
30
+ if (opts.enabled === false)
31
+ return undefined;
32
+ const cwd = resolve(opts.cwd);
33
+ const appRoot = resolve(cwd, opts.appRoot ?? 'apps/web');
34
+ const indexHtml = join(appRoot, 'index.html');
35
+ if (!existsSync(indexHtml)) {
36
+ throw new Error(`[gaon dev · vite] 프론트 진입 HTML 이 없습니다: ${indexHtml}\n` +
37
+ `→ 다음 중 하나를 하세요:\n` +
38
+ ` · gaon new <name> 으로 새 프로젝트를 만들거나\n` +
39
+ ` · 위 경로에 index.html 을 만들고 <script type="module" src="/main.ts"></script> 를 넣거나\n` +
40
+ ` · gaon dev 를 --no-vite 로 실행해 프론트 핫리로드를 비활성화하세요.`);
41
+ }
42
+ const vite = await loadVite();
43
+ const server = await vite.createServer({
44
+ root: appRoot,
45
+ configFile: findViteConfig(cwd, appRoot),
46
+ server: {
47
+ middlewareMode: true,
48
+ // hmr 는 middleware 모드에서도 Vite 가 자체 ws 서버로 처리한다.
49
+ // Fastify 는 이 ws 를 별도 upgrade 핸들러로 얹으면 되며(§ dev.ts 배선),
50
+ // 이 파일은 서버 옵션만 정한다.
51
+ hmr: true,
52
+ },
53
+ appType: 'custom',
54
+ // Vite 는 stderr 에 색상 있는 로그를 낸다 — 통합 콘솔에서 그대로 흘린다.
55
+ logLevel: 'info',
56
+ customLogger: opts.onLog
57
+ ? makeConnectLogger(opts.onLog)
58
+ : undefined,
59
+ });
60
+ return {
61
+ middlewares: server.middlewares,
62
+ async transformIndexHtml(url, html) {
63
+ return server.transformIndexHtml(url, html);
64
+ },
65
+ async ssrLoadModule(id) {
66
+ return (await server.ssrLoadModule(id));
67
+ },
68
+ async close() {
69
+ await server.close();
70
+ },
71
+ root: appRoot,
72
+ };
73
+ }
74
+ /** vite.config.ts 를 프로젝트 루트 · 앱 루트 순으로 찾는다(있으면 반환). */
75
+ function findViteConfig(cwd, appRoot) {
76
+ for (const dir of [appRoot, cwd]) {
77
+ for (const name of ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']) {
78
+ const p = join(dir, name);
79
+ if (existsSync(p))
80
+ return p;
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * vite 를 lazy 하게 로드한다. 없으면 명확한 에러 — 사용자에게 설치 명령을
87
+ * 알려주고, dev.ts 가 다시 던져 통합 콘솔로 표시한다.
88
+ */
89
+ async function loadVite() {
90
+ try {
91
+ return await import('vite');
92
+ }
93
+ catch (err) {
94
+ throw new Error(`[gaon dev · vite] vite 모듈을 찾을 수 없습니다.\n` +
95
+ `→ 프로젝트 루트에서 다음 명령으로 설치하세요: pnpm add -D vite @vitejs/plugin-vue\n` +
96
+ ` (또는 gaon dev --no-vite 로 프론트 핫리로드를 비활성화)\n` +
97
+ `원인: ${err instanceof Error ? err.message : String(err)}`);
98
+ }
99
+ }
100
+ function makeConnectLogger(onLog) {
101
+ let hasWarned = false;
102
+ const errorsSeen = new WeakSet();
103
+ return {
104
+ info: (msg) => onLog(stripAnsi(msg), 'info'),
105
+ warn: (msg) => {
106
+ hasWarned = true;
107
+ onLog(stripAnsi(msg), 'warn');
108
+ },
109
+ warnOnce: (msg) => {
110
+ hasWarned = true;
111
+ onLog(stripAnsi(msg), 'warn');
112
+ },
113
+ error: (msg) => onLog(stripAnsi(msg), 'error'),
114
+ // clearScreen 은 통합 콘솔에서는 무시한다(다른 자식 로그가 지워지면 안 됨).
115
+ clearScreen: () => { },
116
+ hasErrorLogged: (err) => errorsSeen.has(err),
117
+ get hasWarned() {
118
+ return hasWarned;
119
+ },
120
+ };
121
+ }
122
+ // vite 로거는 원문 ANSI 를 그대로 낸다 — 통합 콘솔이 자체 색상을 붙이므로 벗긴다.
123
+ function stripAnsi(s) {
124
+ // ESC [ ... m 시퀀스 제거. 라이브러리 미의존 정책상 정규식으로 처리.
125
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
126
+ }
package/dist/dev.d.ts CHANGED
@@ -40,5 +40,23 @@ export interface DevHandle {
40
40
  * .gaon 파일을 재생성한다. 반환 핸들의 close() 로 멈춘다.
41
41
  */
42
42
  export declare function startDev(deps: DevDeps): Promise<DevHandle>;
43
+ /** 워처 없이 .gaon 타입 브리지를 1회 재생성할 때 필요한 생성기 주입. */
44
+ export interface RegenDeps {
45
+ regenerateTables(schemaDir: string, out: string): Promise<unknown>;
46
+ regenerateRoutes(appDir: string, out: string): Promise<unknown>;
47
+ }
48
+ export interface RegenResult {
49
+ /** tables.d.ts 를 재생성했는가(domain/schema 존재 시에만). */
50
+ readonly tables: boolean;
51
+ /** routes.d.ts 를 재생성한 앱 이름. */
52
+ readonly apps: readonly string[];
53
+ }
54
+ /**
55
+ * 워처 없이 .gaon 타입 브리지를 1회 전체 재생성한다. `gaon check` 처럼
56
+ * 개발 서버를 띄우지 않는 진입점(CI · AI 에이전트)이 검사 직전에 최신
57
+ * 타입 체인을 확보하기 위해 쓴다(§13.4-5). startDev 의 초기 재생성 단계와
58
+ * 동일한 순서(tables → 앱별 routes)를 공유한다.
59
+ */
60
+ export declare function regenerateGaonOnce(layout: DevLayout, deps: RegenDeps): Promise<RegenResult>;
43
61
  /** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
44
62
  export declare function resolveDevLayout(cwd: string): DevLayout;
package/dist/dev.js CHANGED
@@ -67,6 +67,21 @@ export async function startDev(deps) {
67
67
  },
68
68
  };
69
69
  }
70
+ /**
71
+ * 워처 없이 .gaon 타입 브리지를 1회 전체 재생성한다. `gaon check` 처럼
72
+ * 개발 서버를 띄우지 않는 진입점(CI · AI 에이전트)이 검사 직전에 최신
73
+ * 타입 체인을 확보하기 위해 쓴다(§13.4-5). startDev 의 초기 재생성 단계와
74
+ * 동일한 순서(tables → 앱별 routes)를 공유한다.
75
+ */
76
+ export async function regenerateGaonOnce(layout, deps) {
77
+ if (layout.schemaDir) {
78
+ await deps.regenerateTables(layout.schemaDir, layout.tablesOut);
79
+ }
80
+ for (const app of layout.apps) {
81
+ await deps.regenerateRoutes(app.appDir, app.routesOut);
82
+ }
83
+ return { tables: !!layout.schemaDir, apps: layout.apps.map((a) => a.name) };
84
+ }
70
85
  /** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
71
86
  export function resolveDevLayout(cwd) {
72
87
  const root = resolve(cwd);
@@ -0,0 +1,4 @@
1
+ import type { RuleReport } from './types.js';
2
+ /** AGENTS.md 본문에서 `agents/<이름>.md` 참조를 전부 뽑는다(중복 제거·정렬). */
3
+ export declare function extractAgentDocRefs(source: string): string[];
4
+ export declare function checkAgentsDocIndex(cwd: string): Promise<RuleReport>;
@@ -0,0 +1,80 @@
1
+ // @gaonjs/cli · doctor · AGENTS 2층 문서 색인 정합 (결정 40 · 2026-07-25)
2
+ //
3
+ // 관례 문서는 2층 구조다: 루트 AGENTS.md(코어)의 색인이 agents/*.md
4
+ // (카테고리 정본)를 가리키고, 에이전트는 색인 지시대로 작업 전에 카테고리
5
+ // 파일을 읽는다. 색인과 실 파일이 어긋나면 두 방향 모두 관례가 깨진다:
6
+ // · 색인이 가리키는 파일이 없다 → 에이전트가 읽을 정본이 없다 (error)
7
+ // · 파일은 있는데 색인에 없다 → 아무도 읽지 않는 죽은 문서 (warning)
8
+ //
9
+ // 2층 구조를 쓰지 않는 프로젝트(agents/ 없음 · 색인 참조 없음)는 검사할
10
+ // 것이 없으므로 통과 — 기존 프로젝트에 소급 강제하지 않는다.
11
+ import { readFile, readdir } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ /** AGENTS.md 본문에서 `agents/<이름>.md` 참조를 전부 뽑는다(중복 제거·정렬). */
14
+ export function extractAgentDocRefs(source) {
15
+ const out = new Set();
16
+ for (const m of source.matchAll(/\bagents\/([A-Za-z0-9_-]+\.md)\b/g)) {
17
+ out.add(m[1]);
18
+ }
19
+ return [...out].sort();
20
+ }
21
+ export async function checkAgentsDocIndex(cwd) {
22
+ const issues = [];
23
+ let core;
24
+ try {
25
+ core = await readFile(join(cwd, 'AGENTS.md'), 'utf8');
26
+ }
27
+ catch {
28
+ core = undefined;
29
+ }
30
+ let files = [];
31
+ try {
32
+ files = (await readdir(join(cwd, 'agents'))).filter((f) => f.endsWith('.md')).sort();
33
+ }
34
+ catch {
35
+ files = [];
36
+ }
37
+ const refs = core ? extractAgentDocRefs(core) : [];
38
+ // 2층 구조 미사용 프로젝트 — 검사 대상 없음.
39
+ if (refs.length === 0 && files.length === 0) {
40
+ return { rule: 'agents-doc-index', issues };
41
+ }
42
+ if (!core && files.length > 0) {
43
+ issues.push({
44
+ rule: 'agents-doc-index',
45
+ level: 'error',
46
+ file: 'AGENTS.md',
47
+ message: `agents/*.md 카테고리 문서(${files.length}개)가 있는데 루트 AGENTS.md 가 없습니다.\n` +
48
+ `→ 루트에 AGENTS.md(코어)를 두고 색인에 agents/${files[0]} 등 카테고리 파일을 등재하세요 (결정 40).`,
49
+ detail: { files },
50
+ });
51
+ return { rule: 'agents-doc-index', issues };
52
+ }
53
+ const fileSet = new Set(files);
54
+ for (const ref of refs) {
55
+ if (!fileSet.has(ref)) {
56
+ issues.push({
57
+ rule: 'agents-doc-index',
58
+ level: 'error',
59
+ file: 'AGENTS.md',
60
+ message: `AGENTS.md 색인이 agents/${ref} 를 가리키는데 실 파일이 없습니다.\n` +
61
+ `→ agents/${ref} 를 만들거나, AGENTS.md 색인에서 해당 행을 지우세요.`,
62
+ detail: { ref },
63
+ });
64
+ }
65
+ }
66
+ const refSet = new Set(refs);
67
+ for (const f of files) {
68
+ if (!refSet.has(f)) {
69
+ issues.push({
70
+ rule: 'agents-doc-index',
71
+ level: 'warning',
72
+ file: `agents/${f}`,
73
+ message: `agents/${f} 가 루트 AGENTS.md 색인에 없습니다 — 색인에 없는 문서는 에이전트가 읽지 않습니다.\n` +
74
+ `→ AGENTS.md §색인 표에 'agents/${f}' 행을 추가하거나, 불필요하면 파일을 지우세요.`,
75
+ detail: { file: f },
76
+ });
77
+ }
78
+ }
79
+ return { rule: 'agents-doc-index', issues };
80
+ }
@@ -0,0 +1,9 @@
1
+ import type { DoctorCheck } from '../types.js';
2
+ import type { FixerPlan } from './types.js';
3
+ /** 한 파일 안에서 지정 라인의 import 를 type-only 로 바꾼다(순수 계산). */
4
+ export declare function fixDomainToSharedTypeOnly(file: string, source: string, targetLines: readonly number[]): {
5
+ after: string;
6
+ changedLines: number[];
7
+ };
8
+ /** DoctorCheck 목록에서 domain → shared(값) 위반만 골라 FixerPlan 을 낸다. */
9
+ export declare function fixDependencyDirection(issues: readonly DoctorCheck[], cwd: string): Promise<readonly FixerPlan[]>;
@@ -0,0 +1,98 @@
1
+ // @gaonjs/cli · doctor · fixer · 의존 방향 (M9-fix · CLAUDE.md §5 · 규칙 4)
2
+ //
3
+ // 대상 위반: domain 소스가 shared/ 를 값(runtime) 으로 import 하는 경우.
4
+ // 정본은 이 방향을 type-only 로만 허용한다. 자동 정정은 안전하다 — 원본
5
+ // `import { X } from '../shared/x.js'` 를 `import type { X } from '../shared/x.js'`
6
+ // 로 바꾸기만 한다. 값이 실제로 필요했다면 컴파일이 즉시 실패해 사용자가
7
+ // 발견한다(우회 X · 근본 원인 안내).
8
+ //
9
+ // 다른 방향의 위반(domain → app · app → app · shared → app)은 기계적
10
+ // 정정이 불가능하다(파일을 어디로 옮길지 사람 판단). 이 fixer 는 그런
11
+ // 케이스를 무시하고 상위가 "수동 수정 필요" 로 보고한다.
12
+ //
13
+ // 방법: 각 파일을 TS AST 로 파싱해 문제 라인의 ImportDeclaration 을 찾고,
14
+ // `import` 다음에 `type` 키워드가 없으면 삽입한다. 문자열 스캔이 아닌
15
+ // AST 기반 편집이라 주석·공백·복합 import(`import x, { a, b }`) 등 엣지
16
+ // 케이스에 안전하다.
17
+ import { readFile } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+ import ts from 'typescript';
20
+ /** 한 파일 안에서 지정 라인의 import 를 type-only 로 바꾼다(순수 계산). */
21
+ export function fixDomainToSharedTypeOnly(file, source, targetLines) {
22
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
23
+ // 라인 → 편집 대상 ImportDeclaration 노드 매핑.
24
+ const targets = new Set(targetLines);
25
+ const edits = [];
26
+ const changed = [];
27
+ const visit = (node) => {
28
+ if (ts.isImportDeclaration(node) &&
29
+ ts.isStringLiteral(node.moduleSpecifier) &&
30
+ node.importClause &&
31
+ !node.importClause.isTypeOnly) {
32
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
33
+ const oneBased = line + 1;
34
+ if (!targets.has(oneBased)) {
35
+ ts.forEachChild(node, visit);
36
+ return;
37
+ }
38
+ // 편집: `import ` 바로 뒤에 `type ` 을 삽입한다. 노드의 시작 위치
39
+ // (leading comment 뒤) 를 기준으로 첫 `import` 토큰 다음 공백까지를
40
+ // 잡아 재작성한다.
41
+ const nodeStart = node.getStart(sf);
42
+ const original = source.slice(nodeStart, node.end);
43
+ // `import` 로 시작하지 않으면(예상 밖) 스킵.
44
+ if (!original.startsWith('import')) {
45
+ ts.forEachChild(node, visit);
46
+ return;
47
+ }
48
+ const rewritten = 'import type' + original.slice('import'.length);
49
+ edits.push({ start: nodeStart, end: node.end, replacement: rewritten });
50
+ changed.push(oneBased);
51
+ }
52
+ ts.forEachChild(node, visit);
53
+ };
54
+ visit(sf);
55
+ if (edits.length === 0) {
56
+ return { after: source, changedLines: [] };
57
+ }
58
+ // 뒤에서부터 치환 — 앞 위치를 밀지 않도록.
59
+ edits.sort((a, b) => b.start - a.start);
60
+ let out = source;
61
+ for (const e of edits) {
62
+ out = out.slice(0, e.start) + e.replacement + out.slice(e.end);
63
+ }
64
+ return { after: out, changedLines: changed.sort((a, b) => a - b) };
65
+ }
66
+ /** DoctorCheck 목록에서 domain → shared(값) 위반만 골라 FixerPlan 을 낸다. */
67
+ export async function fixDependencyDirection(issues, cwd) {
68
+ // 파일별로 라인 모음.
69
+ const byFile = new Map();
70
+ for (const issue of issues) {
71
+ const from = issue.detail?.from ?? '';
72
+ const to = issue.detail?.to ?? '';
73
+ const typeOnly = issue.detail?.typeOnly === true;
74
+ if (from !== 'domain' || to !== 'shared' || typeOnly)
75
+ continue;
76
+ if (!issue.file || !issue.line)
77
+ continue;
78
+ const arr = byFile.get(issue.file) ?? [];
79
+ arr.push(issue.line);
80
+ byFile.set(issue.file, arr);
81
+ }
82
+ const plans = [];
83
+ for (const [rel, lines] of byFile) {
84
+ const abs = join(cwd, rel);
85
+ const before = await readFile(abs, 'utf8');
86
+ const { after, changedLines } = fixDomainToSharedTypeOnly(abs, before, lines);
87
+ if (changedLines.length === 0 || after === before)
88
+ continue;
89
+ plans.push({
90
+ file: rel,
91
+ before,
92
+ after,
93
+ summary: `domain → shared · line ${changedLines.join(', ')}: 'import ...' → 'import type ...' (${changedLines.length}건)\n` +
94
+ ` 근거: CLAUDE.md §5 규칙 4 · domain 은 shared 를 type-only 로만 참조합니다.`,
95
+ });
96
+ }
97
+ return plans;
98
+ }
@@ -0,0 +1,15 @@
1
+ import type { DoctorRule } from '../types.js';
2
+ import type { Fixer, FixerCapability } from './types.js';
3
+ export type { Fixer, FixerCapability, FixerPlan, RewriteFixerPlan, RenameFixerPlan, RefEdit, } from './types.js';
4
+ export { fixDependencyDirection, fixDomainToSharedTypeOnly, } from './dependency-direction.js';
5
+ export { fixSchemaFilename, rewriteSchemaImport } from './schema-filename.js';
6
+ /**
7
+ * 규칙별 fixer 표. 없는 규칙(값 undefined)은 "수동 수정 필요" 로 리포트된다.
8
+ * 새 fixer 를 만들면 이 표에만 추가하면 된다(runDoctorFix 는 표를 순회).
9
+ */
10
+ export declare const FIXERS: Partial<Record<DoctorRule, Fixer>>;
11
+ /**
12
+ * 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
13
+ * 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
14
+ */
15
+ export declare const FIXER_CAPABILITIES: readonly FixerCapability[];
@@ -0,0 +1,66 @@
1
+ // @gaonjs/cli · doctor · fixer 레지스트리 (M9-fix · v0.16 §7.5.3)
2
+ //
3
+ // 규칙별 fixer 를 여기서 하나로 묶어 상위(runDoctorCommand)에 넘긴다.
4
+ // 규칙 하나 = fixer 하나(또는 없음). 없으면 `hasFixer: false` 로 노출되고,
5
+ // --fix 리포트가 "수동 수정 필요" 라벨을 붙인다.
6
+ //
7
+ // 각 fixer 는 순수 함수(입력 → 출력)로 유지한다. 파일 쓰기·백업은 상위
8
+ // runDoctorFix 가 관장 — fixer 는 테스트가 fs 없이 계산만으로 검증된다.
9
+ import { fixDependencyDirection } from './dependency-direction.js';
10
+ import { fixSchemaFilename } from './schema-filename.js';
11
+ export { fixDependencyDirection, fixDomainToSharedTypeOnly, } from './dependency-direction.js';
12
+ export { fixSchemaFilename, rewriteSchemaImport } from './schema-filename.js';
13
+ /**
14
+ * 규칙별 fixer 표. 없는 규칙(값 undefined)은 "수동 수정 필요" 로 리포트된다.
15
+ * 새 fixer 를 만들면 이 표에만 추가하면 된다(runDoctorFix 는 표를 순회).
16
+ */
17
+ export const FIXERS = {
18
+ 'dependency-direction': fixDependencyDirection,
19
+ 'schema-filename': fixSchemaFilename,
20
+ };
21
+ /**
22
+ * 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
23
+ * 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
24
+ */
25
+ export const FIXER_CAPABILITIES = [
26
+ {
27
+ rule: 'response-mixing',
28
+ hasFixer: false,
29
+ note: '수동 · 액션을 두 개로 분리하거나 JSON 분기를 예외/redirect 로 재편(설계 결정).',
30
+ },
31
+ {
32
+ rule: 'n-plus-one',
33
+ hasFixer: false,
34
+ note: '수동 · include() 도입 · Query.eager() 재작성(성능 튜닝은 코드 리팩터링).',
35
+ },
36
+ {
37
+ rule: 'dependency-direction',
38
+ hasFixer: true,
39
+ note: 'domain → shared 값 import 를 type-only 로 자동 정정(다른 방향 위반은 파일 이동이 필요해 수동).',
40
+ },
41
+ {
42
+ rule: 'connections',
43
+ hasFixer: false,
44
+ note: '수동 · cross-DB belongsTo 는 도메인 재설계가 필요합니다.',
45
+ },
46
+ {
47
+ rule: 'migration-diff',
48
+ hasFixer: false,
49
+ note: '수동 · `gaon db diff` → 리뷰 → `gaon db migrate` (SQL 은 사람이 확인).',
50
+ },
51
+ {
52
+ rule: 'shared-composable-purity',
53
+ hasFixer: false,
54
+ note: '수동 · 컴포저블에 든 앱 종속 로직을 앱 쪽으로 옮겨야 합니다(E-5 §2.2).',
55
+ },
56
+ {
57
+ rule: 'no-auto-import',
58
+ hasFixer: false,
59
+ note: '수동 · 자동 import 플러그인을 제거하고 명시 import 로 전환(빌드 구성 검토).',
60
+ },
61
+ {
62
+ rule: 'schema-filename',
63
+ hasFixer: true,
64
+ note: '스키마 파일을 camelCase(테이블명) 로 자동 rename + 이 스키마를 import 하는 곳의 경로를 함께 갱신(결정 38).',
65
+ },
66
+ ];
@@ -0,0 +1,14 @@
1
+ import type { DoctorCheck } from '../types.js';
2
+ import type { RenameFixerPlan } from './types.js';
3
+ /**
4
+ * 한 importer 소스에서 `.../schema/<oldStem>(.js)?` 참조를 `<newStem>` 로 바꾼다.
5
+ * `schema/` 접두 + 따옴표/`.js` 경계로 못박아 오탐(우연한 부분 문자열)을 막는다.
6
+ * 바뀐 게 없으면 undefined 를 돌려준다(그 importer 는 편집 대상 아님).
7
+ */
8
+ export declare function rewriteSchemaImport(source: string, oldStem: string, newStem: string): string | undefined;
9
+ /**
10
+ * schema-filename 위반 목록을 받아 RenameFixerPlan 을 낸다. 위반마다:
11
+ * 1) 스키마 파일을 camelCase 이름으로 이동(내용 불변).
12
+ * 2) 프로젝트 전체(.ts)에서 그 스키마를 import 하는 곳의 경로를 갱신.
13
+ */
14
+ export declare function fixSchemaFilename(issues: readonly DoctorCheck[], cwd: string): Promise<readonly RenameFixerPlan[]>;
@@ -0,0 +1,104 @@
1
+ // @gaonjs/cli · doctor · fixer · 스키마 파일명 (M9-fix · 결정 38 · §1.1)
2
+ //
3
+ // 대상 위반: 스키마 파일명이 camelCase(테이블명) 관례와 어긋남(스네이크·케밥·
4
+ // Pascal). 예: 조인 테이블 `posts_tags` 의 스키마가 `domain/schema/posts_tags.ts`
5
+ // 로 저장돼 있으면 → `domain/schema/postsTags.ts` 로 이름을 바꾸고, 이 스키마를
6
+ // import 하는 곳(모델 등)의 경로도 함께 고친다.
7
+ //
8
+ // 이 fixer 는 내용 재작성이 아니라 **파일 이동 + 참조 갱신**이라 RenameFixerPlan
9
+ // 을 낸다. 파일 I/O(이동·백업·쓰기)는 상위 runDoctorFix 가 관장 — 이 함수는 순수
10
+ // 계산(어떤 파일을 어디로 옮기고, 어떤 importer 의 어느 문자열을 어떻게 바꿀지)만
11
+ // 한다(테스트가 fs 없이 검증 가능 · dependency-direction fixer 와 같은 원칙).
12
+ import { readdir, readFile, stat } from 'node:fs/promises';
13
+ import { join, relative } from 'node:path';
14
+ /** 정규식 리터럴에서 특수문자를 이스케이프한다. */
15
+ function escapeRe(s) {
16
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+ /**
19
+ * 한 importer 소스에서 `.../schema/<oldStem>(.js)?` 참조를 `<newStem>` 로 바꾼다.
20
+ * `schema/` 접두 + 따옴표/`.js` 경계로 못박아 오탐(우연한 부분 문자열)을 막는다.
21
+ * 바뀐 게 없으면 undefined 를 돌려준다(그 importer 는 편집 대상 아님).
22
+ */
23
+ export function rewriteSchemaImport(source, oldStem, newStem) {
24
+ // import ... from '.../schema/posts_tags.js' · import('.../schema/posts_tags')
25
+ const re = new RegExp(`(schema/)${escapeRe(oldStem)}(\\.js)?(['"\\\`])`, 'g');
26
+ const after = source.replace(re, `$1${newStem}$2$3`);
27
+ return after === source ? undefined : after;
28
+ }
29
+ /**
30
+ * schema-filename 위반 목록을 받아 RenameFixerPlan 을 낸다. 위반마다:
31
+ * 1) 스키마 파일을 camelCase 이름으로 이동(내용 불변).
32
+ * 2) 프로젝트 전체(.ts)에서 그 스키마를 import 하는 곳의 경로를 갱신.
33
+ */
34
+ export async function fixSchemaFilename(issues, cwd) {
35
+ const plans = [];
36
+ // importer 후보를 한 번만 수집(위반이 여러 개여도 재사용).
37
+ let sources;
38
+ for (const issue of issues) {
39
+ const detail = issue.detail;
40
+ if (!issue.file || !detail?.actual || !detail?.expected)
41
+ continue;
42
+ const oldStem = detail.actual.replace(/\.ts$/, '');
43
+ const newStem = detail.expected.replace(/\.ts$/, '');
44
+ if (oldStem === newStem)
45
+ continue;
46
+ const oldRel = issue.file;
47
+ const newRel = oldRel.replace(/[^/]+\.ts$/, `${newStem}.ts`);
48
+ const before = await readFile(join(cwd, oldRel), 'utf8');
49
+ // importer 스캔(최초 1회). node_modules·.gaon·dist·.git 제외.
50
+ if (!sources)
51
+ sources = await collectSources(cwd);
52
+ const refEdits = [];
53
+ for (const s of sources) {
54
+ if (s.rel === oldRel)
55
+ continue; // 이동 대상 자신은 제외
56
+ const after = rewriteSchemaImport(s.text, oldStem, newStem);
57
+ if (after !== undefined)
58
+ refEdits.push({ file: s.rel, before: s.text, after });
59
+ }
60
+ const refSummary = refEdits.length > 0
61
+ ? `참조 ${refEdits.length}곳 경로 갱신(${refEdits.map((r) => r.file).join(', ')})`
62
+ : '참조 갱신 없음(이 스키마를 import 하는 곳을 찾지 못함)';
63
+ plans.push({
64
+ kind: 'rename',
65
+ file: oldRel,
66
+ to: newRel,
67
+ before,
68
+ refEdits,
69
+ summary: `스키마 파일명 관례(§1.1 · camelCase) 정정: '${oldRel}' → '${newRel}'. ${refSummary}.\n` +
70
+ ` 근거: 결정 38 · 스키마 파일명 = 테이블명을 camelCase 로. 테이블명 문자열은 그대로 둡니다.`,
71
+ });
72
+ }
73
+ return plans;
74
+ }
75
+ /** 프로젝트 .ts(선언·테스트 제외) 소스를 수집. importer 참조 스캔용. */
76
+ async function collectSources(cwd) {
77
+ const out = [];
78
+ const skip = new Set(['node_modules', '.gaon', 'dist', '.git']);
79
+ async function walk(dir) {
80
+ let entries;
81
+ try {
82
+ entries = (await readdir(dir, { withFileTypes: true }));
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ for (const e of entries) {
88
+ const p = join(dir, e.name);
89
+ if (e.isDirectory()) {
90
+ if (skip.has(e.name))
91
+ continue;
92
+ await walk(p);
93
+ }
94
+ else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
95
+ const s = await stat(p);
96
+ if (!s.isFile())
97
+ continue;
98
+ out.push({ rel: relative(cwd, p).split('\\').join('/'), text: await readFile(p, 'utf8') });
99
+ }
100
+ }
101
+ }
102
+ await walk(cwd);
103
+ return out;
104
+ }
@@ -0,0 +1,59 @@
1
+ import type { DoctorCheck, DoctorRule } from '../types.js';
2
+ /**
3
+ * 한 파일 안에서 fixer 가 계산한 내용 재작성 계획(가장 흔한 형태).
4
+ * `kind` 생략 = 'rewrite'(하위 호환 — 기존 fixer 는 이 필드를 안 쓴다).
5
+ */
6
+ export interface RewriteFixerPlan {
7
+ readonly kind?: 'rewrite';
8
+ /** 프로젝트 루트 기준 상대 경로(reporter 와 정합). */
9
+ readonly file: string;
10
+ /** 편집 전 원본. rollback · diff 참조용. */
11
+ readonly before: string;
12
+ /** 편집 후 최종 소스. before 와 같으면 스킵(변경 없음). */
13
+ readonly after: string;
14
+ /** 사람용 요약(무엇을 왜 바꿨는지 · 리포트에 그대로 출력). */
15
+ readonly summary: string;
16
+ }
17
+ /**
18
+ * 파일 이름 변경 계획 — 파일을 옮기고, 그 파일을 import 하는 곳의 경로를 함께
19
+ * 고친다. 내용 재작성만으로는 표현할 수 없어(파일 이동 + 다중 파일 참조 갱신)
20
+ * 별도 종류로 둔다. 상위(runDoctorFix)가 이동·참조 편집·백업을 관장한다.
21
+ */
22
+ export interface RenameFixerPlan {
23
+ readonly kind: 'rename';
24
+ /** 현재 경로(프로젝트 상대). 이동 후 삭제된다. */
25
+ readonly file: string;
26
+ /** 새 경로(프로젝트 상대). 내용은 file 과 동일하게 옮겨진다. */
27
+ readonly to: string;
28
+ /** 이동되는 파일의 원본 내용(내용은 바뀌지 않고 경로만 바뀐다). */
29
+ readonly before: string;
30
+ /** 사람용 요약(무엇을 왜 옮겼는지 · 참조 갱신 요약 포함). */
31
+ readonly summary: string;
32
+ /** 이 파일을 import 하는 곳의 경로 갱신(파일 이동에 딸린 참조 수정). */
33
+ readonly refEdits: readonly RefEdit[];
34
+ }
35
+ /** import 경로 갱신 — 한 importer 파일의 before → after. */
36
+ export interface RefEdit {
37
+ /** 프로젝트 루트 기준 상대 경로. */
38
+ readonly file: string;
39
+ readonly before: string;
40
+ readonly after: string;
41
+ }
42
+ /** fixer 가 낸 계획 — 내용 재작성 또는 파일 이름 변경. */
43
+ export type FixerPlan = RewriteFixerPlan | RenameFixerPlan;
44
+ /**
45
+ * 규칙 검사가 낸 DoctorCheck 를 받아 파일별 수정 계획을 낸다. 파일 I/O 는
46
+ * 여기서 하지 않는다(테스트 편의 · 순수 함수). 아무 것도 못 고치면 빈
47
+ * 배열을 낸다.
48
+ *
49
+ * @param issues 규칙이 낸 error/warning check 목록(passed 는 제외됨).
50
+ * @param cwd 프로젝트 루트 절대 경로(파일 읽기용).
51
+ */
52
+ export type Fixer = (issues: readonly DoctorCheck[], cwd: string) => Promise<readonly FixerPlan[]>;
53
+ /** 규칙별 fixer 지원 여부. 리포트가 "수동 수정 필요" 를 판단하는 근거. */
54
+ export interface FixerCapability {
55
+ readonly rule: DoctorRule;
56
+ readonly hasFixer: boolean;
57
+ /** 사람용 짧은 설명(리포트에 그대로). */
58
+ readonly note: string;
59
+ }
@@ -0,0 +1,15 @@
1
+ // @gaonjs/cli · doctor · fixer 공용 타입 (M9-fix · v0.16 §7.5.3)
2
+ //
3
+ // v0.16 §7.5.3 "관례 위반 중 기계적으로 고칠 수 있는 것(파일 위치, 이름
4
+ // 규칙)은 자동 수정한다" 정본에 따라, 각 doctor 규칙에 대응하는 fixer 를
5
+ // 이 자리에 등록한다. 규칙 하나 = fixer 최대 하나. fixer 가 없는 규칙은
6
+ // 리포트에 "수동 수정 필요" 로 표기된다(사용자가 우회 여부를 스스로 판단).
7
+ //
8
+ // 안전 원칙:
9
+ // 1) --fix 는 파괴적 변경을 하지 않는다. 편집한 파일의 원본 백업을
10
+ // 대상 파일 옆에 `<파일>.bak-YYYYMMDD-HHMMSS` 로 남긴다(overwrite X).
11
+ // 2) --yes 없이 --fix 를 주면 dry-run 이다 — 무엇을 고칠지 리포트만.
12
+ // 실 편집은 --fix --yes 조합에서만 일어난다.
13
+ // 3) fixer 는 순수 계산(입력 소스 → 출력 소스)만 담당. 파일 I/O 는
14
+ // 상위(runDoctorFix)가 백업·쓰기·롤백을 관장한다.
15
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { DoctorCheck, RuleReport } from './types.js';
2
+ /**
3
+ * config 파일 하나의 소스에서 자동 import 플러그인 import 를 잡는다
4
+ * (단위 테스트 진입점).
5
+ */
6
+ export declare function inspectConfigForAutoImport(file: string, source: string, cwd: string): DoctorCheck[];
7
+ /** package.json 안의 deps 4종을 훑어 자동 import 플러그인 유무를 낸다. */
8
+ export declare function inspectPackageJson(file: string, source: string, cwd: string): DoctorCheck[];
9
+ /** 프로젝트 루트를 훑어 자동 import 설정·의존을 잡는다. */
10
+ export declare function checkNoAutoImport(cwd: string): Promise<RuleReport>;