@mandujs/cli 0.12.2 → 0.13.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 (51) hide show
  1. package/README.ko.md +234 -234
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/contract.ts +173 -173
  5. package/src/commands/dev.ts +8 -68
  6. package/src/commands/doctor.ts +27 -27
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +367 -357
  12. package/src/commands/routes.ts +228 -228
  13. package/src/commands/start.ts +184 -0
  14. package/src/errors/codes.ts +35 -35
  15. package/src/errors/index.ts +2 -2
  16. package/src/errors/messages.ts +143 -143
  17. package/src/hooks/index.ts +17 -17
  18. package/src/hooks/preaction.ts +256 -256
  19. package/src/main.ts +37 -34
  20. package/src/terminal/banner.ts +166 -166
  21. package/src/terminal/help.ts +306 -306
  22. package/src/terminal/index.ts +71 -71
  23. package/src/terminal/output.ts +295 -295
  24. package/src/terminal/palette.ts +30 -30
  25. package/src/terminal/progress.ts +327 -327
  26. package/src/terminal/stream-writer.ts +214 -214
  27. package/src/terminal/table.ts +354 -354
  28. package/src/terminal/theme.ts +142 -142
  29. package/src/util/bun.ts +6 -6
  30. package/src/util/fs.ts +23 -23
  31. package/src/util/handlers.ts +96 -0
  32. package/src/util/manifest.ts +52 -52
  33. package/src/util/output.ts +22 -22
  34. package/src/util/port.ts +71 -71
  35. package/templates/default/AGENTS.md +96 -96
  36. package/templates/default/app/api/health/route.ts +13 -13
  37. package/templates/default/app/globals.css +49 -49
  38. package/templates/default/app/layout.tsx +27 -27
  39. package/templates/default/app/page.tsx +38 -38
  40. package/templates/default/package.json +1 -0
  41. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  42. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  43. package/templates/default/src/client/shared/ui/card.tsx +78 -78
  44. package/templates/default/src/client/shared/ui/index.ts +21 -21
  45. package/templates/default/src/client/shared/ui/input.tsx +24 -24
  46. package/templates/default/tests/example.test.ts +58 -58
  47. package/templates/default/tests/helpers.ts +52 -52
  48. package/templates/default/tests/setup.ts +9 -9
  49. package/templates/default/tsconfig.json +12 -14
  50. package/templates/default/apps/server/main.ts +0 -67
  51. package/templates/default/apps/web/entry.tsx +0 -35
