@gaonjs/cli 0.2.0 → 0.4.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 (69) 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/db.d.ts +20 -0
  6. package/dist/commands/db.js +74 -0
  7. package/dist/commands/dev.d.ts +68 -0
  8. package/dist/commands/dev.js +287 -0
  9. package/dist/commands/g.d.ts +26 -0
  10. package/dist/commands/g.js +124 -0
  11. package/dist/db/diff.d.ts +17 -0
  12. package/dist/db/diff.js +57 -0
  13. package/dist/db/index.d.ts +4 -0
  14. package/dist/db/index.js +8 -0
  15. package/dist/db/migrate.d.ts +16 -0
  16. package/dist/db/migrate.js +173 -0
  17. package/dist/db/reset.d.ts +18 -0
  18. package/dist/db/reset.js +150 -0
  19. package/dist/db/resolve.d.ts +32 -0
  20. package/dist/db/resolve.js +130 -0
  21. package/dist/dev/console.d.ts +39 -0
  22. package/dist/dev/console.js +100 -0
  23. package/dist/dev/docker.d.ts +52 -0
  24. package/dist/dev/docker.js +163 -0
  25. package/dist/dev/index.d.ts +14 -0
  26. package/dist/dev/index.js +10 -0
  27. package/dist/dev/tsc.d.ts +41 -0
  28. package/dist/dev/tsc.js +127 -0
  29. package/dist/dev/watcher.d.ts +50 -0
  30. package/dist/dev/watcher.js +95 -0
  31. package/dist/dev.d.ts +1 -16
  32. package/dist/dev.js +10 -66
  33. package/dist/doctor/connections.d.ts +14 -0
  34. package/dist/doctor/connections.js +168 -0
  35. package/dist/doctor/dependency-direction.d.ts +11 -0
  36. package/dist/doctor/dependency-direction.js +186 -0
  37. package/dist/doctor/migration-diff.d.ts +11 -0
  38. package/dist/doctor/migration-diff.js +109 -0
  39. package/dist/doctor/n-plus-one.d.ts +5 -0
  40. package/dist/doctor/n-plus-one.js +242 -0
  41. package/dist/doctor/reporter.d.ts +5 -0
  42. package/dist/doctor/reporter.js +41 -0
  43. package/dist/doctor/response-mixing.d.ts +12 -0
  44. package/dist/doctor/response-mixing.js +158 -0
  45. package/dist/doctor/setup.d.ts +26 -0
  46. package/dist/doctor/setup.js +52 -0
  47. package/dist/doctor/types.d.ts +37 -0
  48. package/dist/doctor/types.js +34 -0
  49. package/dist/doctor.d.ts +40 -23
  50. package/dist/doctor.js +131 -202
  51. package/dist/index.d.ts +14 -2
  52. package/dist/index.js +171 -28
  53. package/dist/scaffold/controller.d.ts +12 -0
  54. package/dist/scaffold/controller.js +50 -0
  55. package/dist/scaffold/index.d.ts +20 -0
  56. package/dist/scaffold/index.js +41 -0
  57. package/dist/scaffold/inflect.d.ts +19 -0
  58. package/dist/scaffold/inflect.js +50 -0
  59. package/dist/scaffold/job.d.ts +3 -0
  60. package/dist/scaffold/job.js +46 -0
  61. package/dist/scaffold/model.d.ts +8 -0
  62. package/dist/scaffold/model.js +66 -0
  63. package/dist/scaffold/page.d.ts +7 -0
  64. package/dist/scaffold/page.js +46 -0
  65. package/dist/serve.d.ts +18 -0
  66. package/dist/serve.js +79 -0
  67. package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
  68. package/dist/templates/auth/session.controller.ts.tpl +1 -1
  69. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,31 +1,40 @@
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 { runDevCommand } from "./commands/dev.js";
14
15
  import { runCheckCommand } from "./check.js";
15
16
  import { runGenerateAuthCommand } from "./generate.js";
17
+ import { runGenerateCommand } from "./commands/g.js";
16
18
  import { runHubCommand } from "./hub.js";
19
+ import { runServeCommand } from "./serve.js";
17
20
  import { runWorkCommand } from "./work.js";
18
21
  import { runJobsCommand } from "./jobs.js";
19
- import { runDbSeedCommand } from "./db.js";
22
+ import { runDbCommand } from "./commands/db.js";
20
23
  import { runDoctorCommand } from "./doctor.js";
21
- export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
24
+ export { startDev, resolveDevLayout, } from "./dev.js";
25
+ export { runDevCommand } from "./commands/dev.js";
26
+ export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, } from "./dev/index.js";
22
27
  export { runCheck, runCheckCommand, } from "./check.js";
23
28
  export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, } from "./generate.js";
