@gaonjs/cli 0.1.7 → 0.3.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.
- package/dist/commands/g.d.ts +26 -0
- package/dist/commands/g.js +124 -0
- package/dist/db.d.ts +20 -0
- package/dist/db.js +62 -0
- package/dist/doctor/connections.d.ts +14 -0
- package/dist/doctor/connections.js +168 -0
- package/dist/doctor/dependency-direction.d.ts +11 -0
- package/dist/doctor/dependency-direction.js +186 -0
- package/dist/doctor/migration-diff.d.ts +11 -0
- package/dist/doctor/migration-diff.js +109 -0
- package/dist/doctor/n-plus-one.d.ts +5 -0
- package/dist/doctor/n-plus-one.js +242 -0
- package/dist/doctor/reporter.d.ts +5 -0
- package/dist/doctor/reporter.js +28 -0
- package/dist/doctor/response-mixing.d.ts +12 -0
- package/dist/doctor/response-mixing.js +158 -0
- package/dist/doctor/types.d.ts +24 -0
- package/dist/doctor/types.js +30 -0
- package/dist/doctor.d.ts +39 -0
- package/dist/doctor.js +88 -0
- package/dist/domain.d.ts +1 -0
- package/dist/domain.js +14 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +128 -2
- package/dist/scaffold/controller.d.ts +12 -0
- package/dist/scaffold/controller.js +50 -0
- package/dist/scaffold/index.d.ts +20 -0
- package/dist/scaffold/index.js +41 -0
- package/dist/scaffold/inflect.d.ts +19 -0
- package/dist/scaffold/inflect.js +50 -0
- package/dist/scaffold/job.d.ts +3 -0
- package/dist/scaffold/job.js +46 -0
- package/dist/scaffold/model.d.ts +8 -0
- package/dist/scaffold/model.js +66 -0
- package/dist/scaffold/page.d.ts +7 -0
- package/dist/scaffold/page.js +46 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +79 -0
- package/dist/templates/auth/auth.wiring.ts.tpl +1 -1
- package/dist/templates/auth/session.controller.ts.tpl +1 -1
- package/dist/work.js +9 -0
- package/package.json +8 -5
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성)
|
|
3
|
+
*
|
|
4
|
+
* 5 검사를 조립한다:
|
|
5
|
+
* 1) response-mixing (errata E-3 §C · 라이브)
|
|
6
|
+
* 2) n-plus-one (errata E-4 (e))
|
|
7
|
+
* 3) dependency-direction (CLAUDE.md §5 · 4 규칙)
|
|
8
|
+
* 4) connections (v0.15 §4.5)
|
|
9
|
+
* 5) migration-diff (최소 감지 · 상세는 M9-D)
|
|
10
|
+
*
|
|
11
|
+
* 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
|
|
12
|
+
* DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
|
|
13
|
+
* 구조를 유지한다(CLAUDE.md §4 · 모든 명령 --json).
|
|
14
|
+
*
|
|
15
|
+
* 하위 호환: 기존 응답 혼용 API(`inspectControllerSource`·`runDoctor`)는
|
|
16
|
+
* 그대로 export — 기존 테스트가 계속 동작한다.
|
|
17
|
+
*/
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { checkResponseMixing } from './doctor/response-mixing.js';
|
|
20
|
+
import { checkNPlusOne } from './doctor/n-plus-one.js';
|
|
21
|
+
import { checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
22
|
+
import { checkConnections } from './doctor/connections.js';
|
|
23
|
+
import { checkMigrationDiff } from './doctor/migration-diff.js';
|
|
24
|
+
import { renderHuman, renderJson } from './doctor/reporter.js';
|
|
25
|
+
import { makeResult, } from './doctor/types.js';
|
|
26
|
+
export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
|
|
27
|
+
export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
|
|
28
|
+
export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
29
|
+
export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/connections.js';
|
|
30
|
+
export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
|
|
31
|
+
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
32
|
+
/**
|
|
33
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 5개 모두.
|
|
34
|
+
*/
|
|
35
|
+
const ALL_RULES = [
|
|
36
|
+
'response-mixing',
|
|
37
|
+
'n-plus-one',
|
|
38
|
+
'dependency-direction',
|
|
39
|
+
'connections',
|
|
40
|
+
'migration-diff',
|
|
41
|
+
];
|
|
42
|
+
const CHECKERS = {
|
|
43
|
+
'response-mixing': checkResponseMixing,
|
|
44
|
+
'n-plus-one': checkNPlusOne,
|
|
45
|
+
'dependency-direction': checkDependencyDirection,
|
|
46
|
+
connections: checkConnections,
|
|
47
|
+
'migration-diff': checkMigrationDiff,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `gaon doctor` 진입점 (M9-E · 확장).
|
|
51
|
+
*
|
|
52
|
+
* 반환: DoctorResult({passed, warnings, errors}).
|
|
53
|
+
* exit code 는 진입점(runCli)이 errors.length 로 결정한다.
|
|
54
|
+
*/
|
|
55
|
+
export async function runDoctorCommand(opts = {}) {
|
|
56
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
57
|
+
const root = resolve(cwd);
|
|
58
|
+
const rules = opts.checks && opts.checks.length ? opts.checks : ALL_RULES;
|
|
59
|
+
const reports = [];
|
|
60
|
+
for (const rule of rules) {
|
|
61
|
+
const fn = CHECKERS[rule];
|
|
62
|
+
reports.push(await fn(root));
|
|
63
|
+
}
|
|
64
|
+
const result = makeResult(reports);
|
|
65
|
+
const out = opts.json ? renderJson(result) : renderHuman(result);
|
|
66
|
+
process.stdout.write(out + '\n');
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 하위 호환: 응답 혼용만 검사해 legacy shape 을 낸다.
|
|
71
|
+
* appsDir 은 프로젝트의 apps/ 절대 경로. 상위(진입점)는 runDoctorCommand
|
|
72
|
+
* 를 쓴다.
|
|
73
|
+
*/
|
|
74
|
+
export async function runDoctor(appsDir) {
|
|
75
|
+
// apps/ 의 부모를 cwd 로 간주(관례).
|
|
76
|
+
const cwd = resolve(appsDir, '..');
|
|
77
|
+
const report = await checkResponseMixing(cwd);
|
|
78
|
+
const issues = report.issues.map((i) => ({
|
|
79
|
+
file: i.file ?? '',
|
|
80
|
+
line: i.line ?? 0,
|
|
81
|
+
action: String(i.detail?.action ?? ''),
|
|
82
|
+
kinds: i.detail?.kinds ?? [],
|
|
83
|
+
message: i.message,
|
|
84
|
+
}));
|
|
85
|
+
// checked 는 컨트롤러 액션 수 — 응답 혼용 검사가 순회한 액션 수와 정합.
|
|
86
|
+
// 하위 호환 목적으로 issues 수로 대체(정확한 checked 는 신규 API 를 쓰라).
|
|
87
|
+
return { ok: issues.length === 0, checked: issues.length, issues };
|
|
88
|
+
}
|
package/dist/domain.d.ts
CHANGED
package/dist/domain.js
CHANGED
|
@@ -15,6 +15,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
15
15
|
import { join, basename } from 'node:path';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import { isJobDef, isListener, reidentifyListener } from '@gaonjs/async';
|
|
18
|
+
import { isMailDef } from '@gaonjs/mail';
|
|
18
19
|
import { registerTsResolve } from './tsResolve.js';
|
|
19
20
|
function tsFilesIn(dir) {
|
|
20
21
|
if (!existsSync(dir))
|
|
@@ -36,9 +37,21 @@ export async function loadDomain(root) {
|
|
|
36
37
|
const domainDir = join(root, 'domain');
|
|
37
38
|
let jobs = 0;
|
|
38
39
|
let listeners = 0;
|
|
40
|
+
let mails = 0;
|
|
39
41
|
// 이벤트 정의를 먼저 import 해 리스너가 참조할 수 있게 한다(부수효과 없음).
|
|
40
42
|
for (const file of tsFilesIn(join(domainDir, 'events')))
|
|
41
43
|
await importFile(file);
|
|
44
|
+
// 메일 — domain/mails/ 의 파일이 곧 등록(§7). 파일명을 이름으로 심는다.
|
|
45
|
+
for (const file of tsFilesIn(join(domainDir, 'mails'))) {
|
|
46
|
+
const mod = await importFile(file);
|
|
47
|
+
const nm = basename(file, '.ts');
|
|
48
|
+
for (const value of Object.values(mod)) {
|
|
49
|
+
if (isMailDef(value)) {
|
|
50
|
+
value.assignName(nm);
|
|
51
|
+
mails++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
for (const file of tsFilesIn(join(domainDir, 'jobs'))) {
|
|
43
56
|
const mod = await importFile(file);
|
|
44
57
|
const base = basename(file, '.ts');
|
|
@@ -68,5 +81,5 @@ export async function loadDomain(root) {
|
|
|
68
81
|
schedule = def;
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
|
-
return { jobs, listeners, schedule };
|
|
84
|
+
return { jobs, listeners, mails, schedule };
|
|
72
85
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { type DoctorRule } from "./doctor.js";
|
|
1
2
|
export { runDevCommand, startDev, resolveDevLayout, type DevDeps, type DevLayout, type DevApp, type DevEvent, type DevHandle, type DevCommandOptions, } from "./dev.js";
|
|
2
3
|
export { runCheck, runCheckCommand, type CheckDeps, type CheckResult, type TypecheckResult, type CheckCommandOptions, } from "./check.js";
|
|
3
4
|
export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, type AuthScaffoldOptions, type ScaffoldFile, type ScaffoldResult, type GenerateAuthOptions, } from "./generate.js";
|
|
5
|
+
export { runGenerateCommand, planScaffold, parseGenerateArgs, type GenerateType, type GenerateOptions, type GenerateResult, } from "./commands/g.js";
|
|
4
6
|
export { runHubCommand, type HubCommandOptions } from "./hub.js";
|
|
7
|
+
export { runServeCommand, type ServeCommandOptions } from "./serve.js";
|
|
5
8
|
export { runWorkCommand, type WorkCommandOptions } from "./work.js";
|
|
6
9
|
export { runJobsCommand, type JobsCommandOptions } from "./jobs.js";
|
|
10
|
+
export { runDbSeedCommand, loadSeed, type DbSeedOptions, type DbSeedResult } from "./db.js";
|
|
11
|
+
export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, type DoctorResult, type DoctorCheck, type DoctorLevel, type DoctorRule, type RuleReport, type DoctorIssue, type LegacyDoctorResult, type ActionUsage, type ResponseKind, type DoctorCommandOptions, } from "./doctor.js";
|
|
7
12
|
export { loadDomain, type LoadedDomain } from "./domain.js";
|
|
8
13
|
export interface RoadmapReport {
|
|
9
14
|
readonly name: "gaon";
|
|
@@ -25,5 +30,10 @@ export declare function renderRoadmap(version?: string): string;
|
|
|
25
30
|
export interface RunOptions {
|
|
26
31
|
readonly version?: string;
|
|
27
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* `gaon doctor --check=<이름>[,<이름>...]` 파싱(M9-E).
|
|
35
|
+
* 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseDoctorChecks(argv: readonly string[]): DoctorRule[] | undefined;
|
|
28
38
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
29
39
|
export declare function runCli(argv: readonly string[], opts?: RunOptions): void;
|
package/dist/index.js
CHANGED
|
@@ -13,15 +13,23 @@ import { MILESTONES, VERSION, HOMEPAGE } from "@gaonjs/core";
|
|
|
13
13
|
import { runDevCommand } from "./dev.js";
|
|
14
14
|
import { runCheckCommand } from "./check.js";
|
|
15
15
|
import { runGenerateAuthCommand } from "./generate.js";
|
|
16
|
+
import { runGenerateCommand } from "./commands/g.js";
|
|
16
17
|
import { runHubCommand } from "./hub.js";
|
|
18
|
+
import { runServeCommand } from "./serve.js";
|
|
17
19
|
import { runWorkCommand } from "./work.js";
|
|
18
20
|
import { runJobsCommand } from "./jobs.js";
|
|
21
|
+
import { runDbSeedCommand } from "./db.js";
|
|
22
|
+
import { runDoctorCommand } from "./doctor.js";
|
|
19
23
|
export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
|
|
20
24
|
export { runCheck, runCheckCommand, } from "./check.js";
|
|
21
25
|
export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, } from "./generate.js";
|
|
26
|
+
export { runGenerateCommand, planScaffold, parseGenerateArgs, } from "./commands/g.js";
|
|
22
27
|
export { runHubCommand } from "./hub.js";
|
|
28
|
+
export { runServeCommand } from "./serve.js";
|
|
23
29
|
export { runWorkCommand } from "./work.js";
|
|
24
30
|
export { runJobsCommand } from "./jobs.js";
|
|
31
|
+
export { runDbSeedCommand, loadSeed } from "./db.js";
|
|
32
|
+
export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, } from "./doctor.js";
|
|
25
33
|
export { loadDomain } from "./domain.js";
|
|
26
34
|
/** `--json` 출력용 구조화 리포트. */
|
|
27
35
|
export function roadmapReport(version = VERSION) {
|
|
@@ -68,8 +76,18 @@ function renderHelp(version = VERSION) {
|
|
|
68
76
|
" gaon 로드맵과 개발 상태를 출력",
|
|
69
77
|
" gaon dev .gaon 타입 브리지를 감시·재생성 (스키마·라우트)",
|
|
70
78
|
" gaon dev --json 재생성 이벤트를 JSON 으로 출력",
|
|
79
|
+
" gaon serve 웹 서버 부팅 (gaon.config.ts 자동 배선 · Fastify listen)",
|
|
80
|
+
" gaon serve --port <n> --host <h> 리슨 포트·호스트 (config 값을 덮음)",
|
|
71
81
|
" gaon check .gaon 재생성 후 타입 검사 (CI·AI 정합)",
|
|
82
|
+
" gaon doctor 정적 검사 (5 검사 · 응답 혼용·N+1·의존 방향·커넥션·마이그)",
|
|
83
|
+
" gaon doctor --json 자동화용 JSON 출력",
|
|
84
|
+
" gaon doctor --check=n-plus-one,connections 선택 검사만 실행",
|
|
72
85
|
" gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
|
|
86
|
+
" gaon g controller <name> 컨트롤러 스캐폴드 (Rails 관례 · 페이지+JSON 액션)",
|
|
87
|
+
" gaon g model <Name> 모델 스캐폴드 (스키마+모델 · E-4 컬럼 예시)",
|
|
88
|
+
" gaon g page <Path/Name> Vue 페이지 (Inertia SPA · pageProps 브리지)",
|
|
89
|
+
" gaon g job <Name> 비동기 잡 (domain/jobs · later/in/at)",
|
|
90
|
+
" gaon g <type> --overwrite 기존 파일 덮어쓰기 · --app <이름> · --json",
|
|
73
91
|
" gaon hub 실시간 허브 프로세스 (프레즌스 권위·중계 · 리더 선출 HA)",
|
|
74
92
|
" gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
|
|
75
93
|
" gaon jobs list --failed DLQ(실패 잡) 목록",
|
|
@@ -82,6 +100,30 @@ function renderHelp(version = VERSION) {
|
|
|
82
100
|
"",
|
|
83
101
|
].join("\n");
|
|
84
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* `gaon doctor --check=<이름>[,<이름>...]` 파싱(M9-E).
|
|
105
|
+
* 지정 없음(undefined) = 5 검사 모두 실행. 알 수 없는 이름은 무시(안전).
|
|
106
|
+
*/
|
|
107
|
+
export function parseDoctorChecks(argv) {
|
|
108
|
+
const known = [
|
|
109
|
+
"response-mixing",
|
|
110
|
+
"n-plus-one",
|
|
111
|
+
"dependency-direction",
|
|
112
|
+
"connections",
|
|
113
|
+
"migration-diff",
|
|
114
|
+
];
|
|
115
|
+
const isKnown = (s) => known.includes(s);
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const a of argv) {
|
|
118
|
+
if (a.startsWith("--check=")) {
|
|
119
|
+
for (const nm of a.slice("--check=".length).split(",")) {
|
|
120
|
+
if (isKnown(nm) && !out.includes(nm))
|
|
121
|
+
out.push(nm);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out.length ? out : undefined;
|
|
126
|
+
}
|
|
85
127
|
/** CLI 진입점. argv 는 실행 인자(process.argv.slice(2))를 받는다. */
|
|
86
128
|
export function runCli(argv, opts = {}) {
|
|
87
129
|
const version = opts.version ?? VERSION;
|
|
@@ -95,6 +137,19 @@ export function runCli(argv, opts = {}) {
|
|
|
95
137
|
});
|
|
96
138
|
return;
|
|
97
139
|
}
|
|
140
|
+
// `gaon serve` — 웹 서버 부팅(§7 · M9-A). gaon.config.ts 자동 배선 후 listen.
|
|
141
|
+
if (argv[0] === "serve") {
|
|
142
|
+
const portIdx = argv.indexOf("--port");
|
|
143
|
+
const hostIdx = argv.indexOf("--host");
|
|
144
|
+
const port = portIdx >= 0 ? Number(argv[portIdx + 1]) : undefined;
|
|
145
|
+
const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;
|
|
146
|
+
void runServeCommand({ json: argv.includes("--json"), port, host }).catch((err) => {
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
+
process.stderr.write(` ✗ gaon serve 실패: ${msg}\n`);
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
98
153
|
// `gaon check` — .gaon 재생성 후 타입 검사(§13.4-5). 종료 코드로 결과 전달.
|
|
99
154
|
if (argv[0] === "check") {
|
|
100
155
|
void runCheckCommand({ json: argv.includes("--json") })
|
|
@@ -108,6 +163,21 @@ export function runCli(argv, opts = {}) {
|
|
|
108
163
|
});
|
|
109
164
|
return;
|
|
110
165
|
}
|
|
166
|
+
// `gaon doctor` — 정적 검사(M9-E · 5 검사). --check=<이름>[,<이름>...] 로
|
|
167
|
+
// 선택 실행, --json 은 자동화 파싱용. exit code: errors.length > 0 이면 1.
|
|
168
|
+
if (argv[0] === "doctor") {
|
|
169
|
+
const checks = parseDoctorChecks(argv);
|
|
170
|
+
void runDoctorCommand({ json: argv.includes("--json"), checks })
|
|
171
|
+
.then((result) => {
|
|
172
|
+
process.exitCode = result.errors.length > 0 ? 1 : 0;
|
|
173
|
+
})
|
|
174
|
+
.catch((err) => {
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
process.stderr.write(` ✗ gaon doctor 실패: ${msg}\n`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
111
181
|
// `gaon hub` — 실시간 허브 프로세스(§7 M6). 운영 프로세스 3종 중 하나.
|
|
112
182
|
// NATS·리더 선출 대기로 프로세스를 살려 두고, SIGINT/SIGTERM 에 그레이스풀 종료.
|
|
113
183
|
if (argv[0] === "hub") {
|
|
@@ -141,7 +211,24 @@ export function runCli(argv, opts = {}) {
|
|
|
141
211
|
});
|
|
142
212
|
return;
|
|
143
213
|
}
|
|
144
|
-
// `gaon
|
|
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;
|
|
221
|
+
})
|
|
222
|
+
.catch((err) => {
|
|
223
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
224
|
+
process.stderr.write(` ✗ gaon db seed 실패: ${msg}\n`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// `gaon g <type> <name>` — 스캐폴드(§7 M5·M9-B).
|
|
230
|
+
// · auth 는 인증 스캐폴드(별도 진입점)
|
|
231
|
+
// · controller/model/page/job 은 M9-B 통합 라우터
|
|
145
232
|
if (argv[0] === "g" || argv[0] === "generate") {
|
|
146
233
|
if (argv[1] === "auth") {
|
|
147
234
|
const appIdx = argv.indexOf("--app");
|
|
@@ -150,8 +237,47 @@ export function runCli(argv, opts = {}) {
|
|
|
150
237
|
process.exitCode = code;
|
|
151
238
|
return;
|
|
152
239
|
}
|
|
240
|
+
const known = ["controller", "model", "page", "job"];
|
|
241
|
+
const type = argv[1];
|
|
242
|
+
if (type && known.includes(type)) {
|
|
243
|
+
const rest = argv.slice(2);
|
|
244
|
+
// name = 첫 위치 인자(--플래그 제외 · --app <v> 스킵)
|
|
245
|
+
let name;
|
|
246
|
+
let app;
|
|
247
|
+
let overwrite = false;
|
|
248
|
+
let json = false;
|
|
249
|
+
for (let i = 0; i < rest.length; i++) {
|
|
250
|
+
const a = rest[i];
|
|
251
|
+
if (a === "--json") {
|
|
252
|
+
json = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (a === "--overwrite") {
|
|
256
|
+
overwrite = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (a === "--app") {
|
|
260
|
+
app = rest[++i];
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (a?.startsWith("--"))
|
|
264
|
+
continue;
|
|
265
|
+
if (name === undefined)
|
|
266
|
+
name = a;
|
|
267
|
+
}
|
|
268
|
+
if (!name) {
|
|
269
|
+
process.stderr.write(` ✗ gaon g ${type}: 이름이 없습니다.\n` +
|
|
270
|
+
` → 예: gaon g ${type} ${type === "page" ? "Posts/Index" : "Post"}\n`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const code = runGenerateCommand(type, name, { app, overwrite, json });
|
|
275
|
+
process.exitCode = code;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
153
278
|
process.stderr.write(` ✗ 알 수 없는 제너레이터: ${argv[1] ?? "(없음)"}\n` +
|
|
154
|
-
` → 현재 지원: gaon g auth
|
|
279
|
+
` → 현재 지원: gaon g auth | controller | model | page | job\n` +
|
|
280
|
+
` → 옵션: --app <이름> · --overwrite · --json\n`);
|
|
155
281
|
process.exitCode = 1;
|
|
156
282
|
return;
|
|
157
283
|
}
|
|
@@ -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,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[];
|