@mandujs/cli 0.18.0 → 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 +3 -3
- package/src/commands/build.ts +50 -20
- package/src/commands/dev.ts +3 -1
- package/src/commands/lock.ts +15 -10
- package/src/commands/start.ts +6 -0
- package/src/commands/test-auto.ts +2 -2
- package/src/errors/messages.ts +147 -143
- package/templates/default/AGENTS.md +109 -96
- package/templates/default/app/layout.tsx +20 -27
- package/templates/default/package.json +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.ts",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "0.18.
|
|
36
|
-
"@mandujs/ate": "0.17.0",
|
|
35
|
+
"@mandujs/core": "^0.18.4",
|
|
36
|
+
"@mandujs/ate": "^0.17.0",
|
|
37
37
|
"cfonts": "^3.3.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
package/src/commands/build.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
149
|
+
await fs.access(fsRoutesDir);
|
|
150
|
+
watchDir = fsRoutesDir;
|
|
151
|
+
watchMode = "fs-routes";
|
|
142
152
|
} catch {
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
167
|
+
const watcher = watch(watchDir, { recursive: true }, async (event, filename) => {
|
|
150
168
|
if (!filename) return;
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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🔄 변경 감지: ${
|
|
188
|
+
console.log(`\n🔄 변경 감지: ${normalizedFilename}`);
|
|
162
189
|
|
|
163
190
|
try {
|
|
164
|
-
|
|
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(`✅ 재빌드
|
|
201
|
+
console.log(`✅ 재빌드 완료`);
|
|
172
202
|
} else {
|
|
173
|
-
console.error(`❌ 재빌드
|
|
203
|
+
console.error(`❌ 재빌드 실패`);
|
|
174
204
|
for (const error of result.errors) {
|
|
175
205
|
console.error(` ${error}`);
|
|
176
206
|
}
|
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
|
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 서버 시작 (클라이언트 슬롯이 있는 경우)
|
package/src/commands/lock.ts
CHANGED
|
@@ -284,24 +284,29 @@ async function showDiff(
|
|
|
284
284
|
return false;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
// 스냅샷이 없으면
|
|
287
|
+
// 스냅샷이 없으면 전체 설정을 변경사항으로 표시
|
|
288
288
|
if (!lockfile.snapshot) {
|
|
289
289
|
if (json) {
|
|
290
290
|
console.log(
|
|
291
291
|
JSON.stringify({
|
|
292
|
-
success:
|
|
293
|
-
|
|
294
|
-
message:
|
|
295
|
-
|
|
292
|
+
success: true,
|
|
293
|
+
warning: "SNAPSHOT_MISSING",
|
|
294
|
+
message: "스냅샷이 없어 전체 설정을 변경사항으로 표시",
|
|
295
|
+
hasChanges: true,
|
|
296
296
|
})
|
|
297
297
|
);
|
|
298
298
|
} else {
|
|
299
|
-
console.
|
|
300
|
-
console.
|
|
301
|
-
|
|
302
|
-
);
|
|
299
|
+
console.log("⚠️ Lockfile에 스냅샷이 없습니다.");
|
|
300
|
+
console.log(" 전체 설정을 변경사항으로 표시합니다.");
|
|
301
|
+
console.log(" 정확한 diff를 보려면: mandu lock --include-snapshot\n");
|
|
303
302
|
}
|
|
304
|
-
|
|
303
|
+
|
|
304
|
+
// Show entire config as additions
|
|
305
|
+
const { mcpServers } = resolveMcpSources(config, mcpConfig);
|
|
306
|
+
const configForDiff = mcpServers ? { ...config, mcpServers } : config;
|
|
307
|
+
const fullDiff = diffConfig({}, configForDiff);
|
|
308
|
+
console.log(formatConfigDiff(fullDiff, { color: true, verbose: true, showSecrets }));
|
|
309
|
+
return true;
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
// diff 계산
|
package/src/commands/start.ts
CHANGED
|
@@ -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 });
|
package/src/errors/messages.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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. 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
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
interface RootLayoutProps {
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export default function RootLayout({ children }: RootLayoutProps) {
|
|
15
|
-
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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}}",
|