@mandujs/cli 0.18.2 → 0.18.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "^0.18.2",
35
+ "@mandujs/core": "^0.18.4",
36
36
  "@mandujs/ate": "^0.17.0",
37
37
  "cfonts": "^3.3.0"
38
38
  },
@@ -120,7 +120,7 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
120
120
  console.log("\n👀 파일 감시 모드...");
121
121
  console.log(" Ctrl+C로 종료\n");
122
122
 
123
- await watchAndRebuild(manifest, cwd, resolvedBuildOptions);
123
+ await watchAndRebuild(cwd, resolvedBuildOptions, { fsRoutes: config.fsRoutes });
124
124
  }
125
125
 
126
126
  return true;
@@ -128,49 +128,79 @@ export async function build(options: BuildOptions = {}): Promise<boolean> {
128
128
 
129
129
  /**
130
130
  * 파일 감시 및 재빌드
131
+ * FS Routes 프로젝트: app/ 디렉토리의 island 파일 감시
132
+ *
133
+ * 파일 변경 시마다 resolveManifest를 재호출하여 새로 추가/삭제된
134
+ * 라우트가 번들에 반영되도록 합니다.
131
135
  */
132
136
  async function watchAndRebuild(
133
- manifest: RoutesManifest,
134
137
  rootDir: string,
135
- options: BuildOptions
138
+ options: BuildOptions,
139
+ resolveOptions: Parameters<typeof resolveManifest>[1] = {}
136
140
  ): Promise<void> {
141
+ // FS Routes 프로젝트는 app/ 디렉토리를, 구버전은 spec/slots/ 감시
142
+ const fsRoutesDir = path.join(rootDir, "app");
137
143
  const slotsDir = path.join(rootDir, "spec", "slots");
138
144
 
139
- // 디렉토리 존재 확인
145
+ let watchDir: string;
146
+ let watchMode: "fs-routes" | "slots";
147
+
140
148
  try {
141
- await fs.access(slotsDir);
149
+ await fs.access(fsRoutesDir);
150
+ watchDir = fsRoutesDir;
151
+ watchMode = "fs-routes";
142
152
  } catch {
143
- console.warn(`⚠️ 슬롯 디렉토리가 없습니다: ${slotsDir}`);
144
- return;
153
+ try {
154
+ await fs.access(slotsDir);
155
+ watchDir = slotsDir;
156
+ watchMode = "slots";
157
+ } catch {
158
+ console.warn(`⚠️ 감시할 디렉토리가 없습니다 (app/ 또는 spec/slots/)`);
159
+ return;
160
+ }
145
161
  }
146
162
 
163
+ console.log(`👀 감시 중: ${watchDir}`);
164
+
147
165
  const { watch } = await import("fs");
148
166
 
149
- const watcher = watch(slotsDir, { recursive: true }, async (event, filename) => {
167
+ const watcher = watch(watchDir, { recursive: true }, async (event, filename) => {
150
168
  if (!filename) return;
151
169
 
152
- // .client.ts 파일만 감시
153
- if (!filename.endsWith(".client.ts")) return;
154
-
155
- const routeId = filename.replace(".client.ts", "").replace(/\\/g, "/").split("/").pop();
156
- if (!routeId) return;
157
-
158
- const route = manifest!.routes.find((r) => r.id === routeId);
159
- if (!route || !route.clientModule) return;
170
+ const normalizedFilename = filename.replace(/\\/g, "/");
171
+
172
+ // FS Routes: island 파일 변경 감지
173
+ if (watchMode === "fs-routes") {
174
+ const isIslandFile =
175
+ normalizedFilename.endsWith(".island.tsx") ||
176
+ normalizedFilename.endsWith(".island.ts") ||
177
+ normalizedFilename.endsWith(".island.jsx") ||
178
+ normalizedFilename.endsWith(".island.js");
179
+ // 루트 레벨(page.tsx) 및 중첩 경로(/nested/page.tsx) 모두 감지, .js/.jsx 포함
180
+ const isPageFile = /(?:^|\/)page\.[jt]sx?$/.test(normalizedFilename);
181
+
182
+ if (!isIslandFile && !isPageFile) return;
183
+ } else {
184
+ // Slots: .client.ts 파일만 감시
185
+ if (!normalizedFilename.endsWith(".client.ts")) return;
186
+ }
160
187
 
161
- console.log(`\n🔄 변경 감지: ${routeId}`);
188
+ console.log(`\n🔄 변경 감지: ${normalizedFilename}`);
162
189
 
163
190
  try {
164
- const result = await buildClientBundles(manifest!, rootDir, {
191
+ // 파일 추가/삭제 반영을 위해 재빌드마다 매니페스트 재조회
192
+ const { manifest: freshManifest } = await resolveManifest(rootDir, resolveOptions);
193
+
194
+ const result = await buildClientBundles(freshManifest, rootDir, {
165
195
  minify: options.minify,
166
196
  sourcemap: options.sourcemap,
167
197
  outDir: options.outDir,
168
198
  });
169
199
 
170
200
  if (result.success) {
171
- console.log(`✅ 재빌드 완료: ${routeId}`);
201
+ console.log(`✅ 재빌드 완료`);
172
202
  } else {
173
- console.error(`❌ 재빌드 실패: ${routeId}`);
203
+ console.error(`❌ 재빌드 실패`);
174
204
  for (const error of result.errors) {
175
205
  console.error(` ${error}`);
176
206
  }
@@ -196,7 +196,9 @@ export async function dev(options: DevOptions = {}): Promise<void> {
196
196
  });
197
197
 
198
198
  if (port !== desiredPort) {
199
- console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
199
+ console.warn(`⚠️ Port ${desiredPort} is in use.`);
200
+ console.warn(` Dev server: http://localhost:${port}`);
201
+ console.warn(` HMR WebSocket: ws://localhost:${port + HMR_OFFSET}`);
200
202
  }
201
203
 
202
204
  // HMR 서버 시작 (클라이언트 슬롯이 있는 경우)
@@ -124,6 +124,11 @@ export async function start(options: StartOptions = {}): Promise<void> {
124
124
  console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
125
125
  }
126
126
 
127
+ // CSS 경로 결정 (빌드된 CSS 파일 존재 시 주입)
128
+ const cssFilePath = path.join(rootDir, ".mandu", "client", "globals.css");
129
+ const hasCss = fs.existsSync(cssFilePath);
130
+ const cssPath: string | false = hasCss ? "/.mandu/client/globals.css" : false;
131
+
127
132
  // 메인 서버 시작 (프로덕션 모드)
128
133
  const server = startServer(manifest, {
129
134
  port,
@@ -134,6 +139,7 @@ export async function start(options: StartOptions = {}): Promise<void> {
134
139
  cors: serverConfig.cors,
135
140
  streaming: serverConfig.streaming,
136
141
  rateLimit: serverConfig.rateLimit,
142
+ cssPath,
137
143
  });
138
144
 
139
145
  const actualPort = server.server.port ?? port;
@@ -17,7 +17,7 @@ export async function testAuto(opts: { ci?: boolean; impact?: boolean; baseURL?:
17
17
  let onlyRoutes: string[] | undefined;
18
18
  let impactInfo: any = { mode: "full", changedFiles: [], selectedRoutes: [] };
19
19
  if (opts.impact) {
20
- const impactRes = ateImpact({ repoRoot });
20
+ const impactRes = await ateImpact({ repoRoot });
21
21
  onlyRoutes = impactRes.selectedRoutes.length ? impactRes.selectedRoutes : undefined;
22
22
  impactInfo = {
23
23
  mode: onlyRoutes ? "subset" : "full",
@@ -27,7 +27,7 @@ export async function testAuto(opts: { ci?: boolean; impact?: boolean; baseURL?:
27
27
  }
28
28
 
29
29
  // 3) generate
30
- const genRes = ateGenerate({ repoRoot, oracleLevel, onlyRoutes });
30
+ const genRes = await ateGenerate({ repoRoot, oracleLevel, onlyRoutes });
31
31
 
32
32
  // 4) run
33
33
  const runRes = await ateRun({ repoRoot, ci: opts.ci, headless: opts.ci, baseURL: opts.baseURL });
@@ -1,143 +1,147 @@
1
- import { CLI_ERROR_CODES, type CLIErrorCode } from "./codes";
2
-
3
- interface ErrorInfo {
4
- message: string;
5
- suggestion?: string;
6
- docLink?: string;
7
- }
8
-
9
- export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
10
- [CLI_ERROR_CODES.INIT_DIR_EXISTS]: {
11
- message: "Directory already exists: {path}",
12
- suggestion: "Choose a different project name or remove the existing directory.",
13
- },
14
- [CLI_ERROR_CODES.INIT_BUN_NOT_FOUND]: {
15
- message: "Bun runtime not found.",
16
- suggestion: "Install Bun and ensure it is available in your PATH.",
17
- },
18
- [CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND]: {
19
- message: "Template not found: {template}",
20
- suggestion: "Use a valid template name (default).",
21
- },
22
- [CLI_ERROR_CODES.DEV_PORT_IN_USE]: {
23
- message: "Port {port} is already in use.",
24
- suggestion: "Set PORT or mandu.config server.port to pick a different port, or stop the process using this port.",
25
- },
26
- [CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND]: {
27
- message: "Routes manifest not found.",
28
- suggestion: "Run `mandu routes generate` or create app/ routes before dev.",
29
- },
30
- [CLI_ERROR_CODES.DEV_NO_ROUTES]: {
31
- message: "No routes were found in app/.",
32
- suggestion: "Create app/page.tsx or app/api/*/route.ts to get started.",
33
- },
34
- [CLI_ERROR_CODES.GUARD_CONFIG_INVALID]: {
35
- message: "Invalid guard configuration.",
36
- suggestion: "Check your mandu.config and guard settings.",
37
- },
38
- [CLI_ERROR_CODES.GUARD_PRESET_NOT_FOUND]: {
39
- message: "Unknown architecture preset: {preset}",
40
- suggestion: "Available presets: mandu, fsd, clean, hexagonal, atomic.",
41
- },
42
- [CLI_ERROR_CODES.GUARD_VIOLATION_FOUND]: {
43
- message: "{count} architecture violation(s) found.",
44
- suggestion: "Fix violations above or set MANDU_OUTPUT=agent for AI-friendly output.",
45
- },
46
- [CLI_ERROR_CODES.BUILD_ENTRY_NOT_FOUND]: {
47
- message: "Build entry not found: {entry}",
48
- suggestion: "Check your routes manifest or build inputs.",
49
- },
50
- [CLI_ERROR_CODES.BUILD_BUNDLE_FAILED]: {
51
- message: "Bundle build failed for '{target}'.",
52
- suggestion: "Review build errors above for missing deps or syntax errors.",
53
- },
54
- [CLI_ERROR_CODES.BUILD_OUTDIR_NOT_WRITABLE]: {
55
- message: "Output directory is not writable: {path}",
56
- suggestion: "Ensure the directory exists and you have write permissions.",
57
- },
58
- [CLI_ERROR_CODES.CONFIG_PARSE_FAILED]: {
59
- message: "Failed to parse mandu.config.",
60
- suggestion: "Fix syntax errors in the config file.",
61
- },
62
- [CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED]: {
63
- message: "Configuration validation failed.",
64
- suggestion: "Review validation errors above and fix your config.",
65
- },
66
- [CLI_ERROR_CODES.UNKNOWN_COMMAND]: {
67
- message: "Unknown command: {command}",
68
- suggestion: "Run with --help to see available commands.",
69
- },
70
- [CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND]: {
71
- message: "Unknown subcommand '{subcommand}' for {command}.",
72
- suggestion: "Run the command with --help to see available subcommands.",
73
- },
74
- [CLI_ERROR_CODES.MISSING_ARGUMENT]: {
75
- message: "Missing required argument: {argument}",
76
- suggestion: "Provide the required argument and try again.",
77
- },
78
- };
79
-
80
- function interpolate(text: string, context?: Record<string, string | number>): string {
81
- if (!context) return text;
82
- let result = text;
83
- for (const [key, value] of Object.entries(context)) {
84
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
85
- }
86
- return result;
87
- }
88
-
89
- export function formatCLIError(
90
- code: CLIErrorCode,
91
- context?: Record<string, string | number>
92
- ): string {
93
- const info = ERROR_MESSAGES[code];
94
- const message = interpolate(info?.message ?? "Unknown error", context);
95
- const suggestion = info?.suggestion ? interpolate(info.suggestion, context) : undefined;
96
-
97
- const lines = ["", `❌ Error [${code}]`, ` ${message}`];
98
- if (suggestion) {
99
- lines.push("", `💡 ${suggestion}`);
100
- }
101
- if (info?.docLink) {
102
- lines.push(`📖 ${info.docLink}`);
103
- }
104
- lines.push("");
105
- return lines.join("\n");
106
- }
107
-
108
- export class CLIError extends Error {
109
- readonly code: CLIErrorCode;
110
- readonly context?: Record<string, string | number>;
111
-
112
- constructor(code: CLIErrorCode, context?: Record<string, string | number>) {
113
- super(formatCLIError(code, context));
114
- this.code = code;
115
- this.context = context;
116
- this.name = "CLIError";
117
- }
118
- }
119
-
120
- export function printCLIError(
121
- code: CLIErrorCode,
122
- context?: Record<string, string | number>
123
- ): void {
124
- console.error(formatCLIError(code, context));
125
- }
126
-
127
- export function handleCLIError(error: unknown): never {
128
- if (error instanceof CLIError) {
129
- console.error(error.message);
130
- process.exit(1);
131
- }
132
-
133
- if (error instanceof Error) {
134
- console.error(`\n❌ Unexpected error: ${error.message}\n`);
135
- if (process.env.DEBUG) {
136
- console.error(error.stack);
137
- }
138
- process.exit(1);
139
- }
140
-
141
- console.error("\n❌ Unknown error occurred\n");
142
- process.exit(1);
143
- }
1
+ import { CLI_ERROR_CODES, type CLIErrorCode } from "./codes";
2
+
3
+ interface ErrorInfo {
4
+ message: string;
5
+ suggestion?: string;
6
+ docLink?: string;
7
+ }
8
+
9
+ export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
10
+ [CLI_ERROR_CODES.INIT_DIR_EXISTS]: {
11
+ message: "Directory already exists: {path}",
12
+ suggestion: "Choose a different project name or remove the existing directory.",
13
+ },
14
+ [CLI_ERROR_CODES.INIT_BUN_NOT_FOUND]: {
15
+ message: "Bun runtime not found.",
16
+ suggestion: "Install Bun and ensure it is available in your PATH.",
17
+ },
18
+ [CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND]: {
19
+ message: "Template not found: {template}",
20
+ suggestion: "Use a valid template name (default).",
21
+ },
22
+ [CLI_ERROR_CODES.DEV_PORT_IN_USE]: {
23
+ message: "Port {port} is already in use.",
24
+ suggestion: "Set PORT or mandu.config server.port to pick a different port, or stop the process using this port.",
25
+ },
26
+ [CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND]: {
27
+ message: "Routes manifest not found.",
28
+ suggestion: "Run `mandu routes generate` or create app/ routes before dev.",
29
+ },
30
+ [CLI_ERROR_CODES.DEV_NO_ROUTES]: {
31
+ message: "No routes were found in app/.",
32
+ suggestion: "Create app/page.tsx or app/api/*/route.ts to get started.",
33
+ },
34
+ [CLI_ERROR_CODES.GUARD_CONFIG_INVALID]: {
35
+ message: "Invalid guard configuration.",
36
+ suggestion: "Check your mandu.config and guard settings.",
37
+ },
38
+ [CLI_ERROR_CODES.GUARD_PRESET_NOT_FOUND]: {
39
+ message: "Unknown architecture preset: {preset}",
40
+ suggestion: "Available presets: mandu, fsd, clean, hexagonal, atomic.",
41
+ },
42
+ [CLI_ERROR_CODES.GUARD_VIOLATION_FOUND]: {
43
+ message: "{count} architecture violation(s) found.",
44
+ suggestion: "Fix violations above or set MANDU_OUTPUT=agent for AI-friendly output.",
45
+ },
46
+ [CLI_ERROR_CODES.BUILD_ENTRY_NOT_FOUND]: {
47
+ message: "Build entry not found: {entry}",
48
+ suggestion: "Check your routes manifest or build inputs.",
49
+ },
50
+ [CLI_ERROR_CODES.BUILD_BUNDLE_FAILED]: {
51
+ message: "Bundle build failed for '{target}'.",
52
+ suggestion: "Review build errors above for missing deps or syntax errors.",
53
+ },
54
+ [CLI_ERROR_CODES.BUILD_OUTDIR_NOT_WRITABLE]: {
55
+ message: "Output directory is not writable: {path}",
56
+ suggestion: "Ensure the directory exists and you have write permissions.",
57
+ },
58
+ [CLI_ERROR_CODES.CONFIG_PARSE_FAILED]: {
59
+ message: "Failed to parse mandu.config.",
60
+ suggestion: "Fix syntax errors in the config file.",
61
+ },
62
+ [CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED]: {
63
+ message: "Configuration validation failed.",
64
+ suggestion: "Review validation errors above and fix your config.",
65
+ },
66
+ [CLI_ERROR_CODES.UNKNOWN_COMMAND]: {
67
+ message: "Unknown command: {command}",
68
+ suggestion: "Run with --help to see available commands.",
69
+ },
70
+ [CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND]: {
71
+ message: "Unknown subcommand '{subcommand}' for {command}.",
72
+ suggestion: "Run the command with --help to see available subcommands.",
73
+ },
74
+ [CLI_ERROR_CODES.MISSING_ARGUMENT]: {
75
+ message: "Missing required argument: {argument}",
76
+ suggestion: "Provide the required argument and try again.",
77
+ },
78
+ };
79
+
80
+ function interpolate(text: string, context?: Record<string, string | number>): string {
81
+ if (!context) return text;
82
+ let result = text;
83
+ for (const [key, value] of Object.entries(context)) {
84
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
85
+ }
86
+ return result;
87
+ }
88
+
89
+ export function formatCLIError(
90
+ code: CLIErrorCode,
91
+ context?: Record<string, string | number>
92
+ ): string {
93
+ const info = ERROR_MESSAGES[code];
94
+ const message = interpolate(info?.message ?? "Unknown error", context);
95
+ const suggestion = info?.suggestion ? interpolate(info.suggestion, context) : undefined;
96
+
97
+ const lines = ["", `❌ Error [${code}]`, ` ${message}`];
98
+ if (suggestion) {
99
+ lines.push("", `💡 ${suggestion}`);
100
+ }
101
+ if (info?.docLink) {
102
+ lines.push(`📖 ${info.docLink}`);
103
+ }
104
+ lines.push("");
105
+ return lines.join("\n");
106
+ }
107
+
108
+ export class CLIError extends Error {
109
+ readonly code: CLIErrorCode;
110
+ readonly context?: Record<string, string | number>;
111
+
112
+ constructor(code: CLIErrorCode, context?: Record<string, string | number>) {
113
+ super(formatCLIError(code, context));
114
+ this.code = code;
115
+ this.context = context;
116
+ this.name = "CLIError";
117
+ }
118
+ }
119
+
120
+ export function printCLIError(
121
+ code: CLIErrorCode,
122
+ context?: Record<string, string | number>
123
+ ): void {
124
+ console.error(formatCLIError(code, context));
125
+ }
126
+
127
+ export function handleCLIError(error: unknown): never {
128
+ if (error instanceof CLIError) {
129
+ console.error(error.message);
130
+ process.exit(1);
131
+ }
132
+
133
+ if (error instanceof Error) {
134
+ console.error(`\n❌ Unexpected error: ${error.message}\n`);
135
+ if (process.env.DEBUG) {
136
+ console.error(error.stack);
137
+ }
138
+ process.exit(1);
139
+ }
140
+
141
+ console.error("\n❌ Unknown error occurred (non-Error thrown)\n");
142
+ if (process.env.DEBUG) {
143
+ // eslint-disable-next-line no-console
144
+ console.error("Thrown value:", error);
145
+ }
146
+ process.exit(1);
147
+ }
@@ -1,96 +1,109 @@
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. Layout 컴포넌트
49
+ `html/head/body` 태그는 Mandu SSR이 자동으로 생성합니다. Layout은 body 내부 래퍼만 정의합니다:
50
+ ```tsx
51
+ // app/layout.tsx
52
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
53
+ return (
54
+ <div className="min-h-screen bg-background font-sans antialiased">
55
+ {children}
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ### 2. Island 컴포넌트
62
+ 클라이언트 상호작용이 필요한 컴포넌트는 `*.island.tsx`로 명명:
63
+ ```tsx
64
+ // src/client/widgets/counter/Counter.island.tsx
65
+ "use client";
66
+ export function CounterIsland() { ... }
67
+ ```
68
+
69
+ ### 3. API 라우트
70
+ `app/api/` 폴더에 `route.ts` 파일로 정의:
71
+ ```typescript
72
+ // app/api/users/route.ts
73
+ import { Mandu } from "@mandujs/core";
74
+ export default Mandu.filling()
75
+ .get((ctx) => ctx.ok({ users: [] }))
76
+ .post(async (ctx) => { ... });
77
+ ```
78
+
79
+ ### 4. Tailwind CSS v4
80
+ CSS-first 설정 사용 (`tailwind.config.ts` 없음):
81
+ ```css
82
+ /* app/globals.css */
83
+ @import "tailwindcss";
84
+ @theme {
85
+ --color-primary: hsl(222.2 47.4% 11.2%);
86
+ }
87
+ ```
88
+
89
+ ### 5. Import Alias
90
+ `@/` = `src/` 경로:
91
+ ```typescript
92
+ import { Button } from "@/client/shared/ui/button";
93
+ ```
94
+
95
+ ## 실행 방법
96
+
97
+ ```bash
98
+ bun install # 최초 설치
99
+ bun run dev # 개발 서버 (http://localhost:3333)
100
+ bun run build # 프로덕션 빌드
101
+ bun run guard # 아키텍처 검증
102
+ ```
103
+
104
+ ## 기술 스택
105
+
106
+ - **Runtime**: Bun 1.x
107
+ - **Framework**: Mandu (React 19 + Bun native)
108
+ - **Styling**: Tailwind CSS v4
109
+ - **Language**: TypeScript 5.x
@@ -1,27 +1,20 @@
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
+ * - html/head/body 태그는 Mandu SSR이 자동으로 생성합니다
6
+ * - 여기서는 body 내부의 공통 래퍼만 정의합니다
7
+ * - CSS는 Mandu가 자동으로 주입합니다: /.mandu/client/globals.css
8
+ */
9
+
10
+ interface RootLayoutProps {
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ export default function RootLayout({ children }: RootLayoutProps) {
15
+ return (
16
+ <div className="min-h-screen bg-background font-sans antialiased">
17
+ {children}
18
+ </div>
19
+ );
20
+ }
@@ -8,11 +8,14 @@
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "mandu dev",
11
+ "dev:safe": "mandu lock && mandu dev",
11
12
  "build": "mandu build",
12
13
  "start": "mandu start",
13
14
  "check": "mandu check",
14
15
  "guard": "mandu guard",
15
- "test": "bun test"
16
+ "test": "bun test",
17
+ "test:auto": "mandu test:auto",
18
+ "test:e2e:ci": "mandu test:auto --ci"
16
19
  },
17
20
  "dependencies": {
18
21
  "@mandujs/core": "{{CORE_VERSION}}",