@gaonjs/cli 0.3.0 → 0.5.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 (81) hide show
  1. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
  2. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
  3. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
  4. package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
  5. package/dist/commands/check.d.ts +31 -0
  6. package/dist/commands/check.js +223 -0
  7. package/dist/commands/console.d.ts +46 -0
  8. package/dist/commands/console.js +129 -0
  9. package/dist/commands/db.d.ts +20 -0
  10. package/dist/commands/db.js +74 -0
  11. package/dist/commands/dev.d.ts +68 -0
  12. package/dist/commands/dev.js +287 -0
  13. package/dist/commands/new.d.ts +45 -0
  14. package/dist/commands/new.js +274 -0
  15. package/dist/commands/test.d.ts +11 -0
  16. package/dist/commands/test.js +119 -0
  17. package/dist/db/diff.d.ts +17 -0
  18. package/dist/db/diff.js +57 -0
  19. package/dist/db/index.d.ts +4 -0
  20. package/dist/db/index.js +8 -0
  21. package/dist/db/migrate.d.ts +16 -0
  22. package/dist/db/migrate.js +173 -0
  23. package/dist/db/reset.d.ts +18 -0
  24. package/dist/db/reset.js +150 -0
  25. package/dist/db/resolve.d.ts +32 -0
  26. package/dist/db/resolve.js +130 -0
  27. package/dist/dev/console.d.ts +39 -0
  28. package/dist/dev/console.js +100 -0
  29. package/dist/dev/docker.d.ts +52 -0
  30. package/dist/dev/docker.js +163 -0
  31. package/dist/dev/index.d.ts +14 -0
  32. package/dist/dev/index.js +10 -0
  33. package/dist/dev/tsc.d.ts +41 -0
  34. package/dist/dev/tsc.js +127 -0
  35. package/dist/dev/watcher.d.ts +50 -0
  36. package/dist/dev/watcher.js +95 -0
  37. package/dist/dev.d.ts +1 -16
  38. package/dist/dev.js +10 -66
  39. package/dist/doctor/no-auto-import.d.ts +10 -0
  40. package/dist/doctor/no-auto-import.js +158 -0
  41. package/dist/doctor/reporter.d.ts +1 -1
  42. package/dist/doctor/reporter.js +16 -3
  43. package/dist/doctor/setup.d.ts +26 -0
  44. package/dist/doctor/setup.js +52 -0
  45. package/dist/doctor/shared-composable-purity.d.ts +8 -0
  46. package/dist/doctor/shared-composable-purity.js +164 -0
  47. package/dist/doctor/types.d.ts +14 -1
  48. package/dist/doctor/types.js +10 -5
  49. package/dist/doctor.d.ts +21 -5
  50. package/dist/doctor.js +77 -8
  51. package/dist/index.d.ts +9 -2
  52. package/dist/index.js +175 -27
  53. package/dist/templates/index.d.ts +23 -0
  54. package/dist/templates/index.js +66 -0
  55. package/dist/templates/index.ts +85 -0
  56. package/dist/templates/project/.env.example.tpl +18 -0
  57. package/dist/templates/project/.gitignore.tpl +24 -0
  58. package/dist/templates/project/.npmrc.tpl +4 -0
  59. package/dist/templates/project/CLAUDE.md.tpl +119 -0
  60. package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
  61. package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
  62. package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
  63. package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
  64. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
  65. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
  66. package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
  67. package/dist/templates/project/docker-compose.yaml.tpl +73 -0
  68. package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
  69. package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
  70. package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
  71. package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
  72. package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
  73. package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
  74. package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
  75. package/dist/templates/project/gaon.config.ts.tpl +27 -0
  76. package/dist/templates/project/package.json.tpl +27 -0
  77. package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
  78. package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
  79. package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
  80. package/dist/templates/project/tsconfig.json.tpl +25 -0
  81. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,27 +1,36 @@
