@gaonjs/cli 0.1.7 → 0.2.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/db.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { type SeedDef } from '@gaonjs/data';
2
+ export interface DbSeedOptions {
3
+ readonly json?: boolean;
4
+ /** 도메인 루트(domain/ 의 부모). 기본 cwd. */
5
+ readonly root?: string;
6
+ /** DB URL 오버라이드. 생략 시 env GAON_DATABASE_URL. */
7
+ readonly databaseUrl?: string;
8
+ }
9
+ /** domain/seed.ts 의 default export 를 읽어 SeedDef 를 돌려준다(없으면 안내 에러). */
10
+ export declare function loadSeed(root: string): Promise<SeedDef>;
11
+ export interface DbSeedResult {
12
+ readonly exitCode: number;
13
+ readonly text: string;
14
+ readonly json: unknown;
15
+ }
16
+ /**
17
+ * `gaon db seed` — main 커넥션을 연결하고 domain/seed.ts 를 실행한다.
18
+ * 커넥션 정리(destroyAllConnections)는 항상 수행한다.
19
+ */
20
+ export declare function runDbSeedCommand(opts?: DbSeedOptions): Promise<DbSeedResult>;
package/dist/db.js ADDED
@@ -0,0 +1,62 @@
1
+ // @gaonjs/cli · `gaon db seed` (§7 M8 — domain/seed.ts 실행)
2
+ //
3
+ // 관례: `domain/seed.ts` 의 default export(seed 정의)를 실행한다. 커넥션은
4
+ // `gaon work` 와 **같은 관례**로 연결한다 — `GAON_DATABASE_URL` 로 main
5
+ // 커넥션을 세운다(§4.5 멀티 커넥션의 gaon.config.ts 전체 로더는 M9 CLI 완성).
6
+ // 모든 명령은 --json 을 함께 낸다(CLAUDE.md §4).
7
+ import { existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { env, EnvError } from '@gaonjs/core';
11
+ import { createDb, registerConnection, destroyAllConnections, isSeedDef, } from '@gaonjs/data';
12
+ import { registerTsResolve } from './tsResolve.js';
13
+ /** DB URL 에서 어댑터를 추정한다(gaon work 와 동일 규칙). */
14
+ function dbConfigFromUrl(url) {
15
+ if (url.startsWith('mysql://') || url.startsWith('mariadb://')) {
16
+ return { adapter: 'mysql', url, poolMax: 4 };
17
+ }
18
+ return { adapter: 'postgres', url, poolMax: 4 };
19
+ }
20
+ /** domain/seed.ts 의 default export 를 읽어 SeedDef 를 돌려준다(없으면 안내 에러). */
21
+ export async function loadSeed(root) {
22
+ const file = join(root, 'domain', 'seed.ts');
23
+ if (!existsSync(file)) {
24
+ throw new Error(`시드 파일이 없습니다: domain/seed.ts\n` +
25
+ ` → domain/seed.ts 를 만들고 default export 로 시드를 선언하세요:\n` +
26
+ ` import { seed } from 'gaonjs/data'\n` +
27
+ ` export default seed(async () => { /* await Model.create(...) */ })`);
28
+ }
29
+ registerTsResolve();
30
+ const mod = (await import(pathToFileURL(file).href));
31
+ if (!isSeedDef(mod.default)) {
32
+ throw new Error(`domain/seed.ts 의 default export 가 시드 정의가 아닙니다.\n` +
33
+ ` → seed(...) 로 감싸 default export 하세요: export default seed(async () => {...})`);
34
+ }
35
+ return mod.default;
36
+ }
37
+ /**
38
+ * `gaon db seed` — main 커넥션을 연결하고 domain/seed.ts 를 실행한다.
39
+ * 커넥션 정리(destroyAllConnections)는 항상 수행한다.
40
+ */
41
+ export async function runDbSeedCommand(opts = {}) {
42
+ const root = opts.root ?? process.cwd();
43
+ const dbUrl = opts.databaseUrl ?? env.optional('GAON_DATABASE_URL');
44
+ if (!dbUrl) {
45
+ throw new EnvError(`환경변수 GAON_DATABASE_URL 가 설정되지 않았습니다(시드에 DB 연결 필요).\n` +
46
+ ` → .env 에 다음 줄을 추가하고 다시 실행하세요:\n` +
47
+ ` GAON_DATABASE_URL=postgres://user:pass@localhost:5432/mydb`);
48
+ }
49
+ registerConnection('main', createDb(dbConfigFromUrl(dbUrl)), dbConfigFromUrl(dbUrl).adapter);
50
+ try {
51
+ const def = await loadSeed(root);
52
+ await def.run();
53
+ return {
54
+ exitCode: 0,
55
+ text: ` gaon db seed · 완료 — domain/seed.ts 실행됨`,
56
+ json: { command: 'seed', ok: true },
57
+ };
58
+ }
59
+ finally {
60
+ await destroyAllConnections();
61
+ }
62
+ }
@@ -0,0 +1,36 @@
1
+ export type ResponseKind = 'render' | 'redirect' | 'json' | 'plain';
2
+ export interface ActionUsage {
3
+ readonly file: string;
4
+ readonly action: string;
5
+ readonly line: number;
6
+ readonly kinds: readonly ResponseKind[];
7
+ }
8
+ export interface DoctorIssue {
9
+ readonly file: string;
10
+ readonly line: number;
11
+ readonly action: string;
12
+ readonly kinds: readonly ResponseKind[];
13
+ readonly message: string;
14
+ }
15
+ export interface DoctorResult {
16
+ readonly ok: boolean;
17
+ readonly checked: number;
18
+ readonly issues: readonly DoctorIssue[];
19
+ }
20
+ /** 컨트롤러 파일 하나를 검사한다. */
21
+ export declare function inspectControllerSource(file: string, source: string): ActionUsage[];
22
+ /**
23
+ * apps/<app>/controllers/ 를 훑어 응답 혼용 액션을 찾는다. 액션 하나의 kinds
24
+ * 배열이 2종 이상이면 위반(단, 'plain' 하나만 있는 것과 render 만 있는 것은 통과).
25
+ *
26
+ * 명시적 예외: 반환 없는 return(=undefined→204)만 있는 경우는 통과 — void 액션은
27
+ * 흔히 혼용 없이 정상적으로 쓰인다.
28
+ */
29
+ export declare function runDoctor(appsDir: string): Promise<DoctorResult>;
30
+ export declare function fileExists(file: string): Promise<boolean>;
31
+ export interface DoctorCommandOptions {
32
+ readonly cwd?: string;
33
+ readonly json?: boolean;
34
+ }
35
+ /** `gaon doctor` 진입점. */
36
+ export declare function runDoctorCommand(opts?: DoctorCommandOptions): Promise<number>;
package/dist/doctor.js ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon doctor` — 정적 검사 (M3 · errata E-3)
3
+ *
4
+ * v1 첫 검사 항목: **응답 혼용 금지**. 한 컨트롤러 액션은 render/JSON/
5
+ * redirect 중 한 가지 응답 형태만 낼 수 있다(§C · The One Way). 조건 분기로
6
+ * 섞으면 "이 라우트가 무엇을 돌려주는지"가 코드를 읽어야 알게 돼 AI 첫 시도
7
+ * 성공률이 떨어진다.
8
+ *
9
+ * 검사 방법(정적 분석):
10
+ * · apps/<app>/controllers/*.ts 를 TS AST 로 파싱.
11
+ * · export default controller({ ... }) 의 액션 함수마다 return 노드에서
12
+ * 최종 응답 형태를 분류(render | redirect | json | plain).
13
+ * · 서로 다른 형태가 두 종류 이상이면 위반.
14
+ * · this.render/this.redirect/this.json/평범한 객체 반환의 4종을 구별한다.
15
+ *
16
+ * 위반 시: 파일·라인·액션 이름 + 감지된 형태 목록 + "→ 한 액션은 한 응답
17
+ * 형태만 유지하세요(errata E-3 §C)" 안내를 낸다.
18
+ *
19
+ * 나머지 doctor 검사(의존 방향 §1-5, 커넥션 §4.5 등)는 후속 마일스톤에서
20
+ * 이 모듈에 규칙을 얹는다.
21
+ */
22
+ import { readdir, readFile, stat } from 'node:fs/promises';
23
+ import { join, relative, resolve } from 'node:path';
24
+ import ts from 'typescript';
25
+ /** 컨트롤러 파일 하나를 검사한다. */
26
+ export function inspectControllerSource(file, source) {
27
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
28
+ const usages = [];
29
+ const visit = (node) => {
30
+ if (ts.isCallExpression(node) && isControllerCall(node)) {
31
+ const arg = node.arguments[0];
32
+ if (arg && ts.isObjectLiteralExpression(arg)) {
33
+ for (const prop of arg.properties) {
34
+ const action = actionName(prop);
35
+ if (!action)
36
+ continue;
37
+ const body = actionBody(prop);
38
+ if (!body)
39
+ continue;
40
+ const kinds = classifyReturns(body);
41
+ const { line } = sf.getLineAndCharacterOfPosition(prop.getStart(sf));
42
+ usages.push({ file, action, line: line + 1, kinds });
43
+ }
44
+ }
45
+ }
46
+ ts.forEachChild(node, visit);
47
+ };
48
+ visit(sf);
49
+ return usages;
50
+ }
51
+ function isControllerCall(node) {
52
+ const e = node.expression;
53
+ if (ts.isIdentifier(e) && e.text === 'controller')
54
+ return true;
55
+ if (ts.isPropertyAccessExpression(e) && e.name.text === 'controller')
56
+ return true;
57
+ return false;
58
+ }
59
+ function actionName(prop) {
60
+ if (ts.isMethodDeclaration(prop) && ts.isIdentifier(prop.name))
61
+ return prop.name.text;
62
+ if (ts.isPropertyAssignment(prop) &&
63
+ ts.isIdentifier(prop.name) &&
64
+ (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
65
+ return prop.name.text;
66
+ }
67
+ return undefined;
68
+ }
69
+ function actionBody(prop) {
70
+ if (ts.isMethodDeclaration(prop))
71
+ return prop.body;
72
+ if (ts.isPropertyAssignment(prop)) {
73
+ if (ts.isArrowFunction(prop.initializer))
74
+ return prop.initializer.body;
75
+ if (ts.isFunctionExpression(prop.initializer))
76
+ return prop.initializer.body;
77
+ }
78
+ return undefined;
79
+ }
80
+ /** 액션 본문의 모든 return 표현식을 응답 형태로 분류하고 중복을 제거해 반환한다. */
81
+ function classifyReturns(body) {
82
+ const kinds = new Set();
83
+ const classify = (expr) => {
84
+ // await x → x 로 되짚어 본다.
85
+ if (ts.isAwaitExpression(expr))
86
+ return classify(expr.expression);
87
+ // this.render / this.redirect / this.json
88
+ if (ts.isCallExpression(expr)) {
89
+ const c = expr.expression;
90
+ if (ts.isPropertyAccessExpression(c) && c.expression.kind === ts.SyntaxKind.ThisKeyword) {
91
+ if (c.name.text === 'render')
92
+ return 'render';
93
+ if (c.name.text === 'redirect')
94
+ return 'redirect';
95
+ if (c.name.text === 'json')
96
+ return 'json';
97
+ }
98
+ // 삼항·논리 연산 결과라도 호출식이 나오면 함수 반환값에 의존 — 보수적으로 plain.
99
+ return 'plain';
100
+ }
101
+ // 삼항 연산자 — 양쪽 각각 분류
102
+ if (ts.isConditionalExpression(expr)) {
103
+ kinds.add(classify(expr.whenTrue));
104
+ kinds.add(classify(expr.whenFalse));
105
+ return classify(expr.whenTrue); // 반환값(대표) — Set 에 이미 다 넣음
106
+ }
107
+ // 그 외(객체·배열·literal·식별자 등) → plain(JSON 액션)
108
+ return 'plain';
109
+ };
110
+ const visit = (node) => {
111
+ if (ts.isReturnStatement(node)) {
112
+ if (node.expression)
113
+ kinds.add(classify(node.expression));
114
+ else
115
+ kinds.add('plain'); // 빈 return 은 undefined → 204 (plain 으로 취급, 혼용 검사에서만 의미)
116
+ return; // return 안의 하위 함수는 다른 스코프 — 이 return 만 잡는다
117
+ }
118
+ // 중첩 함수/화살표는 다른 스코프 — 파고들지 않는다.
119
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))
120
+ return;
121
+ ts.forEachChild(node, visit);
122
+ };
123
+ // 표현식 본문 화살표(=> render(...))는 body 가 표현식.
124
+ if (ts.isBlock(body))
125
+ visit(body);
126
+ else
127
+ kinds.add(classify(body));
128
+ return [...kinds];
129
+ }
130
+ /**
131
+ * apps/<app>/controllers/ 를 훑어 응답 혼용 액션을 찾는다. 액션 하나의 kinds
132
+ * 배열이 2종 이상이면 위반(단, 'plain' 하나만 있는 것과 render 만 있는 것은 통과).
133
+ *
134
+ * 명시적 예외: 반환 없는 return(=undefined→204)만 있는 경우는 통과 — void 액션은
135
+ * 흔히 혼용 없이 정상적으로 쓰인다.
136
+ */
137
+ export async function runDoctor(appsDir) {
138
+ const issues = [];
139
+ let checked = 0;
140
+ const apps = await safeListDirs(appsDir);
141
+ for (const app of apps) {
142
+ const ctrlDir = join(appsDir, app, 'controllers');
143
+ for (const file of await safeListFiles(ctrlDir)) {
144
+ if (!file.endsWith('.ts') || file.endsWith('.d.ts') || file.endsWith('.test.ts'))
145
+ continue;
146
+ const full = join(ctrlDir, file);
147
+ const source = await readFile(full, 'utf8');
148
+ const usages = inspectControllerSource(full, source);
149
+ checked += usages.length;
150
+ for (const u of usages) {
151
+ if (u.kinds.length < 2)
152
+ continue;
153
+ // 'plain' 이 유일한 다른 형태이면서 실제 return 문에 undefined 만 있는 경우는 걸러야
154
+ // 하지만, 액션이 render|json 를 하다가 조건에 따라 undefined 반환하면 혼용이다. 유지.
155
+ const rel = relative(process.cwd(), full);
156
+ issues.push({
157
+ file: rel,
158
+ line: u.line,
159
+ action: u.action,
160
+ kinds: u.kinds,
161
+ message: `[doctor] 응답 혼용 감지: ${rel}:${u.line} · 액션 '${u.action}' 이 서로 다른 응답 형태를 섞습니다 (${u.kinds.join(', ')}).\n` +
162
+ `→ 한 액션은 한 응답 형태만 유지하세요 (render | JSON | redirect · errata E-3 §C).\n` +
163
+ `→ 조건 분기가 필요하면 액션을 둘로 나누거나, JSON 반환에는 예외를 던져 상태 코드를 표현하세요.`,
164
+ });
165
+ }
166
+ }
167
+ }
168
+ return { ok: issues.length === 0, checked, issues };
169
+ }
170
+ async function safeListDirs(dir) {
171
+ try {
172
+ const entries = await readdir(dir, { withFileTypes: true });
173
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
174
+ }
175
+ catch {
176
+ return [];
177
+ }
178
+ }
179
+ async function safeListFiles(dir) {
180
+ try {
181
+ const entries = await readdir(dir, { withFileTypes: true });
182
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
183
+ }
184
+ catch {
185
+ return [];
186
+ }
187
+ }
188
+ // stat 은 API 표면 유지용 — 파일 존재 확인이 필요할 때 씀.
189
+ export async function fileExists(file) {
190
+ try {
191
+ await stat(file);
192
+ return true;
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ /** `gaon doctor` 진입점. */
199
+ export async function runDoctorCommand(opts = {}) {
200
+ const cwd = opts.cwd ?? process.cwd();
201
+ const appsDir = resolve(cwd, 'apps');
202
+ const result = await runDoctor(appsDir);
203
+ if (opts.json) {
204
+ process.stdout.write(JSON.stringify(result) + '\n');
205
+ }
206
+ else {
207
+ process.stdout.write(` 검사한 액션: ${result.checked}개\n`);
208
+ if (result.ok) {
209
+ process.stdout.write(` ✓ 응답 혼용 없음\n`);
210
+ }
211
+ else {
212
+ process.stdout.write(` ✗ 응답 혼용 ${result.issues.length}건\n`);
213
+ for (const i of result.issues)
214
+ process.stdout.write(i.message + '\n');
215
+ }
216
+ }
217
+ return result.ok ? 0 : 1;
218
+ }
package/dist/domain.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type ScheduleDef } from '@gaonjs/async';
2
2
  export interface LoadedDomain {
3
3
  readonly jobs: number;
4
4
  readonly listeners: number;
5
+ readonly mails: number;
5
6
  readonly schedule?: ScheduleDef;
6
7
  }
7
8
  /**
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
@@ -4,6 +4,8 @@ export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthComma
4
4
  export { runHubCommand, type HubCommandOptions } from "./hub.js";
5
5
  export { runWorkCommand, type WorkCommandOptions } from "./work.js";
6
6
  export { runJobsCommand, type JobsCommandOptions } from "./jobs.js";
7
+ export { runDbSeedCommand, loadSeed, type DbSeedOptions, type DbSeedResult } from "./db.js";
8
+ export { runDoctorCommand, runDoctor, inspectControllerSource, type DoctorResult, type DoctorIssue, type ActionUsage, type ResponseKind, type DoctorCommandOptions, } from "./doctor.js";
7
9
  export { loadDomain, type LoadedDomain } from "./domain.js";
8
10
  export interface RoadmapReport {
9
11
  readonly name: "gaon";
package/dist/index.js CHANGED
@@ -16,12 +16,16 @@ import { runGenerateAuthCommand } from "./generate.js";
16
16
  import { runHubCommand } from "./hub.js";
17
17
  import { runWorkCommand } from "./work.js";
18
18
  import { runJobsCommand } from "./jobs.js";
19
+ import { runDbSeedCommand } from "./db.js";
20
+ import { runDoctorCommand } from "./doctor.js";
19
21
  export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
20
22
  export { runCheck, runCheckCommand, } from "./check.js";
21
23
  export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, } from "./generate.js";
22
24
  export { runHubCommand } from "./hub.js";
23
25
  export { runWorkCommand } from "./work.js";
24
26
  export { runJobsCommand } from "./jobs.js";
27
+ export { runDbSeedCommand, loadSeed } from "./db.js";
28
+ export { runDoctorCommand, runDoctor, inspectControllerSource, } from "./doctor.js";
25
29
  export { loadDomain } from "./domain.js";
26
30
  /** `--json` 출력용 구조화 리포트. */
27
31
  export function roadmapReport(version = VERSION) {
@@ -69,6 +73,7 @@ function renderHelp(version = VERSION) {
69
73
  " gaon dev .gaon 타입 브리지를 감시·재생성 (스키마·라우트)",
70
74
  " gaon dev --json 재생성 이벤트를 JSON 으로 출력",
71
75
  " gaon check .gaon 재생성 후 타입 검사 (CI·AI 정합)",
76
+ " gaon doctor 정적 검사 (응답 혼용·의존 방향 등)",
72
77
  " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
73
78
  " gaon hub 실시간 허브 프로세스 (프레즌스 권위·중계 · 리더 선출 HA)",
74
79
  " gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
@@ -108,6 +113,19 @@ export function runCli(argv, opts = {}) {
108
113
  });
109
114
  return;
110
115
  }
116
+ // `gaon doctor` — 정적 검사(응답 혼용 등 · errata E-3 §C). --fix 는 후속.
117
+ if (argv[0] === "doctor") {
118
+ void runDoctorCommand({ json: argv.includes("--json") })
119
+ .then((code) => {
120
+ process.exitCode = code;
121
+ })
122
+ .catch((err) => {
123
+ const msg = err instanceof Error ? err.message : String(err);
124
+ process.stderr.write(` ✗ gaon doctor 실패: ${msg}\n`);
125
+ process.exitCode = 1;
126
+ });
127
+ return;
128
+ }
111
129
  // `gaon hub` — 실시간 허브 프로세스(§7 M6). 운영 프로세스 3종 중 하나.
112
130
  // NATS·리더 선출 대기로 프로세스를 살려 두고, SIGINT/SIGTERM 에 그레이스풀 종료.
113
131
  if (argv[0] === "hub") {
@@ -141,6 +159,21 @@ export function runCli(argv, opts = {}) {
141
159
  });
142
160
  return;
143
161
  }
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;
169
+ })
170
+ .catch((err) => {
171
+ const msg = err instanceof Error ? err.message : String(err);
172
+ process.stderr.write(` ✗ gaon db seed 실패: ${msg}\n`);
173
+ process.exitCode = 1;
174
+ });
175
+ return;
176
+ }
144
177
  // `gaon g auth` — 인증 스캐폴드 제너레이터(§7 M5). 다른 제너레이터는 후속.
145
178
  if (argv[0] === "g" || argv[0] === "generate") {
146
179
  if (argv[1] === "auth") {
package/dist/work.js CHANGED
@@ -72,6 +72,15 @@ export async function runWorkCommand(opts = {}) {
72
72
  if (dbUrl)
73
73
  db = createDb(dbConfigFromUrl(dbUrl));
74
74
  const domain = await loadDomain(root);
75
+ // 로드된 도메인 자산 요약(파일=등록 관측용). 잡·리스너·메일 수를 노출한다.
76
+ if (json) {
77
+ process.stdout.write(JSON.stringify({
78
+ kind: 'domain',
79
+ jobs: domain.jobs,
80
+ listeners: domain.listeners,
81
+ mails: domain.mails,
82
+ }) + '\n');
83
+ }
75
84
  let work;
76
85
  try {
77
86
  work = await runWork({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,10 +24,12 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@gaonjs/async": "0.2.1",
28
- "@gaonjs/core": "0.1.3",
29
- "@gaonjs/data": "0.2.2",
30
- "@gaonjs/web": "0.2.2"
27
+ "typescript": "*",
28
+ "@gaonjs/async": "0.2.2",
29
+ "@gaonjs/core": "0.1.4",
30
+ "@gaonjs/data": "0.2.3",
31
+ "@gaonjs/mail": "0.1.0",
32
+ "@gaonjs/web": "0.3.0"
31
33
  },
32
34
  "scripts": {
33
35
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""