@gaonjs/cli 0.55.0 → 0.57.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.
Files changed (33) hide show
  1. package/dist/commands/dev.js +22 -3
  2. package/dist/commands/test.js +28 -4
  3. package/dist/doctor/auth-wiring.js +5 -2
  4. package/dist/doctor/channel-collision.d.ts +9 -0
  5. package/dist/doctor/channel-collision.js +119 -0
  6. package/dist/doctor/dotenv-node-env.d.ts +5 -0
  7. package/dist/doctor/dotenv-node-env.js +68 -0
  8. package/dist/doctor/fixers/index.d.ts +1 -1
  9. package/dist/doctor/fixers/index.js +11 -1
  10. package/dist/doctor/types.d.ts +1 -1
  11. package/dist/doctor.d.ts +10 -2
  12. package/dist/doctor.js +50 -3
  13. package/dist/generate.d.ts +5 -0
  14. package/dist/generate.js +11 -3
  15. package/dist/index.js +7 -5
  16. package/dist/nodeEnv.d.ts +23 -0
  17. package/dist/nodeEnv.js +26 -0
  18. package/dist/scaffold/controller.js +3 -1
  19. package/dist/scaffold/page.js +6 -4
  20. package/dist/serve.js +13 -1
  21. package/dist/templates/project/AGENTS.md.tpl +34 -5
  22. package/dist/templates/project/CLAUDE.md.tpl +1 -1
  23. package/dist/templates/project/agents/async.md.tpl +20 -7
  24. package/dist/templates/project/agents/data.md.tpl +65 -11
  25. package/dist/templates/project/agents/frontend.md.tpl +19 -9
  26. package/dist/templates/project/agents/i18n.md.tpl +17 -4
  27. package/dist/templates/project/agents/realtime.md.tpl +105 -15
  28. package/dist/templates/project/agents/seal.md.tpl +19 -7
  29. package/dist/templates/project/agents/security.md.tpl +13 -1
  30. package/dist/templates/project/agents/storage.md.tpl +21 -9
  31. package/dist/templates/project/agents/testing.md.tpl +58 -0
  32. package/dist/templates/project/agents/web.md.tpl +157 -15
  33. package/package.json +6 -6
@@ -6,6 +6,58 @@
6
6
 
7
7
  ## 정본 규칙
8
8
 
9
+ ### 0. 라우트 DSL — `apps/<앱>/routes.ts` (`packages/web/src/routes.ts`)
10
+
11
+ 라우트는 앱마다 `routes.ts` 하나에 선언한다(`export default routes((r) => {...})`).
12
+ 빌더 `r` 의 표면은 **아래 7개가 전부**다 — 표에 없는 메서드(`r.namespace`·
13
+ `r.scope`·`r.match`·`r.root` 등)는 존재하지 않는다(AGENTS §0 "표에 없는 API 추측 금지").
14
+
15
+ | 호출 | 등록되는 것 |
16
+ |---|---|
17
+ | `r.get(path, '<컨트롤러>#<액션>')` | GET 한 건 |
18
+ | `r.post(path, '<컨트롤러>#<액션>')` | POST 한 건 |
19
+ | `r.put(path, '<컨트롤러>#<액션>')` | PUT 한 건 |
20
+ | `r.patch(path, '<컨트롤러>#<액션>')` | PATCH 한 건 |
21
+ | `r.delete(path, '<컨트롤러>#<액션>')` | DELETE 한 건 |
22
+ | `r.resources('posts')` | **복수** 리소스 7액션 |
23
+ | `r.resource('session')` | **단수** 리소스 5액션(`index`·`:id` 없음) |
24
+
25
+ ```ts
26
+ // apps/web/routes.ts
27
+ import { routes } from 'gaonjs/web'
28
+
29
+ export default routes((r) => {
30
+ r.resources('posts') // 표준 7액션 전개
31
+ r.resource('session') // 단수 리소스(로그인 세션)
32
+ r.get('/dashboard', 'dashboard#show') // 단건 라우트
33
+ r.post('/posts/:id/tagAdd', 'posts#tagAdd') // 액션명 = camelCase 메서드명(§1)
34
+ })
35
+ ```
36
+
37
+ `r.resources('posts')` 가 펴는 7 엔트리(Rails 관례):
38
+
39
+ | 메서드 | 경로 | 액션 |
40
+ |---|---|---|
41
+ | GET | `/posts` | `index` |
42
+ | GET | `/posts/new` | `new` |
43
+ | POST | `/posts` | `create` |
44
+ | GET | `/posts/:id` | `show` |
45
+ | GET | `/posts/:id/edit` | `edit` |
46
+ | PATCH | `/posts/:id` | `update` |
47
+ | DELETE | `/posts/:id` | `destroy` |
48
+
49
+ `r.resource('session')`(단수) 5 엔트리: GET `/session/new`→`new` ·
50
+ POST `/session`→`create` · GET `/session`→`show` · PATCH `/session`→`update` ·
51
+ DELETE `/session`→`destroy`.
52
+
53
+ - **`update` 는 PATCH 다** — `resources`/`resource` 는 PUT 을 등록하지 않는다.
54
+ PUT 이 필요하면 `r.put(...)` 로 직접 건다.
55
+ - **라우트 표는 후보다** — 컨트롤러에 그 액션이 구현돼 있지 않으면 조용히
56
+ 스킵된다(정상 경로). 반면 타깃 형식 불량(`r.get('/x', 'posts')` 처럼 `#`
57
+ 누락)은 **부팅 throw** 다(결정 293 · §알려진 함정).
58
+ - **경로는 앱 프리픽스 이전의 상대 경로**다 — admin 앱의 `r.get('/posts', …)`
59
+ 는 최종 `/admin/posts` 로 뜬다(§7 멀티앱 URL 관례).
60
+
9
61
  ### 1. JSON 액션 — 반환값이 곧 응답 (errata E-3 §3.1)