1
1
  /**
2
2
  * @gaonjs/cli — Gaon CLI 구현.
3
3
  *
4
- * 인자 없이 실행하면 로드맵·개발 상태를 출력하는 스텁이다. M3 부터
5
- * `gaon dev` 가 실제로 동작한다.gaon 타입 브리지(tables·routes)를
6
- * 감시·재생성한다(dev.ts). 모든 명령은 `--json` 출력을 함께 제공한다
7
- * (CLAUDE.md §4). 스캐폴딩·나머지 제너레이터는 이후 마일스톤이다.
4
+ * 인자 없이 실행하면 로드맵·개발 상태를 출력하는 스텁이다. M9-C 부터
5
+ * `gaon dev` 가 통합 개발 오케스트레이션을 담당한다 Docker Compose
6
+ * 자동 기동 + .gaon 타입 브리지 재생성 + serve 자식 프로세스 + tsc/vue-tsc
7
+ * --watch + 소스 변경 시 서버 재시작(commands/dev.ts). 모든 명령은 `--json`
8
+ * 출력을 함께 제공한다 (CLAUDE.md §4).
8
9
  *
9
10
  * 표시 버전은 호출자(파사드)가 주입한다 — 사용자가 설치한 패키지
10
11
  * (`gaonjs`) 버전을 그대로 보여주기 위함. 미주입 시 core 버전을 쓴다.
11
12
  */
12
13
  import { MILESTONES, VERSION, HOMEPAGE } from "@gaonjs/core";
13
- import { runDevCommand } from "./dev.js";
14
- import { runCheckCommand } from "./check.js";
14
+ import { runDevCommand } from "./commands/dev.js";
15
+ import { runCheckCommand } from "./commands/check.js";
16
+ import { runNewCommand } from "./commands/new.js";
17
+ import { runConsoleCommand } from "./commands/console.js";
18
+ import { runTestCommand } from "./commands/test.js";
15
19
  import { runGenerateAuthCommand } from "./generate.js";
16
20
  import { runGenerateCommand } from "./commands/g.js";
17
21
  import { runHubCommand } from "./hub.js";
18
22
  import { runServeCommand } from "./serve.js";
19
23
  import { runWorkCommand } from "./work.js";
20
24
  import { runJobsCommand } from "./jobs.js";
21
- import { runDbSeedCommand } from "./db.js";
25
+ import { runDbCommand } from "./commands/db.js";
22
26
  import { runDoctorCommand } from "./doctor.js";
23
- export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
24
- export { runCheck, runCheckCommand, } from "./check.js";
27
+ export { startDev, resolveDevLayout, } from "./dev.js";
28
+ export { runDevCommand } from "./commands/dev.js";
29
+ export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, } from "./dev/index.js";
30
+ export { runCheckCommand, } from "./commands/check.js";
31
+ export { runNewCommand } from "./commands/new.js";
32
+ export { runConsoleCommand } from "./commands/console.js";
33
+ export { runTestCommand } from "./commands/test.js";
25
34
  export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, } from "./generate.js";
26
35
  export { runGenerateCommand, planScaffold, parseGenerateArgs, } from "./commands/g.js";
27
36
  export { runHubCommand } from "./hub.js";
@@ -29,6 +38,8 @@ export { runServeCommand } from "./serve.js";
29
38
  export { runWorkCommand } from "./work.js";
30
39
  export { runJobsCommand } from "./jobs.js";
31
40
  export { runDbSeedCommand, loadSeed } from "./db.js";
41
+ export { runDbCommand, } from "./commands/db.js";
42
+ export { runDbDiff, runDbMigrate, runDbReset, resolveDbTarget, } from "./db/index.js";
32
43
  export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, } from "./doctor.js";
33
44
  export { loadDomain } from "./domain.js";
34
45
  /** `--json` 출력용 구조화 리포트. */
