@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
@@ -2,7 +2,8 @@ export type { ScaffoldFile } from './controller.js';
2
2
  export { controllerScaffold } from './controller.js';
3
3
  export { modelScaffold, modelScaffoldFiles, schemaScaffold } from './model.js';
4
4
  export { pageScaffold } from './page.js';
5
- export { jobScaffold } from './job.js';
5
+ export { jobScaffold, jobTestScaffold } from './job.js';
6
+ export { appScaffoldFiles, validateAppName } from './app.js';
6
7
  export { inflectModel, toCamel, toPascal, singularize, pluralize, type ModelNames, } from './inflect.js';
7
8
  import type { ScaffoldFile } from './controller.js';
8
9
  export interface WriteResult {
@@ -8,7 +8,8 @@ import { dirname, join, resolve } from 'node:path';
8
8
  export { controllerScaffold } from './controller.js';
9
9
  export { modelScaffold, modelScaffoldFiles, schemaScaffold } from './model.js';
10
10
  export { pageScaffold } from './page.js';
11
- export { jobScaffold } from './job.js';
11
+ export { jobScaffold, jobTestScaffold } from './job.js';
12
+ export { appScaffoldFiles, validateAppName } from './app.js';
12
13
  export { inflectModel, toCamel, toPascal, singularize, pluralize, } from './inflect.js';
13
14
  /**
14
15
  * 파일 계획을 실제로 쓴다. 기본은 기존 파일 skip(멱등 · 사고 방지).
@@ -1,3 +1,8 @@
1
1
  import type { ScaffoldFile } from './controller.js';
2
2
  /** 잡 이름(파스칼) → 파일·잡명 파생. */
3
3
  export declare function jobScaffold(pascalName: string): ScaffoldFile;
4
+ /**
5
+ * 잡 통합 테스트 골격 (결정 42) — 발행→실 처리 검증을 `expectJobProcessed`
6
+ * 한 호출로. 실 NATS 필수(§9 · 목업 금지) — `docker compose up -d nats`.
7
+ */
8
+ export declare function jobTestScaffold(pascalName: string): ScaffoldFile;
@@ -44,3 +44,38 @@ export function jobScaffold(pascalName) {
44
44
  ];
45
45
  return { path: `domain/jobs/${camel}.ts`, contents: lines.join('\n') };
46
46
  }
47
+ /**
48
+ * 잡 통합 테스트 골격 (결정 42) — 발행→실 처리 검증을 `expectJobProcessed`
49
+ * 한 호출로. 실 NATS 필수(§9 · 목업 금지) — `docker compose up -d nats`.
50
+ */
51
+ export function jobTestScaffold(pascalName) {
52
+ const trimmed = pascalName.trim();
53
+ if (!trimmed) {
54
+ throw new Error(`[gaon g job] 잡 이름이 비어 있습니다. 예: gaon g job SendEmail`);
55
+ }
56
+ const pascal = trimmed
57
+ .split(/[_\-\s]+/)
58
+ .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
59
+ .join('');
60
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
61
+ const lines = [
62
+ `// ${pascal} 잡 통합 테스트 — gaon g job (결정 42).`,
63
+ `// 실 NATS JetStream 필수(§9 · 목업 금지): docker compose up -d nats`,
64
+ `import { describe, it } from 'vitest'`,
65
+ `import { connectNats, expectJobProcessed } from 'gaonjs/testing'`,
66
+ `import ${camel} from '../../domain/jobs/${camel}.js'`,
67
+ ``,
68
+ `describe('${pascal} (실 NATS JetStream)', () => {`,
69
+ ` it('발행한 잡이 워커에서 처리된다', async () => {`,
70
+ ` const nats = await connectNats(process.env.NATS_URL ?? 'nats://localhost:4222')`,
71
+ ` try {`,
72
+ ` await expectJobProcessed(${camel}, () => ${camel}.later({ id: 'test' }), { nats })`,
73
+ ` } finally {`,
74
+ ` await nats.close()`,
75
+ ` }`,
76
+ ` })`,
77
+ `})`,
78
+ ``,
79
+ ];
80
+ return { path: `test/integration/${camel}.integration.test.ts`, contents: lines.join('\n') };
81
+ }
@@ -4,13 +4,13 @@
4
4
  // 스키마는 errata E-4 신규 컬럼 타입(decimal · enum · uuid)과 수식어(unique ·
5
5
  // nullable · default) 예시를 담아 새 개발자가 바로 참고할 수 있게 한다.
6
6
  //
