@gaonjs/cli 0.15.0 → 0.18.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 (52) hide show
  1. package/dist/commands/g.js +8 -0
  2. package/dist/commands/new.js +5 -0
  3. package/dist/dev/health.d.ts +4 -1
  4. package/dist/dev/health.js +5 -3
  5. package/dist/doctor/types.d.ts +1 -1
  6. package/dist/doctor/types.js +3 -3
  7. package/dist/doctor/ui-kit-wiring.d.ts +5 -0
  8. package/dist/doctor/ui-kit-wiring.js +93 -0
  9. package/dist/doctor.d.ts +1 -0
  10. package/dist/doctor.js +7 -2
  11. package/dist/generate.js +8 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +13 -4
  14. package/dist/scaffold/app-wiring.d.ts +12 -0
  15. package/dist/scaffold/app-wiring.js +68 -0
  16. package/dist/templates/auth/Dashboard.vue.tpl +21 -5
  17. package/dist/templates/auth/Login.vue.tpl +37 -8
  18. package/dist/templates/auth/Signup.vue.tpl +40 -9
  19. package/dist/templates/project/AGENTS.md.tpl +3 -2
  20. package/dist/templates/project/CLAUDE.md.tpl +3 -2
  21. package/dist/templates/project/agents/async.md.tpl +15 -7
  22. package/dist/templates/project/agents/frontend.md.tpl +62 -4
  23. package/dist/templates/project/apps/web/composables/useGaonHealth.ts.tpl +2 -0
  24. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +19 -94
  25. package/dist/templates/project/apps/web/main.ts.tpl +4 -0
  26. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +68 -251
  27. package/dist/templates/project/apps/web/style.css.tpl +66 -0
  28. package/dist/templates/project/package.json.tpl +3 -0
  29. package/dist/templates/project/postcss.config.js.tpl +15 -0
  30. package/dist/templates/project/tailwind.config.ts.tpl +56 -0
  31. package/dist/templates/ui-kit/Alert.vue.tpl +23 -0
  32. package/dist/templates/ui-kit/AlertDescription.vue.tpl +9 -0
  33. package/dist/templates/ui-kit/AlertTitle.vue.tpl +9 -0
  34. package/dist/templates/ui-kit/Badge.vue.tpl +25 -0
  35. package/dist/templates/ui-kit/Button.vue.tpl +39 -0
  36. package/dist/templates/ui-kit/Card.vue.tpl +10 -0
  37. package/dist/templates/ui-kit/CardContent.vue.tpl +9 -0
  38. package/dist/templates/ui-kit/CardDescription.vue.tpl +9 -0
  39. package/dist/templates/ui-kit/CardFooter.vue.tpl +9 -0
  40. package/dist/templates/ui-kit/CardHeader.vue.tpl +9 -0
  41. package/dist/templates/ui-kit/CardTitle.vue.tpl +9 -0
  42. package/dist/templates/ui-kit/Dialog.vue.tpl +68 -0
  43. package/dist/templates/ui-kit/Form.vue.tpl +13 -0
  44. package/dist/templates/ui-kit/FormField.vue.tpl +16 -0
  45. package/dist/templates/ui-kit/FormMessage.vue.tpl +9 -0
  46. package/dist/templates/ui-kit/Input.vue.tpl +22 -0
  47. package/dist/templates/ui-kit/Label.vue.tpl +9 -0
  48. package/dist/templates/ui-kit/Sheet.vue.tpl +73 -0
  49. package/dist/templates/ui-kit/utils.ts.tpl +26 -0
  50. package/dist/uikit.d.ts +28 -0
  51. package/dist/uikit.js +138 -0
  52. package/package.json +6 -6
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · CardContent (결정 75). 카드 본문. 헤더가 있으면 상단 패딩을 뺀다.
3
+ </script>
4
+
5
+ <template>
6
+ <div class="p-6 pt-0">
7
+ <slot />
8
+ </div>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · CardDescription (결정 75). 카드 부제·설명.
3
+ </script>
4
+
5
+ <template>
6
+ <p class="text-sm text-muted-foreground">
7
+ <slot />
8
+ </p>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · CardFooter (결정 75). 카드 하단 액션 영역.
3
+ </script>
4
+
5
+ <template>
6
+ <div class="flex items-center p-6 pt-0">
7
+ <slot />
8
+ </div>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · CardHeader (결정 75). CardTitle·CardDescription 을 감싼다.
3
+ </script>
4
+
5
+ <template>
6
+ <div class="flex flex-col space-y-1.5 p-6">
7
+ <slot />
8
+ </div>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · CardTitle (결정 75). 카드 제목.
3
+ </script>
4
+
5
+ <template>
6
+ <h3 class="text-lg font-semibold leading-none tracking-tight">
7
+ <slot />
8
+ </h3>
9
+ </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · Dialog (결정 75). v-model:open 으로 여닫는 모달. reka-ui 등 헤드리스
3
+ // 라이브러리를 끌어오지 않고 Teleport + Transition 으로 자작한다 — 백드롭 클릭·
4
+ // Escape 로 닫히고 role=dialog·aria-modal 로 접근성 기본을 지킨다.
5
+ import { watch, onBeforeUnmount } from 'vue'
6
+
7
+ const props = defineProps<{ open: boolean }>()
8
+ const emit = defineEmits<{ 'update:open': [value: boolean] }>()
9
+
10
+ function close(): void {
11
+ emit('update:open', false)
12
+ }
13
+ function onKey(e: KeyboardEvent): void {
14
+ if (e.key === 'Escape') close()
15
+ }
16
+
17
+ // 열려 있는 동안만 전역 Escape 를 듣는다(문서 어디에 포커스가 있든 닫히게).
18
+ watch(
19
+ () => props.open,
20
+ (isOpen) => {
21
+ if (typeof window === 'undefined') return
22
+ if (isOpen) window.addEventListener('keydown', onKey)
23
+ else window.removeEventListener('keydown', onKey)
24
+ },
25
+ )
26
+ onBeforeUnmount(() => {
27
+ if (typeof window !== 'undefined') window.removeEventListener('keydown', onKey)
28
+ })
29
+ </script>
30
+
31
+ <template>
32
+ <Teleport to="body">
33
+ <Transition
34
+ enter-active-class="transition-opacity duration-200 ease-out"
35
+ enter-from-class="opacity-0"
36
+ leave-active-class="transition-opacity duration-200 ease-in"
37
+ leave-to-class="opacity-0"
38
+ >
39
+ <div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center p-4">
40
+ <div class="absolute inset-0 bg-black/80" @click="close" />
41
+ <Transition
42
+ appear
43
+ enter-active-class="transition-all duration-200 ease-out"
44
+ enter-from-class="opacity-0 scale-95"
45
+ leave-active-class="transition-all duration-200 ease-in"
46
+ leave-to-class="opacity-0 scale-95"
47
+ >
48
+ <div
49
+ v-if="open"
50
+ role="dialog"
51
+ aria-modal="true"
52
+ class="relative z-50 w-full max-w-lg rounded-lg border bg-background p-6 shadow-lg"
53
+ >
54
+ <slot />
55
+ <button
56
+ type="button"
57
+ aria-label="닫기"
58
+ class="absolute right-4 top-4 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
59
+ @click="close"
60
+ >
61
+
62
+ </button>
63
+ </div>
64
+ </Transition>
65
+ </div>
66
+ </Transition>
67
+ </Teleport>
68
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · Form (결정 75 · 결정 64). gaonjs 의 useForm 과 짝을 이루는 얇은 <form>
3
+ // 래퍼다. 제출은 @submit 이벤트로 넘겨 useForm 의 post/put/delete 를 호출한다 —
4
+ // fetch() 로 폼을 만들지 않는다(결정 64). vee-validate 등 외부 폼 라이브러리를
5
+ // 끌어오지 않는다(검증·상태는 useForm 이 갖는다).
6
+ const emit = defineEmits<{ submit: [] }>()
7
+ </script>
8
+
9
+ <template>
10
+ <form class="space-y-4" @submit.prevent="emit('submit')">
11
+ <slot />
12
+ </form>
13
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · FormField (결정 75). 라벨 + 컨트롤(슬롯) + 검증 메시지를 묶는 그룹.
3
+ // label/error 는 문자열로 받는다 — useForm 의 form.errors.<field> 를 :error 로 넘긴다.
4
+ import Label from './Label.vue'
5
+ import FormMessage from './FormMessage.vue'
6
+
7
+ defineProps<{ label?: string; error?: string }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div class="space-y-1.5">
12
+ <Label v-if="label">{{ label }}</Label>
13
+ <slot />
14
+ <FormMessage :message="error" />
15
+ </div>
16
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · FormMessage (결정 75). 필드 검증 메시지. message 가 있을 때만 그린다.
3
+ // useForm 의 form.errors.<field> 를 그대로 넘기면 된다.
4
+ defineProps<{ message?: string }>()
5
+ </script>
6
+
7
+ <template>
8
+ <p v-if="message" class="text-sm font-medium text-destructive">{{ message }}</p>
9
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · Input (결정 75). v-model 지원. type·placeholder 등은 폴스루로 전달된다.
3
+ defineProps<{ modelValue?: string | number }>()
4
+ const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
5
+
6
+ function onInput(e: Event): void {
7
+ emit('update:modelValue', (e.target as HTMLInputElement).value)
8
+ }
9
+ </script>
10
+
11
+ <template>
12
+ <input
13
+ :value="modelValue"
14
+ :class="[
15
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
16
+ 'ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none',
17
+ 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
18
+ 'disabled:cursor-not-allowed disabled:opacity-50',
19
+ ]"
20
+ @input="onInput"
21
+ />
22
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · Label (결정 75). for 속성은 폴스루로 전달한다(<Label for="email">).
3
+ </script>
4
+
5
+ <template>
6
+ <label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
7
+ <slot />
8
+ </label>
9
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ // UI 킷 · Sheet (결정 75). 화면 가장자리에서 미끄러져 나오는 패널(모바일 메뉴·
3
+ // 필터 등). Dialog 와 같은 자작 방식 — Teleport + Transition, 백드롭/Escape 로 닫힘.
4
+ import { watch, onBeforeUnmount, computed } from 'vue'
5
+
6
+ const props = withDefaults(defineProps<{ open: boolean; side?: 'left' | 'right' }>(), {
7
+ side: 'right',
8
+ })
9
+ const emit = defineEmits<{ 'update:open': [value: boolean] }>()
10
+
11
+ function close(): void {
12
+ emit('update:open', false)
13
+ }
14
+ function onKey(e: KeyboardEvent): void {
15
+ if (e.key === 'Escape') close()
16
+ }
17
+ watch(
18
+ () => props.open,
19
+ (isOpen) => {
20
+ if (typeof window === 'undefined') return
21
+ if (isOpen) window.addEventListener('keydown', onKey)
22
+ else window.removeEventListener('keydown', onKey)
23
+ },
24
+ )
25
+ onBeforeUnmount(() => {
26
+ if (typeof window !== 'undefined') window.removeEventListener('keydown', onKey)
27
+ })
28
+
29
+ const sideClass = computed(() => (props.side === 'left' ? 'left-0' : 'right-0'))
30
+ const hiddenClass = computed(() => (props.side === 'left' ? '-translate-x-full' : 'translate-x-full'))
31
+ </script>
32
+
33
+ <template>
34
+ <Teleport to="body">
35
+ <Transition
36
+ enter-active-class="transition-opacity duration-300 ease-out"
37
+ enter-from-class="opacity-0"
38
+ leave-active-class="transition-opacity duration-300 ease-in"
39
+ leave-to-class="opacity-0"
40
+ >
41
+ <div v-if="open" class="fixed inset-0 z-50">
42
+ <div class="absolute inset-0 bg-black/80" @click="close" />
43
+ <Transition
44
+ appear
45
+ enter-active-class="transition-transform duration-300 ease-out"
46
+ :enter-from-class="hiddenClass"
47
+ leave-active-class="transition-transform duration-300 ease-in"
48
+ :leave-to-class="hiddenClass"
49
+ >
50
+ <div
51
+ v-if="open"
52
+ role="dialog"
53
+ aria-modal="true"
54
+ :class="[
55
+ 'absolute top-0 z-50 h-full w-3/4 max-w-sm border bg-background p-6 shadow-lg',
56
+ sideClass,
57
+ ]"
58
+ >
59
+ <slot />
60
+ <button
61
+ type="button"
62
+ aria-label="닫기"
63
+ class="absolute right-4 top-4 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
64
+ @click="close"
65
+ >
66
+
67
+ </button>
68
+ </div>
69
+ </Transition>
70
+ </div>
71
+ </Transition>
72
+ </Teleport>
73
+ </template>
@@ -0,0 +1,26 @@
1
+ // apps/<app>/lib/utils.ts — UI 킷 공용 헬퍼 (결정 75 · shadcn 참조 · 복사-소유).
2
+ //
3
+ // UI 킷은 외부 런타임 의존 없이 자기완결이다(gaon g ui-kit 로 프로젝트에
4
+ // 복사된 뒤엔 이 파일도 여러분 코드다 — 자유롭게 고친다).
5
+
6
+ export type ClassValue = string | false | null | undefined | ClassValue[]
7
+
8
+ /**
9
+ * 조건부 클래스 병합. `cn('base', active && 'on', extra)` 처럼 falsy 를 걸러
10
+ * 공백으로 잇는다. clsx/tailwind-merge 를 끌어오지 않는 자작 최소 구현 —
11
+ * 충돌 클래스 자동 해소(p-2 vs p-4)는 하지 않는다. 오버라이드가 자주 필요하면
12
+ * tailwind-merge 를 설치해 이 함수만 교체한다.
13
+ */
14
+ export function cn(...inputs: ClassValue[]): string {
15
+ const out: string[] = []
16
+ const walk = (v: ClassValue): void => {
17
+ if (v === false || v == null || v === '') return
18
+ if (Array.isArray(v)) {
19
+ for (const x of v) walk(x)
20
+ return
21
+ }
22
+ out.push(v)
23
+ }
24
+ for (const i of inputs) walk(i)
25
+ return out.join(' ').replace(/\s+/g, ' ').trim()
26
+ }
@@ -0,0 +1,28 @@
1
+ export interface UiKitScaffoldOptions {
2
+ /** 대상 앱(apps/<app>). 기본 'web'. */
3
+ readonly app?: string;
4
+ }
5
+ /** 생성할 파일 하나 — 경로는 프로젝트 루트 기준. */
6
+ export interface UiKitScaffoldFile {
7
+ readonly path: string;
8
+ readonly contents: string;
9
+ }
10
+ export interface UiKitScaffoldResult {
11
+ readonly created: string[];
12
+ readonly skipped: string[];
13
+ }
14
+ /** UI 킷 전체가 생성하는 파일 목록. */
15
+ export declare function uiKitScaffoldFiles(opts?: UiKitScaffoldOptions): UiKitScaffoldFile[];
16
+ /** `gaon g auth` 가 필요로 하는 UI 킷 최소 세트 파일 목록. */
17
+ export declare function authUiKitFiles(app?: string): UiKitScaffoldFile[];
18
+ /** 파일 목록을 디스크에 쓴다. 기존 파일은 덮어쓰지 않고 skip(멱등 · 복사-소유). */
19
+ export declare function writeUiKitFiles(cwd: string, files: readonly UiKitScaffoldFile[]): UiKitScaffoldResult;
20
+ /** `gaon g ui-kit` — UI 킷 전체를 쓴다. */
21
+ export declare function writeUiKitScaffold(cwd: string, opts?: UiKitScaffoldOptions): UiKitScaffoldResult;
22
+ export interface GenerateUiKitOptions {
23
+ readonly cwd?: string;
24
+ readonly app?: string;
25
+ readonly json?: boolean;
26
+ }
27
+ /** `gaon g ui-kit` 진입점. exitCode 반환(항상 0 — 멱등 스킵은 실패가 아니다). */
28
+ export declare function runGenerateUiKitCommand(opts?: GenerateUiKitOptions): number;
package/dist/uikit.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon g ui-kit` — shadcn 식 UI 킷 제너레이터 (결정 75)
3
+ *
4
+ * shadcn 을 참조해 Vue 3 로 새로 쓴 UI 컴포넌트를 프로젝트에 **복사**한다
5
+ * (복사-소유 — 생성된 뒤엔 사용자 코드다). npm 의존이 아니라 파일로 심으므로
6
+ * 자유롭게 고칠 수 있고, 외부 런타임 의존이 전혀 없다(cn 은 자작 · reka-ui·
7
+ * clsx·cva 미도입 · 결정 75). Tailwind(결정 74)를 전제로 한다.
8
+ *
9
+ * 배치:
10
+ * apps/<app>/lib/utils.ts — cn() 헬퍼
11
+ * apps/<app>/components/ui/*.vue — Button·Input·Card·Form·Dialog … (앱 전용 순수 UI)
12
+ *
13
+ * 앱 전용에 두는 이유: 컴포넌트에서 lib/utils 로 가는 import 를 얕게 유지해
14
+ * (`../../lib/utils.js`) AI·사람 모두 실수 없이 참조하게 하고, `gaon g auth`
15
+ * 처럼 앱 단위(--app)로 생성한다. 여러 앱이 각자 소유한다(복사-소유 관례).
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { appWiringFiles, readProjectName } from './scaffold/app-wiring.js';
21
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'ui-kit');
22
+ /** UI 킷 컴포넌트 전체(파일명 stem = 컴포넌트명). */
23
+ const COMPONENTS = [
24
+ 'Button',
25
+ 'Input',
26
+ 'Label',
27
+ 'Badge',
28
+ 'Card',
29
+ 'CardHeader',
30
+ 'CardTitle',
31
+ 'CardDescription',
32
+ 'CardContent',
33
+ 'CardFooter',
34
+ 'Alert',
35
+ 'AlertTitle',
36
+ 'AlertDescription',
37
+ 'Form',
38
+ 'FormField',
39
+ 'FormMessage',
40
+ 'Dialog',
41
+ 'Sheet',
42
+ ];
43
+ /** `gaon g auth` 가 스캐폴드하는 로그인·가입·대시보드 페이지가 쓰는 최소 세트.
44
+ * auth 생성 시 이 컴포넌트가 없으면 페이지가 컴파일되지 않으므로 함께 보장한다. */
45
+ const AUTH_SUBSET = [
46
+ 'Button',
47
+ 'Input',
48
+ 'Label',
49
+ 'Card',
50
+ 'CardHeader',
51
+ 'CardTitle',
52
+ 'CardDescription',
53
+ 'CardContent',
54
+ 'CardFooter',
55
+ 'Alert',
56
+ 'AlertDescription',
57
+ 'Form',
58
+ 'FormField',
59
+ 'FormMessage',
60
+ ];
61
+ /** 템플릿을 읽는다. UI 킷 템플릿엔 치환 토큰이 없다(경로만 앱별). */
62
+ function read(name) {
63
+ return readFileSync(join(TEMPLATE_DIR, name), 'utf8');
64
+ }
65
+ /** 컴포넌트명 목록 → 생성 파일 목록(utils 포함). */
66
+ function filesFor(components, app) {
67
+ const files = [
68
+ { path: `apps/${app}/lib/utils.ts`, contents: read('utils.ts.tpl') },
69
+ ];
70
+ for (const name of components) {
71
+ files.push({
72
+ path: `apps/${app}/components/ui/${name}.vue`,
73
+ contents: read(`${name}.vue.tpl`),
74
+ });
75
+ }
76
+ return files;
77
+ }
78
+ /** UI 킷 전체가 생성하는 파일 목록. */
79
+ export function uiKitScaffoldFiles(opts = {}) {
80
+ return filesFor(COMPONENTS, opts.app ?? 'web');
81
+ }
82
+ /** `gaon g auth` 가 필요로 하는 UI 킷 최소 세트 파일 목록. */
83
+ export function authUiKitFiles(app = 'web') {
84
+ return filesFor(AUTH_SUBSET, app);
85
+ }
86
+ /** 파일 목록을 디스크에 쓴다. 기존 파일은 덮어쓰지 않고 skip(멱등 · 복사-소유). */
87
+ export function writeUiKitFiles(cwd, files) {
88
+ const root = resolve(cwd);
89
+ const created = [];
90
+ const skipped = [];
91
+ for (const file of files) {
92
+ const abs = join(root, file.path);
93
+ if (existsSync(abs)) {
94
+ skipped.push(file.path);
95
+ continue;
96
+ }
97
+ mkdirSync(dirname(abs), { recursive: true });
98
+ writeFileSync(abs, file.contents, 'utf8');
99
+ created.push(file.path);
100
+ }
101
+ return { created: created.sort(), skipped: skipped.sort() };
102
+ }
103
+ /** `gaon g ui-kit` — UI 킷 전체를 쓴다. */
104
+ export function writeUiKitScaffold(cwd, opts = {}) {
105
+ return writeUiKitFiles(cwd, uiKitScaffoldFiles(opts));
106
+ }
107
+ /** `gaon g ui-kit` 진입점. exitCode 반환(항상 0 — 멱등 스킵은 실패가 아니다). */
108
+ export function runGenerateUiKitCommand(opts = {}) {
109
+ const cwd = opts.cwd ?? process.cwd();
110
+ const app = opts.app ?? 'web';
111
+ // 대상 앱의 Tailwind 배선(style.css·main.ts·index.html)을 먼저 멱등 보정한다
112
+ // — 없으면 UI 킷이 유틸 클래스를 못 받는다(결정 76). web 은 gaon new 가 이미
113
+ // 심었으므로 스킵된다. 그다음 UI 킷 컴포넌트를 심는다.
114
+ const wiring = writeUiKitFiles(cwd, appWiringFiles(app, readProjectName(cwd)));
115
+ const kit = writeUiKitScaffold(cwd, { app });
116
+ const result = {
117
+ created: [...wiring.created, ...kit.created].sort(),
118
+ skipped: [...wiring.skipped, ...kit.skipped].sort(),
119
+ };
120
+ if (opts.json) {
121
+ process.stdout.write(JSON.stringify({ command: 'g ui-kit', app, ...result }, null, 2) + '\n');
122
+ return 0;
123
+ }
124
+ const lines = [''];
125
+ lines.push(` gaon g ui-kit — UI 킷 (${app} 앱 · shadcn 참조 · 복사-소유)`);
126
+ lines.push('');
127
+ for (const f of result.created)
128
+ lines.push(` + ${f}`);
129
+ for (const f of result.skipped)
130
+ lines.push(` · ${f} (이미 있음 — 건너뜀)`);
131
+ lines.push('');
132
+ lines.push(' Tailwind 배선(tailwind.config.ts·postcss.config.js·apps/' + app + '/style.css)이 함께 보정됩니다.');
133
+ lines.push(' 컴포넌트는 이제 여러분 코드입니다 — apps/' + app + '/components/ui/ 에서 자유롭게 고치세요.');
134
+ lines.push(' 예) import Button from \'../../components/ui/Button.vue\'');
135
+ lines.push('');
136
+ process.stdout.write(lines.join('\n') + '\n');
137
+ return 0;
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.15.0",
3
+ "version": "0.18.0",
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/async": "0.4.0",
31
- "@gaonjs/web": "0.6.1",
32
- "@gaonjs/config": "0.4.1",
30
+ "@gaonjs/async": "0.5.0",
33
31
  "@gaonjs/core": "0.2.0",
34
- "@gaonjs/data": "0.8.4",
35
- "@gaonjs/mail": "0.1.1"
32
+ "@gaonjs/config": "0.4.1",
33
+ "@gaonjs/web": "0.6.1",
34
+ "@gaonjs/mail": "0.1.1",
35
+ "@gaonjs/data": "0.8.4"
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})\""