@@ -74,12 +85,18 @@ function renderHelp(version = VERSION) {
74
85
  "",
75
86
  " 사용법:",
76
87
  " gaon 로드맵과 개발 상태를 출력",
77
- " gaon dev .gaon 타입 브리지를 감시·재생성 (스키마·라우트)",
78
- " gaon dev --json 재생성 이벤트를 JSON 으로 출력",
88
+ " gaon new <name> 새 프로젝트 스캐폴드 (파일 → 설치 → git · --skip-install · --skip-git · --pm <이름>)",
89
+ " gaon dev 개발 스택 통합 (Docker · .gaon · serve · tsc/vue-tsc · 재시작 워처)",
90
+ " gaon dev --stop-docker Ctrl+C 시 Docker Compose 도 down",
91
+ " gaon dev --no-watch|--no-tsc|--no-vue-tsc|--no-docker 개별 debug 옵션",
92
+ " gaon dev --port <n> --host <h> serve 리슨 지정",
93
+ " gaon dev --json 통합 콘솔을 JSON 라인으로 출력(자동화)",
79
94
  " gaon serve 웹 서버 부팅 (gaon.config.ts 자동 배선 · Fastify listen)",
80
95
  " gaon serve --port <n> --host <h> 리슨 포트·호스트 (config 값을 덮음)",
81
- " gaon check .gaon 재생성 타입 검사 (CI·AI 정합)",
82
- " gaon doctor 정적 검사 (5 검사 · 응답 혼용·N+1·의존 방향·커넥션·마이그)",
96
+ " gaon check typecheck · vue-tsc · build 통합 검사 (--only <step> · --include-doctor)",
97
+ " gaon console 프로젝트 컨텍스트 REPL (--no-config)",
98
+ " gaon test 테스트 러너 (--scope unit|integration|all · -- vitest 인자)",
99
+ " gaon doctor 정적 검사 (7 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import)",
83
100
  " gaon doctor --json 자동화용 JSON 출력",
84
101
  " gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
85
102
  " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
@@ -92,6 +109,11 @@ function renderHelp(version = VERSION) {
92
109
  " gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
93
110
  " gaon jobs list --failed DLQ(실패 잡) 목록",
94
111
  " 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 만 출력",
115
+ " gaon db reset --yes DROP ALL → 재마이그 → seed (--yes 필수 · production 거부)",
116
+ " gaon db seed domain/seed.ts 실행 (M8)",
95
117
  " gaon --json 같은 정보를 JSON 으로 출력",
96
118
  " gaon --version 버전 출력",
97
119
  " gaon --help 이 도움말",
@@ -111,6 +133,8 @@ export function parseDoctorChecks(argv) {
111
133
  "dependency-direction",
112
134
  "connections",
113
135
  "migration-diff",
136
+ "shared-composable-purity",
137
+ "no-auto-import",
114
138
  ];
115
139
  const isKnown = (s) => known.includes(s);
116
140
  const out = [];
@@ -127,10 +151,26 @@ export function parseDoctorChecks(argv) {
127
151
  /** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
128
152
  export function runCli(argv, opts = {}) {
129
153
  const version = opts.version ?? VERSION;
130
- // `gaon dev` — .gaon 갱신 파이프라인(장기 실행). 워처가 활성 핸들로
131
- // 프로세스를 살려 두고, SIGINT 종료된다. 에러는 stderr + exit 1.
154
+ // `gaon dev` — 통합 개발 오케스트레이션(M9-C · v0.15 §13.5). Docker Compose
155
+ // 자동 기동 + .gaon 재생성 + serve 자식 + tsc/vue-tsc watch + 서버 재시작 워처.
156
+ // SIGINT/SIGTERM 시 순서대로 정리(serve → tsc → 워처 → Docker[--stop-docker 시]).
157
+ // 개별 debug 옵션은 --no-watch / --no-tsc / --no-vue-tsc / --no-docker 뿐.
132
158
  if (argv[0] === "dev") {
133
- void runDevCommand({ json: argv.includes("--json") }).catch((err) => {
159
+ const portIdx = argv.indexOf("--port");
160
+ const hostIdx = argv.indexOf("--host");
161
+ const port = portIdx >= 0 ? Number(argv[portIdx + 1]) : undefined;
162
+ const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;
163
+ void runDevCommand({
164
+ json: argv.includes("--json"),
165
+ stopDocker: argv.includes("--stop-docker"),
166
+ noWatch: argv.includes("--no-watch"),
167
+ noTsc: argv.includes("--no-tsc"),
168
+ noVueTsc: argv.includes("--no-vue-tsc"),
169
+ noDocker: argv.includes("--no-docker"),
170
+ timestamp: argv.includes("--timestamp"),
171
+ port,
172
+ host,
173
+ }).catch((err) => {
134
174
  const msg = err instanceof Error ? err.message : String(err);
135
175
  process.stderr.write(` ✗ gaon dev 실패: ${msg}\n`);
136
176
  process.exitCode = 1;
@@ -150,9 +190,21 @@ export function runCli(argv, opts = {}) {
150
190
  });
151
191
  return;
152
192
  }
153
- // `gaon check` — .gaon 재생성 타입 검사(§13.4-5). 종료 코드로 결과 전달.
193
+ // `gaon check` — typecheck · vue-tsc · build (·doctor) 통합 검사(M9-G).
194
+ // package.json 스크립트 관례 재사용. --only <step> 로 단일 단계, --include-doctor
195
+ // 로 doctor 포함. 종료 코드로 결과 전달.
154
196
  if (argv[0] === "check") {
155
- void runCheckCommand({ json: argv.includes("--json") })
197
+ const knownSteps = ["typecheck", "vue-tsc", "build", "doctor"];
198
+ const onlyIdx = argv.indexOf("--only");
199
+ const onlyRaw = onlyIdx >= 0 ? argv[onlyIdx + 1] : undefined;
200
+ const only = onlyRaw && knownSteps.includes(onlyRaw)
201
+ ? onlyRaw
202
+ : undefined;
203
+ void runCheckCommand({
204
+ json: argv.includes("--json"),
205
+ only,
206
+ includeDoctor: argv.includes("--include-doctor"),
207
+ })
156
208
  .then((code) => {
157
209
  process.exitCode = code;
158
210
  })
@@ -164,11 +216,16 @@ export function runCli(argv, opts = {}) {
164
216
  return;
165
217
  }
166
218
  // `gaon doctor` — 정적 검사(M9-E · 5 검사). --check=<이름>[,<이름>...] 로
167
- // 선택 실행, --json 은 자동화 파싱용. exit code: errors.length > 0 이면 1.
219
+ // 선택 실행, --json 은 자동화 파싱용.
220
+ // exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
168
221
  if (argv[0] === "doctor") {
169
222
  const checks = parseDoctorChecks(argv);
170
223
  void runDoctorCommand({ json: argv.includes("--json"), checks })
171
224
  .then((result) => {
225
+ if (result.fatal) {
226
+ process.exitCode = 2;
227
+ return;
228
+ }
172
229
  process.exitCode = result.errors.length > 0 ? 1 : 0;
173
230
  })
174
231
  .catch((err) => {
@@ -211,17 +268,33 @@ export function runCli(argv, opts = {}) {
211
268
  });
212
269
  return;
213
270
  }
214
- // `gaon db seed`domain/seed.ts 실행(§7 M8). diff·migrate 는 M9 CLI 완성.
215
- if (argv[0] === "db" && argv[1] === "seed") {
216
- const json = argv.includes("--json");
217
- void runDbSeedCommand({ json })
218
- .then((res) => {
219
- process.stdout.write((json ? JSON.stringify(res.json) : res.text) + "\n");
220
- process.exitCode = res.exitCode;
271
+ // `gaon db <sub>`diff · migrate · reset · seed (§7 M8/M9-D).
272
+ if (argv[0] === "db") {
273
+ const sub = argv[1];
274
+ const known = ["diff", "migrate", "reset", "seed"];
275
+ if (!sub || !known.includes(sub)) {
276
+ process.stderr.write(` ✗ 없는 db 서브커맨드: ${sub ?? "(없음)"}\n` +
277
+ ` → 지원: gaon db diff | migrate | reset | seed\n` +
278
+ ` → 옵션: --json · --db <키> · --config <path> · --yes · --dry-run\n`);
279
+ process.exitCode = 1;
280
+ return;
281
+ }
282
+ const dbIdx = argv.indexOf("--db");
283
+ const cfgIdx = argv.indexOf("--config");
284
+ const dbOpts = {
285
+ json: argv.includes("--json"),
286
+ db: dbIdx >= 0 ? argv[dbIdx + 1] : undefined,
287
+ config: cfgIdx >= 0 ? argv[cfgIdx + 1] : undefined,
288
+ yes: argv.includes("--yes"),
289
+ dryRun: argv.includes("--dry-run"),
290
+ };
291
+ void runDbCommand(sub, dbOpts)
292
+ .then((code) => {
293
+ process.exitCode = code;
221
294
  })
222
295
  .catch((err) => {
223
296
  const msg = err instanceof Error ? err.message : String(err);
224
- process.stderr.write(` ✗ gaon db seed 실패: ${msg}\n`);
297
+ process.stderr.write(` ✗ gaon db ${sub} 실패: ${msg}\n`);
225
298
  process.exitCode = 1;
226
299
  });
227
300
  return;
@@ -281,6 +354,81 @@ export function runCli(argv, opts = {}) {
281
354
  process.exitCode = 1;
282
355
  return;
283
356
  }
357
+ // `gaon new <name>` — 프로젝트 스캐폴드(M9-F). 파일 생성 → 의존성 설치 → git init.
358
+ if (argv[0] === "new") {
359
+ const rest = argv.slice(1);
360
+ let name;
361
+ for (const a of rest) {
362
+ if (!a.startsWith("--") && name === undefined)
363
+ name = a;
364
+ }
365
+ if (!name) {
366
+ process.stderr.write(" ✗ gaon new: 프로젝트 이름이 없습니다.\n → 예: gaon new demo\n");
367
+ process.exitCode = 1;
368
+ return;
369
+ }
370
+ const pmIdx = rest.indexOf("--pm");
371
+ const pmRaw = pmIdx >= 0 ? rest[pmIdx + 1] : undefined;
372
+ const packageManager = pmRaw === "pnpm" || pmRaw === "npm" || pmRaw === "yarn" ? pmRaw : undefined;
373
+ void runNewCommand(name, {
374
+ json: argv.includes("--json"),
375
+ skipInstall: argv.includes("--skip-install"),
376
+ skipGit: argv.includes("--skip-git"),
377
+ packageManager,
378
+ })
379
+ .then((code) => {
380
+ process.exitCode = code;
381
+ })
382
+ .catch((err) => {
383
+ const msg = err instanceof Error ? err.message : String(err);
384
+ process.stderr.write(` ✗ gaon new 실패: ${msg}\n`);
385
+ process.exitCode = 1;
386
+ });
387
+ return;
388
+ }
389
+ // `gaon console` — 프로젝트 컨텍스트 REPL(M9-G). --no-config 로 배선 없이 기동.
390
+ if (argv[0] === "console") {
391
+ void runConsoleCommand({
392
+ json: argv.includes("--json"),
393
+ noConfig: argv.includes("--no-config"),
394
+ }).catch((err) => {
395
+ const msg = err instanceof Error ? err.message : String(err);
396
+ process.stderr.write(` ✗ gaon console 실패: ${msg}\n`);
397
+ process.exitCode = 1;
398
+ });
399
+ return;
400
+ }
401
+ // `gaon test [--scope unit|integration|all] [-- vitest 인자]` — 테스트 러너(M9-G).
402
+ if (argv[0] === "test") {
403
+ const rest = argv.slice(1);
404
+ const scopeIdx = rest.indexOf("--scope");
405
+ const scopeRaw = scopeIdx >= 0 ? rest[scopeIdx + 1] : undefined;
406
+ const scope = scopeRaw === "unit" || scopeRaw === "integration" || scopeRaw === "all"
407
+ ? scopeRaw
408
+ : undefined;
409
+ const passthrough = [];
410
+ for (let i = 0; i < rest.length; i++) {
411
+ const a = rest[i];
412
+ if (a === "--scope") {
413
+ i++;
414
+ continue;
415
+ }
416
+ if (a === "--json")
417
+ continue;
418
+ if (a !== undefined)
419
+ passthrough.push(a);
420
+ }
421
+ void runTestCommand(passthrough, { json: argv.includes("--json"), scope })
422
+ .then((code) => {
423
+ process.exitCode = code;
424
+ })
425
+ .catch((err) => {
426
+ const msg = err instanceof Error ? err.message : String(err);
427
+ process.stderr.write(` ✗ gaon test 실패: ${msg}\n`);
428
+ process.exitCode = 1;
429
+ });
430
+ return;
431
+ }
284
432
  if (argv.includes("--help") || argv.includes("-h")) {
285
433
  process.stdout.write(renderHelp(version) + "\n");
286
434
  return;
@@ -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,119 @@
1
+ # {{PROJECT_NAME}} — AI 작업 지침
2
+
3
+ 이 파일은 이 프로젝트에서 AI(Claude 등)가 코드를 만질 때 지켜야 할
4
+ 관례를 담는다. Gaon 프레임웍이 이 관례를 스캐폴드(`gaon new`)로 자동
5
+ 심어 두는 이유는 하나다 — 첫 시도부터 프로젝트 관례에 맞게 코드를 짜
6
+ 반복 수정을 없애기 위함(§1.1 AI 첫 시도 성공률).
7
+
8
+ ## 0. 먼저 읽을 것
9
+
10
+ Gaon 프레임웍 문서: https://gaonjs.dev
11
+
12
+ - 설계 정본(v0.15) · errata E-1(파사드 = `gaonjs`) · E-3(JSON 액션 +
13
+ `api()`) · E-4(컬럼 확장) · E-5(컴포저블·레이아웃)
14
+ - 앱 내 One Way(§1) — 선택지가 있는 것을 만들지 않는다. 하나로 정한다.
15
+
16
+ ## 1. 절대 규칙 (12개 · 프레임웍 정본에서 유래)
17
+
18
+ 1. **TypeScript 전용.** JS 파일 추가 금지. 데코레이터 금지 — 함수·객체
19
+ 스타일(`model()`·`controller()`·`job()`) 만 쓴다.
20
+ 2. **`.gaon/` 자동 생성 파일 편집 금지.** `routes.d.ts`·`tables.d.ts`
21
+ 는 `gaon check` / `gaon dev` 가 재생성한다.
22
+ 3. **의존 방향 4규칙**(doctor 강제): 앱→domain 허용 · domain→앱 금지 ·
23
+ 앱→앱 금지 · 앱→shared 허용(shared 는 앱 import 금지, domain 은
24
+ 타입 import 만).
25
+ 4. **PM2·pnpm dev-server 금지.** 클러스터는 `node:cluster` 내장이 유일.
26
+ 컨테이너 기본 워커 1.
27
+ 5. **프론트엔드는 프로젝트당 하나.** v1 은 Vue 단일. 앱 간 Vue/React
28
+ 혼용 금지.
29
+ 6. **보안 기본값(CORS·rate limit·CSRF)은 켠 채로 둔다.** 끄는 것은
30
+ 명시적 설정으로만.
31
+ 7. **멀티 DB 커넥션 규칙**(§4.5): 스키마는 `{ db: '키' }` 로 바인딩,
32
+ 생략 = main. 커넥션을 가로지르는 belongsTo 금지, 서비스 트랜잭션은
33
+ 단일 커넥션에서만 원자적. MongoDB 는 v1 구현 금지.
34
+ 8. **테스트는 Docker 실 인프라 필수 · DB·NATS 목업 절대 금지**(§9).
35
+ SQLite 인메모리·NATS 목업으로 테스트 돌리는 코드 만들지 말 것.
36
+ 9. **실시간은 v1 포함**(§7): 웹서버 ↔ 허브는 TCP 지속 연결 · NATS 는
37
+ broadcast 전용(errata E-2). 운영 프로세스는 serve·work·hub 3종.
38
+ 10. **인증·폼은 Inertia SPA**(§6 · SSR 아님). 로그인/회원가입은
39
+ `this.render('auth/Login')` + `Inertia.post()` → 서버 redirect.
40
+ REST + `fetch()` 는 API 앱(JWT) 전용.
41
+ 11. **컴포저블·레이아웃**(errata E-5): 컴포저블은 컴포넌트와 대칭
42
+ (`apps/<앱>/composables/` + `shared/composables/`, `use` 접두사).
43
+ shared 컴포저블은 인자로만 받는 순수 로직(api·pageProps 금지).
44
+ 레이아웃은 `apps/<앱>/layouts/Default.vue` 존재 시 자동 적용.
45
+ **자동 import 금지** — 모든 import 는 명시적으로.
46
+ 12. **JSON 액션 + 타입드 `api()`**(errata E-3): 페이지와 무관한 데이터
47
+ 요청은 컨트롤러의 JSON 액션(반환값 = 응답) · Vue 는 `api()`
48
+ 클라이언트로 호출. `this.params()` 출처 우선순위 = 라우트 > body >
49
+ query(고정). 출처 명시는 `this.body()`·`this.query()`.
50
+
51
+ ## 2. 프로젝트 구조
52
+
53
+ ```
54
+ {{PROJECT_NAME}}/
55
+ ├─ apps/ 앱마다 폴더 = URL 프리픽스
56
+ │ └─ web/ web 앱 (프리픽스 '/')
57
+ │ ├─ app.config.ts (선택) 앱 오버라이드
58
+ │ ├─ routes.ts 라우트 정의
59
+ │ ├─ controllers/ 컨트롤러 (Rails 관례)
60
+ │ ├─ pages/ Vue 페이지 (Inertia SPA)
61
+ │ ├─ components/ 앱 전용 컴포넌트
62
+ │ ├─ composables/ 앱 전용 컴포저블 (E-5)
63
+ │ ├─ layouts/ 앱별 레이아웃 (E-5 · Default.vue 자동)
64
+ │ └─ channels/ (선택) 실시간 채널 (M6)
65
+ ├─ domain/ 비즈니스 로직 · 앱 간 공유
66
+ │ ├─ schema/ 테이블 스키마 (@gaonjs/data)
67
+ │ ├─ models/ 모델 (조회·연관·훅)
68
+ │ ├─ services/ 트랜잭션 · 규칙
69
+ │ ├─ jobs/ 비동기 잡 (@gaonjs/async)
70
+ │ ├─ events/ 이벤트 정의
71
+ │ ├─ listeners/ 이벤트 리스너
72
+ │ ├─ mails/ 메일 (@gaonjs/mail)
73
+ │ └─ schedule.ts (선택) 크론 스케줄
74
+ ├─ shared/ 앱 간 공용 (props로만)
75
+ │ ├─ components/ shared 컴포넌트 (순수 UI)
76
+ │ └─ composables/ shared 컴포저블 (인자로만 · E-5)
77
+ ├─ gaon.config.ts 루트 설정 (DB · Redis · NATS · ...)
78
+ ├─ docker-compose.yaml 개발 인프라 (gaon dev 자동 기동)
79
+ ├─ .env.example env 템플릿 (cp .env.example .env)
80
+ └─ package.json 개발자는 gaonjs 하나만 설치
81
+ ```
82
+
83
+ ## 3. 개발 검증 루프 (작업마다 실행)
84
+
85
+ ```bash
86
+ gaon check # .gaon 재생성 후 타입 검사 (CI 정합)
87
+ gaon doctor # 정적 검사 5종 (응답·N+1·의존·커넥션·마이그)
88
+ npm test # Vitest · DB 테스트는 실 Docker 필수 (§9)
89
+ ```
90
+
91
+ ## 4. 작업 관례
92
+
93
+ - **추론 금지 · 사실 검증.** 확인 안 된 것은 실행·측정으로 검증하거나
94
+ 사용자에게 묻는다.
95
+ - **주석은 개발자가 단 것처럼.** AI가 단 티가 나는 주석(`// AI 판단…`
96
+ `// TODO(AI)` 등) 금지. 기술적 이유(왜 이 코드가 이런지, 특수 케이스
97
+ 근거) 만 담는다.
98
+ - **파일 작게 · 역할은 하나로.** 영리한 코드보다 읽히는 코드.
99
+ - **에러 메시지**(§7.5.3): "어느 파일에 무엇을 추가/수정하고 어떤
100
+ 명령을 실행하라" 까지 쓴다.
101
+
102
+ ## 5. 자주 쓰는 명령
103
+
104
+ ```bash
105
+ gaon dev # 개발 (Docker · 타입 브리지 · watch)
106
+ gaon serve # 서버만 (운영 프로세스 1/3)
107
+ gaon work # 워커 (잡·리스너·아웃박스)
108
+ gaon hub # 실시간 허브 (프레즌스 · 리더 선출 HA)
109
+ gaon g controller <name> # 컨트롤러 스캐폴드
110
+ gaon g model <Name> # 스키마 + 모델 스캐폴드 (E-4)
111
+ gaon g page <Path>/<Name> # Vue 페이지 (Inertia SPA · pageProps)
112
+ gaon g job <Name> # 비동기 잡
113
+ gaon g auth # 인증 스캐폴드 (세션 + JWT 옵션)
114
+ gaon db diff # 스키마 ↔ DB 차이 (적용 X)
115
+ gaon db migrate # 실제 적용 + _gaon_migrations 이력
116
+ gaon db seed # domain/seed.ts 실행
117
+ ```
118
+
119
+ 문서 · 진행 상황: https://gaonjs.dev
@@ -0,0 +1 @@
1
+ # 이 폴더는 gaon g <type> <name> 로 채워집니다. .gitkeep 은 스캐폴드 관례상 유지.
@@ -0,0 +1 @@
1
+ # 이 폴더는 gaon g <type> <name> 로 채워집니다. .gitkeep 은 스캐폴드 관례상 유지.