@gaonjs/cli 0.15.0 → 0.21.2
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/commands/g.js +8 -0
- package/dist/commands/new.js +5 -0
- package/dist/dev/health.d.ts +4 -1
- package/dist/dev/health.js +5 -3
- package/dist/doctor/method-override.d.ts +5 -0
- package/dist/doctor/method-override.js +75 -0
- package/dist/doctor/route-registration.d.ts +5 -0
- package/dist/doctor/route-registration.js +77 -0
- package/dist/doctor/static-collision.d.ts +5 -0
- package/dist/doctor/static-collision.js +84 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +3 -3
- package/dist/doctor/ui-kit-wiring.d.ts +5 -0
- package/dist/doctor/ui-kit-wiring.js +93 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +19 -2
- package/dist/generate.js +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -5
- package/dist/scaffold/app-wiring.d.ts +12 -0
- package/dist/scaffold/app-wiring.js +68 -0
- package/dist/serve.d.ts +10 -0
- package/dist/serve.js +90 -2
- package/dist/templates/auth/Dashboard.vue.tpl +21 -5
- package/dist/templates/auth/Login.vue.tpl +37 -8
- package/dist/templates/auth/Signup.vue.tpl +40 -9
- package/dist/templates/project/.dockerignore.tpl +12 -0
- package/dist/templates/project/AGENTS.md.tpl +10 -5
- package/dist/templates/project/CLAUDE.md.tpl +4 -3
- package/dist/templates/project/Dockerfile.tpl +30 -0
- package/dist/templates/project/agents/async.md.tpl +19 -7
- package/dist/templates/project/agents/data.md.tpl +37 -15
- package/dist/templates/project/agents/frontend.md.tpl +81 -4
- package/dist/templates/project/agents/realtime.md.tpl +24 -17
- package/dist/templates/project/agents/security.md.tpl +11 -2
- package/dist/templates/project/agents/web.md.tpl +5 -0
- package/dist/templates/project/apps/web/composables/useGaonHealth.ts.tpl +2 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +19 -94
- package/dist/templates/project/apps/web/main.ts.tpl +4 -0
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +68 -251
- package/dist/templates/project/apps/web/static/robots.txt.tpl +4 -0
- package/dist/templates/project/apps/web/style.css.tpl +66 -0
- package/dist/templates/project/compose.prod.yaml.tpl +98 -0
- package/dist/templates/project/package.json.tpl +4 -0
- package/dist/templates/project/postcss.config.js.tpl +15 -0
- package/dist/templates/project/tailwind.config.ts.tpl +56 -0
- package/dist/templates/ui-kit/Alert.vue.tpl +23 -0
- package/dist/templates/ui-kit/AlertDescription.vue.tpl +9 -0
- package/dist/templates/ui-kit/AlertTitle.vue.tpl +9 -0
- package/dist/templates/ui-kit/Badge.vue.tpl +25 -0
- package/dist/templates/ui-kit/Button.vue.tpl +39 -0
- package/dist/templates/ui-kit/Card.vue.tpl +10 -0
- package/dist/templates/ui-kit/CardContent.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardDescription.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardFooter.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardHeader.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardTitle.vue.tpl +9 -0
- package/dist/templates/ui-kit/Dialog.vue.tpl +68 -0
- package/dist/templates/ui-kit/Form.vue.tpl +13 -0
- package/dist/templates/ui-kit/FormField.vue.tpl +16 -0
- package/dist/templates/ui-kit/FormMessage.vue.tpl +9 -0
- package/dist/templates/ui-kit/Input.vue.tpl +22 -0
- package/dist/templates/ui-kit/Label.vue.tpl +9 -0
- package/dist/templates/ui-kit/Sheet.vue.tpl +73 -0
- package/dist/templates/ui-kit/utils.ts.tpl +26 -0
- package/dist/uikit.d.ts +28 -0
- package/dist/uikit.js +138 -0
- package/dist/work.js +2 -0
- package/package.json +6 -6
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// UI 킷 · Badge (결정 75). 작은 상태 라벨. variant 로 색을 고른다.
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { cn } from '../../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
type Variant = 'default' | 'secondary' | 'destructive' | 'outline'
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<{ variant?: Variant }>(), { variant: 'default' })
|
|
9
|
+
|
|
10
|
+
const VARIANTS: Record<Variant, string> = {
|
|
11
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
12
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
13
|
+
destructive: 'border-transparent bg-destructive text-destructive-foreground',
|
|
14
|
+
outline: 'text-foreground',
|
|
15
|
+
}
|
|
16
|
+
const BASE =
|
|
17
|
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold ' +
|
|
18
|
+
'transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2'
|
|
19
|
+
|
|
20
|
+
const classes = computed(() => cn(BASE, VARIANTS[props.variant]))
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<span :class="classes"><slot /></span>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// UI 킷 · Button (결정 75). variant/size 로 모양을 고르고, 나머지 속성·클래스는
|
|
3
|
+
// 자연스럽게 <button> 으로 흘러간다(Vue 폴스루). 부모가 class 를 주면 뒤에 병합된다.
|
|
4
|
+
import { computed } from 'vue'
|
|
5
|
+
import { cn } from '../../lib/utils.js'
|
|
6
|
+
|
|
7
|
+
type Variant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'
|
|
8
|
+
type Size = 'default' | 'sm' | 'lg' | 'icon'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(
|
|
11
|
+
defineProps<{ variant?: Variant; size?: Size; type?: 'button' | 'submit' | 'reset' }>(),
|
|
12
|
+
{ variant: 'default', size: 'default', type: 'button' },
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const VARIANTS: Record<Variant, string> = {
|
|
16
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
17
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
18
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
19
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
20
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
}
|
|
23
|
+
const SIZES: Record<Size, string> = {
|
|
24
|
+
default: 'h-10 px-4 py-2',
|
|
25
|
+
sm: 'h-9 rounded-md px-3',
|
|
26
|
+
lg: 'h-11 rounded-md px-8',
|
|
27
|
+
icon: 'h-10 w-10',
|
|
28
|
+
}
|
|
29
|
+
const BASE =
|
|
30
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ' +
|
|
31
|
+
'ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
|
|
32
|
+
'focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
|
|
33
|
+
|
|
34
|
+
const classes = computed(() => cn(BASE, VARIANTS[props.variant], SIZES[props.size]))
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<button :type="type" :class="classes"><slot /></button>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// UI 킷 · Card (결정 75). 컨테이너. 헤더/본문/푸터는 CardHeader·CardContent·
|
|
3
|
+
// CardFooter 로 조합한다(shadcn 관례). 부모 class 는 폴스루로 병합된다.
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
8
|
+
<slot />
|
|
9
|
+
</div>
|
|
10
|
+
</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,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
|
+
}
|
package/dist/uikit.d.ts
ADDED
|
@@ -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/dist/work.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.2",
|
|
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.
|
|
31
|
-
"@gaonjs/
|
|
32
|
-
"@gaonjs/
|
|
30
|
+
"@gaonjs/async": "0.6.0",
|
|
31
|
+
"@gaonjs/config": "0.5.1",
|
|
32
|
+
"@gaonjs/data": "0.9.0",
|
|
33
33
|
"@gaonjs/core": "0.2.0",
|
|
34
|
-
"@gaonjs/
|
|
35
|
-
"@gaonjs/
|
|
34
|
+
"@gaonjs/mail": "0.1.2",
|
|
35
|
+
"@gaonjs/web": "0.7.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})\""
|