@gaonjs/cli 0.43.0 → 0.47.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/README.md +1 -1
- package/dist/commands/db.js +29 -7
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +8 -3
- package/dist/db/resolve.d.ts +15 -0
- package/dist/db/resolve.js +24 -2
- package/dist/doctor/pageprops-destructure.d.ts +2 -2
- package/dist/doctor/pageprops-destructure.js +29 -23
- package/dist/doctor.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -2
- package/dist/scaffold/job.js +3 -1
- package/dist/templates/project/.dockerignore.tpl +3 -0
- package/dist/templates/project/.env.example.tpl +1 -1
- package/dist/templates/project/AGENTS.md.tpl +2 -1
- package/dist/templates/project/CLAUDE.md.tpl +4 -3
- package/dist/templates/project/Dockerfile.tpl +6 -1
- package/dist/templates/project/agents/async.md.tpl +44 -6
- package/dist/templates/project/agents/data.md.tpl +86 -13
- package/dist/templates/project/agents/frontend.md.tpl +36 -8
- package/dist/templates/project/agents/mail.md.tpl +2 -1
- package/dist/templates/project/agents/realtime.md.tpl +26 -2
- package/dist/templates/project/agents/seal.md.tpl +9 -1
- package/dist/templates/project/agents/security.md.tpl +18 -0
- package/dist/templates/project/agents/storage.md.tpl +15 -8
- package/dist/templates/project/agents/web.md.tpl +38 -2
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +4 -3
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +1 -1
- package/dist/templates/project/apps/web/main.ts.tpl +1 -1
- package/dist/templates/project/apps/web/routes.ts.tpl +1 -1
- package/dist/templates/project/docker-compose.yaml.tpl +1 -1
- package/dist/templates/project/pnpm-workspace.yaml.tpl +1 -1
- package/dist/templates/project/vite.config.ts.tpl +1 -1
- package/dist/work.d.ts +21 -0
- package/dist/work.js +45 -1
- package/package.json +12 -7
- package/dist/templates/index.ts +0 -109
package/dist/work.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorkEvent } from '@gaonjs/async';
|
|
1
2
|
export interface WorkCommandOptions {
|
|
2
3
|
readonly json?: boolean;
|
|
3
4
|
/** NATS 접속지. 생략 시 NATS_URL(그다음 하위호환 GAON_NATS_URL), 기본(4222). */
|
|
@@ -12,12 +13,32 @@ export interface WorkCommandOptions {
|
|
|
12
13
|
readonly ackWaitMs?: number;
|
|
13
14
|
/** graceful drain 상한(ms). 생략 시 GAON_WORKER_DRAIN_MS. */
|
|
14
15
|
readonly drainTimeoutMs?: number;
|
|
16
|
+
/** 발행 완료 아웃박스 행 보존(ms · 0=purge 비활성). 생략 시 GAON_OUTBOX_RETENTION_MS(결정 312). */
|
|
17
|
+
readonly outboxRetentionMs?: number;
|
|
18
|
+
/** 아웃박스 purge 최소 간격(ms). 생략 시 GAON_OUTBOX_PURGE_INTERVAL_MS(결정 312). */
|
|
19
|
+
readonly outboxPurgeIntervalMs?: number;
|
|
20
|
+
/** 아웃박스 릴레이 폴링 주기(ms). 생략 시 GAON_OUTBOX_RELAY_POLL_MS(결정 312). */
|
|
21
|
+
readonly relayPollMs?: number;
|
|
15
22
|
/** 시그널 등록·해제(테스트 주입). 기본 process. */
|
|
16
23
|
readonly signals?: {
|
|
17
24
|
on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
|
|
18
25
|
off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
|
|
19
26
|
};
|
|
20
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 아웃박스 릴레이 튜닝을 env 에서 읽는다(결정 312 · GAON_WORKER_* 와 대칭).
|
|
30
|
+
* 이전엔 runWork() 프로그래매틱 옵션으로만 존재해 정본 운영 경로(`gaon work`)에서
|
|
31
|
+
* 보존 기간·purge 간격·폴링 주기를 조정할 방법이 없었다. env 미설정·비정상 값은
|
|
32
|
+
* undefined → runWork 기본(보존 7일 · purge 1시간 · 폴 1초 · 결정 78)이 그대로다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function outboxTuningFromEnv(env?: Record<string, string | undefined>): {
|
|
35
|
+
outboxRetentionMs?: number;
|
|
36
|
+
outboxPurgeIntervalMs?: number;
|
|
37
|
+
relayPollMs?: number;
|
|
38
|
+
};
|
|
39
|
+
/** WorkEvent → 사람용 한 줄(없으면 undefined). 결정 306·308: 조용한 실패(릴레이
|
|
40
|
+
* 오류·리스너 폐기·워커 인프라 오류)가 기본(human) 모드에서 0 신호이던 갭을 닫는다. */
|
|
41
|
+
export declare function emitHuman(e: WorkEvent): string | undefined;
|
|
21
42
|
/**
|
|
22
43
|
* 워커를 띄우고 시그널까지 살려 둔다. 반환 프라미스는 graceful 종료 시 resolve.
|
|
23
44
|
*/
|
package/dist/work.js
CHANGED
|
@@ -29,7 +29,29 @@ function envInt(name) {
|
|
|
29
29
|
const n = Number(raw);
|
|
30
30
|
return Number.isFinite(n) ? n : undefined;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 아웃박스 릴레이 튜닝을 env 에서 읽는다(결정 312 · GAON_WORKER_* 와 대칭).
|
|
34
|
+
* 이전엔 runWork() 프로그래매틱 옵션으로만 존재해 정본 운영 경로(`gaon work`)에서
|
|
35
|
+
* 보존 기간·purge 간격·폴링 주기를 조정할 방법이 없었다. env 미설정·비정상 값은
|
|
36
|
+
* undefined → runWork 기본(보존 7일 · purge 1시간 · 폴 1초 · 결정 78)이 그대로다.
|
|
37
|
+
*/
|
|
38
|
+
export function outboxTuningFromEnv(env = process.env) {
|
|
39
|
+
const readInt = (name) => {
|
|
40
|
+
const raw = env[name];
|
|
41
|
+
if (raw == null || raw === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
const n = Number(raw);
|
|
44
|
+
return Number.isFinite(n) ? n : undefined;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
outboxRetentionMs: readInt('GAON_OUTBOX_RETENTION_MS'),
|
|
48
|
+
outboxPurgeIntervalMs: readInt('GAON_OUTBOX_PURGE_INTERVAL_MS'),
|
|
49
|
+
relayPollMs: readInt('GAON_OUTBOX_RELAY_POLL_MS'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** WorkEvent → 사람용 한 줄(없으면 undefined). 결정 306·308: 조용한 실패(릴레이
|
|
53
|
+
* 오류·리스너 폐기·워커 인프라 오류)가 기본(human) 모드에서 0 신호이던 갭을 닫는다. */
|
|
54
|
+
export function emitHuman(e) {
|
|
33
55
|
switch (e.kind) {
|
|
34
56
|
case 'ready':
|
|
35
57
|
return ` gaon work · 준비 — 잡 ${e.jobs} · 리스너 ${e.listeners} · 스케줄 ${e.scheduled}`;
|
|
@@ -38,13 +60,30 @@ function emitHuman(e) {
|
|
|
38
60
|
return e.event.leader ? ' ▶ 스케줄러 리더 — 틱 발행 시작' : ' · 스케줄러 대기(standby)';
|
|
39
61
|
if (e.event.kind === 'fired')
|
|
40
62
|
return ` ⏰ 스케줄 발행 — ${e.event.label}`;
|
|
63
|
+
if (e.event.kind === 'error')
|
|
64
|
+
return ` ⚠ 스케줄러 오류 — ${e.event.error}`;
|
|
41
65
|
return undefined;
|
|
42
66
|
case 'worker':
|
|
43
67
|
if (e.event.kind === 'dead')
|
|
44
68
|
return ` ✗ 잡 DLQ — ${e.event.job} (${e.event.error})`;
|
|
69
|
+
// 결정 308: 재시도·인프라 오류(재적재/DLQ 발행 실패 nak · 결정 258)도 신호한다.
|
|
70
|
+
if (e.event.kind === 'retrying')
|
|
71
|
+
return ` ↻ 잡 재시도 — ${e.event.job} (시도 ${e.event.attempt} · ${e.event.delayMs}ms 뒤)`;
|
|
72
|
+
if (e.event.kind === 'error')
|
|
73
|
+
return ` ⚠ 워커 오류 — ${e.event.error}`;
|
|
74
|
+
return undefined;
|
|
75
|
+
case 'listener':
|
|
76
|
+
// 결정 308: maxDeliver 소진 = 이벤트 영구 폐기(DLQ 없음 · agents/async.md §3) — 반드시 신호.
|
|
77
|
+
if (e.event.kind === 'dropped')
|
|
78
|
+
return ` ✗ 이벤트 폐기 — ${e.event.listener} ← ${e.event.event} (재전달 소진: ${e.event.error})`;
|
|
79
|
+
if (e.event.kind === 'error')
|
|
80
|
+
return ` ⚠ 리스너 오류 — ${e.event.error}`;
|
|
45
81
|
return undefined;
|
|
46
82
|
case 'relay':
|
|
47
83
|
return ` ↪ 아웃박스 릴레이 — ${e.count}건 발행`;
|
|
84
|
+
case 'relay-error':
|
|
85
|
+
// 결정 306: 아웃박스 발행 실패(행 격리·재시도 유지)를 조용히 삼키지 않는다.
|
|
86
|
+
return ` ⚠ 아웃박스 릴레이 오류 — ${e.error}`;
|
|
48
87
|
case 'purge':
|
|
49
88
|
return ` 🧹 아웃박스 정리 — ${e.count}건 삭제(보존 기간 초과)`;
|
|
50
89
|
default:
|
|
@@ -99,6 +138,7 @@ export async function runWorkCommand(opts = {}) {
|
|
|
99
138
|
registerConnection('main', createDb(cfg), cfg.adapter);
|
|
100
139
|
}
|
|
101
140
|
const db = hasConnection('main') ? getConnection('main') : undefined;
|
|
141
|
+
const outboxEnv = outboxTuningFromEnv();
|
|
102
142
|
const domain = await loadDomain(root);
|
|
103
143
|
// 로드된 도메인 자산 요약(파일=등록 관측용). 잡·리스너·메일 수를 노출한다.
|
|
104
144
|
if (json) {
|
|
@@ -120,6 +160,10 @@ export async function runWorkCommand(opts = {}) {
|
|
|
120
160
|
concurrency: opts.concurrency ?? envInt('GAON_WORKER_CONCURRENCY'),
|
|
121
161
|
ackWaitMs: opts.ackWaitMs ?? envInt('GAON_WORKER_ACK_WAIT_MS'),
|
|
122
162
|
drainTimeoutMs: opts.drainTimeoutMs ?? envInt('GAON_WORKER_DRAIN_MS'),
|
|
163
|
+
// 아웃박스 릴레이 튜닝: 옵션 > GAON_OUTBOX_* env > runWork 기본(결정 78·312).
|
|
164
|
+
outboxRetentionMs: opts.outboxRetentionMs ?? outboxEnv.outboxRetentionMs,
|
|
165
|
+
outboxPurgeIntervalMs: opts.outboxPurgeIntervalMs ?? outboxEnv.outboxPurgeIntervalMs,
|
|
166
|
+
relayPollMs: opts.relayPollMs ?? outboxEnv.relayPollMs,
|
|
123
167
|
onEvent: emit,
|
|
124
168
|
});
|
|
125
169
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://gaonjs.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://git.nyx-zone.com/gaon/framework.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
8
13
|
"engines": {
|
|
9
14
|
"node": ">=22"
|
|
10
15
|
},
|
|
@@ -27,15 +32,15 @@
|
|
|
27
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
33
|
"typescript": "^5.9.0",
|
|
29
34
|
"vite": "^7.0.0",
|
|
30
|
-
"@gaonjs/
|
|
31
|
-
"@gaonjs/config": "0.19.0",
|
|
32
|
-
"@gaonjs/data": "0.21.0",
|
|
35
|
+
"@gaonjs/config": "0.20.0",
|
|
33
36
|
"@gaonjs/core": "0.2.4",
|
|
37
|
+
"@gaonjs/async": "0.16.0",
|
|
34
38
|
"@gaonjs/i18n": "0.2.4",
|
|
35
|
-
"@gaonjs/
|
|
36
|
-
"@gaonjs/
|
|
39
|
+
"@gaonjs/mail": "0.3.3",
|
|
40
|
+
"@gaonjs/data": "0.22.0",
|
|
41
|
+
"@gaonjs/web": "0.24.1"
|
|
37
42
|
},
|
|
38
43
|
"scripts": {
|
|
39
|
-
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|
|
44
|
+
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
|
|
40
45
|
}
|
|
41
46
|
}
|
package/dist/templates/index.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// @gaonjs/cli · templates/index.ts — 프로젝트 스캐폴드 템플릿 로더 (M9-F).
|
|
2
|
-
//
|
|
3
|
-
// `gaon new <name>` 이 소비하는 파일 트리. 소스 트리(src/templates/project/*)
|
|
4
|
-
// 를 재귀 스캔해 각 `.tpl` 파일의 최종 경로와 렌더된 내용을 반환한다.
|
|
5
|
-
// 렌더는 `{{TOKEN}}` 리터럴 replaceAll — Vue 의 `{{ }}` 보간과 겹치지 않는
|
|
6
|
-
// 고정 리터럴이라 정규식 없이 안전하다(generate.ts 와 동일 관례).
|
|
7
|
-
//
|
|
8
|
-
// 폴더는 관례상 유지되어야 하지만 git 이 빈 폴더를 추적하지 않으므로
|
|
9
|
-
// `.gitkeep.tpl` 을 두어 실 파일 `.gitkeep` 으로 렌더한다. 최종 경로는
|
|
10
|
-
// `.tpl` 접미사를 벗긴 값이다.
|
|
11
|
-
|
|
12
|
-
import { readFileSync, readdirSync } from 'node:fs'
|
|
13
|
-
import { dirname, join, posix, relative, sep } from 'node:path'
|
|
14
|
-
import { fileURLToPath } from 'node:url'
|
|
15
|
-
|
|
16
|
-
/** 생성할 파일 하나 — path 는 프로젝트 루트 기준 상대 경로(POSIX). */
|
|
17
|
-
export interface ProjectFile {
|
|
18
|
-
readonly path: string
|
|
19
|
-
readonly contents: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 템플릿 렌더 시 치환되는 토큰 값. */
|
|
23
|
-
export interface ProjectTemplateTokens {
|
|
24
|
-
readonly projectName: string
|
|
25
|
-
readonly gaonjsVersion: string
|
|
26
|
-
/**
|
|
27
|
-
* package.json 의 `packageManager` 필드(corepack 핀 · 예 `pnpm@10.27.0`).
|
|
28
|
-
* 미지정 시 blessed 기본 pnpm. 선택한 pm 에 맞춰 채운다(결정 169).
|
|
29
|
-
*/
|
|
30
|
-
readonly packageManager?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'project')
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* pm 별 `packageManager` 핀(corepack 형식 `<pm>@<x.y.z>`). 선택한 pm 을
|
|
37
|
-
* 그대로 적어 corepack enforcement 가 install 을 막지 않게 한다(결정 169 ·
|
|
38
|
-
* 12차 실사용 yarn 파손). 정본 버전 근거:
|
|
39
|
-
* · pnpm@10.27.0 — blessed 툴체인(Dockerfile corepack·CI)과 정합.
|
|
40
|
-
* · yarn@1.22.22 — yarn **classic** 최종 안정판. berry(2·4.x)는 `.yarnrc.yml`
|
|
41
|
-
* 없이 기본 PnP 라 node_modules 를 읽는 vite·gaon serve 를 깨므로 배제.
|
|
42
|
-
* · npm@10.9.9 — engines.node≥22(Node 22 LTS) 동봉 npm 라인의 tip.
|
|
43
|
-
*/
|
|
44
|
-
export const PACKAGE_MANAGER_PINS: Readonly<Record<'pnpm' | 'npm' | 'yarn', string>> = {
|
|
45
|
-
pnpm: 'pnpm@10.27.0',
|
|
46
|
-
npm: 'npm@10.9.9',
|
|
47
|
-
yarn: 'yarn@1.22.22',
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** blessed 기본(pm 미선택 시). */
|
|
51
|
-
export const DEFAULT_PACKAGE_MANAGER = PACKAGE_MANAGER_PINS.pnpm
|
|
52
|
-
|
|
53
|
-
/** 템플릿 문자열의 {{TOKEN}} 을 치환한다. 알 수 없는 토큰은 그대로 둔다. */
|
|
54
|
-
export function renderTemplate(raw: string, tokens: ProjectTemplateTokens): string {
|
|
55
|
-
return raw
|
|
56
|
-
.replaceAll('{{PROJECT_NAME}}', tokens.projectName)
|
|
57
|
-
.replaceAll('{{GAONJS_VERSION}}', tokens.gaonjsVersion)
|
|
58
|
-
.replaceAll('{{PACKAGE_MANAGER}}', tokens.packageManager ?? DEFAULT_PACKAGE_MANAGER)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 템플릿 폴더를 재귀 스캔해 파일 목록을 만든다(POSIX 경로 · 정렬). */
|
|
62
|
-
export function listTemplateFiles(root: string = TEMPLATE_DIR): string[] {
|
|
63
|
-
const out: string[] = []
|
|
64
|
-
const walk = (dir: string): void => {
|
|
65
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
-
const abs = join(dir, entry.name)
|
|
67
|
-
if (entry.isDirectory()) {
|
|
68
|
-
walk(abs)
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
if (!entry.isFile()) continue
|
|
72
|
-
out.push(abs)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
walk(root)
|
|
76
|
-
return out.sort()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 스캐폴드 파일 전체를 렌더링해 반환한다.
|
|
81
|
-
* `.tpl` 접미사는 벗기고, `{{PROJECT_NAME}}`·`{{GAONJS_VERSION}}` 을 치환한다.
|
|
82
|
-
*/
|
|
83
|
-
export function renderProjectFiles(tokens: ProjectTemplateTokens): ProjectFile[] {
|
|
84
|
-
const paths = listTemplateFiles()
|
|
85
|
-
return paths.map((abs) => {
|
|
86
|
-
const rel = relative(TEMPLATE_DIR, abs).split(sep).join(posix.sep)
|
|
87
|
-
const outPath = rel.endsWith('.tpl') ? rel.slice(0, -'.tpl'.length) : rel
|
|
88
|
-
const raw = readFileSync(abs, 'utf8')
|
|
89
|
-
return { path: outPath, contents: renderTemplate(raw, tokens) }
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** 테스트·검증용 — 템플릿 폴더의 절대 경로. */
|
|
94
|
-
export function templateDir(): string {
|
|
95
|
-
return TEMPLATE_DIR
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 렌더링된 내용에 미치환 토큰이 남아 있는지 검사(회귀 방지). */
|
|
99
|
-
export function findUnresolvedTokens(contents: string): string[] {
|
|
100
|
-
const re = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g
|
|
101
|
-
const found = new Set<string>()
|
|
102
|
-
for (const m of contents.matchAll(re)) {
|
|
103
|
-
// Vue 템플릿의 {{ prop }} (소문자·공백 시작) 은 제외 — 위 정규식이 대문자만
|
|
104
|
-
// 잡으므로 자연스럽게 걸러진다. 남으면 진짜 미치환.
|
|
105
|
-
found.add(m[1]!)
|
|
106
|
-
}
|
|
107
|
-
return [...found].sort()
|
|
108
|
-
}
|
|
109
|
-
|