7
- // 파일 위치 (CLAUDE.md §2 · rule 5):
8
- // · 스키마 → domain/schema/<name>.ts (테이블 정의 · tables.d.ts 원천)
9
- // · 모델 → domain/models/<name>.ts (Active Record · scopes/methods)
7
+ // 파일 위치 (CLAUDE.md §2 · rule 5 · 파일 네이밍 표 v0.16 §3.4):
8
+ // · 스키마 → domain/schema/<posts>.ts (파일명 = 테이블명 · camelCase 복수)
9
+ // · 모델 → domain/models/<Post>.ts (파일명 = 모델명 · PascalCase 단수)
10
10
  // 앱→도메인 import 만 허용되므로 이 위치가 유일한 정답이다.
11
11
  /** 스키마 스캐폴드 — E-4 컬럼 타입 예시를 함께 담는다. */
12
12
  export function schemaScaffold(names) {
13
- const { pascal, camel, plural } = names;
13
+ const { pascal, plural } = names;
14
14
  const lines = [
15
15
  `// ${pascal} 스키마 — gaon g model (M9-B).`,
16
16
  `// errata E-4 예시: unique · default · enum · nullable. 컬럼은 자유롭게 추가/삭제한다.`,
@@ -34,19 +34,19 @@ export function schemaScaffold(names) {
34
34
  ``,
35
35
  ];
36
36
  return {
37
- path: `domain/schema/${camel}.ts`,
37
+ path: `domain/schema/${plural}.ts`,
38
38
  contents: lines.join('\n'),
39
39
  };
40
40
  }
41
41
  /** 모델 스캐폴드 — scopes 예시를 담는다(체이닝 진입점 도우미). */
42
42
  export function modelScaffold(names) {
43
- const { pascal, camel, plural } = names;
43
+ const { pascal, plural } = names;
44
44
  const lines = [
45
45
  `// ${pascal} 모델 — gaon g model (M9-B).`,
46
46
  `// scopes 는 체인 어느 지점에서든 호출 가능하다(§4.4). E-4 체이닝 예시:`,
47
47
  `// ${pascal}.published().orderBy('createdAt', 'desc').limit(20).all()`,
48
48
  `import { model } from 'gaonjs/data'`,
49
- `import { ${plural} } from '../schema/${camel}.js'`,
49
+ `import { ${plural} } from '../schema/${plural}.js'`,
50
50
  ``,
51
51
  `export const ${pascal} = model(${plural}, {`,
52
52
  ` scopes: {`,
@@ -56,7 +56,7 @@ export function modelScaffold(names) {
56
56
  ``,
57
57
  ];
58
58
  return {
59
- path: `domain/models/${camel}.ts`,
59
+ path: `domain/models/${pascal}.ts`,
60
60
  contents: lines.join('\n'),
61
61
  };
62
62
  }
@@ -1,6 +1,6 @@
1
1
  // 인증 배선 — gaon g auth 스캐폴드.
2
2
  import type { AuthOptions } from 'gaonjs/web'
3
- import { User } from '../../domain/models/user.js'
3
+ import { User } from '../../domain/models/User.js'
4
4
 
5
5
  // 세션에 심긴 userId 로 사용자를 로드한다(§7). web 은 도메인을 loadUser 로 받는다.
6
6
  export const loadUser: AuthOptions['loadUser'] = async (id) =>
@@ -1,6 +1,6 @@
1
1
  // 회원가입 컨트롤러 — gaon g auth 스캐폴드.
2
2
  import { controller, hashPassword } from 'gaonjs/web'
3
- import { User } from '../../../domain/models/user.js'
3
+ import { User } from '../../../domain/models/User.js'
4
4
 
5
5
  export default controller({
6
6
  // GET /registration/new — 회원가입 폼
@@ -1,6 +1,6 @@
1
1
  // 세션 컨트롤러(로그인/로그아웃) — gaon g auth 스캐폴드.
2
2
  import { controller, verifyPassword } from 'gaonjs/web'
3
- import { User } from '../../../domain/models/user.js'
3
+ import { User } from '../../../domain/models/User.js'
4
4
 
5
5
  export default controller({
6
6
  // GET /session/new — 로그인 폼
@@ -1,5 +1,5 @@
1
1
  // 사용자 모델 — gaon g auth 스캐폴드.
2
2
  import { model } from 'gaonjs/data'
3
- import { users } from '../schema/user.js'
3
+ import { users } from '../schema/users.js'
4
4
 
5
5
  export const User = model(users, {})
@@ -0,0 +1,23 @@
1
+ /** 생성할 파일 하나 — path 는 프로젝트 루트 기준 상대 경로(POSIX). */
2
+ export interface ProjectFile {
3
+ readonly path: string;
4
+ readonly contents: string;
5
+ }
6
+ /** 템플릿 렌더 시 치환되는 토큰 값. */
7
+ export interface ProjectTemplateTokens {
8
+ readonly projectName: string;
9
+ readonly gaonjsVersion: string;
10
+ }
11
+ /** 템플릿 문자열의 {{TOKEN}} 을 치환한다. 알 수 없는 토큰은 그대로 둔다. */
12
+ export declare function renderTemplate(raw: string, tokens: ProjectTemplateTokens): string;
13
+ /** 템플릿 폴더를 재귀 스캔해 파일 목록을 만든다(POSIX 경로 · 정렬). */
14
+ export declare function listTemplateFiles(root?: string): string[];
15
+ /**
16
+ * 스캐폴드 파일 전체를 렌더링해 반환한다.
17
+ * `.tpl` 접미사는 벗기고, `{{PROJECT_NAME}}`·`{{GAONJS_VERSION}}` 을 치환한다.
18
+ */
19
+ export declare function renderProjectFiles(tokens: ProjectTemplateTokens): ProjectFile[];
20
+ /** 테스트·검증용 — 템플릿 폴더의 절대 경로. */
21
+ export declare function templateDir(): string;
22
+ /** 렌더링된 내용에 미치환 토큰이 남아 있는지 검사(회귀 방지). */
23
+ export declare function findUnresolvedTokens(contents: string): string[];
@@ -0,0 +1,66 @@
1
+ // @gaonjs/cli · templates/index.ts — 프로젝트 스캐폴드 템플릿 로더 (M9-F).
2
+ //
3
+ // `gaon new <name>` 이 소비하는 파일 트리. 소스 트리(src/templates/project/*)
4
+ // 를 재귀 스캔해 각 `.tpl` 파일의 최종 경로와 렌더된 내용을 반환한다.
5
+ // 렌더는 `{{TOKEN}}` 리터럴 replaceAll — Vue 의 `{{ }}` 보간과 겹치지 않는
6
+ // 고정 리터럴이라 정규식 없이 안전하다(generate.ts 와 동일 관례).
7
+ //
8
+ // 폴더는 관례상 유지되어야 하지만 git 이 빈 폴더를 추적하지 않으므로
9
+ // `.gitkeep.tpl` 을 두어 실 파일 `.gitkeep` 으로 렌더한다. 최종 경로는
10
+ // `.tpl` 접미사를 벗긴 값이다.
11
+ import { readFileSync, readdirSync } from 'node:fs';
12
+ import { dirname, join, posix, relative, sep } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'project');
15
+ /** 템플릿 문자열의 {{TOKEN}} 을 치환한다. 알 수 없는 토큰은 그대로 둔다. */
16
+ export function renderTemplate(raw, tokens) {
17
+ return raw
18
+ .replaceAll('{{PROJECT_NAME}}', tokens.projectName)
19
+ .replaceAll('{{GAONJS_VERSION}}', tokens.gaonjsVersion);
20
+ }
21
+ /** 템플릿 폴더를 재귀 스캔해 파일 목록을 만든다(POSIX 경로 · 정렬). */
22
+ export function listTemplateFiles(root = TEMPLATE_DIR) {
23
+ const out = [];
24
+ const walk = (dir) => {
25
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
26
+ const abs = join(dir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ walk(abs);
29
+ continue;
30
+ }
31
+ if (!entry.isFile())
32
+ continue;
33
+ out.push(abs);
34
+ }
35
+ };
36
+ walk(root);
37
+ return out.sort();
38
+ }
39
+ /**
40
+ * 스캐폴드 파일 전체를 렌더링해 반환한다.
41
+ * `.tpl` 접미사는 벗기고, `{{PROJECT_NAME}}`·`{{GAONJS_VERSION}}` 을 치환한다.
42
+ */
43
+ export function renderProjectFiles(tokens) {
44
+ const paths = listTemplateFiles();
45
+ return paths.map((abs) => {
46
+ const rel = relative(TEMPLATE_DIR, abs).split(sep).join(posix.sep);
47
+ const outPath = rel.endsWith('.tpl') ? rel.slice(0, -'.tpl'.length) : rel;
48
+ const raw = readFileSync(abs, 'utf8');
49
+ return { path: outPath, contents: renderTemplate(raw, tokens) };
50
+ });
51
+ }
52
+ /** 테스트·검증용 — 템플릿 폴더의 절대 경로. */
53
+ export function templateDir() {
54
+ return TEMPLATE_DIR;
55
+ }
56
+ /** 렌더링된 내용에 미치환 토큰이 남아 있는지 검사(회귀 방지). */
57
+ export function findUnresolvedTokens(contents) {
58
+ const re = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
59
+ const found = new Set();
60
+ for (const m of contents.matchAll(re)) {
61
+ // Vue 템플릿의 {{ prop }} (소문자·공백 시작) 은 제외 — 위 정규식이 대문자만
62
+ // 잡으므로 자연스럽게 걸러진다. 남으면 진짜 미치환.
63
+ found.add(m[1]);
64
+ }
65
+ return [...found].sort();
66
+ }
@@ -0,0 +1,85 @@
1
+ // @gaonjs/cli · templates/index.ts — 프로젝트 스캐폴드 템플릿 로더 (M9-F).
2
+ //
3
+ // `gaon new <name>` 이 소비하는 파일 트리. 소스 트리(src/templates/project/*)
4
+ // 를 재귀 스캔해 각 `.tpl` 파일의 최종 경로와 렌더된 내용을 반환한다.
5
+ // 렌더는 `{{TOKEN}}` 리터럴 replaceAll — Vue 의 `{{ }}` 보간과 겹치지 않는
6
+ // 고정 리터럴이라 정규식 없이 안전하다(generate.ts 와 동일 관례).
7
+ //
8
+ // 폴더는 관례상 유지되어야 하지만 git 이 빈 폴더를 추적하지 않으므로
9
+ // `.gitkeep.tpl` 을 두어 실 파일 `.gitkeep` 으로 렌더한다. 최종 경로는
10
+ // `.tpl` 접미사를 벗긴 값이다.
11
+
12
+ import { readFileSync, readdirSync } from 'node:fs'
13
+ import { dirname, join, posix, relative, sep } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ /** 생성할 파일 하나 — path 는 프로젝트 루트 기준 상대 경로(POSIX). */
17
+ export interface ProjectFile {
18
+ readonly path: string
19
+ readonly contents: string
20
+ }
21
+
22
+ /** 템플릿 렌더 시 치환되는 토큰 값. */
23
+ export interface ProjectTemplateTokens {
24
+ readonly projectName: string
25
+ readonly gaonjsVersion: string
26
+ }
27
+
28
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'project')
29
+
30
+ /** 템플릿 문자열의 {{TOKEN}} 을 치환한다. 알 수 없는 토큰은 그대로 둔다. */
31
+ export function renderTemplate(raw: string, tokens: ProjectTemplateTokens): string {
32
+ return raw
33
+ .replaceAll('{{PROJECT_NAME}}', tokens.projectName)
34
+ .replaceAll('{{GAONJS_VERSION}}', tokens.gaonjsVersion)
35
+ }
36
+
37
+ /** 템플릿 폴더를 재귀 스캔해 파일 목록을 만든다(POSIX 경로 · 정렬). */
38
+ export function listTemplateFiles(root: string = TEMPLATE_DIR): string[] {
39
+ const out: string[] = []
40
+ const walk = (dir: string): void => {
41
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
42
+ const abs = join(dir, entry.name)
43
+ if (entry.isDirectory()) {
44
+ walk(abs)
45
+ continue
46
+ }
47
+ if (!entry.isFile()) continue
48
+ out.push(abs)
49
+ }
50
+ }
51
+ walk(root)
52
+ return out.sort()
53
+ }
54
+
55
+ /**
56
+ * 스캐폴드 파일 전체를 렌더링해 반환한다.
57
+ * `.tpl` 접미사는 벗기고, `{{PROJECT_NAME}}`·`{{GAONJS_VERSION}}` 을 치환한다.
58
+ */
59
+ export function renderProjectFiles(tokens: ProjectTemplateTokens): ProjectFile[] {
60
+ const paths = listTemplateFiles()
61
+ return paths.map((abs) => {
62
+ const rel = relative(TEMPLATE_DIR, abs).split(sep).join(posix.sep)
63
+ const outPath = rel.endsWith('.tpl') ? rel.slice(0, -'.tpl'.length) : rel
64
+ const raw = readFileSync(abs, 'utf8')
65
+ return { path: outPath, contents: renderTemplate(raw, tokens) }
66
+ })
67
+ }
68
+
69
+ /** 테스트·검증용 — 템플릿 폴더의 절대 경로. */
70
+ export function templateDir(): string {
71
+ return TEMPLATE_DIR
72
+ }
73
+
74
+ /** 렌더링된 내용에 미치환 토큰이 남아 있는지 검사(회귀 방지). */
75
+ export function findUnresolvedTokens(contents: string): string[] {
76
+ const re = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g
77
+ const found = new Set<string>()
78
+ for (const m of contents.matchAll(re)) {
79
+ // Vue 템플릿의 {{ prop }} (소문자·공백 시작) 은 제외 — 위 정규식이 대문자만
80
+ // 잡으므로 자연스럽게 걸러진다. 남으면 진짜 미치환.
81
+ found.add(m[1]!)
82
+ }
83
+ return [...found].sort()
84
+ }
85
+
@@ -0,0 +1,18 @@
1
+ # {{PROJECT_NAME}} 환경 변수 — cp .env.example .env 후 편집.
2
+ # gaon.config.ts 의 env('KEY') 로 참조된다. gaon dev · gaon serve 가 자동 로드.
3
+
4
+ # DB — docker-compose.yaml 의 postgres 서비스와 정합.
5
+ DATABASE_URL=postgres://{{PROJECT_NAME}}:{{PROJECT_NAME}}@127.0.0.1:5432/{{PROJECT_NAME}}_dev
6
+
7
+ # Redis — 세션 스토어.
8
+ REDIS_URL=redis://127.0.0.1:6379
9
+
10
+ # NATS — 실시간·비동기 백본(§7).
11
+ NATS_URL=nats://127.0.0.1:4222
12
+
13
+ # 세션 · 쿠키 서명 비밀 (32자 이상, 운영은 반드시 교체).
14
+ SESSION_SECRET=change-me-to-a-32-char-random-secret!!
15
+ COOKIE_SECRET=change-me-too-32-char-random-secret!!
16
+
17
+ # 리슨 포트 (gaon serve --port 로 덮음).
18
+ PORT=3000
@@ -0,0 +1,24 @@
1
+ # 의존성 · 빌드 산출물
2
+ node_modules/
3
+ dist/
4
+ *.tsbuildinfo
5
+
6
+ # 자동 생성 타입 브리지(§6.3) — gaon check / gaon dev 가 재생성한다.
7
+ .gaon/
8
+
9
+ # 환경 변수
10
+ .env
11
+ .env.local
12
+ .env.*.local
13
+
14
+ # 로그 · 캐시
15
+ *.log
16
+ .DS_Store
17
+ .cache/
18
+ coverage/
19
+
20
+ # 에디터
21
+ .idea/
22
+ .vscode/*
23
+ !.vscode/settings.json
24
+ !.vscode/launch.json
@@ -0,0 +1,4 @@
1
+ # pnpm 로컬 설정(선택). 대부분의 pnpm 설정은 pnpm-workspace.yaml 로
2
+ # 이동했다(구 package.json 의 pnpm 필드도 마찬가지). 여기는 로그·레지스트리
3
+ # 처럼 파일 단위로 다르게 둘 수 있는 것만 남긴다.
4
+ loglevel=info
@@ -0,0 +1,210 @@
1
+ # AGENTS.md — Gaon 프로젝트 AI 개발자 지침 (코어)
2
+
3
+ 이 문서는 **AI 코딩 에이전트**(Claude · Codex · Cursor · Copilot 등)와
4
+ 사람 개발자가 Gaon 프로젝트에서 작업할 때 참조하는 관례의 진입점이다.
5
+ 정본은 설계 문서(v0.15 동결 + errata E-1~E-5, v0.16 편입)이며, 관례
6
+ 문서는 **2층 구조**다 (결정 40):
7
+
8
+ - **이 파일 (코어)** — 절대 규칙 · 로직 배치 판단표 · 검증 루프 ·
9
+ 카테고리 색인. 여기엔 요약만 있다.
10
+ - **`agents/*.md` (카테고리)** — 시그니처·체이닝 표·정본 예시·함정의
11
+ 정본. **해당 영역 파일을 만지기 전에 반드시 그 카테고리 문서를 읽는다.**
12
+
13
+ **대상 프로젝트 = Gaon 프레임웍으로 만든 사용자 프로젝트**(`gaonjs`
14
+ 설치 후 `gaon new` 로 생성한 앱).
15
+
16
+ ## 0. 카테고리 색인 — 작업 전에 반드시 읽어라
17
+
18
+ 작업이 아래 영역에 걸치면, 코드를 만지기 **전에** 해당 파일을 읽는다.
19
+ 코어에는 요약만 있다 — 메서드 시그니처·표·정본 예시는 전부 카테고리
20
+ 파일에 있고, 표에 없는 API 를 추측하면 실패한다.
21
+
22
+ | 작업 영역 | 먼저 읽을 파일 |
23
+ |---|---|
24
+ | 스키마 · 모델 · 관계 · 쿼리/체이닝 · 서비스 · 마이그레이션 | `agents/data.md` |
25
+ | 라우트 · 컨트롤러 · params · JSON 액션 · 인증/비밀번호 | `agents/web.md` |
26
+ | 페이지 · 컴포넌트 · 컴포저블 · 레이아웃 · `api()` · bigint key | `agents/frontend.md` |
27
+ | 잡 · 이벤트 · 리스너 · 아웃박스 · 스케줄 | `agents/async.md` |
28
+ | 채널 · 프레즌스 · 허브 | `agents/realtime.md` |
29
+ | 테스트 작성·실행 (실 인프라 · `expectJobProcessed`) | `agents/testing.md` |
30
+ | 보안 기본값 · 탈출구(v-html · raw SQL) 사용 | `agents/security.md` |
31
+
32
+ 예: 회원가입 세로 조각(스키마+서비스+잡+컨트롤러+테스트)이면
33
+ `data · web · async · testing` 네 파일을 먼저 읽는다.
34
+
35
+ ## 1. The One Way 원칙
36
+
37
+ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍"** 이다
38
+ (v0.15 §1.2). 우선순위는 **AI 첫 시도 성공률 > 개발자 편의 > 구현
39
+ 편의** — 선택지가 생기면 정답이 하나가 되는 쪽을 고른다.
40
+
41
+ 1. **The One Way** — 모든 문제에 공식 답이 하나. 선택지는 탈출구
42
+ (escape hatch)로만 남긴다.
43
+ 2. **관례가 곧 문서** — 파일 위치·이름이 곧 동작. 설정은 관례를
44
+ 벗어날 때만 쓴다.
45
+ 3. **마법은 타입 추론으로** — 데코레이터·DI 컨테이너 없음. 스키마 →
46
+ 모델 → 컨트롤러 → Vue 까지 **타입 추론만**으로 흐른다 (`.gaon/`
47
+ 생성 파일만 예외 · 프레임웍이 관리).
48
+
49
+ **파일 하나 = 한 개념 · 명시적 import**. Nuxt식 자동 import 는 넣지
50
+ 않는다 (errata E-5 §2.4).
51
+
52
+ ## 2. 절대 규칙
53
+
54
+ 1. **TypeScript 전용 · 함수/객체 스타일.** JS 파일 추가 금지, 클래스형·
55
+ 데코레이터 금지 — `model()`·`controller()`·`job()`·`service()`·
56
+ `channel()` 함수형 API 만.
57
+ 2. **파사드 import 만.** 프레임웍 심볼은 `gaonjs/*` (`gaonjs/web`·
58
+ `gaonjs/data`·`gaonjs/vue`·`gaonjs/async`·`gaonjs/service`·
59
+ `gaonjs/testing`)에서 import 한다. `@gaonjs/*`(내부 스코프)·
60
+ `@inertiajs/vue3`(어댑터 내부 의존)는 앱 코드에서 직접 import 금지.
61
+ (설치명 `gaonjs` · CLI 명령 `gaon` — errata E-1.)
62
+ 3. **의존 방향 4규칙** (doctor 강제): ① 앱→`domain/` 허용 ②
63
+ `domain/`→앱 금지 ③ 앱→앱 금지 ④ 앱→`shared/` 허용 — `shared/` 는
64
+ 앱 import 금지, `domain/` 은 **타입 import 만** (공용 컴포넌트는
65
+ props 로만 받는 순수 UI).
66
+ 4. **보안 기본값(CORS·rate limit·CSRF)은 기본 켬.** 끄는 것은 명시적
67
+ 설정으로만 (`agents/security.md`).
68
+ 5. **테스트는 Docker 실인프라 필수** — DB·NATS 목업·인메모리 대체 절대
69
+ 금지 (§9 · `agents/testing.md`).
70
+ 6. **인증·폼은 Inertia SPA 방식** (SSR 아님). 폼은 `Inertia.post()` →
71
+ 서버 redirect. REST + `fetch()` 는 **API 앱(JWT) 전용**.
72
+ 7. **한 액션은 한 종류 응답만** (render 또는 JSON 또는 redirect —
73
+ 혼용 금지 · doctor response-mixing).
74
+ 8. **`.gaon/` 자동 생성 파일 편집 금지** — `routes.d.ts`·`tables.d.ts`
75
+ 는 `gaon check`/`gaon dev` 가 재생성한다.
76
+
77
+ ### 2.1 파일 네이밍 표 (2026-07-24 승인 · 벤치마크 R1 실측 고정)
78
+
79
+ "Pascal 인가 camel 인가" 를 추측하지 않는다 — 아래 표가 전부다.
80
+
81
+ | 대상 | 파일명 | 예시 |
82
+ |---|---|---|
83
+ | 모델 | **PascalCase** | `domain/models/Post.ts` |
84
+ | 스키마 | **camelCase** (테이블명) | `domain/schema/posts.ts` · `posts_tags`→`postsTags.ts` |
85
+ | 서비스 | **camelCase** | `domain/services/registerUser.ts` |
86
+ | 잡 | **camelCase** | `domain/jobs/sendWelcomeMail.ts` |
87
+ | 이벤트 | **camelCase** | `domain/events/orderPlaced.ts` |
88
+ | 리스너 | **camelCase** | `domain/listeners/notifyAdmin.ts` |
89
+ | 메일 | **camelCase** | `domain/mails/welcome.ts` |
90
+ | 컨트롤러 | **camelCase** (액션 단위) | `apps/web/controllers/posts.ts` |
91
+ | Vue 페이지 | **PascalCase** (Route 이름) | `apps/web/pages/Posts/Index.vue` |
92
+ | Vue 컴포넌트 | **PascalCase** | `apps/web/components/PostCard.vue` |
93
+ | 컴포저블 | **use** + PascalCase | `apps/web/composables/usePostSearch.ts` |
94
+ | 레이아웃 | **PascalCase** | `apps/web/layouts/Default.vue` |
95
+ | 채널 | **camelCase** | `apps/web/channels/chatMessages.ts` |
96
+ | 마이그레이션 | **timestamp_action** | `db/migrations/20260724_add_posts.ts` |
97
+
98
+ - 다단어 테이블의 스키마 **파일명**은 camelCase (`posts_tags` →
99
+ `postsTags.ts`) — 파일 **안**의 `table('posts_tags', …)` 문자열은
100
+ 스네이크 그대로.
101
+
102
+ ### 2.2 `gaon doctor` 검사 9종
103
+
104
+ 1. `response-mixing` — 한 액션 안 render/JSON/redirect 혼용 (E-3)
105
+ 2. `n-plus-one` — include 미사용 · loop 안 관계 호출 (E-4)
106
+ 3. `dependency-direction` — 의존 방향 4규칙 위반
107
+ 4. `connections` — 커넥션 간 belongsTo · service 트랜잭션 (§4.5)
108
+ 5. `migration-diff` — 스키마 vs DB 상태 불일치
109
+ 6. `shared-composable-purity` — shared 안 `api`/`pageProps` import (결정 25)
110
+ 7. `no-auto-import` — 자동 import 설정 (E-5 §2.4)
111
+ 8. `schema-filename` — 스키마 파일명 camelCase 관례 (결정 38 · `--fix` 지원)
112
+ 9. `agents-doc-index` — 이 문서 색인(§0) ↔ `agents/` 실 파일 불일치 (결정 40)
113
+
114
+ ## 3. 로직 배치 One Way 판단표
115
+
116
+ ### 3.1 서버 로직 (정본 §5.3 원문)
117
+
118
+ > 1. 한 모델 안에서 끝나는 로직 → **모델 메서드**
119
+ > 2. 여러 모델·외부 API·트랜잭션이 얽히는 작업 흐름 → **`domain/services/`**
120
+ > 3. 컨트롤러에는 비즈니스 로직을 두지 않는다 — HTTP와의 **번역만**
121
+
122
+ ### 3.2 프론트 로직 (errata E-5 §2.1 원문 · 결정 25)
123
+
124
+ > 1. 컴포저블에는 **프론트 전용 로직만** — UI 상태, 브라우저 API,
125
+ > `api()` 호출 래핑, 채널 구독 래핑.
126
+ > 2. 비즈니스 로직(도메인 규칙·계산·트랜잭션)은 컴포저블에 두지 않는다
127
+ > — 서버의 `domain/`(모델 메서드·서비스)에만.
128
+ > 3. 한 컴포넌트 안에서만 쓰는 상태는 컴포저블로 뽑지 않는다 — 그냥
129
+ > `<script setup>` 에. 컴포저블은 **재사용될 때만**.
130
+
131
+ ### 3.3 데이터 경로 4종 (errata E-3 §2 원문)
132
+
133
+ | 상황 | 경로 | 근거 |
134
+ |---|---|---|
135
+ | 지금 페이지의 데이터를 다시 받기 (필터 변경·새로고침·무한 스크롤) | **Inertia partial reload** — 같은 액션 재호출, 필요한 props만 | §6.1 |
136
+ | 서버가 먼저 밀어주는 데이터 (알림·채팅·접속자) | **채널/프레즌스** (`agents/realtime.md`) | §7 |
137
+ | 페이지와 무관한 데이터 요청 (자동완성·옵션 조회 등 앱 내부용) | **JSON 액션 + `api()` 클라이언트** (`agents/web.md`·`agents/frontend.md`) | E-3 |
138
+ | 외부에 공개하는 API (모바일 앱·서드파티) | **별도 API 앱 + JWT 옵션** | §3, §7 |
139
+
140
+ ### 3.4 잡 발행 위치 (결정 32)
141
+
142
+ 잡 `.later(...)` 은 **컨트롤러 · 서비스 · 리스너 어디서든** 발행 가능 —
143
+ 위치를 강제하지 않는다 (`agents/async.md` §2). DB 커밋 정합이 필요하면
144
+ 서비스 `afterCommit()` 또는 아웃박스.
145
+
146
+ ## 4. 검증 루프
147
+
148
+ 작업마다 실행한다:
149
+
150
+ ```bash
151
+ gaon check # .gaon 재생성 → typecheck + vue-tsc + build (+doctor)
152
+ gaon test # vitest — DB·NATS 는 실 인프라 (agents/testing.md)
153
+ gaon doctor # 정적 검사 8종 (§2.2)
154
+ ```
155
+
156
+ ### 4.1 CLI 명령 (전 명령 `--json` 지원)
157
+
158
+ | 명령 | 역할 |
159
+ |---|---|
160
+ | `gaon new <name>` | 프로젝트 스캐폴드 |
161
+ | `gaon dev` | 통합 개발 오케스트레이션 (Docker·`.gaon` 재생성·워처) |
162
+ | `gaon serve` / `work` / `hub` | 운영 프로세스 3종 (웹 · 워커 · 실시간 허브) |
163
+ | `gaon g <type> <name>` | 스캐폴드: `auth`·`controller`·`model`·`page`·`job` |
164
+ | `gaon db <sub>` | `diff`·`migrate`(`down`)·`status`·`reset`·`seed` (`agents/data.md` §10) |
165
+ | `gaon check` / `test` / `doctor` | 검증 루프 |
166
+ | `gaon console` | 프로젝트 컨텍스트 REPL |
167
+ | `gaon jobs` | DLQ 조회·재적재 |
168
+ | `gaon mcp` | 내장 MCP 서버 — 도구: `list_routes`·`get_schema`·`run_migration`·`run_tests`·`read_agent_doc` |
169
+
170
+ `gaon mcp` 는 프레임웍이 자신을 AI 도구로 노출한다 (§7.5.3) — grep
171
+ 으로 더듬는 대신 프레임웍에게 직접 묻는다. `read_agent_doc` 은 §0
172
+ 카테고리 문서를 조회한다 (결정 40).
173
+
174
+ ## 5. npm 배포본 (2026-07-23 실측 · `npm view <pkg> version`)
175
+
176
+ | 패키지 | 버전 | 역할 |
177
+ |---|---|---|
178
+ | `gaonjs` | 0.6.0 | 파사드(설치 단위) · CLI `gaon` |
179
+ | `@gaonjs/cli` | 0.5.0 | 제너레이터·스캐폴딩·명령 라우팅 |
180
+ | `@gaonjs/data` | 0.3.0 | 스키마 DSL · 모델 · 마이그레이션 |
181
+ | `@gaonjs/config` | 0.1.0 | `gaon.config.ts`·`app.config.ts` |
182
+ | `@gaonjs/web` | 0.3.0 | Fastify 웹 레이어 · 인증 · JSON 액션 |
183
+ | `@gaonjs/vue` | 0.2.0 | Vue 어댑터 · pageProps · 타입드 `api()` |
184
+ | `@gaonjs/async` | 0.2.2 | 채널 · 프레즌스 · 허브 · 잡 · 스케줄 |
185
+ | `@gaonjs/core` | 0.1.4 | 메타데이터 · 직렬화 프리미티브(Hidden) |
186
+ | `@gaonjs/mail` / `storage` / `i18n` | 0.1.0 | 메일 · 파일 스토리지 · 다국어 |
187
+
188
+ ## 6. 원칙 · 엄수
189
+
190
+ - **추론 금지 · 사실에 입각.** 확인 안 된 것은 실행·측정으로 검증
191
+ 하거나 사용자에게 묻는다. 표에 없는 API 를 추측해서 쓰지 않는다 —
192
+ 카테고리 문서(§0)를 먼저 읽는다.
193
+ - **무조건 긍정 금지.** 문제가 있으면 문제라고 말하고, 비용·제약이
194
+ 있으면 반영 전에 명시한다.
195
+ - **에러 메시지는 수리 안내서.** "무엇이 잘못됐다" 가 아니라 → "어느
196
+ 파일에 무엇을 추가/수정하고 어떤 명령을 실행하라" 까지 (v0.15 §7.5.3).
197
+ - **관례가 곧 문서** — 관례를 어길 때만 설정을 쓴다.
198
+ - **정본·정오표 원문 우선.** 이 문서·카테고리 문서와 정본이 충돌하면
199
+ 정본·최신 정오표를 따른다.
200
+
201
+ ## 7. 참고 문서
202
+
203
+ - 설계 정본: `docs/gaondesignv0.15.md` (동결) + errata E-1~E-5
204
+ (E-1 파사드명 · E-2 실시간 TCP · E-3 JSON 액션/params · E-4 컬럼·
205
+ 체이닝 · E-5 컴포저블·레이아웃).
206
+ - 가이드: `docs/guides/*.md` (getting-started · data · data-flow ·
207
+ serialization · authentication · realtime · async · pipeline ·
208
+ operations · configuration).
209
+ - 프레임웍 구현 저장소의 AI 지침은 `CLAUDE.md` — 이 문서와 대상이
210
+ 다르다 (CLAUDE = 프레임웍 구현자용, AGENTS = 프레임웍 사용자용).