10
62
 
11
63
  컨트롤러 액션이 `this.render(...)` 나 `this.redirect(...)` 대신
@@ -415,8 +467,14 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
415
467
  빌드 없이 어느 환경에서나 설치가 확실하다.
416
468
  - **저장 위치** — 결과는 스키마의 hidden 컬럼(`passwordDigest: t.string().hidden()`)
417
469
  에 담는다 — 응답 경계에서 타입·런타임 양쪽으로 페이지 노출이 막힌다.
418
- - 회원 생성 로직은 컨트롤러가 아니라 **서비스**(`agents/data.md` §9 · registerUser)에
419
- 둔다 `const user = await RegisterUser.call({ name, email, password })`.
470
+ - **가입 컨트롤러는 이메일 중복을 pre-check 한다 (결정 256).** `gaon g auth` 스키마의
471
+ `email` `.unique()` DDL UNIQUE 중복을 막지만, 그것만 믿으면 중복 가입이 raw DB
472
+ 에러(500)로 터진다. 컨트롤러가 먼저 조회해 친절한 폼 에러로 마감하고, 유니크 제약은
473
+ TOCTOU 레이스의 backstop 으로 남긴다(아래 §정본 예시 = 스캐폴드와 같은 형태).
474
+ - 모델 여럿·트랜잭션·외부 API 가 얽히면 회원 생성 로직을 **서비스**(`domain/services/` ·
475
+ `service()` 시그니처·트랜잭션 계약은 `agents/data.md` §9)로 뺀다 —
476
+ `const user = await RegisterUser.call({ name, email, password })`. 스캐폴드 기본형은
477
+ 모델 하나뿐이라 컨트롤러에 둔다(§5.3 판단표 · 아래 §정본 예시).
420
478
 
421
479
  ### 6. 인증·세션 (v0.15 §7 · M5)
422
480
 
@@ -425,6 +483,10 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
425
483
  서명 secret · Redis 키 prefix 가 앱 단위로 갇힌다. (쿠키 path 는 `/` 고정 —
426
484
  프리픽스·서브도메인 양쪽 접근에 쿠키가 실리려면 정적 path 가 `/` 여야 한다 ·
427
485
  결정 142. 분리는 위 세 축으로 완성된다.)
486
+ - **세션 secret env 이름 = web 은 `SESSION_SECRET`, 그 외 앱은 `<APP>_SESSION_SECRET`**
487
+ (앱 이름 대문자화 · 예: admin → `ADMIN_SESSION_SECRET` · 결정 141). `gaon g auth --app <앱>`
488
+ 이 `.env`·`.env.example` 에 그 키를 시드하고 app.config 가 읽는다 — 운영에서는 앱마다
489
+ **다른** 값을 주입한다(운영 fail-loud 는 `agents/security.md` §2 · 결정 255).
428
490
  - **쿠키 secure·sameSite 는 app.config 의 session 에서 조정한다(결정 295).**
