@gaonjs/cli 0.27.0 → 0.27.1
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/templates/project/agents/data.md.tpl +32 -3
- package/dist/templates/project/agents/frontend.md.tpl +16 -0
- package/dist/templates/project/agents/security.md.tpl +26 -0
- package/dist/templates/project/agents/web.md.tpl +25 -0
- package/dist/templates/project/compose.prod.yaml.tpl +6 -0
- package/package.json +5 -5
|
@@ -178,7 +178,8 @@ export const posts = table('posts', {
|
|
|
178
178
|
|---|---|---|---|
|
|
179
179
|
| `where` | `(col, op, val?)` | `Chain` | op 에 따라 val 형태 강제 (위 표) |
|
|
180
180
|
| `whereIn` | `(col, vals)` | `Chain` | `where(col, 'in', vals)` 축약 |
|
|
181
|
-
| `
|
|
181
|
+
| `whereAny` | `(cols, op, val?)` | `Chain` | **여러 컬럼에 같은 조건을 OR 로 묶어 괄호로 감쌈** = `(c1 op v OR c2 op v)` · 앞선 where 와 **AND 로 안전 결합**(결정 118). 다중 컬럼 검색의 정본 — `orWhere` 로 흩뜨리면 앞 조건이 샌다(아래 함정) |
|
|
182
|
+
| `orWhere` | `(col, op, val?)` | `Chain` | op 12종 전부 (M2C) · 결합은 `(a AND b) OR c` (Rails 관습). **다중 컬럼 검색엔 쓰지 말 것** — `whereAny` 를 쓴다(결정 118) |
|
|
182
183
|
| `orderBy` | `(col, dir?)` | `Chain` | dir 기본 `'asc'` · 호출마다 누적 (다중 정렬) · 정렬 뒤 PK 타이브레이커 자동 부가(결정 110) |
|
|
183
184
|
| `reorder` | `(col, dir?)` | `Chain` | 기존 정렬 전부 버리고 재지정 |
|
|
184
185
|
| `latest` | `()` | `Chain` | `orderBy('createdAt', 'desc')` 고정 축약 · id 타이브레이커로 결정적(결정 110) |
|
|
@@ -186,6 +187,7 @@ export const posts = table('posts', {
|
|
|
186
187
|
| `offset` | `(n)` | `Chain` | 페이지네이션 = `orderBy·offset·limit` 조합 |
|
|
187
188
|
| `first` | `()` | `Promise<Rec \| undefined>` | 자동 `limit 1` |
|
|
188
189
|
| `all` | `()` | `Promise<Rec[]>` | |
|
|
190
|
+
| `paginate` | `(page, perPage)` | `Promise<PaginatedResult<Rec>>` | **체인 종단 페이지네이션**(결정 119) — `{ rows, total, page, pageCount, perPage }` 를 한 번에. count·rows 를 내부 계산(호출자 쿼리 한 번). page/perPage **클램프 내장**(0·음수·초과 = 마지막 페이지 · total 0 = pageCount 1). total=**number**. **result 통째로 render props 안전**(rows 는 Serialized 경계). `latest()` 타이브레이커(결정 110)로 페이지 경계 결정적. 집계 그룹(GroupChain)엔 없음 |
|
|
189
191
|
| `count` | `()` | `Promise<bigint>` | driver 별 반환을 **bigint 로 통일** (E-4 (g) · `model.ts:390`) |
|
|
190
192
|
| `exists` | `()` | `Promise<boolean>` | |
|
|
191
193
|
| `sum` · `avg` | `(col)` | `Promise<string \| null>` | numeric 정확성 · 대상 행 없으면 null |
|
|
@@ -426,11 +428,13 @@ export const Post = model(posts, {
|
|
|
426
428
|
// ❌ 컨트롤러 인라인 조립 (검색 교집합을 컨트롤러가 조립)
|
|
427
429
|
// const ids = await Post.where('title','ilike',p).orWhere('body','ilike',p).pluck('id')
|
|
428
430
|
// const rows = await Post.published().whereIn('id', ids).latest().all()
|
|
429
|
-
// ✅ 이름 붙인 스코프 — 컨트롤러는 한
|
|
431
|
+
// ✅ 이름 붙인 스코프 — 컨트롤러는 한 줄. 다중 컬럼 검색은 whereAny(결정 118)로
|
|
432
|
+
// 괄호로 묶어 published 가 안 새게 한다.
|
|
430
433
|
scopes: {
|
|
431
434
|
searchPublished: (q, term: string) =>
|
|
432
435
|
q.where('published', '=', true)
|
|
433
|
-
.
|
|
436
|
+
.whereAny(['title', 'body'], 'ilike', `%${term}%`),
|
|
437
|
+
// → WHERE published AND (title ILIKE .. OR body ILIKE ..) — 미발행 글이 검색에 안 샘.
|
|
434
438
|
}
|
|
435
439
|
// 컨트롤러: const rows = await Post.searchPublished(term).latest().all()
|
|
436
440
|
```
|
|
@@ -590,6 +594,15 @@ const page2 = await Post.latest().offset(20).limit(20).all()
|
|
|
590
594
|
// 부분 문자열 검색 · OR 조합
|
|
591
595
|
const found = await Post.where('title', 'like', '%gaon%').first()
|
|
592
596
|
const mine = await Post.where('published', '=', true).orWhere('authorId', '=', me.id).all()
|
|
597
|
+
// 다중 컬럼 검색은 whereAny — 앞 조건(published)과 AND 로 안전 결합(결정 118)
|
|
598
|
+
const hits = await Post.where('published', '=', true)
|
|
599
|
+
.whereAny(['title', 'body'], 'ilike', `%${term}%`).latest().all()
|
|
600
|
+
|
|
601
|
+
// 페이지네이션은 paginate — 체인 종단 · {rows,total,page,pageCount,perPage} 한 번에(결정 119)
|
|
602
|
+
const result = await Post.published().include('author').withCount('comments').latest().paginate(page, 20)
|
|
603
|
+
// result.rows → 이 페이지 (include·withCount 반영)
|
|
604
|
+
// result.total → 필터 반영 전체 개수(number) · result.pageCount → 전체 페이지(최소 1)
|
|
605
|
+
// 컨트롤러에서 this.render('Posts/Index', { page: result }) 로 통째로 넘겨도 안전.
|
|
593
606
|
|
|
594
607
|
// 집계
|
|
595
608
|
const total = await Post.where('published', '=', true).count() // bigint
|
|
@@ -675,6 +688,20 @@ await Post.upsert({ id, title, body }) // onConflict 생략 = 기
|
|
|
675
688
|
- **폼 변형은 `pick()` 뿐** (결정 104) — `Model.form.omit/extend/merge` 는
|
|
676
689
|
없다. 검증되는 부분 폼 = `pick()`, 스키마와 무관한 입력만 애드혹
|
|
677
690
|
`{ _row: {} as T }`(검증 없음 · `agents/web.md` §3).
|
|
691
|
+
- **페이지네이션을 손으로 조립하지 말 것** (결정 119) — "count 쿼리 + `orderBy·offset·limit`
|
|
692
|
+
목록 쿼리를 따로 조립하고, `count()`(bigint)를 number 로 캐스팅하고, `Math.ceil` 로 페이지 수를
|
|
693
|
+
계산하고, page 를 손으로 클램프" 하는 것은 **반정본**이다. 정본은 `chain.paginate(page, perPage)`
|
|
694
|
+
한 줄 — `{ rows, total, page, pageCount, perPage }` 를 한 번에 주고 클램프(0·음수·초과 = 마지막
|
|
695
|
+
페이지 · total 0 = pageCount 1)와 개수 number 변환을 내장한다. `include`·`withCount`·`latest`
|
|
696
|
+
뒤에 그대로 붙는다. result 는 통째로 render props 에 실어도 안전하고 UI 킷 `Pagination` 블록
|
|
697
|
+
(`:page`·`:pageCount`)에 필드가 그대로 맞는다. 집계 그룹(`groupBy`)에는 없다(행 목록 전용).
|
|
698
|
+
- **다중 컬럼 검색을 `orWhere` 로 흩뜨리면 앞 조건이 샌다** (결정 118) — `where('published',
|
|
699
|
+
'=', true).orWhere('title','ilike',t).orWhere('body','ilike',t)` 는 `published OR title OR
|
|
700
|
+
body` 로 접혀 **미발행 글이 검색에 새어 나온다**. 정본은 `whereAny(['title','body'], 'ilike',
|
|
701
|
+
t)` — 여러 컬럼을 괄호로 묶어 `published AND (title OR body)` 로 만든다. 컬럼 배열은 타입
|
|
702
|
+
안전(오타 방지)이고, 값은 op 에 맞는 타입이다. `whereAny` 로 표현 못 하는 복합 논리(컬럼별
|
|
703
|
+
다른 op·중첩 그룹)는 `Post.query()` Kysely 탈출구(§5)로 내려간다 — `whereGroup` 같은 범용
|
|
704
|
+
그룹핑 API 는 없다(선택지 증식 회피 · 결정 118).
|
|
678
705
|
|
|
679
706
|
## 관련 결정 번호
|
|
680
707
|
|
|
@@ -694,4 +721,6 @@ await Post.upsert({ id, title, body }) // onConflict 생략 = 기
|
|
|
694
721
|
| 결정 110 | `latest()`·`orderBy` 정렬 뒤 PK 타이브레이커 자동 부가(결정적 페이지네이션 · §4) |
|
|
695
722
|
| 결정 114 | 여러 모델 조합 읽기 = 이름 붙임(모델 정적 메서드/`domain/services/`) · 컨트롤러는 스코프 체인 한 줄까지(§8 · §5.3) |
|
|
696
723
|
| 결정 115 | 원자 프리미티브 increment·decrement·touch·toggle(Rec) + incrementAll·decrementAll(Chain) · read-modify-write 금지(§4) |
|
|
724
|
+
| 결정 118 | `whereAny(cols, op, val)` — 다중 컬럼 동일 조건 OR 를 괄호로 묶어 AND 안전 결합(§8·정본 예시·함정) · 범용 그룹핑(whereGroup) 은 기각(복합 논리는 Kysely 탈출구) |
|
|
725
|
+
| 결정 119 | `paginate(page, perPage)` — 체인 종단 `{rows,total,page,pageCount,perPage}` · 클램프·개수 number 내장 · UI 킷 Pagination 정합 · 손 조립(쿼리 2회·count 캐스팅·페이지 수학)은 반정본 · GroupChain 미탑재(행 목록 전용) |
|
|
697
726
|
| E-4 | 컬럼 타입·수식어·체이닝 확장 · `Post.query()` 정정 · Serialized 명명 |
|
|
@@ -281,6 +281,21 @@ import PageShell from '@shared/components/ui/PageShell.vue'
|
|
|
281
281
|
한다: 허용 = 라우트 키와 무관한 범용 API(`useForm`·`Link`·`router`) · 금지 =
|
|
282
282
|
앱 라우트 지식(`api()`·`pageProps`). 데이터는 props 로 받는다(예 `Pagination` 은
|
|
283
283
|
`v-model:page` 로 현재 페이지만 올려보내고 실제 이동은 페이지가 정한다).
|
|
284
|
+
- **`Pagination` 블록은 `paginate()` 결과에 바로 맞는다(결정 119·106)** — 컨트롤러가
|
|
285
|
+
`chain.paginate(page, perPage)` 로 만든 `{ rows, total, page, pageCount, perPage }` 를
|
|
286
|
+
통째로 넘기면, 블록의 `:page`·`:pageCount` 가 필드명 그대로 붙는다(매핑 보일러플레이트 0).
|
|
287
|
+
```vue
|
|
288
|
+
<script setup lang="ts">
|
|
289
|
+
import { Pagination } from '@shared/components/ui'
|
|
290
|
+
import { router } from 'gaonjs/vue'
|
|
291
|
+
const props = pageProps<'web:posts#index'>() // props.page = paginate 결과
|
|
292
|
+
function goto(p: number) { router.get('/posts', { page: p }, { preserveState: true }) }
|
|
293
|
+
</script>
|
|
294
|
+
<template>
|
|
295
|
+
<article v-for="post in props.page.rows" :key="post.id">…</article>
|
|
296
|
+
<Pagination :page="props.page.page" :page-count="props.page.pageCount" @update:page="goto" />
|
|
297
|
+
</template>
|
|
298
|
+
```
|
|
284
299
|
- **멀티앱은 앱마다 Tailwind 배선이 따로다(결정 76)** — 킷은 shared 한 벌이지만,
|
|
285
300
|
각 앱이 Tailwind 유틸을 받으려면 그 앱에 `style.css` 배선이 있어야 한다.
|
|
286
301
|
`gaon g app admin` 이 배선을 동봉하고, `gaon g ui-kit --app admin` 은 배선이
|
|
@@ -385,4 +400,5 @@ async function runSearch(q: string) {
|
|
|
385
400
|
| 결정 109 | 서버 스키마 검증 실패 → `form.errors.<field>` 자동 반영(303 back + 플래시 · `agents/web.md` §4.1) |
|
|
386
401
|
| 결정 113 | 버튼 모양 링크 = `<Button href>`(Link 로 Button 감싸지 않음 · `<a><button>` 중첩 방지 · doctor link-button-nesting) |
|
|
387
402
|
| 결정 116 | 공유 prop(currentUser·csrf·flash) 자동 주입 · `useShared()` 로 읽기(라우트 키 불요 · `agents/web.md`) |
|
|
403
|
+
| 결정 119 | `Pagination` 블록이 `chain.paginate()` 결과에 정합(`:page`·`:pageCount` 필드 그대로 · 매핑 0 · `agents/data.md`) |
|
|
388
404
|
| E-3 §C | 타입드 `api()` 클라이언트 (routes.d.ts 브리지 재사용) |
|
|
@@ -73,6 +73,26 @@
|
|
|
73
73
|
- **`presenceInfo`** — 접속자 목록은 채널 전원에게 공개된다. 공개 메타만
|
|
74
74
|
(`agents/realtime.md`).
|
|
75
75
|
|
|
76
|
+
### 6. 클라이언트 IP · 프록시 신뢰 (결정 120)
|
|
77
|
+
|
|
78
|
+
**헤더는 클라이언트 입력이다.** `X-Forwarded-For`·`CF-Connecting-IP` 같은 헤더의
|
|
79
|
+
신뢰는 "**바로 앞 홉이 신뢰 장비이고, 그 장비가 이 헤더를 덮어쓴다**"는 조건에서만
|
|
80
|
+
성립한다. 이 조건이 없으면 누구나 헤더를 위조해 rate limit 을 우회하고 로그를
|
|
81
|
+
오염시킬 수 있다. `gaon.config.ts` 의 `web.clientIp` 3모드가 조건을 어떻게 충족하는가:
|
|
82
|
+
|
|
83
|
+
| 모드 | 신뢰 조건 | 네트워크 전제 |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `'direct'`(기본) | 헤더를 아예 안 믿음 — 소켓 피어만 | 서버가 인터넷에 직접 노출 |
|
|
86
|
+
| `{ proxy: n \| CIDR }` | 신뢰 홉/대역 안의 XFF 만 반영 · 밖은 위조로 무시 | 리버스 프록시/LB 가 XFF 를 올바로 덮어씀 |
|
|
87
|
+
| `{ header: '<이름>' }` | 지정 헤더를 정본으로 | **오리진 직접 접속 차단이 필수** — 방화벽/대역 allowlist 로 프록시(예 Cloudflare)만 오리진에 닿게 한다. 안 하면 위조로 우회. 상급: Cloudflare **Authenticated Origin Pulls**(mTLS)로 오리진이 CF 만 받게 강제 |
|
|
88
|
+
|
|
89
|
+
- **IP 는 약한 신호다.** NAT·공유 IP·모바일 캐리어·IPv6 프리픽스 회전으로 한 IP 가
|
|
90
|
+
여러 사람이거나 한 사람이 여러 IP 다. IP 는 **rate limit·어뷰즈 억제·로깅**까지만
|
|
91
|
+
쓴다 — **인가(authorization) 판단에 쓰지 않는다.** "특정 IP 면 관리자 허용" 같은
|
|
92
|
+
분기를 만들지 말 것(위조·회전으로 즉시 무너진다). 인가는 세션/JWT 신원으로만.
|
|
93
|
+
- 세 모드의 산출은 전부 `this.request.ip` 하나로 통일된다(별도 표면 없음 · `agents/web.md` §4.4).
|
|
94
|
+
운영 배치(프록시/Cloudflare 뒤)의 선언은 `compose.prod.yaml` 배치에 맞춘다.
|
|
95
|
+
|
|
76
96
|
## 정본 예시
|
|
77
97
|
|
|
78
98
|
```ts
|
|
@@ -97,6 +117,11 @@ const rows = await Post.query()
|
|
|
97
117
|
값은 바인딩.
|
|
98
118
|
- **시크릿 하드코딩 · `.env` 커밋 금지** — env 주입만.
|
|
99
119
|
- **JWT 를 세션 앱에 섞지 않는다** — JWT 는 API 앱 전용.
|
|
120
|
+
- **프록시 뒤인데 `clientIp` 미선언 = rate limit 무력화** (결정 120) — `web.clientIp` 를
|
|
121
|
+
안 두면 프록시/CF 뒤에서 전 사용자가 프록시 IP 한 버킷으로 묶인다. 배치 환경에 맞게
|
|
122
|
+
`{ proxy }`·`{ header }` 를 선언한다(§6).
|
|
123
|
+
- **IP 로 인가 판단 금지** (결정 120) — "특정 IP = 관리자" 류는 위조·회전에 무너진다.
|
|
124
|
+
IP 는 rate limit·로깅까지만, 인가는 신원(세션/JWT)으로.
|
|
100
125
|
|
|
101
126
|
## 관련 결정 번호
|
|
102
127
|
|
|
@@ -106,3 +131,4 @@ const rows = await Post.query()
|
|
|
106
131
|
| 결정 24 (E-3 §5) | `this.params` 고정 우선순위 — 파라미터 오염 차단 |
|
|
107
132
|
| §7 (v0.15) | 세션 앱별 분리 · JWT 는 API 앱 전용 |
|
|
108
133
|
| 결정 93 (W2) | 기본 web 앱 세션 기본 배선 = CSRF 기본 켬 실태 · doctor `csrf-wiring` 경고 |
|
|
134
|
+
| 결정 120 | 클라이언트 IP 신뢰 = `web.clientIp` direct/proxy/header · 헤더는 신뢰 홉 전제에서만 · IP 는 약한 신호(인가 금지) · `this.request.ip` 단일 산출(§6 · `agents/web.md` §4.4) |
|
|
@@ -241,8 +241,31 @@ async create() {
|
|
|
241
241
|
```ts
|
|
242
242
|
// ❌ 컨트롤러가 검색 교집합·태그 필터를 인라인 조립
|
|
243
243
|
// ✅ const rows = await Post.searchPublished(term).latest().offset(o).limit(n).all()
|
|
244
|
+
// ✅ 페이지네이션은 스코프 체인 종단 paginate — 컨트롤러는 여전히 한 줄(결정 119)
|
|
245
|
+
// const page = await Post.searchPublished(term).latest().paginate(this.query('page') ?? 1, 20)
|
|
246
|
+
// return this.render('Posts/Index', { page }) // page 통째로 안전(rows Serialized · 나머지 number)
|
|
244
247
|
```
|
|
245
248
|
|
|
249
|
+
### 4.4 클라이언트 IP · 헤더는 `this.request` (FastifyRequest 탈출구 · 결정 120)
|
|
250
|
+
|
|
251
|
+
컨트롤러에서 IP·요청 헤더가 필요하면 `this.request`(FastifyRequest)로 내려간다 —
|
|
252
|
+
**`this.request.ip`** 가 클라이언트 IP 다. `this.clientIp` 같은 별도 표면은 없다.
|
|
253
|
+
|
|
254
|
+
**IP 는 프록시 뒤에서 반드시 설정을 선언해야 맞다.** `request.ip`·rate limit(기본
|
|
255
|
+
켬 · IP 기준)·구조화 로깅이 **같은 산출**을 쓰며, `gaon.config.ts` 의 `web.clientIp`
|
|
256
|
+
가 그 산출을 정한다(생략 시 `'direct'`).
|
|
257
|
+
|
|
258
|
+
| 모드 | 설정 | request.ip = | 언제 |
|
|
259
|
+
|---|---|---|---|
|
|
260
|
+
| direct(기본) | `web: { clientIp: 'direct' }` | 소켓 피어 주소 | 인터넷에 직접 노출 · XFF/CF 헤더 무시(위조 안 통함) |
|
|
261
|
+
| proxy | `{ clientIp: { proxy: 1 } }` | 신뢰 홉 내 `X-Forwarded-For` | 리버스 프록시/로드밸런서 뒤 · 홉 수 또는 CIDR/IP 로 신뢰 범위 지정 |
|
|
262
|
+
| header | `{ clientIp: { header: 'cf-connecting-ip' } }` | 지정 헤더의 첫 값 | Cloudflare 등 특정 헤더가 정본 · **오리진 직접 접속 차단이 전제**(agents/security.md) |
|
|
263
|
+
|
|
264
|
+
- **프록시/CF 뒤인데 direct 로 두면** 전 사용자가 프록시 IP 하나로 묶여 rate limit 이
|
|
265
|
+
무의미하고 로그의 IP 가 전부 프록시다 — 배치 환경에 맞게 선언한다(운영은 `agents/security.md`
|
|
266
|
+
위협 모델 · `compose.prod.yaml`).
|
|
267
|
+
- 잘못된 설정(빈 헤더 이름 등)은 **부팅 에러**로 즉시 잡힌다(→ 수리 안내 포함).
|
|
268
|
+
|
|
246
269
|
### 5. 비밀번호 해싱 — `hashPassword` · `verifyPassword` (`gaonjs/web`)
|
|
247
270
|
|
|
248
271
|
회원가입·로그인에서 비밀번호를 다룰 때는 **직접 crypto/bcrypt 를 import 하거나
|
|
@@ -415,4 +438,6 @@ export default controller({
|
|
|
415
438
|
| 결정 114 | 여러 모델 조합 읽기는 이름 붙임(정적 메서드/서비스) · 컨트롤러는 스코프 체인 한 줄까지(§4.3 · `agents/data.md` §8) |
|
|
416
439
|
| 결정 116 | 공유 prop(currentUser·csrf·flash) 자동 주입 · `this.flash(k,v)` · 페이지는 `useShared()`(§4.2) |
|
|
417
440
|
| 결정 117 | render props 에 예약 공유 키 = 컴파일 에러 + 런타임 방어(자동 주입값 조용한 덮어쓰기 금지 · §4.2) |
|
|
441
|
+
| 결정 119 | 목록 액션 페이지네이션 = `chain.paginate(page, perPage)` 종단(§4.3 · `agents/data.md`) · 손 조립 반정본 · result 통째로 render props 안전 |
|
|
442
|
+
| 결정 120 | 클라이언트 IP = `this.request.ip`(별도 표면 없음) · `web.clientIp` direct/proxy/header 로 rate limit·로깅과 같은 산출 배선(§4.4 · `agents/security.md`) |
|
|
418
443
|
| E-1 | 파사드 = `gaonjs` · CLI = `gaon` |
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
# hub 를 별도 서비스로 띄운다(운영 프로세스 3종 · CLAUDE.md §2 · 결정 60). 인프라는
|
|
5
5
|
# 실 pg·redis·nats(목업 금지 §9). 시크릿은 env 로 주입한다 — 이 파일에 값을 박지 말 것.
|
|
6
6
|
#
|
|
7
|
+
# 리버스 프록시/로드밸런서/Cloudflare 뒤에 배치한다면 gaon.config.ts 의 web.clientIp 를
|
|
8
|
+
# 선언한다(결정 120) — 안 하면 rate limit(IP 기준·기본 켬)이 전 사용자를 프록시 IP 한
|
|
9
|
+
# 버킷으로 묶고 로그의 IP 가 전부 프록시가 된다. 프록시면 { proxy: 1 }, Cloudflare 등
|
|
10
|
+
# 특정 헤더가 정본이면 { header: 'cf-connecting-ip' }(오리진 직접 접속 차단 전제).
|
|
11
|
+
# 위협 모델·전제는 agents/security.md §6.
|
|
12
|
+
#
|
|
7
13
|
# 기동: docker compose -f compose.prod.yaml up -d --build
|
|
8
14
|
# 정지: docker compose -f compose.prod.yaml down
|
|
9
15
|
name: {{PROJECT_NAME}}-prod
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
|
-
"@gaonjs/
|
|
30
|
+
"@gaonjs/async": "0.6.1",
|
|
31
|
+
"@gaonjs/data": "0.13.0",
|
|
31
32
|
"@gaonjs/core": "0.2.1",
|
|
32
33
|
"@gaonjs/mail": "0.1.3",
|
|
33
|
-
"@gaonjs/
|
|
34
|
-
"@gaonjs/
|
|
35
|
-
"@gaonjs/async": "0.6.1"
|
|
34
|
+
"@gaonjs/config": "0.8.0",
|
|
35
|
+
"@gaonjs/web": "0.10.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|