package/src/util/port.ts CHANGED
@@ -1,71 +1,71 @@
1
- import { createServer } from "net";
2
-
3
- const DEFAULT_MAX_ATTEMPTS = 10;
4
-
5
- function isPortUsable(error: unknown): boolean {
6
- if (!error || typeof error !== "object") return false;
7
- const code = (error as { code?: string }).code;
8
- return code === "EADDRINUSE" || code === "EACCES";
9
- }
10
-
11
- async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
12
- return new Promise((resolve) => {
13
- const server = createServer();
14
-
15
- server.once("error", (error) => {
16
- if (isPortUsable(error)) {
17
- resolve(false);
18
- } else {
19
- resolve(false);
20
- }
21
- });
22
-
23
- server.once("listening", () => {
24
- server.close(() => resolve(true));
25
- });
26
-
27
- try {
28
- server.listen(port, hostname);
29
- server.unref();
30
- } catch {
31
- resolve(false);
32
- }
33
- });
34
- }
35
-
36
- export async function resolveAvailablePort(
37
- startPort: number,
38
- options: {
39
- hostname?: string;
40
- offsets?: number[];
41
- maxAttempts?: number;
42
- } = {}
43
- ): Promise<{ port: number; attempts: number }> {
44
- const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
45
- const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
46
-
47
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
48
- const candidate = startPort + attempt;
49
- if (candidate < 1 || candidate > 65535) {
50
- continue;
51
- }
52
-
53
- const targets = offsets
54
- .map((offset) => candidate + offset)
55
- .filter((port) => port >= 1 && port <= 65535);
56
-
57
- if (targets.length !== offsets.length) {
58
- continue;
59
- }
60
-
61
- const results = await Promise.all(
62
- targets.map((port) => isPortAvailable(port, options.hostname))
63
- );
64
-
65
- if (results.every(Boolean)) {
66
- return { port: candidate, attempts: attempt };
67
- }
68
- }
69
-
70
- throw new Error(`No available port found starting at ${startPort}`);
71
- }
1
+ import { createServer } from "net";
2
+
3
+ const DEFAULT_MAX_ATTEMPTS = 10;
4
+
5
+ function isPortUsable(error: unknown): boolean {
6
+ if (!error || typeof error !== "object") return false;
7
+ const code = (error as { code?: string }).code;
8
+ return code === "EADDRINUSE" || code === "EACCES";
9
+ }
10
+
11
+ async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
12
+ return new Promise((resolve) => {
13
+ const server = createServer();
14
+
15
+ server.once("error", (error) => {
16
+ if (isPortUsable(error)) {
17
+ resolve(false);
18
+ } else {
19
+ resolve(false);
20
+ }
21
+ });
22
+
23
+ server.once("listening", () => {
24
+ server.close(() => resolve(true));
25
+ });
26
+
27
+ try {
28
+ server.listen(port, hostname);
29
+ server.unref();
30
+ } catch {
31
+ resolve(false);
32
+ }
33
+ });
34
+ }
35
+
36
+ export async function resolveAvailablePort(
37
+ startPort: number,
38
+ options: {
39
+ hostname?: string;
40
+ offsets?: number[];
41
+ maxAttempts?: number;
42
+ } = {}
43
+ ): Promise<{ port: number; attempts: number }> {
44
+ const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
45
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
46
+
47
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
48
+ const candidate = startPort + attempt;
49
+ if (candidate < 1 || candidate > 65535) {
50
+ continue;
51
+ }
52
+
53
+ const targets = offsets
54
+ .map((offset) => candidate + offset)
55
+ .filter((port) => port >= 1 && port <= 65535);
56
+
57
+ if (targets.length !== offsets.length) {
58
+ continue;
59
+ }
60
+
61
+ const results = await Promise.all(
62
+ targets.map((port) => isPortAvailable(port, options.hostname))
63
+ );
64
+
65
+ if (results.every(Boolean)) {
66
+ return { port: candidate, attempts: attempt };
67
+ }
68
+ }
69
+
70
+ throw new Error(`No available port found starting at ${startPort}`);
71
+ }
@@ -1,96 +1,96 @@
1
- # AI Agent Instructions for Mandu Project
2
-
3
- 이 프로젝트는 **Mandu Framework**로 구축되었습니다. AI 에이전트가 이 프로젝트를 다룰 때 아래 지침을 따라주세요.
4
-
5
- ## 패키지 매니저: Bun (필수)
6
-
7
- **⚠️ 중요: 이 프로젝트는 Bun만 사용합니다. npm/yarn/pnpm을 사용하지 마세요.**
8
-
9
- ```bash
10
- # ✅ 올바른 명령어
11
- bun install # 의존성 설치
12
- bun add <package> # 패키지 추가
13
- bun remove <package> # 패키지 제거
14
- bun run dev # 개발 서버 시작
15
- bun run build # 프로덕션 빌드
16
- bun test # 테스트 실행
17
-
18
- # ❌ 사용 금지
19
- npm install / yarn install / pnpm install
20
- ```
21
-
22
- ## 프로젝트 구조
23
-
24
- ```
25
- ├── app/ # FS 기반 라우팅 (페이지, API)
26
- │ ├── page.tsx # / 라우트
27
- │ ├── layout.tsx # 루트 레이아웃
28
- │ ├── globals.css # Tailwind CSS (v4)
29
- │ └── api/ # API 라우트
30
- ├── src/
31
- │ ├── client/ # 클라이언트 코드 (FSD 구조)
32
- │ │ ├── shared/ # 공용 UI, 유틸리티
33
- │ │ ├── entities/ # 엔티티 컴포넌트
34
- │ │ ├── features/ # 기능 컴포넌트
35
- │ │ └── widgets/ # 위젯/Island 컴포넌트
36
- │ ├── server/ # 서버 코드 (Clean Architecture)
37
- │ │ ├── domain/ # 도메인 모델
38
- │ │ ├── application/ # 비즈니스 로직
39
- │ │ └── infra/ # 인프라/DB
40
- │ └── shared/ # 클라이언트-서버 공유 코드
41
- │ ├── contracts/ # API 계약 타입
42
- │ └── types/ # 공용 타입
43
- └── mandu.config.ts # Mandu 설정 (선택)
44
- ```
45
-
46
- ## 주요 규칙
47
-
48
- ### 1. Island 컴포넌트
49
- 클라이언트 상호작용이 필요한 컴포넌트는 `*.island.tsx`로 명명:
50
- ```tsx
51
- // src/client/widgets/counter/Counter.island.tsx
52
- "use client";
53
- export function CounterIsland() { ... }
54
- ```
55
-
56
- ### 2. API 라우트
57
- `app/api/` 폴더에 `route.ts` 파일로 정의:
58
- ```typescript
59
- // app/api/users/route.ts
60
- import { Mandu } from "@mandujs/core";
61
- export default Mandu.filling()
62
- .get((ctx) => ctx.ok({ users: [] }))
63
- .post(async (ctx) => { ... });
64
- ```
65
-
66
- ### 3. Tailwind CSS v4
67
- CSS-first 설정 사용 (`tailwind.config.ts` 없음):
68
- ```css
69
- /* app/globals.css */
70
- @import "tailwindcss";
71
- @theme {
72
- --color-primary: hsl(222.2 47.4% 11.2%);
73
- }
74
- ```
75
-
76
- ### 4. Import Alias
77
- `@/` = `src/` 경로:
78
- ```typescript
79
- import { Button } from "@/client/shared/ui/button";
80
- ```
81
-
82
- ## 실행 방법
83
-
84
- ```bash
85
- bun install # 최초 설치
86
- bun run dev # 개발 서버 (http://localhost:4000)
87
- bun run build # 프로덕션 빌드
88
- bun run guard # 아키텍처 검증
89
- ```
90
-
91
- ## 기술 스택
92
-
93
- - **Runtime**: Bun 1.x
94
- - **Framework**: Mandu (React 19 + Bun native)
95
- - **Styling**: Tailwind CSS v4
96
- - **Language**: TypeScript 5.x
1
+ # AI Agent Instructions for Mandu Project
2
+
3
+ 이 프로젝트는 **Mandu Framework**로 구축되었습니다. AI 에이전트가 이 프로젝트를 다룰 때 아래 지침을 따라주세요.
4
+
5
+ ## 패키지 매니저: Bun (필수)
6
+
7
+ **⚠️ 중요: 이 프로젝트는 Bun만 사용합니다. npm/yarn/pnpm을 사용하지 마세요.**
8
+
9
+ ```bash
10
+ # ✅ 올바른 명령어
11
+ bun install # 의존성 설치
12
+ bun add <package> # 패키지 추가
13
+ bun remove <package> # 패키지 제거
14
+ bun run dev # 개발 서버 시작
15
+ bun run build # 프로덕션 빌드
16
+ bun test # 테스트 실행
17
+
18
+ # ❌ 사용 금지
19
+ npm install / yarn install / pnpm install
20
+ ```
21
+
22
+ ## 프로젝트 구조
23
+
24
+ ```
25
+ ├── app/ # FS 기반 라우팅 (페이지, API)
26
+ │ ├── page.tsx # / 라우트
27
+ │ ├── layout.tsx # 루트 레이아웃
28
+ │ ├── globals.css # Tailwind CSS (v4)
29
+ │ └── api/ # API 라우트
30
+ ├── src/
31
+ │ ├── client/ # 클라이언트 코드 (FSD 구조)
32
+ │ │ ├── shared/ # 공용 UI, 유틸리티
33
+ │ │ ├── entities/ # 엔티티 컴포넌트
34
+ │ │ ├── features/ # 기능 컴포넌트
35
+ │ │ └── widgets/ # 위젯/Island 컴포넌트
36
+ │ ├── server/ # 서버 코드 (Clean Architecture)
37
+ │ │ ├── domain/ # 도메인 모델
38
+ │ │ ├── application/ # 비즈니스 로직
39
+ │ │ └── infra/ # 인프라/DB
40
+ │ └── shared/ # 클라이언트-서버 공유 코드
41
+ │ ├── contracts/ # API 계약 타입
42
+ │ └── types/ # 공용 타입
43
+ └── mandu.config.ts # Mandu 설정 (선택)
44
+ ```
45
+
46
+ ## 주요 규칙
47
+
48
+ ### 1. Island 컴포넌트
49
+ 클라이언트 상호작용이 필요한 컴포넌트는 `*.island.tsx`로 명명:
50
+ ```tsx
51
+ // src/client/widgets/counter/Counter.island.tsx
52
+ "use client";
53
+ export function CounterIsland() { ... }
54
+ ```
55
+
56
+ ### 2. API 라우트
57
+ `app/api/` 폴더에 `route.ts` 파일로 정의:
58
+ ```typescript
59
+ // app/api/users/route.ts
60
+ import { Mandu } from "@mandujs/core";
61
+ export default Mandu.filling()
62
+ .get((ctx) => ctx.ok({ users: [] }))
63
+ .post(async (ctx) => { ... });
64
+ ```
65
+
66
+ ### 3. Tailwind CSS v4
67
+ CSS-first 설정 사용 (`tailwind.config.ts` 없음):
68
+ ```css
69
+ /* app/globals.css */
70
+ @import "tailwindcss";
71
+ @theme {
72
+ --color-primary: hsl(222.2 47.4% 11.2%);
73
+ }
74
+ ```
75
+
76
+ ### 4. Import Alias
77
+ `@/` = `src/` 경로:
78
+ ```typescript
79
+ import { Button } from "@/client/shared/ui/button";
80
+ ```
81
+
82
+ ## 실행 방법
83
+
84
+ ```bash
85
+ bun install # 최초 설치
86
+ bun run dev # 개발 서버 (http://localhost:4000)
87
+ bun run build # 프로덕션 빌드
88
+ bun run guard # 아키텍처 검증
89
+ ```
90
+
91
+ ## 기술 스택
92
+
93
+ - **Runtime**: Bun 1.x
94
+ - **Framework**: Mandu (React 19 + Bun native)
95
+ - **Styling**: Tailwind CSS v4
96
+ - **Language**: TypeScript 5.x
@@ -1,13 +1,13 @@
1
- /**
2
- * Health Check API
3
- *
4
- * GET /api/health
5
- */
6
-
7
- export function GET() {
8
- return Response.json({
9
- status: "ok",
10
- timestamp: new Date().toISOString(),
11
- framework: "Mandu",
12
- });
13
- }
1
+ /**
2
+ * Health Check API
3
+ *
4
+ * GET /api/health
5
+ */
6
+
7
+ export function GET() {
8
+ return Response.json({
9
+ status: "ok",
10
+ timestamp: new Date().toISOString(),
11
+ framework: "Mandu",
12
+ });
13
+ }
@@ -1,49 +1,49 @@
1
- @import "tailwindcss";
2
-
3
- /*
4
- * Tailwind CSS v4 - CSS-first Configuration
5
- * https://tailwindcss.com/docs/v4
6
- */
7
-
8
- @theme {
9
- /* Colors - shadcn/ui compatible */
10
- --color-background: hsl(0 0% 100%);
11
- --color-foreground: hsl(222.2 84% 4.9%);
12
- --color-card: hsl(0 0% 100%);
13
- --color-card-foreground: hsl(222.2 84% 4.9%);
14
- --color-popover: hsl(0 0% 100%);
15
- --color-popover-foreground: hsl(222.2 84% 4.9%);
16
- --color-primary: hsl(222.2 47.4% 11.2%);
17
- --color-primary-foreground: hsl(210 40% 98%);
18
- --color-secondary: hsl(210 40% 96.1%);
19
- --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
20
- --color-muted: hsl(210 40% 96.1%);
21
- --color-muted-foreground: hsl(215.4 16.3% 46.9%);
22
- --color-accent: hsl(210 40% 96.1%);
23
- --color-accent-foreground: hsl(222.2 47.4% 11.2%);
24
- --color-destructive: hsl(0 84.2% 60.2%);
25
- --color-destructive-foreground: hsl(210 40% 98%);
26
- --color-border: hsl(214.3 31.8% 91.4%);
27
- --color-input: hsl(214.3 31.8% 91.4%);
28
- --color-ring: hsl(222.2 84% 4.9%);
29
-
30
- /* Radius */
31
- --radius-sm: 0.25rem;
32
- --radius-md: 0.5rem;
33
- --radius-lg: 0.75rem;
34
- --radius-xl: 1rem;
35
-
36
- /* Fonts */
37
- --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
38
- }
39
-
40
- /* Base styles */
41
- * {
42
- border-color: var(--color-border);
43
- }
44
-
45
- body {
46
- background-color: var(--color-background);
47
- color: var(--color-foreground);
48
- font-family: var(--font-sans);
49
- }
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * Tailwind CSS v4 - CSS-first Configuration
5
+ * https://tailwindcss.com/docs/v4
6
+ */
7
+
8
+ @theme {
9
+ /* Colors - shadcn/ui compatible */
10
+ --color-background: hsl(0 0% 100%);
11
+ --color-foreground: hsl(222.2 84% 4.9%);
12
+ --color-card: hsl(0 0% 100%);
13
+ --color-card-foreground: hsl(222.2 84% 4.9%);
14
+ --color-popover: hsl(0 0% 100%);
15
+ --color-popover-foreground: hsl(222.2 84% 4.9%);
16
+ --color-primary: hsl(222.2 47.4% 11.2%);
17
+ --color-primary-foreground: hsl(210 40% 98%);
18
+ --color-secondary: hsl(210 40% 96.1%);
19
+ --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
20
+ --color-muted: hsl(210 40% 96.1%);
21
+ --color-muted-foreground: hsl(215.4 16.3% 46.9%);
22
+ --color-accent: hsl(210 40% 96.1%);
23
+ --color-accent-foreground: hsl(222.2 47.4% 11.2%);
24
+ --color-destructive: hsl(0 84.2% 60.2%);
25
+ --color-destructive-foreground: hsl(210 40% 98%);
26
+ --color-border: hsl(214.3 31.8% 91.4%);
27
+ --color-input: hsl(214.3 31.8% 91.4%);
28
+ --color-ring: hsl(222.2 84% 4.9%);
29
+
30
+ /* Radius */
31
+ --radius-sm: 0.25rem;
32
+ --radius-md: 0.5rem;
33
+ --radius-lg: 0.75rem;
34
+ --radius-xl: 1rem;
35
+
36
+ /* Fonts */
37
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
38
+ }
39
+
40
+ /* Base styles */
41
+ * {
42
+ border-color: var(--color-border);
43
+ }
44
+
45
+ body {
46
+ background-color: var(--color-background);
47
+ color: var(--color-foreground);
48
+ font-family: var(--font-sans);
49
+ }
@@ -1,27 +1,27 @@
1
- /**
2
- * Root Layout
3
- *
4
- * 모든 페이지의 공통 레이아웃
5
- * globals.css를 여기서 임포트
6
- */
7
-
8
- import "./globals.css";
9
-
10
- interface RootLayoutProps {
11
- children: React.ReactNode;
12
- }
13
-
14
- export default function RootLayout({ children }: RootLayoutProps) {
15
- return (
16
- <html lang="ko">
17
- <head>
18
- <meta charSet="UTF-8" />
19
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
- <title>{{PROJECT_NAME}}</title>
21
- </head>
22
- <body className="min-h-screen bg-background font-sans antialiased">
23
- {children}
24
- </body>
25
- </html>
26
- );
27
- }
1
+ /**
2
+ * Root Layout
3
+ *
4
+ * 모든 페이지의 공통 레이아웃
5
+ * globals.css를 여기서 임포트
6
+ */
7
+
8
+ import "./globals.css";
9
+
10
+ interface RootLayoutProps {
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ export default function RootLayout({ children }: RootLayoutProps) {
15
+ return (
16
+ <html lang="ko">
17
+ <head>
18
+ <meta charSet="UTF-8" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <title>{{PROJECT_NAME}}</title>
21
+ </head>
22
+ <body className="min-h-screen bg-background font-sans antialiased">
23
+ {children}
24
+ </body>
25
+ </html>
26
+ );
27
+ }
@@ -1,38 +1,38 @@
1
- /**
2
- * Home Page
3
- *
4
- * Edit this file and see changes at http://localhost:3000
5
- */
6
-
7
- import { Button } from "@/client/shared/ui/button";
8
- import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/client/shared/ui/card";
9
-
10
- export default function HomePage() {
11
- return (
12
- <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-indigo-500 to-purple-600 p-8">
13
- <Card className="w-full max-w-md">
14
- <CardHeader className="text-center">
15
- <CardTitle className="text-4xl">🥟 Mandu</CardTitle>
16
- <CardDescription>
17
- Welcome to your new Mandu project!
18
- </CardDescription>
19
- </CardHeader>
20
- <CardContent className="flex flex-col gap-4">
21
- <p className="text-center text-muted-foreground">
22
- Edit <code className="rounded bg-muted px-1.5 py-0.5 text-sm">app/page.tsx</code> to get started.
23
- </p>
24
- <div className="flex justify-center gap-2">
25
- <Button asChild variant="default">
26
- <a href="/api/health">API Health →</a>
27
- </Button>
28
- <Button asChild variant="outline">
29
- <a href="https://mandujs.dev/docs" target="_blank" rel="noopener noreferrer">
30
- Documentation
31
- </a>
32
- </Button>
33
- </div>
34
- </CardContent>
35
- </Card>
36
- </main>
37
- );
38
- }
1
+ /**
2
+ * Home Page
3
+ *
4
+ * Edit this file and see changes at http://localhost:3000
5
+ */
6
+
7
+ import { Button } from "@/client/shared/ui/button";
8
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/client/shared/ui/card";
9
+
10
+ export default function HomePage() {
11
+ return (
12
+ <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-indigo-500 to-purple-600 p-8">
13
+ <Card className="w-full max-w-md">
14
+ <CardHeader className="text-center">
15
+ <CardTitle className="text-4xl">🥟 Mandu</CardTitle>
16
+ <CardDescription>
17
+ Welcome to your new Mandu project!
18
+ </CardDescription>
19
+ </CardHeader>
20
+ <CardContent className="flex flex-col gap-4">
21
+ <p className="text-center text-muted-foreground">
22
+ Edit <code className="rounded bg-muted px-1.5 py-0.5 text-sm">app/page.tsx</code> to get started.
23
+ </p>
24
+ <div className="flex justify-center gap-2">
25
+ <Button asChild variant="default">
26
+ <a href="/api/health">API Health →</a>
27
+ </Button>
28
+ <Button asChild variant="outline">
29
+ <a href="https://mandujs.dev/docs" target="_blank" rel="noopener noreferrer">
30
+ Documentation
31
+ </a>
32
+ </Button>
33
+ </div>
34
+ </CardContent>
35
+ </Card>
36
+ </main>
37
+ );
38
+ }
@@ -9,6 +9,7 @@
9
9
  "scripts": {
10
10
  "dev": "mandu dev",
11
11
  "build": "mandu build",
12
+ "start": "mandu start",
12
13
  "check": "mandu check",
13
14
  "guard": "mandu guard",
14
15
  "test": "bun test"
@@ -1,16 +1,16 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- /**
5
- * cn - Tailwind CSS 클래스 병합 유틸리티
6
- *
7
- * clsx로 조건부 클래스를 결합하고
8
- * tailwind-merge로 충돌하는 클래스를 스마트하게 병합
9
- *
10
- * @example
11
- * cn("px-4 py-2", isActive && "bg-primary", className)
12
- * cn("text-sm", "text-lg") // → "text-lg" (충돌 해결)
13
- */
14
- export function cn(...inputs: ClassValue[]) {
15
- return twMerge(clsx(inputs));
16
- }
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * cn - Tailwind CSS 클래스 병합 유틸리티
6
+ *
7
+ * clsx로 조건부 클래스를 결합하고
8
+ * tailwind-merge로 충돌하는 클래스를 스마트하게 병합
9
+ *
10
+ * @example
11
+ * cn("px-4 py-2", isActive && "bg-primary", className)
12
+ * cn("text-sm", "text-lg") // → "text-lg" (충돌 해결)
13
+ */
14
+ export function cn(...inputs: ClassValue[]) {
15
+ return twMerge(clsx(inputs));
16
+ }