429
491
  `secure` 생략 시 운영(NODE_ENV=production)이면 켬 — 운영인데 비-TLS(사내
430
492
  내부망 http)로 서빙하면 Secure 쿠키가 안 실려 로그인이 조용히 실패하므로 그
@@ -459,7 +521,7 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
459
521
  세션이 없는 앱(세션 미구성·JWT/API)에서 `login`/`logout` 을 부르면 **즉시
460
522
  throw(500 + 수리 안내)** 한다(결정 296) — 이전엔 조용한 no-op 이라 부팅
461
523
  green·로그인만 영구 실패였다. 세션 앱이면 app.config 에 session 을 배선하고,
462
- JWT 앱이면 `this.jwt.issue`/`this.jwt.refresh` 를 쓴다.
524
+ JWT 앱이면 `this.jwt!.issue`/`this.jwt!.refresh` 를 쓴다.
463
525
 
464
526
  `login`/`logout` 은 세션 ID 재생성·파기(비동기)를 하므로 `await` 를 붙인다. 생략해도
465
527
  디스패처가 응답 직전에 정착시켜 동작하지만(기존 코드 호환), 정본은 `await` 다.
@@ -483,7 +545,10 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
483
545
  }
484
546
  ```
485
547
 
486
- API 앱(JWT)은 세션 대신 `this.jwt.issue(user)` / `this.jwt.refresh(token)` 를 쓴다.
548
+ API 앱(JWT)은 세션 대신 `this.jwt!.issue(user)` / `this.jwt!.refresh(token)` 를 쓴다 —
549
+ `this.jwt` 는 **optional 프로퍼티**(`readonly jwt?: JwtApi`)라 JWT 앱에서도 non-null
550
+ 단언 `!` 이 필요하다(세션 앱엔 없기 때문 · 스캐폴드 `jwt.session.controller` 동형).
551
+ `refresh` 는 토큰이 무효·만료면 `null` 을 돌려주므로 401 로 마감한다.
487
552
 
488
553
  - **JWT 하드닝 (결정 337 · 세션 결정 255 와 대칭).** JWT secret 은 **32자 이상**이
489
554
  부팅 요건이고(미만 = 부팅 에러), 운영(NODE_ENV=production)에서 dev 폴백/
@@ -498,12 +563,31 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
498
563
  그대로). ② `Authorization` 의 Bearer 스킴은 대소문자 무관이다(RFC 7235 —
499
564
  `bearer`/`BEARER` 클라이언트도 인증된다).
500
565
 
501
- - **JWT 스캐폴드 = `gaon g auth --jwt --app <api>`** (결정 338 · `--app` 필수 ·
502
- web 불가 — web 은 세션이 정본). 페이지·회원가입 없이 스키마/모델(공유) +
503
- 토큰 컨트롤러(JSON 전용: `POST /session` 발급 · `POST /session/refresh` 재발급 ·
504
- `GET /session` 현재 사용자[Bearer]) + `auth: { strategy:'jwt', secret, loadUser }`
505
- 배선 + `.env` 에 `<APP>_JWT_SECRET` 시드를 깐다. 계정은 web 앱 가입 또는
506
- seed 만든다(API 앱에 공개 가입 없음).
566
+ - **API(JWT) 생성 The One Way = `gaon g auth --jwt --app <이름>` 단독** (결정 338 ·
567
+ `--app` 필수 · web 불가 — web 은 세션이 정본). **`gaon g app <이름>` 을 먼저 돌리지
568
+ 않는다** 명령 하나가 폴더째 낳는다:
569
+
570
+ ```bash
571
+ gaon g auth --jwt --app api # apps/api/ 가 이 한 줄로 생긴다(g app 선행 불필요)
572
+ ```
573
+
574
+ | 생성물 | 내용 |
575
+ |---|---|
576
+ | `domain/schema/users.ts` · `domain/models/User.ts` | 스키마·모델(세션 앱과 **공유** · 이미 있으면 skip) |
577
+ | `apps/<app>/routes.ts` | 토큰 라우트 3종(없으면 생성 · 있으면 패치) |
578
+ | `apps/<app>/controllers/session.ts` | 토큰 컨트롤러(JSON 전용) |
579
+ | `apps/<app>/auth.ts` | `loadUser`(JWT sub → 사용자) + `GaonCurrentUser` 증강 |
580
+ | `apps/<app>/app.config.ts` | `auth: { strategy:'jwt', secret, loadUser }` 배선 |
581
+ | `.env`·`.env.example` | `<APP>_JWT_SECRET` 시드(예: `API_JWT_SECRET`) |
582
+
583
+ 라우트 3종은 `POST /session` 발급 · `POST /session/refresh` 재발급 ·
584
+ `GET /session` 현재 사용자(Bearer)다. 계정은 web 앱 가입 또는 seed 로 만든다
585
+ (API 앱에 공개 가입 없음 — `--public` 은 JWT 변형에서 거부된다).
586
+
587
+ - **`gaon g app` 을 먼저 돌리면 오히려 어긋난다** — `g app` 은 **프론트 앱**을 깔기
588
+ 때문에 `index.html`·`main.ts`·`pages/`·Tailwind 배선까지 함께 생긴다. API 앱엔 이
589
+ 파일들이 불필요하고, 남아 있으면 `gaon build`·`gaon check` 가 이 앱을 프론트 앱으로
590
+ 보고 빌드·검사한다. 이미 `g app` 으로 만들어 버렸다면 그 프론트 파일들을 지운다.
507
591
 
508
592
  - **API 앱은 프론트엔드가 없다(JSON 전용).** `apps/<app>/app.config.ts`(`auth: { strategy:'jwt', … }`)
509
593
  + `routes.ts` + `controllers/` 만 두면 된다 — `index.html`·`main.ts`·`pages/` 는 만들지 않는다.
@@ -522,6 +606,20 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
522
606
  }
523
607
  ```
