@gaonjs/cli 0.1.5 → 0.1.7

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.
@@ -0,0 +1,11 @@
1
+ import { type ScheduleDef } from '@gaonjs/async';
2
+ export interface LoadedDomain {
3
+ readonly jobs: number;
4
+ readonly listeners: number;
5
+ readonly schedule?: ScheduleDef;
6
+ }
7
+ /**
8
+ * domain/ 을 스캔·import 해 잡·리스너·스케줄을 등록한다. 반환값은 등록 요약.
9
+ * .ts 상대 import 해석을 위해 tsResolve 훅을 먼저 켠다.
10
+ */
11
+ export declare function loadDomain(root: string): Promise<LoadedDomain>;
package/dist/domain.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @gaonjs/cli · 도메인 로더 (§7 M7 — 파일=등록)
3
+ *
4
+ * `gaon work`(그리고 향후 serve)가 부팅 시 도메인의 비동기 자산을 불러
5
+ * 레지스트리를 채운다. 관례대로 **디렉터리에 파일을 놓는 것이 곧 등록**이고,
6
+ * 잡·리스너의 이름/식별자는 파일명에서 채운다(잡은 그래야 `.later()` 가
7
+ * 어느 프로세스에서든 동작한다 — assignName 이 원본 객체를 mutate).
8
+ *
9
+ * domain/jobs/*.ts → 잡(이름 = 파일명)
10
+ * domain/events/*.ts → 이벤트 정의(리스너가 참조 · import 만)
11
+ * domain/listeners/*.ts → 리스너(durable id = 파일명)
12
+ * domain/schedule.ts → 스케줄 정의(default export)
13
+ */
14
+ import { existsSync, readdirSync } from 'node:fs';
15
+ import { join, basename } from 'node:path';
16
+ import { pathToFileURL } from 'node:url';
17
+ import { isJobDef, isListener, reidentifyListener } from '@gaonjs/async';
18
+ import { registerTsResolve } from './tsResolve.js';
19
+ function tsFilesIn(dir) {
20
+ if (!existsSync(dir))
21
+ return [];
22
+ return readdirSync(dir, { withFileTypes: true })
23
+ .filter((e) => e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.test.ts'))
24
+ .map((e) => join(dir, e.name))
25
+ .sort();
26
+ }
27
+ async function importFile(file) {
28
+ return (await import(pathToFileURL(file).href));
29
+ }
30
+ /**
31
+ * domain/ 을 스캔·import 해 잡·리스너·스케줄을 등록한다. 반환값은 등록 요약.
32
+ * .ts 상대 import 해석을 위해 tsResolve 훅을 먼저 켠다.
33
+ */
34
+ export async function loadDomain(root) {
35
+ registerTsResolve();
36
+ const domainDir = join(root, 'domain');
37
+ let jobs = 0;
38
+ let listeners = 0;
39
+ // 이벤트 정의를 먼저 import 해 리스너가 참조할 수 있게 한다(부수효과 없음).
40
+ for (const file of tsFilesIn(join(domainDir, 'events')))
41
+ await importFile(file);
42
+ for (const file of tsFilesIn(join(domainDir, 'jobs'))) {
43
+ const mod = await importFile(file);
44
+ const base = basename(file, '.ts');
45
+ for (const value of Object.values(mod)) {
46
+ if (isJobDef(value)) {
47
+ value.assignName(base);
48
+ jobs++;
49
+ }
50
+ }
51
+ }
52
+ for (const file of tsFilesIn(join(domainDir, 'listeners'))) {
53
+ const mod = await importFile(file);
54
+ const base = basename(file, '.ts');
55
+ for (const value of Object.values(mod)) {
56
+ if (isListener(value)) {
57
+ reidentifyListener(value, base);
58
+ listeners++;
59
+ }
60
+ }
61
+ }
62
+ let schedule;
63
+ const scheduleFile = join(domainDir, 'schedule.ts');
64
+ if (existsSync(scheduleFile)) {
65
+ const mod = await importFile(scheduleFile);
66
+ const def = mod.default;
67
+ if (def && typeof def === 'object' && Array.isArray(def.entries)) {
68
+ schedule = def;
69
+ }
70
+ }
71
+ return { jobs, listeners, schedule };
72
+ }
package/dist/hub.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface HubCommandOptions {
2
+ readonly json?: boolean;
3
+ /** NATS 접속지. 생략 시 GAON_NATS_URL, 그다음 기본(4222). */
4
+ readonly natsUrl?: string;
5
+ /** 리스 TTL(ms). 생략 시 env GAON_HUB_TTL_MS, 그다음 허브 기본(5000). */
6
+ readonly ttlMs?: number;
7
+ /** TCP 리슨 포트(errata E-2). 생략 시 env GAON_HUB_PORT, 그다음 4001. */
8
+ readonly port?: number;
9
+ /** 소켓 무활동 임계(ms). 생략 시 env GAON_HUB_PING_TIMEOUT_MS. */
10
+ readonly pingTimeoutMs?: number;
11
+ /** 무활동·회수 스윕 주기(ms). 생략 시 env GAON_HUB_SWEEP_MS. */
12
+ readonly sweepMs?: number;
13
+ /** 시그널 등록·해제(테스트 주입). 기본 process. */
14
+ readonly signals?: {
15
+ on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
16
+ off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
17
+ };
18
+ }
19
+ /**
20
+ * 허브를 띄우고 시그널까지 살아 있게 한다. 반환 프라미스는 종료 시 resolve.
21
+ * 그레이스풀 종료 시 리더 키를 비워(즉시 승계) 대기 인스턴스가 곧바로
22
+ * 승격하게 한다.
23
+ */
24
+ export declare function runHubCommand(opts?: HubCommandOptions): Promise<void>;
package/dist/hub.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon hub` — 실시간 허브 프로세스 (§7 · M6)
3
+ *
4
+ * 운영 프로세스 3종(serve · work · hub) 중 하나. 접속자 목록의 단일
5
+ * 권위이자 중계다(§7 실시간). NATS 에 붙어 프레즌스 명령을 처리하고
6
+ * KV 에 영속하며, HA 는 리스 기반 리더 선출로 잡는다(active-standby) —
7
+ * 여러 인스턴스를 띄워도 리더 하나만 명령을 처리한다.
8
+ *
9
+ * 개발 모드는 `gaon dev` 가 허브를 같은 프로세스에 내장 실행하므로,
10
+ * 이 명령은 운영 배치용이다. 모든 명령과 같이 `--json` 을 제공한다.
11
+ */
12
+ import { hostname } from 'node:os';
13
+ import { connectNats, runHub } from '@gaonjs/async';
14
+ /** 환경변수를 정수로 파싱(미설정·비정상 시 undefined). */
15
+ function envInt(name) {
16
+ const raw = process.env[name];
17
+ if (raw == null || raw === '')
18
+ return undefined;
19
+ const n = Number(raw);
20
+ return Number.isFinite(n) ? n : undefined;
21
+ }
22
+ function humanEvent(e) {
23
+ switch (e.kind) {
24
+ case 'starting':
25
+ return ` gaon hub · 시작 — ${e.id} (NATS 연결·리더 선출 대기)`;
26
+ case 'leader':
27
+ return e.leader
28
+ ? ` ▶ 리더로 승격 — 프레즌스 명령 처리·KV 권위·델타 중계 시작 (${e.id})`
29
+ : ` · 대기(standby) — 다른 인스턴스가 리더 (${e.id})`;
30
+ case 'stopped':
31
+ return ' gaon hub · 종료';
32
+ }
33
+ }
34
+ /**
35
+ * 허브를 띄우고 시그널까지 살아 있게 한다. 반환 프라미스는 종료 시 resolve.
36
+ * 그레이스풀 종료 시 리더 키를 비워(즉시 승계) 대기 인스턴스가 곧바로
37
+ * 승격하게 한다.
38
+ */
39
+ export async function runHubCommand(opts = {}) {
40
+ const json = opts.json ?? false;
41
+ const signals = opts.signals ?? process;
42
+ const id = `${hostname()}#${process.pid}`;
43
+ const emit = (e) => {
44
+ if (json)
45
+ process.stdout.write(JSON.stringify(e) + '\n');
46
+ else
47
+ process.stdout.write(humanEvent(e) + '\n');
48
+ };
49
+ emit({ kind: 'starting', id });
50
+ const nats = await connectNats({ servers: opts.natsUrl, name: `hub@${id}` });
51
+ let hub;
52
+ try {
53
+ hub = await runHub({
54
+ nats,
55
+ id,
56
+ // 운영 튜닝: 옵션 > 환경변수 > 허브 기본. 컨테이너 배치에서 헬스체크
57
+ // 주기에 맞춰 리스 TTL·TCP 포트·소켓 임계를 조정한다.
58
+ ttlMs: opts.ttlMs ?? envInt('GAON_HUB_TTL_MS'),
59
+ port: opts.port ?? envInt('GAON_HUB_PORT'),
60
+ host: process.env.GAON_HUB_HOST,
61
+ advertiseAddr: process.env.GAON_HUB_ADVERTISE,
62
+ pingTimeoutMs: opts.pingTimeoutMs ?? envInt('GAON_HUB_PING_TIMEOUT_MS'),
63
+ sweepMs: opts.sweepMs ?? envInt('GAON_HUB_SWEEP_MS'),
64
+ reclaimGraceMs: envInt('GAON_HUB_RECLAIM_GRACE_MS'),
65
+ onState: (s) => emit({ kind: 'leader', leader: s.leader, id }),
66
+ });
67
+ }
68
+ catch (err) {
69
+ await nats.close();
70
+ throw err;
71
+ }
72
+ await new Promise((resolve) => {
73
+ const stop = () => {
74
+ signals.off('SIGINT', stop);
75
+ signals.off('SIGTERM', stop);
76
+ void (async () => {
77
+ await hub.stop();
78
+ await nats.close();
79
+ emit({ kind: 'stopped' });
80
+ resolve();
81
+ })();
82
+ };
83
+ signals.on('SIGINT', stop);
84
+ signals.on('SIGTERM', stop);
85
+ });
86
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export { runDevCommand, startDev, resolveDevLayout, type DevDeps, type DevLayout, type DevApp, type DevEvent, type DevHandle, type DevCommandOptions, } from "./dev.js";
2
2
  export { runCheck, runCheckCommand, type CheckDeps, type CheckResult, type TypecheckResult, type CheckCommandOptions, } from "./check.js";
3
3
  export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, type AuthScaffoldOptions, type ScaffoldFile, type ScaffoldResult, type GenerateAuthOptions, } from "./generate.js";
