@gaonjs/cli 0.4.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.
- package/dist/commands/check.d.ts +31 -0
- package/dist/commands/check.js +223 -0
- package/dist/commands/console.d.ts +46 -0
- package/dist/commands/console.js +129 -0
- package/dist/commands/new.d.ts +45 -0
- package/dist/commands/new.js +274 -0
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +119 -0
- package/dist/doctor/no-auto-import.d.ts +10 -0
- package/dist/doctor/no-auto-import.js +158 -0
- package/dist/doctor/shared-composable-purity.d.ts +8 -0
- package/dist/doctor/shared-composable-purity.js +164 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +6 -5
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +13 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +104 -6
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.js +66 -0
- package/dist/templates/index.ts +85 -0
- package/dist/templates/project/.env.example.tpl +18 -0
- package/dist/templates/project/.gitignore.tpl +24 -0
- package/dist/templates/project/.npmrc.tpl +4 -0
- package/dist/templates/project/CLAUDE.md.tpl +119 -0
- package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
- package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
- package/dist/templates/project/docker-compose.yaml.tpl +73 -0
- package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
- package/dist/templates/project/gaon.config.ts.tpl +27 -0
- package/dist/templates/project/package.json.tpl +27 -0
- package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
- package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
- package/dist/templates/project/tsconfig.json.tpl +25 -0
- package/package.json +3 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CheckStep = 'typecheck' | 'vue-tsc' | 'build' | 'doctor';
|
|
2
|
+
export interface CheckCommandOptions {
|
|
3
|
+
readonly cwd?: string;
|
|
4
|
+
readonly json?: boolean;
|
|
5
|
+
/** 지정 시 그 검사 하나만 실행. 나머지는 스킵. */
|
|
6
|
+
readonly only?: CheckStep;
|
|
7
|
+
/** true 면 doctor 도 포함. 기본 false. */
|
|
8
|
+
readonly includeDoctor?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type CheckStepStatus = 'passed' | 'failed' | 'skipped';
|
|
11
|
+
export interface CheckStepResult {
|
|
12
|
+
readonly step: CheckStep;
|
|
13
|
+
readonly status: CheckStepStatus;
|
|
14
|
+
/** 실행에 쓴 커맨드(디버깅용) — skipped 는 undefined. */
|
|
15
|
+
readonly command?: string;
|
|
16
|
+
/** 원본 exit code. skipped/doctor 는 undefined. */
|
|
17
|
+
readonly exitCode?: number;
|
|
18
|
+
/** stdout+stderr 합본(사람 UI 에 그대로 출력). skipped 는 짧은 사유. */
|
|
19
|
+
readonly output?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CheckReport {
|
|
22
|
+
readonly ok: boolean;
|
|
23
|
+
readonly steps: readonly CheckStepResult[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `gaon check` 진입점. 각 단계를 순서대로 실행하고, 하나라도 실패하면
|
|
27
|
+
* exit 1. --only 지정 시 그 단계만 실행. --include-doctor 시 doctor 추가.
|
|
28
|
+
*
|
|
29
|
+
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runCheckCommand(opts?: CheckCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon check` — 통합 검증 (M9-G · v0.15 §13.5)
|
|
3
|
+
*
|
|
4
|
+
* 배포 · CI · AI 에이전트가 한 번에 신뢰할 수 있는 검사 묶음. 사용자의
|
|
5
|
+
* package.json 관례를 그대로 재사용한다 — `pnpm typecheck` · `pnpm vue-tsc`
|
|
6
|
+
* · `pnpm build`. 스크립트가 없으면 폴백으로 로컬 바이너리를 직접 부른다
|
|
7
|
+
* (`node_modules/.bin/tsc --noEmit` · `node_modules/.bin/vue-tsc --noEmit`
|
|
8
|
+
* · `pnpm -r build` → `npm run build`).
|
|
9
|
+
*
|
|
10
|
+
* The One Way — 하나의 명령이 3(선택 4) 검사를 순서대로 돌린다:
|
|
11
|
+
* 1) typecheck (pnpm typecheck 또는 tsc --noEmit)
|
|
12
|
+
* 2) vue-tsc (pnpm vue-tsc 또는 vue-tsc --noEmit)
|
|
13
|
+
* 3) build (pnpm build)
|
|
14
|
+
* (4) doctor (--include-doctor 시 · 코어 재사용)
|
|
15
|
+
*
|
|
16
|
+
* 옵션 최소:
|
|
17
|
+
* --json 자동화 · 각 검사의 passed/failed/skipped
|
|
18
|
+
* --only <name> 단일 검사만
|
|
19
|
+
* --include-doctor check 안에서 doctor 도 · 기본 off
|
|
20
|
+
*
|
|
21
|
+
* exit code: 검사가 하나라도 실패하면 1, 그 외 0. 미리 검사할 대상이
|
|
22
|
+
* 하나도 없으면(예: 스크립트도 없고 바이너리도 없음) 스킵으로 리포트 —
|
|
23
|
+
* 우회가 아니라 사용자에게 "설정이 부족하다" 를 명확히 노출.
|
|
24
|
+
*
|
|
25
|
+
* §9 실 인프라 — 서브 프로세스 실행은 실 `child_process.spawn` 을 쓴다.
|
|
26
|
+
* 목업 X.
|
|
27
|
+
*/
|
|
28
|
+
import { spawn } from 'node:child_process';
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { runDoctorCommand } from '../doctor.js';
|
|
32
|
+
/**
|
|
33
|
+
* 프로젝트 스크립트 존재 여부. pnpm/npm 어느 쪽이든 `scripts.<name>` 을
|
|
34
|
+
* 정의해 두면 우선 사용한다.
|
|
35
|
+
*/
|
|
36
|
+
function hasScript(cwd, name) {
|
|
37
|
+
const pkgPath = join(cwd, 'package.json');
|
|
38
|
+
if (!existsSync(pkgPath))
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
42
|
+
return typeof pkg.scripts?.[name] === 'string' && pkg.scripts[name].length > 0;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function binExists(cwd, name) {
|
|
49
|
+
return existsSync(join(cwd, 'node_modules', '.bin', name));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 단일 서브 프로세스를 spawn 해서 stdout+stderr 를 모으고 exit 코드를
|
|
53
|
+
* 돌려준다. 실행 실패(파일 없음)는 exit 127 로 매핑.
|
|
54
|
+
*/
|
|
55
|
+
function runSubprocess(cwd, cmd, args) {
|
|
56
|
+
return new Promise((resolvePromise) => {
|
|
57
|
+
const child = spawn(cmd, args, { cwd, env: process.env });
|
|
58
|
+
let output = '';
|
|
59
|
+
child.stdout.on('data', (b) => (output += b.toString('utf8')));
|
|
60
|
+
child.stderr.on('data', (b) => (output += b.toString('utf8')));
|
|
61
|
+
child.on('error', (err) => {
|
|
62
|
+
resolvePromise({ exitCode: 127, output: output + String(err) });
|
|
63
|
+
});
|
|
64
|
+
child.on('close', (code) => {
|
|
65
|
+
resolvePromise({ exitCode: code ?? 1, output });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** typecheck · vue-tsc 는 노말 검사 · build 는 산출물 생성. */
|
|
70
|
+
async function runStep(step, cwd) {
|
|
71
|
+
// 1) 사용자 스크립트가 정의돼 있으면 그걸 쓴다(한 곳에서 관리).
|
|
72
|
+
const scriptName = step;
|
|
73
|
+
if (hasScript(cwd, scriptName)) {
|
|
74
|
+
const cmd = 'pnpm';
|
|
75
|
+
const args = ['run', scriptName];
|
|
76
|
+
const { exitCode, output } = await runSubprocess(cwd, cmd, args);
|
|
77
|
+
return {
|
|
78
|
+
step,
|
|
79
|
+
status: exitCode === 0 ? 'passed' : 'failed',
|
|
80
|
+
command: `${cmd} ${args.join(' ')}`,
|
|
81
|
+
exitCode,
|
|
82
|
+
output,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// 2) 폴백 — 로컬 바이너리를 직접 부른다.
|
|
86
|
+
if (step === 'typecheck') {
|
|
87
|
+
// tsc 는 로컬 node_modules 또는 workspace 루트에 있다.
|
|
88
|
+
const tsconfigPath = join(cwd, 'tsconfig.json');
|
|
89
|
+
if (!existsSync(tsconfigPath)) {
|
|
90
|
+
return {
|
|
91
|
+
step,
|
|
92
|
+
status: 'skipped',
|
|
93
|
+
output: `tsconfig.json 이 없어 typecheck 를 건너뜁니다: ${tsconfigPath}\n→ 프로젝트 루트에 tsconfig.json 을 만들거나 package.json 에 "typecheck" 스크립트를 정의하세요.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const tsBin = join(cwd, 'node_modules', 'typescript', 'bin', 'tsc');
|
|
97
|
+
if (!existsSync(tsBin)) {
|
|
98
|
+
return {
|
|
99
|
+
step,
|
|
100
|
+
status: 'skipped',
|
|
101
|
+
output: `typescript 가 설치돼 있지 않아 typecheck 를 건너뜁니다.\n→ pnpm add -D typescript 후 다시 실행하세요.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const cmd = process.execPath;
|
|
105
|
+
const args = [tsBin, '--noEmit', '-p', tsconfigPath];
|
|
106
|
+
const { exitCode, output } = await runSubprocess(cwd, cmd, args);
|
|
107
|
+
return {
|
|
108
|
+
step,
|
|
109
|
+
status: exitCode === 0 ? 'passed' : 'failed',
|
|
110
|
+
command: `node tsc --noEmit -p ${tsconfigPath}`,
|
|
111
|
+
exitCode,
|
|
112
|
+
output,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (step === 'vue-tsc') {
|
|
116
|
+
// vue-tsc 는 .vue 가 하나라도 있는 프로젝트에서만 의미가 있다 —
|
|
117
|
+
// 없으면 스킵(설치 안 됐어도 정상).
|
|
118
|
+
if (!binExists(cwd, 'vue-tsc')) {
|
|
119
|
+
return {
|
|
120
|
+
step,
|
|
121
|
+
status: 'skipped',
|
|
122
|
+
output: 'vue-tsc 가 설치돼 있지 않아 스킵합니다(.vue 파일이 없으면 정상).',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const tsconfigPath = join(cwd, 'tsconfig.json');
|
|
126
|
+
if (!existsSync(tsconfigPath)) {
|
|
127
|
+
return {
|
|
128
|
+
step,
|
|
129
|
+
status: 'skipped',
|
|
130
|
+
output: 'tsconfig.json 이 없어 vue-tsc 를 건너뜁니다.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const cmd = join(cwd, 'node_modules', '.bin', 'vue-tsc');
|
|
134
|
+
const args = ['--noEmit', '-p', tsconfigPath];
|
|
135
|
+
const { exitCode, output } = await runSubprocess(cwd, cmd, args);
|
|
136
|
+
return {
|
|
137
|
+
step,
|
|
138
|
+
status: exitCode === 0 ? 'passed' : 'failed',
|
|
139
|
+
command: `vue-tsc --noEmit -p ${tsconfigPath}`,
|
|
140
|
+
exitCode,
|
|
141
|
+
output,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// build — 사용자 스크립트가 없으면 pnpm -r build 를 최선의 폴백으로 시도.
|
|
145
|
+
const cmd = 'pnpm';
|
|
146
|
+
const args = ['-r', 'run', 'build'];
|
|
147
|
+
const { exitCode, output } = await runSubprocess(cwd, cmd, args);
|
|
148
|
+
return {
|
|
149
|
+
step,
|
|
150
|
+
status: exitCode === 0 ? 'passed' : 'failed',
|
|
151
|
+
command: `${cmd} ${args.join(' ')}`,
|
|
152
|
+
exitCode,
|
|
153
|
+
output,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** doctor 는 이미 있는 명령을 재사용 — cwd 만 넘긴다. json 은 상위에서. */
|
|
157
|
+
async function runDoctorStep(cwd) {
|
|
158
|
+
try {
|
|
159
|
+
// runDoctorCommand 는 stdout 에 리포트를 그대로 찍는다. 결과 객체로
|
|
160
|
+
// 성공/실패를 판단한다.
|
|
161
|
+
const result = await runDoctorCommand({ cwd, json: false });
|
|
162
|
+
const failed = !!result.fatal || result.errors.length > 0;
|
|
163
|
+
return {
|
|
164
|
+
step: 'doctor',
|
|
165
|
+
status: failed ? 'failed' : 'passed',
|
|
166
|
+
command: 'gaon doctor',
|
|
167
|
+
exitCode: failed ? 1 : 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
+
return {
|
|
173
|
+
step: 'doctor',
|
|
174
|
+
status: 'failed',
|
|
175
|
+
command: 'gaon doctor',
|
|
176
|
+
exitCode: 1,
|
|
177
|
+
output: msg,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `gaon check` 진입점. 각 단계를 순서대로 실행하고, 하나라도 실패하면
|
|
183
|
+
* exit 1. --only 지정 시 그 단계만 실행. --include-doctor 시 doctor 추가.
|
|
184
|
+
*
|
|
185
|
+
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
186
|
+
*/
|
|
187
|
+
export async function runCheckCommand(opts = {}) {
|
|
188
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
189
|
+
const json = opts.json ?? false;
|
|
190
|
+
const only = opts.only;
|
|
191
|
+
const includeDoctor = opts.includeDoctor ?? false;
|
|
192
|
+
const steps = only
|
|
193
|
+
? [only]
|
|
194
|
+
: ['typecheck', 'vue-tsc', 'build', ...(includeDoctor ? ['doctor'] : [])];
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const step of steps) {
|
|
197
|
+
if (step === 'doctor') {
|
|
198
|
+
results.push(await runDoctorStep(cwd));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
results.push(await runStep(step, cwd));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const ok = results.every((r) => r.status !== 'failed');
|
|
205
|
+
const report = { ok, steps: results };
|
|
206
|
+
if (json) {
|
|
207
|
+
process.stdout.write(JSON.stringify(report) + '\n');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const r of results) {
|
|
211
|
+
const mark = r.status === 'passed' ? '✓' : r.status === 'failed' ? '✗' : '·';
|
|
212
|
+
process.stdout.write(` ${mark} ${r.step} — ${r.status}${r.command ? ` (${r.command})` : ''}\n`);
|
|
213
|
+
if (r.status === 'failed' && r.output) {
|
|
214
|
+
process.stdout.write(r.output.endsWith('\n') ? r.output : r.output + '\n');
|
|
215
|
+
}
|
|
216
|
+
if (r.status === 'skipped' && r.output) {
|
|
217
|
+
process.stdout.write(` ${r.output.split('\n').join('\n ')}\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
process.stdout.write(ok ? ' ▶ gaon check · 통과\n' : ' ✗ gaon check · 실패\n');
|
|
221
|
+
}
|
|
222
|
+
return ok ? 0 : 1;
|
|
223
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon console` — REPL 진입점 (M9-G · v0.15 §13.5)
|
|
3
|
+
*
|
|
4
|
+
* Rails console 스타일 — 프로젝트 컨텍스트(gaon.config.ts + apps/*)를
|
|
5
|
+
* 로드해 배터리(DB · 모델 · 서비스)가 살아있는 상태로 Node REPL 을 띄운다.
|
|
6
|
+
* 종료는 REPL 관례대로 `.exit` 또는 Ctrl+C(2회).
|
|
7
|
+
*
|
|
8
|
+
* The One Way — 단일 진입점 · 옵션 최소:
|
|
9
|
+
* · (기본) gaon.config.ts 로드 → wireGaon → REPL
|
|
10
|
+
* · --no-config 순수 REPL (config 없이 · 프로젝트 밖 실험용)
|
|
11
|
+
* · --json 부팅 이벤트만 JSON 라인(자동화 · CI 스모크). REPL 자체는
|
|
12
|
+
* stdin TTY 를 그대로 물려 준다.
|
|
13
|
+
*
|
|
14
|
+
* loadDomain 은 domain/models · services 를 REPL 컨텍스트에 노출한다.
|
|
15
|
+
* `.later()` 로 잡을 스케줄해 볼 수 있게 하기 위해 도메인 잡도 함께 로드한다.
|
|
16
|
+
*
|
|
17
|
+
* fail-closed(§7.5.3): config 로드 실패는 stderr 에 수리 안내를 던지고
|
|
18
|
+
* exit code 1 로 나간다 — 우회 없이 정확한 원인을 노출.
|
|
19
|
+
*/
|
|
20
|
+
import { type REPLServer } from 'node:repl';
|
|
21
|
+
export interface ConsoleCommandOptions {
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
/** 프로젝트 컨텍스트 로드 없이 순수 REPL. 기본 false. */
|
|
24
|
+
readonly noConfig?: boolean;
|
|
25
|
+
/** 부팅 이벤트를 JSON 라인으로 출력. REPL 세션 자체는 사람 UI. */
|
|
26
|
+
readonly json?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* REPL 서버 팩토리 — 테스트가 stdin/stdout 을 갈아 끼울 수 있게 주입점을
|
|
29
|
+
* 남긴다. 미주입 시 process.stdin/stdout 을 쓰는 기본 REPL.
|
|
30
|
+
*/
|
|
31
|
+
readonly replFactory?: () => REPLServer;
|
|
32
|
+
/** 종료 감지용 signals — 테스트에서 SIGINT 를 주입한다. 기본 process. */
|
|
33
|
+
readonly signals?: {
|
|
34
|
+
on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
|
|
35
|
+
off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `gaon console` 진입점.
|
|
40
|
+
*
|
|
41
|
+
* --no-config: env 만 로드하고 순수 REPL 을 띄운다.
|
|
42
|
+
* 기본: loadDotEnv → loadGaonConfig → wireGaon → loadDomain →
|
|
43
|
+
* REPL 컨텍스트에 gaon(wired) · config · apps · models · services
|
|
44
|
+
* · domain 을 심고, .exit 대기.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runConsoleCommand(opts?: ConsoleCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gaonjs/cli · `gaon console` — REPL 진입점 (M9-G · v0.15 §13.5)
|
|
3
|
+
*
|
|
4
|
+
* Rails console 스타일 — 프로젝트 컨텍스트(gaon.config.ts + apps/*)를
|
|
5
|
+
* 로드해 배터리(DB · 모델 · 서비스)가 살아있는 상태로 Node REPL 을 띄운다.
|
|
6
|
+
* 종료는 REPL 관례대로 `.exit` 또는 Ctrl+C(2회).
|
|
7
|
+
*
|
|
8
|
+
* The One Way — 단일 진입점 · 옵션 최소:
|
|
9
|
+
* · (기본) gaon.config.ts 로드 → wireGaon → REPL
|
|
10
|
+
* · --no-config 순수 REPL (config 없이 · 프로젝트 밖 실험용)
|
|
11
|
+
* · --json 부팅 이벤트만 JSON 라인(자동화 · CI 스모크). REPL 자체는
|
|
12
|
+
* stdin TTY 를 그대로 물려 준다.
|
|
13
|
+
*
|
|
14
|
+
* loadDomain 은 domain/models · services 를 REPL 컨텍스트에 노출한다.
|
|
15
|
+
* `.later()` 로 잡을 스케줄해 볼 수 있게 하기 위해 도메인 잡도 함께 로드한다.
|
|
16
|
+
*
|
|
17
|
+
* fail-closed(§7.5.3): config 로드 실패는 stderr 에 수리 안내를 던지고
|
|
18
|
+
* exit code 1 로 나간다 — 우회 없이 정확한 원인을 노출.
|
|
19
|
+
*/
|
|
20
|
+
import { start as startRepl } from 'node:repl';
|
|
21
|
+
import { loadDotEnv } from '@gaonjs/core';
|
|
22
|
+
import { loadGaonConfig, wireGaon, findConfigPath } from '@gaonjs/config';
|
|
23
|
+
import { registerTsResolve } from '../tsResolve.js';
|
|
24
|
+
import { loadDomain } from '../domain.js';
|
|
25
|
+
function humanEvent(e) {
|
|
26
|
+
switch (e.kind) {
|
|
27
|
+
case 'starting':
|
|
28
|
+
return ` gaon console · 시작 — cwd=${e.cwd}${e.noConfig ? ' · --no-config' : ''}`;
|
|
29
|
+
case 'loaded': {
|
|
30
|
+
const cfg = e.configPath ? `config=${e.configPath}` : 'config=(없음 · 기본값)';
|
|
31
|
+
const apps = e.apps.length ? `apps=[${e.apps.join(', ')}]` : 'apps=(없음)';
|
|
32
|
+
const d = `domain(jobs=${e.domain.jobs} · listeners=${e.domain.listeners} · mails=${e.domain.mails})`;
|
|
33
|
+
return ` ▶ 로드됨 — ${cfg} · ${apps} · ${d}`;
|
|
34
|
+
}
|
|
35
|
+
case 'ready':
|
|
36
|
+
return e.noConfig
|
|
37
|
+
? ' ▶ REPL 준비 (--no-config · 프로젝트 컨텍스트 없음). .exit 로 종료.'
|
|
38
|
+
: ' ▶ REPL 준비. 컨텍스트: gaon, config, apps, models, services, domain. .exit 로 종료.';
|
|
39
|
+
case 'exited':
|
|
40
|
+
return ' gaon console · 종료';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `gaon console` 진입점.
|
|
45
|
+
*
|
|
46
|
+
* --no-config: env 만 로드하고 순수 REPL 을 띄운다.
|
|
47
|
+
* 기본: loadDotEnv → loadGaonConfig → wireGaon → loadDomain →
|
|
48
|
+
* REPL 컨텍스트에 gaon(wired) · config · apps · models · services
|
|
49
|
+
* · domain 을 심고, .exit 대기.
|
|
50
|
+
*/
|
|
51
|
+
export async function runConsoleCommand(opts = {}) {
|
|
52
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
53
|
+
const json = opts.json ?? false;
|
|
54
|
+
const noConfig = opts.noConfig ?? false;
|
|
55
|
+
const signals = opts.signals ?? process;
|
|
56
|
+
const emit = (e) => {
|
|
57
|
+
if (json)
|
|
58
|
+
process.stdout.write(JSON.stringify(e) + '\n');
|
|
59
|
+
else
|
|
60
|
+
process.stdout.write(humanEvent(e) + '\n');
|
|
61
|
+
};
|
|
62
|
+
// .ts 상대 import 해석기 — 사용자 gaon.config.ts · domain/* 로드에 필요.
|
|
63
|
+
registerTsResolve();
|
|
64
|
+
loadDotEnv(cwd);
|
|
65
|
+
emit({ kind: 'starting', cwd, noConfig });
|
|
66
|
+
let wired;
|
|
67
|
+
let domain;
|
|
68
|
+
let configPath;
|
|
69
|
+
const context = {};
|
|
70
|
+
if (!noConfig) {
|
|
71
|
+
configPath = findConfigPath(cwd);
|
|
72
|
+
const config = await loadGaonConfig(cwd);
|
|
73
|
+
wired = await wireGaon(config, cwd);
|
|
74
|
+
domain = await loadDomain(cwd);
|
|
75
|
+
context.gaon = wired;
|
|
76
|
+
context.config = config;
|
|
77
|
+
context.apps = wired.apps;
|
|
78
|
+
context.domain = domain;
|
|
79
|
+
// 모델·서비스 자동 노출은 사용자 코드 스캔이 필요해 v1 은 domain 요약만
|
|
80
|
+
// 노출한다(§13.5 M9-G 최소 스코프). 사용자는 컨텍스트에서 필요 파일을
|
|
81
|
+
// `await import(...)` 로 가져올 수 있다(REPL 은 top-level await 허용).
|
|
82
|
+
context.models = {};
|
|
83
|
+
context.services = {};
|
|
84
|
+
emit({
|
|
85
|
+
kind: 'loaded',
|
|
86
|
+
configPath,
|
|
87
|
+
apps: wired.apps.map((a) => a.name),
|
|
88
|
+
domain: {
|
|
89
|
+
jobs: domain.jobs,
|
|
90
|
+
listeners: domain.listeners,
|
|
91
|
+
mails: domain.mails,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
emit({ kind: 'ready', noConfig });
|
|
96
|
+
// REPL 시작 — 테스트는 replFactory 로 stdin/stdout 을 갈아 끼운다.
|
|
97
|
+
const repl = opts.replFactory
|
|
98
|
+
? opts.replFactory()
|
|
99
|
+
: startRepl({
|
|
100
|
+
prompt: noConfig ? 'gaon(no-config)> ' : 'gaon> ',
|
|
101
|
+
useColors: true,
|
|
102
|
+
// top-level await 허용 — 배터리 검증용 fetch/DB 조회에 편리.
|
|
103
|
+
// (Node ≥ 22 에서 안정 지원.)
|
|
104
|
+
useGlobal: false,
|
|
105
|
+
});
|
|
106
|
+
// 컨텍스트를 REPL 에 주입.
|
|
107
|
+
Object.assign(repl.context, context);
|
|
108
|
+
// Ctrl+C 로 nudge, 종료는 SIGINT 로 배선 close.
|
|
109
|
+
await new Promise((resolveExit) => {
|
|
110
|
+
const done = async () => {
|
|
111
|
+
signals.off('SIGINT', done);
|
|
112
|
+
signals.off('SIGTERM', done);
|
|
113
|
+
if (wired) {
|
|
114
|
+
try {
|
|
115
|
+
await wired.close();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
process.stderr.write(` ✗ 종료 중 오류: ${msg}\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
emit({ kind: 'exited' });
|
|
123
|
+
resolveExit();
|
|
124
|
+
};
|
|
125
|
+
repl.on('exit', () => void done());
|
|
126
|
+
signals.on('SIGINT', () => void done());
|
|
127
|
+
signals.on('SIGTERM', () => void done());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** `gaon new` 옵션. 모두 선택적 — 기본값이 The One Way. */
|
|
2
|
+
export interface NewCommandOptions {
|
|
3
|
+
/** 프로젝트 생성 위치의 부모 폴더. 기본 process.cwd(). */
|
|
4
|
+
readonly cwd?: string;
|
|
5
|
+
/** 자동화용 구조화 출력. */
|
|
6
|
+
readonly json?: boolean;
|
|
7
|
+
/** 의존성 설치 스킵(테스트·오프라인). */
|
|
8
|
+
readonly skipInstall?: boolean;
|
|
9
|
+
/** git init · 첫 커밋 스킵(테스트·기존 git 저장소에 삽입). */
|
|
10
|
+
readonly skipGit?: boolean;
|
|
11
|
+
/** 패키지 매니저. 기본 pnpm(모노레포 관례 정합). */
|
|
12
|
+
readonly packageManager?: 'pnpm' | 'npm' | 'yarn';
|
|
13
|
+
/**
|
|
14
|
+
* (테스트 훅) 템플릿의 gaonjs 의존성 버전. 미지정 시 파사드(gaonjs)
|
|
15
|
+
* 패키지의 실 버전을 자동 조회한다(package.json).
|
|
16
|
+
*/
|
|
17
|
+
readonly gaonjsVersion?: string;
|
|
18
|
+
}
|
|
19
|
+
/** JSON 리포트(--json). 사람 모드는 콘솔에 사람이 읽는 요약을 출력한다. */
|
|
20
|
+
export interface NewCommandResult {
|
|
21
|
+
readonly ok: boolean;
|
|
22
|
+
readonly project: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly filesCreated: number;
|
|
25
|
+
readonly install: {
|
|
26
|
+
readonly ran: boolean;
|
|
27
|
+
readonly skipped: boolean;
|
|
28
|
+
readonly packageManager: string;
|
|
29
|
+
readonly durationMs: number;
|
|
30
|
+
readonly exitCode: number | null;
|
|
31
|
+
};
|
|
32
|
+
readonly git: {
|
|
33
|
+
readonly ran: boolean;
|
|
34
|
+
readonly skipped: boolean;
|
|
35
|
+
readonly initialized: boolean;
|
|
36
|
+
readonly firstCommit: boolean;
|
|
37
|
+
};
|
|
38
|
+
readonly totalMs: number;
|
|
39
|
+
readonly error?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `gaon new <name>` 실행. 파일 생성 → pnpm install → git init 순서.
|
|
43
|
+
* 각 단계는 옵션으로 스킵 가능. 반환값은 프로세스 종료 코드(0=성공).
|
|
44
|
+
*/
|
|
45
|
+
export declare function runNewCommand(name: string, opts?: NewCommandOptions): Promise<number>;
|