524
608
 
609
+ **스키마에 컬럼을 더하면 이 증강도 함께 갱신한다.** 증강은 자동 생성이 아니라
610
+ 스캐폴드가 한 번 심는 손 선언이라 스키마와 자동 동기되지 않는다 — 예컨대 역할
611
+ 게이트(`this.authorize(this.currentUser?.role === 'admin')` · `agents/security.md` §2)
612
+ 를 쓰려고 `domain/schema/users.ts` 에 `role` 컬럼을 추가했다면, `apps/<app>/auth.ts`
613
+ 의 `GaonCurrentUser` 에도 `role: string` 을 더해야 한다. 안 그러면 그 접근이
614
+ **TS2339**(`role` 없음)로 컴파일에서 막힌다.
615
+
616
+ ```ts
617
+ // apps/<app>/auth.ts — role 컬럼을 추가했다면 증강도 함께
618
+ declare module 'gaonjs/web' {
619
+ interface GaonCurrentUser { id: bigint; name: string; email: string; role: string }
620
+ }
621
+ ```
622
+
525
623
  - **인증 배선 = `apps/<app>/app.config.ts` — 로그인 기능의 필수 구성 요소다**
526
624
  (결정 59). 컨트롤러·페이지만 만들면 컴파일은 통과하지만, 이 배선이 없으면
527
625
  세션에 로그인해도 요청마다 사용자를 로드할 길이 없어 `this.currentUser`/
@@ -548,23 +646,62 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
548
646
  await User.where('id', '=', BigInt(String(id))).first()