4
+ export { runHubCommand, type HubCommandOptions } from "./hub.js";
5
+ export { runWorkCommand, type WorkCommandOptions } from "./work.js";
6
+ export { runJobsCommand, type JobsCommandOptions } from "./jobs.js";
7
+ export { loadDomain, type LoadedDomain } from "./domain.js";
4
8
  export interface RoadmapReport {
5
9
  readonly name: "gaon";
6
10
  readonly version: string;
package/dist/index.js CHANGED
@@ -13,9 +13,16 @@ 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 { runHubCommand } from "./hub.js";
17
+ import { runWorkCommand } from "./work.js";
18
+ import { runJobsCommand } from "./jobs.js";
16
19
  export { runDevCommand, startDev, resolveDevLayout, } from "./dev.js";
17
20
  export { runCheck, runCheckCommand, } from "./check.js";
18
21
  export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, } from "./generate.js";
22
+ export { runHubCommand } from "./hub.js";
23
+ export { runWorkCommand } from "./work.js";
24
+ export { runJobsCommand } from "./jobs.js";
25
+ export { loadDomain } from "./domain.js";
19
26
  /** `--json` 출력용 구조화 리포트. */
20
27
  export function roadmapReport(version = VERSION) {
21
28
  return {
@@ -63,6 +70,10 @@ function renderHelp(version = VERSION) {
63
70
  " gaon dev --json 재생성 이벤트를 JSON 으로 출력",
64
71
  " gaon check .gaon 재생성 후 타입 검사 (CI·AI 정합)",
65
72
  " gaon g auth 인증 스캐폴드 생성 (회원가입·로그인·세션·보호 라우트)",
73
+ " gaon hub 실시간 허브 프로세스 (프레즌스 권위·중계 · 리더 선출 HA)",
74
+ " gaon work 워커 프로세스 (잡·리스너·스케줄·아웃박스 · graceful drain)",
75
+ " gaon jobs list --failed DLQ(실패 잡) 목록",
76
+ " gaon jobs retry <id> DLQ 잡 재적재",
66
77
  " gaon --json 같은 정보를 JSON 으로 출력",
67
78
  " gaon --version 버전 출력",
68
79
  " gaon --help 이 도움말",
@@ -97,6 +108,39 @@ export function runCli(argv, opts = {}) {
97
108
  });
98
109
  return;
99
110
  }
111
+ // `gaon hub` — 실시간 허브 프로세스(§7 M6). 운영 프로세스 3종 중 하나.
112
+ // NATS·리더 선출 대기로 프로세스를 살려 두고, SIGINT/SIGTERM 에 그레이스풀 종료.
113
+ if (argv[0] === "hub") {
114
+ void runHubCommand({ json: argv.includes("--json") }).catch((err) => {
115
+ const msg = err instanceof Error ? err.message : String(err);
116
+ process.stderr.write(` ✗ gaon hub 실패: ${msg}\n`);
117
+ process.exitCode = 1;
118
+ });
119
+ return;
120
+ }
121
+ // `gaon work` — 워커 프로세스(§7 M7). 잡·리스너·스케줄·아웃박스 조립.
122
+ // 시그널에 graceful drain 후 종료.
123
+ if (argv[0] === "work") {
124
+ void runWorkCommand({ json: argv.includes("--json") }).catch((err) => {
125
+ const msg = err instanceof Error ? err.message : String(err);
126
+ process.stderr.write(` ✗ gaon work 실패: ${msg}\n`);
127
+ process.exitCode = 1;
128
+ });
129
+ return;
130
+ }
131
+ // `gaon jobs` — DLQ 조회·재적재(§7 M7). list --failed | retry <id>.
132
+ if (argv[0] === "jobs") {
133
+ void runJobsCommand(argv.slice(1), { json: argv.includes("--json") })
134
+ .then((code) => {
135
+ process.exitCode = code;
136
+ })
137
+ .catch((err) => {
138
+ const msg = err instanceof Error ? err.message : String(err);
139
+ process.stderr.write(` ✗ gaon jobs 실패: ${msg}\n`);
140
+ process.exitCode = 1;
141
+ });
142
+ return;
143
+ }
100
144
  // `gaon g auth` — 인증 스캐폴드 제너레이터(§7 M5). 다른 제너레이터는 후속.
101
145
  if (argv[0] === "g" || argv[0] === "generate") {
102
146
  if (argv[1] === "auth") {
package/dist/jobs.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface JobsCommandOptions {
2
+ readonly json?: boolean;
3
+ /** NATS 접속지. 생략 시 GAON_NATS_URL, 그다음 기본(4222). */
4
+ readonly natsUrl?: string;
5
+ }
6
+ /** `gaon jobs ...` 진입점. 종료 코드를 돌려준다(0 성공 · 1 실패). */
7
+ export declare function runJobsCommand(argv: readonly string[], opts?: JobsCommandOptions): Promise<number>;
package/dist/jobs.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon jobs` — DLQ 조회·재적재 (§7 · M7 · line 886~887)
3
+ *
4
+ * 최대 재시도를 소진해 DLQ 로 간 잡을 운영자가 확인하고 재실행한다:
5
+ * gaon jobs list --failed 실패 잡 목록(최신순)
6
+ * gaon jobs retry <id> DLQ 잡을 잡 큐로 재적재
7
+ * 모든 명령과 같이 `--json` 을 제공한다.
8
+ */
9
+ import { connectNats, listDlq, retryDlq } from '@gaonjs/async';
10
+ function fmtTime(ms) {
11
+ return new Date(ms).toISOString();
12
+ }
13
+ function renderList(entries) {
14
+ if (entries.length === 0)
15
+ return ' DLQ 가 비어 있습니다 — 실패한 잡이 없습니다.';
16
+ const lines = [` 실패 잡 ${entries.length}건 (최신순):`, ''];
17
+ for (const e of entries) {
18
+ lines.push(` ${e.id} ${e.name} (시도 ${e.attempts}회 · ${fmtTime(e.failedAt)})`);
19
+ lines.push(` ↳ ${e.error}`);
20
+ }
21
+ lines.push('');
22
+ lines.push(' 재적재: gaon jobs retry <id>');
23
+ return lines.join('\n');
24
+ }
25
+ /** `gaon jobs ...` 진입점. 종료 코드를 돌려준다(0 성공 · 1 실패). */
26
+ export async function runJobsCommand(argv, opts = {}) {
27
+ const json = opts.json ?? argv.includes('--json');
28
+ const sub = argv[0];
29
+ if (sub === 'list') {
30
+ const failedOnly = argv.includes('--failed');
31
+ if (!failedOnly) {
32
+ process.stderr.write(` ✗ gaon jobs list 는 현재 --failed(DLQ) 만 지원합니다.\n` +
33
+ ` → gaon jobs list --failed\n`);
34
+ return 1;
35
+ }
36
+ const nats = await connectNats({ servers: opts.natsUrl, name: 'jobs-cli' });
37
+ try {
38
+ const entries = await listDlq(nats);
39
+ if (json)
40
+ process.stdout.write(JSON.stringify({ failed: entries }, null, 2) + '\n');
41
+ else
42
+ process.stdout.write(renderList(entries) + '\n');
43
+ return 0;
44
+ }
45
+ finally {
46
+ await nats.close();
47
+ }
48
+ }
49
+ if (sub === 'retry') {
50
+ const id = argv[1];
51
+ if (!id || id.startsWith('--')) {
52
+ process.stderr.write(` ✗ 재적재할 잡 id 가 필요합니다.\n → gaon jobs retry <id>\n`);
53
+ return 1;
54
+ }
55
+ const nats = await connectNats({ servers: opts.natsUrl, name: 'jobs-cli' });
56
+ try {
57
+ const entry = await retryDlq(nats, id);
58
+ if (json)
59
+ process.stdout.write(JSON.stringify({ retried: entry }, null, 2) + '\n');
60
+ else
61
+ process.stdout.write(` ↺ 재적재 완료 — ${entry.name} (id ${entry.id})\n`);
62
+ return 0;
63
+ }
64
+ catch (err) {
65
+ const msg = err instanceof Error ? err.message : String(err);
66
+ process.stderr.write(` ✗ ${msg}\n`);
67
+ return 1;
68
+ }
69
+ finally {
70
+ await nats.close();
71
+ }
72
+ }
73
+ process.stderr.write(` ✗ 알 수 없는 jobs 명령: ${sub ?? '(없음)'}\n` +
74
+ ` → gaon jobs list --failed | gaon jobs retry <id>\n`);
75
+ return 1;
76
+ }
package/dist/work.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface WorkCommandOptions {
2
+ readonly json?: boolean;
3
+ /** NATS 접속지. 생략 시 GAON_NATS_URL, 그다음 기본(4222). */
4
+ readonly natsUrl?: string;
5
+ /** 도메인 루트(domain/ 의 부모). 기본 cwd. */
6
+ readonly root?: string;
7
+ /** 아웃박스 릴레이용 DB URL. 생략 시 GAON_DATABASE_URL, 없으면 릴레이 미기동. */
8
+ readonly databaseUrl?: string;
9
+ /** 큐 기본 동시성. */
10
+ readonly concurrency?: number;
11
+ /** ack 대기(ms). 생략 시 GAON_WORKER_ACK_WAIT_MS. */
12
+ readonly ackWaitMs?: number;
13
+ /** graceful drain 상한(ms). 생략 시 GAON_WORKER_DRAIN_MS. */
14
+ readonly drainTimeoutMs?: number;
15
+ /** 시그널 등록·해제(테스트 주입). 기본 process. */
16
+ readonly signals?: {
17
+ on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
18
+ off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
19
+ };
20
+ }
21
+ /**
22
+ * 워커를 띄우고 시그널까지 살려 둔다. 반환 프라미스는 graceful 종료 시 resolve.
23
+ */
24
+ export declare function runWorkCommand(opts?: WorkCommandOptions): Promise<void>;
package/dist/work.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon work` — 워커 프로세스 (§7 · M7)
3
+ *
4
+ * 운영 프로세스 3종(serve · work · hub) 중 하나. 도메인의 잡·리스너·스케줄·
5
+ * 아웃박스를 한 프로세스에 조립해 돌린다(@gaonjs/async runWork). 개발
6
+ * 모드는 `gaon dev` 가 워커를 내장 실행하므로 이 명령은 운영 배치용이다.
7
+ *
8
+ * 종료 시그널(SIGTERM·SIGINT)에 graceful drain — 스케줄러 리더 반납 →
9
+ * 릴레이·리스너·워커 순으로 내리고, 진행 중 잡은 상한까지 기다린다. 모든
10
+ * 명령과 같이 `--json` 을 제공한다.
11
+ */
12
+ import { hostname } from 'node:os';
13
+ import { connectNats, runWork } from '@gaonjs/async';
14
+ import { createDb } from '@gaonjs/data';
15
+ import { loadDomain } from './domain.js';
16
+ /** DB URL 에서 어댑터를 추정한다(postgres·mysql). */
17
+ function dbConfigFromUrl(url) {
18
+ if (url.startsWith('mysql://') || url.startsWith('mariadb://')) {
19
+ return { adapter: 'mysql', url, poolMax: 4 };
20
+ }
21
+ return { adapter: 'postgres', url, poolMax: 4 };
22
+ }
23
+ /** 환경변수를 정수로 파싱(미설정·비정상 시 undefined). */
24
+ function envInt(name) {
25
+ const raw = process.env[name];
26
+ if (raw == null || raw === '')
27
+ return undefined;
28
+ const n = Number(raw);
29
+ return Number.isFinite(n) ? n : undefined;
30
+ }
31
+ function emitHuman(e) {
32
+ switch (e.kind) {
33
+ case 'ready':
34
+ return ` gaon work · 준비 — 잡 ${e.jobs} · 리스너 ${e.listeners} · 스케줄 ${e.scheduled}`;
35
+ case 'scheduler':
36
+ if (e.event.kind === 'leader')
37
+ return e.event.leader ? ' ▶ 스케줄러 리더 — 틱 발행 시작' : ' · 스케줄러 대기(standby)';
38
+ if (e.event.kind === 'fired')
39
+ return ` ⏰ 스케줄 발행 — ${e.event.label}`;
40
+ return undefined;
41
+ case 'worker':
42
+ if (e.event.kind === 'dead')
43
+ return ` ✗ 잡 DLQ — ${e.event.job} (${e.event.error})`;
44
+ return undefined;
45
+ case 'relay':
46
+ return ` ↪ 아웃박스 릴레이 — ${e.count}건 발행`;
47
+ default:
48
+ return undefined;
49
+ }
50
+ }
51
+ /**
52
+ * 워커를 띄우고 시그널까지 살려 둔다. 반환 프라미스는 graceful 종료 시 resolve.
53
+ */
54
+ export async function runWorkCommand(opts = {}) {
55
+ const json = opts.json ?? false;
56
+ const signals = opts.signals ?? process;
57
+ const root = opts.root ?? process.cwd();
58
+ const id = `${hostname()}#${process.pid}`;
59
+ const emit = (e) => {
60
+ if (json) {
61
+ process.stdout.write(JSON.stringify(e) + '\n');
62
+ }
63
+ else {
64
+ const line = emitHuman(e);
65
+ if (line)
66
+ process.stdout.write(line + '\n');
67
+ }
68
+ };
69
+ const nats = await connectNats({ servers: opts.natsUrl, name: `work@${id}` });
70
+ let db;
71
+ const dbUrl = opts.databaseUrl ?? process.env.GAON_DATABASE_URL;
72
+ if (dbUrl)
73
+ db = createDb(dbConfigFromUrl(dbUrl));
74
+ const domain = await loadDomain(root);
75
+ let work;
76
+ try {
77
+ work = await runWork({
78
+ nats,
79
+ id,
80
+ db,
81
+ schedule: domain.schedule,
82
+ concurrency: opts.concurrency,
83
+ ackWaitMs: opts.ackWaitMs ?? envInt('GAON_WORKER_ACK_WAIT_MS'),
84
+ drainTimeoutMs: opts.drainTimeoutMs ?? envInt('GAON_WORKER_DRAIN_MS'),
85
+ onEvent: emit,
86
+ });
87
+ }
88
+ catch (err) {
89
+ if (db)
90
+ await db.destroy().catch(() => { });
91
+ await nats.close();
92
+ throw err;
93
+ }
94
+ await new Promise((resolve) => {
95
+ const stop = () => {
96
+ signals.off('SIGINT', stop);
97
+ signals.off('SIGTERM', stop);
98
+ void (async () => {
99
+ await work.stop();
100
+ if (db)
101
+ await db.destroy().catch(() => { });
102
+ await nats.close();
103
+ if (json)
104
+ process.stdout.write(JSON.stringify({ kind: 'stopped' }) + '\n');
105
+ else
106
+ process.stdout.write(' gaon work · 종료\n');
107
+ resolve();
108
+ })();
109
+ };
110
+ signals.on('SIGINT', stop);
111
+ signals.on('SIGTERM', stop);
112
+ });
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,9 +24,10 @@
24
24
  "README.md"
25
25
  ],
26
26
  "dependencies": {
27
+ "@gaonjs/async": "0.2.1",
27
28
  "@gaonjs/core": "0.1.3",
28
29
  "@gaonjs/data": "0.2.2",
29
- "@gaonjs/web": "0.2.0"
30
+ "@gaonjs/web": "0.2.2"
30
31
  },
31
32
  "scripts": {
32
33
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""