29
+ export { runGenerateCommand, planScaffold, parseGenerateArgs, } from "./commands/g.js";
24
30
  export { runHubCommand } from "./hub.js";
31
+ export { runServeCommand } from "./serve.js";
25
32
  export { runWorkCommand } from "./work.js";
26
33
  export { runJobsCommand } from "./jobs.js";
27
34
  export { runDbSeedCommand, loadSeed } from "./db.js";
28
- export { runDoctorCommand, runDoctor, inspectControllerSource, } from "./doctor.js";
35
+ export { runDbCommand, } from "./commands/db.js";
36
+ export { runDbDiff, runDbMigrate, runDbReset, resolveDbTarget, } from "./db/index.js";
37
+ export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, } from "./doctor.js";
29
38
  export { loadDomain } from "./domain.js";
30
39
  /** `--json` 출력용 구조화 리포트. */
31
40
  export function roadmapReport(version = VERSION) {
@@ -70,15 +79,32 @@ function renderHelp(version = VERSION) {
70
79
  "",
71
80
  " 사용법:",
72
81
  " gaon 로드맵과 개발 상태를 출력",
73
- " gaon dev .gaon 타입 브리지를 감시·재생성 (스키마·라우트)",
74
- " gaon dev --json 재생성 이벤트를 JSON 으로 출력",
82
+ " gaon dev 개발 스택 통합 (Docker · .gaon · serve · tsc/vue-tsc · 재시작 워처)",
83
+ " gaon dev --stop-docker Ctrl+C Docker Compose 도 down",
84
+ " gaon dev --no-watch|--no-tsc|--no-vue-tsc|--no-docker 개별 debug 옵션",
85
+ " gaon dev --port <n> --host <h> serve 리슨 지정",
86
+ " gaon dev --json 통합 콘솔을 JSON 라인으로 출력(자동화)",
87
+ " gaon serve 웹 서버 부팅 (gaon.config.ts 자동 배선 · Fastify listen)",
88
+ " gaon serve --port <n> --host <h> 리슨 포트·호스트 (config 값을 덮음)",
75
89
  " gaon check .gaon 재생성 후 타입 검사 (CI·AI 정합)",
76
- " gaon doctor 정적 검사 (응답 혼용·의존 방향 등)",
90
+ " gaon doctor 정적 검사 (5 검사 · 응답 혼용·N+1·의존 방향·커넥션·마이그)",
91
+ " gaon doctor --json 자동화용 JSON 출력",
92
+ " gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
77
93
  " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
94
+ " gaon g controller <name> 컨트롤러 스캐폴드 (Rails 관례 · 페이지+JSON 액션)",
95
+ " gaon g model <Name> 모델 스캐폴드 (스키마+모델 · E-4 컬럼 예시)",
96
+ " gaon g page <Path/Name> Vue 페이지 (Inertia SPA · pageProps 브리지)",
97
+ " gaon g job <Name> 비동기 잡 (domain/jobs · later/in/at)",
98
+ " gaon g <type> --overwrite 기존 파일 덮어쓰기 · --app <이름> · --json",
78
99
  " gaon hub 실시간 허브 프로세스 (프레즌스 권위·중계 · 리더 선출 HA)",
79
100
  " gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
80
101
  " gaon jobs list --failed DLQ(실패 잡) 목록",
81
102
  " gaon jobs retry <id> DLQ 잡 재적재",
103
+ " gaon db diff 스키마 ↔ DB 차이 계산 (적용 X · --json · --db <키>)",
104
+ " gaon db migrate 스키마 변경을 실제 적용 + _gaon_migrations 이력",
105
+ " gaon db migrate --dry-run 적용 없이 up SQL 만 출력",
106
+ " gaon db reset --yes DROP ALL → 재마이그 → seed (--yes 필수 · production 거부)",
107
+ " gaon db seed domain/seed.ts 실행 (M8)",
82
108
  " gaon --json 같은 정보를 JSON 으로 출력",
83
109
  " gaon --version 버전 출력",
84
110
  " gaon --help 이 도움말",
@@ -87,19 +113,72 @@ function renderHelp(version = VERSION) {
87
113
  "",
88
114
  ].join("\n");
89
115
  }
116
+ /**
117
+ * `gaon doctor --check=<이름>[,<이름>...]` 파싱(M9-E).
118
+ * 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
119
+ */
120
+ export function parseDoctorChecks(argv) {
121
+ const known = [
122
+ "response-mixing",
123
+ "n-plus-one",
124
+ "dependency-direction",
125
+ "connections",
126
+ "migration-diff",
127
+ ];
128
+ const isKnown = (s) => known.includes(s);
129
+ const out = [];
130
+ for (const a of argv) {
131
+ if (a.startsWith("--check=")) {
132
+ for (const nm of a.slice("--check=".length).split(",")) {
133
+ if (isKnown(nm) && !out.includes(nm))
134
+ out.push(nm);
135
+ }
136
+ }
137
+ }
138
+ return out.length ? out : undefined;
139
+ }
90
140
  /** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
91
141
  export function runCli(argv, opts = {}) {
92
142
  const version = opts.version ?? VERSION;
93
- // `gaon dev` — .gaon 갱신 파이프라인(장기 실행). 워처가 활성 핸들로
94
- // 프로세스를 살려 두고, SIGINT 종료된다. 에러는 stderr + exit 1.
143
+ // `gaon dev` — 통합 개발 오케스트레이션(M9-C · v0.15 §13.5). Docker Compose
144
+ // 자동 기동 + .gaon 재생성 + serve 자식 + tsc/vue-tsc watch + 서버 재시작 워처.
145
+ // SIGINT/SIGTERM 시 순서대로 정리(serve → tsc → 워처 → Docker[--stop-docker 시]).
146
+ // 개별 debug 옵션은 --no-watch / --no-tsc / --no-vue-tsc / --no-docker 뿐.
95
147
  if (argv[0] === "dev") {
96
- void runDevCommand({ json: argv.includes("--json") }).catch((err) => {
148
+ const portIdx = argv.indexOf("--port");
149
+ const hostIdx = argv.indexOf("--host");
150
+ const port = portIdx >= 0 ? Number(argv[portIdx + 1]) : undefined;
151
+ const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;
152
+ void runDevCommand({
153
+ json: argv.includes("--json"),
154
+ stopDocker: argv.includes("--stop-docker"),
155
+ noWatch: argv.includes("--no-watch"),
156
+ noTsc: argv.includes("--no-tsc"),
157
+ noVueTsc: argv.includes("--no-vue-tsc"),
158
+ noDocker: argv.includes("--no-docker"),
159
+ timestamp: argv.includes("--timestamp"),
160
+ port,
161
+ host,
162
+ }).catch((err) => {
97
163
  const msg = err instanceof Error ? err.message : String(err);
98
164
  process.stderr.write(` ✗ gaon dev 실패: ${msg}\n`);
99
165
  process.exitCode = 1;
100
166
  });
101
167
  return;
102
168
  }
169
+ // `gaon serve` — 웹 서버 부팅(§7 · M9-A). gaon.config.ts 자동 배선 후 listen.
170
+ if (argv[0] === "serve") {
171
+ const portIdx = argv.indexOf("--port");
172
+ const hostIdx = argv.indexOf("--host");
173
+ const port = portIdx >= 0 ? Number(argv[portIdx + 1]) : undefined;
174
+ const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;
175
+ void runServeCommand({ json: argv.includes("--json"), port, host }).catch((err) => {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ process.stderr.write(` ✗ gaon serve 실패: ${msg}\n`);
178
+ process.exitCode = 1;
179
+ });
180
+ return;
181
+ }
103
182
  // `gaon check` — .gaon 재생성 후 타입 검사(§13.4-5). 종료 코드로 결과 전달.
104
183
  if (argv[0] === "check") {
105
184
  void runCheckCommand({ json: argv.includes("--json") })
@@ -113,11 +192,18 @@ export function runCli(argv, opts = {}) {
113
192
  });
114
193
  return;
115
194
  }
116
- // `gaon doctor` — 정적 검사(응답 혼용 등 · errata E-3 §C). --fix 는 후속.
195
+ // `gaon doctor` — 정적 검사(M9-E · 5 검사). --check=<이름>[,<이름>...]
196
+ // 선택 실행, --json 은 자동화 파싱용.
197
+ // exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
117
198
  if (argv[0] === "doctor") {
118
- void runDoctorCommand({ json: argv.includes("--json") })
119
- .then((code) => {
120
- process.exitCode = code;
199
+ const checks = parseDoctorChecks(argv);
200
+ void runDoctorCommand({ json: argv.includes("--json"), checks })
201
+ .then((result) => {
202
+ if (result.fatal) {
203
+ process.exitCode = 2;
204
+ return;
205
+ }
206
+ process.exitCode = result.errors.length > 0 ? 1 : 0;
121
207
  })
122
208
  .catch((err) => {
123
209
  const msg = err instanceof Error ? err.message : String(err);
@@ -159,22 +245,40 @@ export function runCli(argv, opts = {}) {
159
245
  });
160
246
  return;
161
247
  }
162
- // `gaon db seed`domain/seed.ts 실행(§7 M8). diff·migrate 는 M9 CLI 완성.
163
- if (argv[0] === "db" && argv[1] === "seed") {
164
- const json = argv.includes("--json");
165
- void runDbSeedCommand({ json })
166
- .then((res) => {
167
- process.stdout.write((json ? JSON.stringify(res.json) : res.text) + "\n");
168
- process.exitCode = res.exitCode;
248
+ // `gaon db <sub>`diff · migrate · reset · seed (§7 M8/M9-D).
249
+ if (argv[0] === "db") {
250
+ const sub = argv[1];
251
+ const known = ["diff", "migrate", "reset", "seed"];
252
+ if (!sub || !known.includes(sub)) {
253
+ process.stderr.write(` ✗ 없는 db 서브커맨드: ${sub ?? "(없음)"}\n` +
254
+ ` → 지원: gaon db diff | migrate | reset | seed\n` +
255
+ ` → 옵션: --json · --db <키> · --config <path> · --yes · --dry-run\n`);
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+ const dbIdx = argv.indexOf("--db");
260
+ const cfgIdx = argv.indexOf("--config");
261
+ const dbOpts = {
262
+ json: argv.includes("--json"),
263
+ db: dbIdx >= 0 ? argv[dbIdx + 1] : undefined,
264
+ config: cfgIdx >= 0 ? argv[cfgIdx + 1] : undefined,
265
+ yes: argv.includes("--yes"),
266
+ dryRun: argv.includes("--dry-run"),
267
+ };
268
+ void runDbCommand(sub, dbOpts)
269
+ .then((code) => {
270
+ process.exitCode = code;
169
271
  })
170
272
  .catch((err) => {
171
273
  const msg = err instanceof Error ? err.message : String(err);
172
- process.stderr.write(` ✗ gaon db seed 실패: ${msg}\n`);
274
+ process.stderr.write(` ✗ gaon db ${sub} 실패: ${msg}\n`);
173
275
  process.exitCode = 1;
174
276
  });
175
277
  return;
176
278
  }
177
- // `gaon g auth`인증 스캐폴드 제너레이터(§7 M5). 다른 제너레이터는 후속.
279
+ // `gaon g <type> <name>` — 스캐폴드(§7 M5·M9-B).
280
+ // · auth 는 인증 스캐폴드(별도 진입점)
281
+ // · controller/model/page/job 은 M9-B 통합 라우터
178
282
  if (argv[0] === "g" || argv[0] === "generate") {
179
283
  if (argv[1] === "auth") {
180
284
  const appIdx = argv.indexOf("--app");
@@ -183,8 +287,47 @@ export function runCli(argv, opts = {}) {
183
287
  process.exitCode = code;
184
288
  return;
185
289
  }
290
+ const known = ["controller", "model", "page", "job"];
291
+ const type = argv[1];
292
+ if (type && known.includes(type)) {
293
+ const rest = argv.slice(2);
294
+ // name = 첫 위치 인자(--플래그 제외 · --app <v> 스킵)
295
+ let name;
296
+ let app;
297
+ let overwrite = false;
298
+ let json = false;
299
+ for (let i = 0; i < rest.length; i++) {
300
+ const a = rest[i];
301
+ if (a === "--json") {
302
+ json = true;
303
+ continue;
304
+ }
305
+ if (a === "--overwrite") {
306
+ overwrite = true;
307
+ continue;
308
+ }
309
+ if (a === "--app") {
310
+ app = rest[++i];
311
+ continue;
312
+ }
313
+ if (a?.startsWith("--"))
314
+ continue;
315
+ if (name === undefined)
316
+ name = a;
317
+ }
318
+ if (!name) {
319
+ process.stderr.write(` ✗ gaon g ${type}: 이름이 없습니다.\n` +
320
+ ` → 예: gaon g ${type} ${type === "page" ? "Posts/Index" : "Post"}\n`);
321
+ process.exitCode = 1;
322
+ return;
323
+ }
324
+ const code = runGenerateCommand(type, name, { app, overwrite, json });
325
+ process.exitCode = code;
326
+ return;
327
+ }
186
328
  process.stderr.write(` ✗ 알 수 없는 제너레이터: ${argv[1] ?? "(없음)"}\n` +
187
- ` → 현재 지원: gaon g auth [--app <이름>]\n`);
329
+ ` → 현재 지원: gaon g auth | controller | model | page | job\n` +
330
+ ` → 옵션: --app <이름> · --overwrite · --json\n`);
188
331
  process.exitCode = 1;
189
332
  return;
190
333
  }
@@ -0,0 +1,12 @@
1
+ import type { ModelNames } from './inflect.js';
2
+ /** 스캐폴드가 만들 파일 하나 — path 는 프로젝트 루트 기준 상대 경로. */
3
+ export interface ScaffoldFile {
4
+ readonly path: string;
5
+ readonly contents: string;
6
+ }
7
+ /**
8
+ * 컨트롤러 스캐폴드 파일을 만든다.
9
+ * @param names 이름 변형(카멜/파스칼/복수) — inflect() 결과.
10
+ * @param app 대상 앱 폴더(apps/<app>/).
11
+ */
12
+ export declare function controllerScaffold(names: ModelNames, app: string): ScaffoldFile;
@@ -0,0 +1,50 @@
1
+ // @gaonjs/cli · scaffold · controller (M9-B)
2
+ //
3
+ // `gaon g controller <name>` — Rails 관례의 컨트롤러 스캐폴드.
4
+ // 페이지 액션(this.render)과 JSON 액션(반환값=응답 · errata E-3) 두 예시를
5
+ // 함께 담는다 — 실 프로젝트에서 가장 자주 쓰는 두 패턴이라 첫 코드에서
6
+ // 노출하는 편이 AI/사람 첫 시도 성공률에 유리하다.
7
+ //
8
+ // 응답 혼용 주의: 한 컨트롤러 안에서 페이지 액션과 JSON 액션을 섞는 것은
9
+ // doctor 가 경고한다(errata E-3 §C). 스캐폴드는 이유를 주석으로 남기고,
10
+ // 규모가 커지면 분리하도록 안내한다.
11
+ //
12
+ // 모델 참조: apps/<app>/controllers/*.ts → domain/models/*.ts. 프로젝트 관습
13
+ // (CLAUDE.md §2): 모델·잡은 domain/ 아래 두어 앱 간 재사용을 허용한다
14
+ // (apps→apps 금지 · rule 5).
15
+ /**
16
+ * 컨트롤러 스캐폴드 파일을 만든다.
17
+ * @param names 이름 변형(카멜/파스칼/복수) — inflect() 결과.
18
+ * @param app 대상 앱 폴더(apps/<app>/).
19
+ */
20
+ export function controllerScaffold(names, app) {
21
+ const { pascal, camel, plural } = names;
22
+ const lines = [
23
+ `// ${plural} 컨트롤러 — gaon g controller (M9-B).`,
24
+ `// 페이지 액션(this.render)과 JSON 액션(반환값=응답 · errata E-3) 예시를 담는다.`,
25
+ `// 응답 종류를 한 컨트롤러에서 섞으면 doctor 가 경고한다(§C) — 규모가 커지면 분리.`,
26
+ `import { controller } from 'gaonjs/web'`,
27
+ `import { ${pascal} } from '../../../domain/models/${camel}.js'`,
28
+ ``,
29
+ `export default controller({`,
30
+ ` // GET /${plural} — 목록 페이지 (Inertia render).`,
31
+ ` // E-4 체이닝 예시: orderBy · limit · all.`,
32
+ ` async index() {`,
33
+ ` const items = await ${pascal}.orderBy('createdAt', 'desc').limit(20).all()`,
34
+ ` return this.render('${pascal}/Index', { items })`,
35
+ ` },`,
36
+ ``,
37
+ ` // GET /${plural}/count.json — JSON 액션 (errata E-3).`,
38
+ ` // 반환값 = 응답. api('${plural}#count') 클라이언트가 Serialized<> 로 받는다.`,
39
+ ` // this.params() 안전 규칙: 라우트 > body > query (errata E-3 §5.1).`,
40
+ ` async count() {`,
41
+ ` return { total: await ${pascal}.count() }`,
42
+ ` },`,
43
+ `})`,
44
+ ``,
45
+ ];
46
+ return {
47
+ path: `apps/${app}/controllers/${plural}.ts`,
48
+ contents: lines.join('\n'),
49
+ };
50
+ }
@@ -0,0 +1,20 @@
1
+ export type { ScaffoldFile } from './controller.js';
2
+ export { controllerScaffold } from './controller.js';
3
+ export { modelScaffold, modelScaffoldFiles, schemaScaffold } from './model.js';
4
+ export { pageScaffold } from './page.js';
5
+ export { jobScaffold } from './job.js';
6
+ export { inflectModel, toCamel, toPascal, singularize, pluralize, type ModelNames, } from './inflect.js';
7
+ import type { ScaffoldFile } from './controller.js';
8
+ export interface WriteResult {
9
+ readonly created: string[];
10
+ readonly overwritten: string[];
11
+ /** overwrite=false 로 스킵된 기존 파일. */
12
+ readonly skipped: string[];
13
+ }
14
+ /**
15
+ * 파일 계획을 실제로 쓴다. 기본은 기존 파일 skip(멱등 · 사고 방지).
16
+ * overwrite=true 이면 덮어쓴다(기존 파일 목록을 overwritten 에 반환).
17
+ */
18
+ export declare function writeScaffold(cwd: string, files: readonly ScaffoldFile[], opts?: {
19
+ overwrite?: boolean;
20
+ }): WriteResult;
@@ -0,0 +1,41 @@
1
+ // @gaonjs/cli · scaffold · public export (M9-B)
2
+ //
3
+ // `gaon g <type> <name>` 스캐폴드 진입점 모음. runCli 가 여기서 팩토리를
4
+ // 골라 파일을 쓴다. 각 팩토리는 순수 함수 — 디스크 접근 없이 파일 계획을
5
+ // 반환한다. 실제 쓰기는 writeScaffold 가 담당(멱등·overwrite 옵션 처리).
6
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ export { controllerScaffold } from './controller.js';
9
+ export { modelScaffold, modelScaffoldFiles, schemaScaffold } from './model.js';
10
+ export { pageScaffold } from './page.js';
11
+ export { jobScaffold } from './job.js';
12
+ export { inflectModel, toCamel, toPascal, singularize, pluralize, } from './inflect.js';
13
+ /**
14
+ * 파일 계획을 실제로 쓴다. 기본은 기존 파일 skip(멱등 · 사고 방지).
15
+ * overwrite=true 이면 덮어쓴다(기존 파일 목록을 overwritten 에 반환).
16
+ */
17
+ export function writeScaffold(cwd, files, opts = {}) {
18
+ const root = resolve(cwd);
19
+ const created = [];
20
+ const overwritten = [];
21
+ const skipped = [];
22
+ for (const file of files) {
23
+ const abs = join(root, file.path);
24
+ const exists = existsSync(abs);
25
+ if (exists && !opts.overwrite) {
26
+ skipped.push(file.path);
27
+ continue;
28
+ }
29
+ mkdirSync(dirname(abs), { recursive: true });
30
+ writeFileSync(abs, file.contents, 'utf8');
31
+ if (exists)
32
+ overwritten.push(file.path);
33
+ else
34
+ created.push(file.path);
35
+ }
36
+ return {
37
+ created: created.sort(),
38
+ overwritten: overwritten.sort(),
39
+ skipped: skipped.sort(),
40
+ };
41
+ }
@@ -0,0 +1,19 @@
1
+ /** 파스칼케이스 → 카멜케이스 (`Post` → `post`, `SendEmail` → `sendEmail`). */
2
+ export declare function toCamel(s: string): string;
3
+ /** 카멜/스네이크/케밥 → 파스칼케이스 (`post` → `Post`, `send_email` → `SendEmail`). */
4
+ export declare function toPascal(s: string): string;
5
+ /** 단순 단수화 — 끝의 s 하나만 벗긴다 ('users' → 'user' · 'ss' 는 유지). */
6
+ export declare function singularize(s: string): string;
7
+ /** 단순 복수화 — 끝에 s 를 붙인다 ('user' → 'users' · 이미 s 로 끝나면 유지). */
8
+ export declare function pluralize(s: string): string;
9
+ /** 모델 이름 · 표준 변형 묶음. 스캐폴드가 파일명·클래스명·테이블명에 쓴다. */
10
+ export interface ModelNames {
11
+ /** 파스칼 단수 — 클래스/const 명 (`Post`). */
12
+ readonly pascal: string;
13
+ /** 카멜 단수 — 파일 stem·변수명 (`post`). */
14
+ readonly camel: string;
15
+ /** 카멜 복수 — 테이블명·경로 (`posts`). */
16
+ readonly plural: string;
17
+ }
18
+ /** 사용자 입력(어느 형태든)에서 세 가지 변형을 파생한다. */
19
+ export declare function inflectModel(input: string): ModelNames;
@@ -0,0 +1,50 @@
1
+ // @gaonjs/cli · scaffold · 이름 변환 헬퍼 (M9-B)
2
+ //
3
+ // Rails 관례: 사용자가 아무 형태(Post·post·Posts·posts)로 넣어도 스캐폴드가
4
+ // 필요한 변형(파일명·클래스명·테이블명)을 일관되게 뽑는다. 완벽한 영어
5
+ // 복수형 규칙(children/geese …)은 v1 범위 밖 — 기본 규칙(s 추가/삭제)만
6
+ // 다룬다. 예외가 필요하면 사용자가 --table/--singular 로 덮어쓸 수 있다
7
+ // (v1.1 계획 · 현재는 기본 규칙만).
8
+ /** 파스칼케이스 → 카멜케이스 (`Post` → `post`, `SendEmail` → `sendEmail`). */
9
+ export function toCamel(s) {
10
+ if (!s)
11
+ return s;
12
+ return s.charAt(0).toLowerCase() + s.slice(1);
13
+ }
14
+ /** 카멜/스네이크/케밥 → 파스칼케이스 (`post` → `Post`, `send_email` → `SendEmail`). */
15
+ export function toPascal(s) {
16
+ if (!s)
17
+ return s;
18
+ return s
19
+ .split(/[_\-\s]+/)
20
+ .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
21
+ .join('');
22
+ }
23
+ /** 단순 단수화 — 끝의 s 하나만 벗긴다 ('users' → 'user' · 'ss' 는 유지). */
24
+ export function singularize(s) {
25
+ if (s.endsWith('ies') && s.length > 3)
26
+ return s.slice(0, -3) + 'y';
27
+ if (s.endsWith('ss'))
28
+ return s;
29
+ if (s.endsWith('s') && s.length > 1)
30
+ return s.slice(0, -1);
31
+ return s;
32
+ }
33
+ /** 단순 복수화 — 끝에 s 를 붙인다 ('user' → 'users' · 이미 s 로 끝나면 유지). */
34
+ export function pluralize(s) {
35
+ if (s.endsWith('s'))
36
+ return s;
37
+ if (s.endsWith('y') && s.length > 1 && !'aeiou'.includes(s[s.length - 2])) {
38
+ return s.slice(0, -1) + 'ies';
39
+ }
40
+ return s + 's';
41
+ }
42
+ /** 사용자 입력(어느 형태든)에서 세 가지 변형을 파생한다. */
43
+ export function inflectModel(input) {
44
+ const trimmed = input.trim();
45
+ const singular = singularize(toCamel(toPascal(trimmed)));
46
+ const pascal = toPascal(singular);
47
+ const camel = toCamel(pascal);
48
+ const plural = pluralize(camel);
49
+ return { pascal, camel, plural };
50
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScaffoldFile } from './controller.js';
2
+ /** 잡 이름(파스칼) → 파일·잡명 파생. */
3
+ export declare function jobScaffold(pascalName: string): ScaffoldFile;
@@ -0,0 +1,46 @@
1
+ // @gaonjs/cli · scaffold · job (M9-B)
2
+ //
3
+ // `gaon g job <Name>` — 비동기 잡 스캐폴드(§7 M7). domain/jobs/<name>.ts 에
4
+ // 두면 워커가 자동 로드·등록한다(파일 로더가 assignName 으로 이름 채움).
5
+ //
6
+ // 파일 위치 (CLAUDE.md §2 · rule 5): 잡은 domain/jobs/ 아래 둔다 —
7
+ // 여러 앱이 같은 잡을 예약/실행할 수 있어야 하고, 앱→앱 import 는 금지다.
8
+ //
9
+ // 사용 예:
10
+ // import send${Pascal} from '../../domain/jobs/${camel}.js'
11
+ // await send${Pascal}.later({ id: 'abc' }) // 즉시 큐 적재
12
+ // await send${Pascal}.in('5m', { id: 'abc' }) // 5분 지연 후 실행
13
+ // await send${Pascal}.at(new Date(...), args) // 특정 시각 실행
14
+ /** 잡 이름(파스칼) → 파일·잡명 파생. */
15
+ export function jobScaffold(pascalName) {
16
+ const trimmed = pascalName.trim();
17
+ if (!trimmed) {
18
+ throw new Error(`[gaon g job] 잡 이름이 비어 있습니다. 예: gaon g job SendEmail`);
19
+ }
20
+ // 파스칼 정규화(사용자가 send_email/sendEmail 로 넣어도 동작)
21
+ const pascal = trimmed
22
+ .split(/[_\-\s]+/)
23
+ .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
24
+ .join('');
25
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
26
+ const lines = [
27
+ `// ${pascal} 잡 — gaon g job (M9-B).`,
28
+ `// 반환값을 export 하면 워커가 자동 등록한다(§7). 이름은 { name } 옵션이 우선,`,
29
+ `// 없으면 파일명에서 채워진다(파일 로더 assignName).`,
30
+ `//`,
31
+ `// 실행:`,
32
+ `// import ${camel} from '../../domain/jobs/${camel}.js'`,
33
+ `// await ${camel}.later({ id: 'abc' }) // 즉시 큐 적재`,
34
+ `// await ${camel}.in('5m', { id: 'abc' }) // 5분 지연 후 실행`,
35
+ `// await ${camel}.at(new Date(...), payload) // 특정 시각 실행`,
36
+ `import { job } from 'gaonjs/async'`,
37
+ ``,
38
+ `export default job(async (payload: { id: string }) => {`,
39
+ ` // 실 로직을 여기에.`,
40
+ ` // 예: const record = await SomeModel.where('id', '=', payload.id).first()`,
41
+ ` console.log('[${pascal}] 실행:', payload.id)`,
42
+ `}, { name: '${camel}' })`,
43
+ ``,
44
+ ];
45
+ return { path: `domain/jobs/${camel}.ts`, contents: lines.join('\n') };
46
+ }
@@ -0,0 +1,8 @@
1
+ import type { ScaffoldFile } from './controller.js';
2
+ import type { ModelNames } from './inflect.js';
3
+ /** 스키마 스캐폴드 — E-4 컬럼 타입 예시를 함께 담는다. */
4
+ export declare function schemaScaffold(names: ModelNames): ScaffoldFile;
5
+ /** 모델 스캐폴드 — scopes 예시를 담는다(체이닝 진입점 도우미). */
6
+ export declare function modelScaffold(names: ModelNames): ScaffoldFile;
7
+ /** 모델 스캐폴드 = 스키마 + 모델 두 파일. */
8
+ export declare function modelScaffoldFiles(names: ModelNames): ScaffoldFile[];
@@ -0,0 +1,66 @@
1
+ // @gaonjs/cli · scaffold · model (M9-B)
2
+ //
3
+ // `gaon g model <Name>` — 스키마 + 모델 두 파일을 함께 만든다.
4
+ // 스키마는 errata E-4 신규 컬럼 타입(decimal · enum · uuid)과 수식어(unique ·
5
+ // nullable · default) 예시를 담아 새 개발자가 바로 참고할 수 있게 한다.
6
+ //
7
+ // 파일 위치 (CLAUDE.md §2 · rule 5):
8
+ // · 스키마 → domain/schema/<name>.ts (테이블 정의 · tables.d.ts 원천)
9
+ // · 모델 → domain/models/<name>.ts (Active Record · scopes/methods)
10
+ // 앱→도메인 import 만 허용되므로 이 위치가 유일한 정답이다.
11
+ /** 스키마 스캐폴드 — E-4 컬럼 타입 예시를 함께 담는다. */
12
+ export function schemaScaffold(names) {
13
+ const { pascal, camel, plural } = names;
14
+ const lines = [
15
+ `// ${pascal} 스키마 — gaon g model (M9-B).`,
16
+ `// errata E-4 예시: unique · default · enum · nullable. 컬럼은 자유롭게 추가/삭제한다.`,
17
+ `// .gaon/tables.d.ts 가 자동 재생성돼 모델·컨트롤러·페이지 전 체인이 즉시 반영된다(§6.3).`,
18
+ `import { table, t, type RowOf } from 'gaonjs/data'`,
19
+ ``,
20
+ `export const ${plural} = table('${plural}', {`,
21
+ ` id: t.id(),`,
22
+ ` title: t.string().max(200).unique(),`,
23
+ ` status: t.enum(['draft', 'published']).default('draft'),`,
24
+ ` ...t.timestamps(),`,
25
+ `})`,
26
+ ``,
27
+ `// tables.d.ts 자동 재생성 전(dev 서버 미기동)이라도 이 augmentation 이 있으면`,
28
+ `// 모델·컨트롤러에서 즉시 참조 가능하다. 재생성 후에는 이 블록이 중복돼도 무해하다.`,
29
+ `declare module 'gaonjs/data' {`,
30
+ ` interface GaonTables {`,
31
+ ` ${plural}: RowOf<typeof ${plural}>`,
32
+ ` }`,
33
+ `}`,
34
+ ``,
35
+ ];
36
+ return {
37
+ path: `domain/schema/${camel}.ts`,
38
+ contents: lines.join('\n'),
39
+ };
40
+ }
41
+ /** 모델 스캐폴드 — scopes 예시를 담는다(체이닝 진입점 도우미). */
42
+ export function modelScaffold(names) {
43
+ const { pascal, camel, plural } = names;
44
+ const lines = [
45
+ `// ${pascal} 모델 — gaon g model (M9-B).`,
46
+ `// scopes 는 체인 어느 지점에서든 호출 가능하다(§4.4). E-4 체이닝 예시:`,
47
+ `// ${pascal}.published().orderBy('createdAt', 'desc').limit(20).all()`,
48
+ `import { model } from 'gaonjs/data'`,
49
+ `import { ${plural} } from '../schema/${camel}.js'`,
50
+ ``,
51
+ `export const ${pascal} = model(${plural}, {`,
52
+ ` scopes: {`,
53
+ ` published: (q) => q.where('status', '=', 'published'),`,
54
+ ` },`,
55
+ `})`,
56
+ ``,
57
+ ];
58
+ return {
59
+ path: `domain/models/${camel}.ts`,
60
+ contents: lines.join('\n'),
61
+ };
62
+ }
63
+ /** 모델 스캐폴드 = 스키마 + 모델 두 파일. */
64
+ export function modelScaffoldFiles(names) {
65
+ return [schemaScaffold(names), modelScaffold(names)];
66
+ }
@@ -0,0 +1,7 @@
1
+ import type { ScaffoldFile } from './controller.js';
2
+ /**
3
+ * 페이지 경로에서 스캐폴드를 만든다.
4
+ * @param pagePath 예: 'Posts/Index' · 'Dashboard' · 'admin/Users/Show'.
5
+ * @param app 대상 앱.
6
+ */
7
+ export declare function pageScaffold(pagePath: string, app: string): ScaffoldFile;