549
647
  ```
550
648
 
649
+ ### 7. 멀티앱 URL 관례 (`packages/web/src/dispatch.ts` · `host-router.ts`)
650
+
651
+ 앱이 둘 이상이면 URL 이 앱 경계를 표현한다. 규칙은 셋이다:
652
+
653
+ 1. **앱 폴더명 = URL 프리픽스 · web 만 `/`** — `prefixFor(name)` = `web` → `/`,
654
+ 그 외 → `/<name>`. `apps/admin/routes.ts` 의 `r.get('/posts', 'posts#index')`
655
+ 는 최종 `/admin/posts` 로 뜬다(라우트 경로는 프리픽스 **이전**의 상대 경로 · §0).
656
+ 2. **개발 모드는 `<앱>.localhost` 매핑도 함께 동작** — `admin.localhost:3000/posts`
657
+ 요청은 URL 이 `/admin/posts` 로 재작성돼 같은 프리픽스 라우팅에 도달한다(새 라우팅
658
+ 층이 아니라 재작성 한 겹). 이미 프리픽스가 붙어 있으면 재작성하지 않고(중복 방지),
659
+ web 앱(프리픽스 `/`)은 재작성 대상이 아니다. 운영 도메인은 `app.config.ts` 의
660
+ `hosts: ['admin.example.com']` 로 같은 경로를 탄다.
661
+ 3. **프리픽스는 그 앱의 예약 경로다** — admin 앱이 있으면 web 앱이 `/admin/...` 을
662
+ 라우팅하지 않는다. 기계적 강제(doctor 검사)는 없는 **관례**라 어기면 이렇게 갈린다:
663
+ 최종 URL 이 **정확히 겹치면 부팅 실패**(`FST_ERR_DUPLICATED_ROUTE` · 2026-08-05 실측),
664
+ 부분만 겹치면 **둘 다 조용히 살아** 앱 경계가 흐려진다. 후자가 더 나쁘므로 관례를 지킨다.
665
+
666
+ **앱 안의 절대 경로는 항상 풀 프리픽스로 쓴다.** 리다이렉트·폼 action·`Link href`
667
+ 가 앱 안 상대 경로가 아니라 **최종 URL** 이라서, admin 앱의 대시보드 리다이렉트는
668
+ `/dashboard` 가 아니라 **`/admin/dashboard`** 다. `gaon g auth --app admin` 스캐폴드가
669
+ 이 형태로 생성한다(`loginRedirect: '/admin/session/new'` · `this.redirect('/admin/dashboard')` ·
670
+ `form.post('/admin/session')`). 프리픽스를 빼면 web 앱 경로로 새 나가 404 나 엉뚱한 앱에
671
+ 도달한다.
672
+
673
+ - `api()` 라우트 키도 앱을 포함한다 — `api('admin:posts#search', …)`(`<app>:<컨트롤러>#<액션>` ·
674
+ `agents/frontend.md` §1·§2). URL 은 매니페스트가 프리픽스까지 채워 주므로 손으로 붙이지 않는다.
675
+ - 채널 WS 접속 경로도 앱 프리픽스를 탄다(`/admin/gaon/ws/<채널>`) — `useChannel` 이 자동
676
+ 주입한다. 단 **채널 이름 자체는 전역 네임스페이스**다(`agents/realtime.md` §2).
677
+ - 앱별 세션 분리(쿠키 이름 `<app>_sid`·secret·Redis prefix)는 §6, 앱 스코프 보안
678
+ override 는 `agents/security.md` §1.
679
+
551
680
  ## 정본 예시
552
681
 
553
682
  ```ts
554
- // apps/web/controllers/registration.ts — 회원 가입 (E-3 · Inertia SPA)
555
- import { controller } from 'gaonjs/web'
556
- import { RegisterUser } from '../../../domain/services/registerUser.js'
683
+ // apps/web/controllers/registration.ts — 회원 가입 (E-3 · Inertia SPA · gaon g auth 스캐폴드 형태)
684
+ import { controller, hashPassword } from 'gaonjs/web'
685
+ import { User } from '../../../domain/models/User.js'
557
686
  import { SendWelcomeMail } from '../../../domain/jobs/sendWelcomeMail.js'
558
687
 
559
688
  export default controller({
560
689
  async new() {
561
- return this.render('Auth/Signup', {})
690
+ return this.render('Auth/Signup', { error: null as string | null })
562
691
  },
563
692
  async create() {
564
693
  const { name, email, password } = this.params({
565
694
  _row: {} as { name: string; email: string; password: string },
566
695
  })
567
- const user = await RegisterUser.call({ name, email, password })
696
+ // 결정 256: 이메일 중복은 스키마 .unique() 강제하지만(무결성 backstop),
697
+ // pre-check 로 raw DB 500 대신 폼 에러를 준다. 폼 액션이라 실패=render·성공=redirect
698
+ // 혼용이 허용된다(§2 결정 57 보완).
699
+ if (await User.where('email', '=', email).first()) {
700
+ return this.render('Auth/Signup', { error: '이미 사용 중인 이메일입니다.' })
701
+ }
702
+ const passwordDigest = await hashPassword(password) // §5 · bcrypt 직접 import 금지
703
+ const user = await User.create({ name, email, passwordDigest })
704
+ await this.auth.login(user) // 세션 ID 재생성 + 로그인 확정(결정 254)
568
705
  await SendWelcomeMail.later(user.id) // 잡 발행 위치는 결정 32 — 서비스 afterCommit 도 정합
569
706
  return this.redirect('/dashboard')
570
707
  },
@@ -614,11 +751,16 @@ export default controller({
614
751
 
615
752
  | 결정 | 내용 |
616
753
  |---|---|
754
+ | §5.1 (v0.17) | 라우트 DSL 표면 7종 — `get/post/put/patch/delete` + `resources`(7액션)·`resource`(5액션) · `update`=PATCH · 경로는 프리픽스 이전 상대 경로(§0) |
755
+ | §3.3 (v0.16) | 멀티앱 URL — 앱 폴더명 = 프리픽스(web `/`) · 개발 `<앱>.localhost` 매핑(URL 재작성 한 겹) · 프리픽스 = 그 앱 예약 경로(관례 · 완전 중복은 부팅 실패) (§7) |
617
756
  | 결정 23 (E-3) | 앱 내 JSON 액션 — 반환값 = 응답 |
618
757
  | 결정 24 (E-3 §5) | `this.params` 안전 규칙 (라우트 > body > query · 중복 키 · body/query 탈출구) |
619
758
  | 결정 32 | 잡 발행 위치 자유 (컨트롤러·서비스·리스너 — `agents/async.md`) |
620
759
  | 결정 37 | bigint PK 컨트롤러 `String()` 정규화 (`agents/frontend.md`) |
760
+ | 결정 58 | `this.auth.user`/`requireAuth()` 사용자 타입 = 앱의 `GaonCurrentUser` 선언 병합 증강(`apps/<app>/auth.ts`) — 스키마 컬럼(role 등) 추가 시 증강도 함께 갱신(미갱신 = TS2339 · §6) |
621
761
  | 결정 59 | 인증 배선 = `app.config.ts` 의 `session`+`auth(loadUser)` — 없으면 currentUser 영구 null |
762
+ | 결정 141 | 앱별 세션 secret env — web=`SESSION_SECRET` · 그 외=`<APP>_SESSION_SECRET` · 스캐폴드가 앱 프리픽스 URL(`/admin/...`)로 리다이렉트·폼 경로를 생성(§6·§7) |
763
+ | 결정 256 | `gaon g auth` email `.unique()` + 가입 컨트롤러 이메일 pre-check(raw DB 500 대신 폼 에러 · 유니크는 TOCTOU backstop · §5·§정본 예시) |
622
764
  | 결정 95 (W4) | 폼 모양 2종 — 스키마 파생 `Model.form`(검증) vs 애드혹 `{ _row }`(타입만) · 라우트 파라미터는 둘 다 자동 병합 |
623
765
  | 결정 104 | `Model.form.pick('a','b')` = 검증되는 부분 폼(결정 95 회부 종결) · 폼 변형은 pick 하나(omit/extend/merge 없음) |
624
766
  | 결정 108 | 정적 default 컬럼 빈 입력 채움(coerceParams) · 동적 default 는 DB 위임 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,13 +32,13 @@
32
32
  "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "typescript": "^5.9.0",
34
34
  "vite": "^7.0.0",
35
- "@gaonjs/config": "0.23.0",
36
- "@gaonjs/async": "0.18.0",
37
- "@gaonjs/core": "0.2.4",
35
+ "@gaonjs/async": "0.18.1",
38
36
  "@gaonjs/i18n": "0.3.0",
39
- "@gaonjs/data": "0.25.0",
37
+ "@gaonjs/config": "0.25.0",
38
+ "@gaonjs/data": "0.25.2",
39
+ "@gaonjs/web": "0.30.0",
40
40
  "@gaonjs/mail": "0.5.0",
41
- "@gaonjs/web": "0.29.0"
41
+ "@gaonjs/core": "0.2.4"
42
42
  },
43
43
  "scripts": {
44
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})\""