@mandujs/cli 0.9.24 β†’ 0.9.43

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/src/main.ts CHANGED
@@ -23,6 +23,7 @@ import { watch } from "./commands/watch";
23
23
  import { brainSetup, brainStatus } from "./commands/brain";
24
24
  import { routesGenerate, routesList, routesWatch } from "./commands/routes";
25
25
  import { monitor } from "./commands/monitor";
26
+ import { CLI_ERROR_CODES, handleCLIError, printCLIError } from "./errors";
26
27
 
27
28
  const HELP_TEXT = `
28
29
  πŸ₯Ÿ Mandu CLI - Agent-Native Fullstack Framework
@@ -30,30 +31,25 @@ const HELP_TEXT = `
30
31
  Usage: bunx mandu <command> [options]
31
32
 
32
33
  Commands:
33
- init μƒˆ ν”„λ‘œμ νŠΈ 생성 (Tailwind + shadcn/ui κΈ°λ³Έ 포함)
34
- check FS Routes + Guard 톡합 검사
35
- routes generate FS Routes μŠ€μΊ” 및 λ§€λ‹ˆνŽ˜μŠ€νŠΈ 생성
36
- routes list ν˜„μž¬ 라우트 λͺ©λ‘ 좜λ ₯
37
- routes watch μ‹€μ‹œκ°„ 라우트 κ°μ‹œ
38
- dev 개발 μ„œλ²„ μ‹€ν–‰ (FS Routes + Guard κΈ°λ³Έ)
39
- dev --no-guard Guard κ°μ‹œ λΉ„ν™œμ„±ν™”
40
- build ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€ λΉŒλ“œ (Hydration)
41
- guard μ•„ν‚€ν…μ²˜ μœ„λ°˜ 검사 (κΈ°λ³Έ)
42
- guard arch μ•„ν‚€ν…μ²˜ μœ„λ°˜ 검사 (FSD/Clean/Hexagonal)
43
- guard legacy λ ˆκ±°μ‹œ Spec Guard 검사
44
- guard arch --watch μ‹€μ‹œκ°„ μ•„ν‚€ν…μ²˜ κ°μ‹œ
45
- guard arch --list-presets μ‚¬μš© κ°€λŠ₯ν•œ 프리셋 λͺ©λ‘
46
- guard arch --output report.md 리포트 파일 생성
47
- guard arch --show-trend νŠΈλ Œλ“œ 뢄석 ν‘œμ‹œ
48
- spec-upsert Spec 파일 검증 및 lock κ°±μ‹  (λ ˆκ±°μ‹œ)
49
- generate Specμ—μ„œ μ½”λ“œ 생성 (λ ˆκ±°μ‹œ)
50
-
51
- doctor Guard μ‹€νŒ¨ 뢄석 + 패치 μ œμ•ˆ (Brain)
52
- watch μ‹€μ‹œκ°„ 파일 κ°μ‹œ - 경고만 (Brain)
53
- monitor MCP Activity Monitor 둜그 슀트림
54
-
55
- brain setup sLLM μ„€μ • (선택)
56
- brain status Brain μƒνƒœ 확인
34
+ init μƒˆ ν”„λ‘œμ νŠΈ 생성 (Tailwind + shadcn/ui κΈ°λ³Έ 포함)
35
+ check FS Routes + Guard 톡합 검사
36
+ routes generate FS Routes μŠ€μΊ” 및 λ§€λ‹ˆνŽ˜μŠ€νŠΈ 생성
37
+ routes list ν˜„μž¬ 라우트 λͺ©λ‘ 좜λ ₯
38
+ routes watch μ‹€μ‹œκ°„ 라우트 κ°μ‹œ
39
+ dev 개발 μ„œλ²„ μ‹€ν–‰ (FS Routes + Guard κΈ°λ³Έ)
40
+ build ν΄λΌμ΄μ–ΈνŠΈ λ²ˆλ“€ λΉŒλ“œ (Hydration)
41
+ guard μ•„ν‚€ν…μ²˜ μœ„λ°˜ 검사 (κΈ°λ³Έ)
42
+ guard arch μ•„ν‚€ν…μ²˜ μœ„λ°˜ 검사 (FSD/Clean/Hexagonal)
43
+ guard legacy λ ˆκ±°μ‹œ Spec Guard 검사
44
+ spec-upsert Spec 파일 검증 및 lock κ°±μ‹  (λ ˆκ±°μ‹œ)
45
+ generate Specμ—μ„œ μ½”λ“œ 생성 (λ ˆκ±°μ‹œ)
46
+
47
+ doctor Guard μ‹€νŒ¨ 뢄석 + 패치 μ œμ•ˆ (Brain)
48
+ watch μ‹€μ‹œκ°„ 파일 κ°μ‹œ - 경고만 (Brain)
49
+ monitor MCP Activity Monitor 둜그 슀트림
50
+
51
+ brain setup sLLM μ„€μ • (선택)
52
+ brain status Brain μƒνƒœ 확인
57
53
 
58
54
  contract create <routeId> λΌμš°νŠΈμ— λŒ€ν•œ Contract 생성
59
55
  contract validate Contract-Slot 일관성 검증
@@ -63,79 +59,68 @@ Commands:
63
59
  openapi generate OpenAPI 3.0 μŠ€νŽ™ 생성
64
60
  openapi serve Swagger UI 둜컬 μ„œλ²„ μ‹€ν–‰
65
61
 
66
- change begin λ³€κ²½ νŠΈλžœμž­μ…˜ μ‹œμž‘ (μŠ€λƒ…μƒ· 생성)
67
- change commit λ³€κ²½ ν™•μ •
68
- change rollback μŠ€λƒ…μƒ·μœΌλ‘œ 볡원
69
- change status ν˜„μž¬ νŠΈλžœμž­μ…˜ μƒνƒœ
70
- change list λ³€κ²½ 이λ ₯ 쑰회
71
- change prune 였래된 μŠ€λƒ…μƒ· 정리
62
+ change begin λ³€κ²½ νŠΈλžœμž­μ…˜ μ‹œμž‘ (μŠ€λƒ…μƒ· 생성)
63
+ change commit λ³€κ²½ ν™•μ •
64
+ change rollback μŠ€λƒ…μƒ·μœΌλ‘œ 볡원
65
+ change status ν˜„μž¬ νŠΈλžœμž­μ…˜ μƒνƒœ
66
+ change list λ³€κ²½ 이λ ₯ 쑰회
67
+ change prune 였래된 μŠ€λƒ…μƒ· 정리
72
68
 
73
69
  Options:
74
- --name <name> init μ‹œ ν”„λ‘œμ νŠΈ 이름 (κΈ°λ³Έ: my-mandu-app)
75
- --css <framework> init μ‹œ CSS ν”„λ ˆμž„μ›Œν¬: tailwind, panda, none (κΈ°λ³Έ: tailwind)
76
- --ui <library> init μ‹œ UI 라이브러리: shadcn, ark, none (κΈ°λ³Έ: shadcn)
77
- --theme init μ‹œ 닀크λͺ¨λ“œ ν…Œλ§ˆ μ‹œμŠ€ν…œ μΆ”κ°€
78
- --minimal init μ‹œ CSS/UI 없이 μ΅œμ†Œ ν…œν”Œλ¦Ώ 생성 (--css none --ui none)
79
- --file <path> spec-upsert μ‹œ μ‚¬μš©ν•  spec 파일 경둜
80
- --port <port> dev/openapi serve 포트 (기본: 3000/8080)
81
- --guard dev μ‹œ Architecture Guard μ‹€μ‹œκ°„ κ°μ‹œ ν™œμ„±ν™” (κΈ°λ³Έ: ON)
82
- --no-guard dev μ‹œ Guard λΉ„ν™œμ„±ν™”
83
- --guard-preset <p> dev --guard μ‹œ 프리셋 (κΈ°λ³Έ: mandu)
84
- --guard-format <f> dev --guard 좜λ ₯ ν˜•μ‹: console, json, agent (κΈ°λ³Έ: μžλ™)
85
- --legacy FS Routes λΉ„ν™œμ„±ν™” (λ ˆκ±°μ‹œ λͺ¨λ“œ)
86
- --no-auto-correct guard μ‹œ μžλ™ μˆ˜μ • λΉ„ν™œμ„±ν™”
87
- --preset <name> guard/check 프리셋 (κΈ°λ³Έ: mandu) - fsd, clean, hexagonal, atomic 선택 κ°€λŠ₯
88
- --ci guard/check CI λͺ¨λ“œ (warning도 μ‹€νŒ¨ 처리)
89
- --quiet guard/check μš”μ•½λ§Œ 좜λ ₯
90
- --report-format guard arch 리포트 ν˜•μ‹: json, markdown, html
91
- --save-stats guard arch 톡계 μ €μž₯ (νŠΈλ Œλ“œ λΆ„μ„μš©)
92
- --show-trend guard arch νŠΈλ Œλ“œ 뢄석 ν‘œμ‹œ
93
- --minify build μ‹œ μ½”λ“œ μ••μΆ•
94
- --sourcemap build μ‹œ μ†ŒμŠ€λ§΅ 생성
95
- --watch build/guard arch 파일 κ°μ‹œ λͺ¨λ“œ
96
- --summary monitor μš”μ•½ 좜λ ₯ (JSON λ‘œκ·Έμ—μ„œλ§Œ)
97
- --since <duration> monitor μš”μ•½ κΈ°κ°„ (예: 5m, 30s, 1h)
98
- --follow <bool> monitor follow λͺ¨λ“œ (κΈ°λ³Έ: true)
99
- --file <path> monitor 둜그 파일 직접 μ§€μ •
100
- --message <msg> change begin μ‹œ μ„€λͺ… λ©”μ‹œμ§€
101
- --id <id> change rollback μ‹œ νŠΉμ • λ³€κ²½ ID
102
- --keep <n> change prune μ‹œ μœ μ§€ν•  μŠ€λƒ…μƒ· 수 (κΈ°λ³Έ: 5)
103
- --output <path> openapi/doctor 좜λ ₯ 경둜
104
- --from <path> contract diff κΈ°μ€€ λ ˆμ§€μŠ€νŠΈλ¦¬ 경둜
105
- --to <path> contract diff λŒ€μƒ λ ˆμ§€μŠ€νŠΈλ¦¬ 경둜
106
- --json contract diff κ²°κ³Ό JSON 좜λ ₯
107
- --format <fmt> guard/check 좜λ ₯ ν˜•μ‹: console, json, agent (κΈ°λ³Έ: μžλ™)
108
- --format <fmt> doctor 좜λ ₯ ν˜•μ‹: console, json, markdown (κΈ°λ³Έ: console)
109
- --no-llm doctorμ—μ„œ LLM μ‚¬μš© μ•ˆ 함 (ν…œν”Œλ¦Ώ λͺ¨λ“œ)
110
- --model <name> brain setup μ‹œ λͺ¨λΈ 이름 (κΈ°λ³Έ: llama3.2)
111
- --url <url> brain setup μ‹œ Ollama URL
112
- --verbose 상세 좜λ ₯
113
- --help, -h 도움말 ν‘œμ‹œ
70
+ --name <name> init μ‹œ ν”„λ‘œμ νŠΈ 이름 (κΈ°λ³Έ: my-mandu-app)
71
+ --css <framework> init μ‹œ CSS ν”„λ ˆμž„μ›Œν¬: tailwind, panda, none (κΈ°λ³Έ: tailwind)
72
+ --ui <library> init μ‹œ UI 라이브러리: shadcn, ark, none (κΈ°λ³Έ: shadcn)
73
+ --theme init μ‹œ 닀크λͺ¨λ“œ ν…Œλ§ˆ μ‹œμŠ€ν…œ μΆ”κ°€
74
+ --minimal init μ‹œ CSS/UI 없이 μ΅œμ†Œ ν…œν”Œλ¦Ώ 생성 (--css none --ui none)
75
+ --file <path> spec-upsert spec 파일/monitor 둜그 파일 경둜
76
+ --watch build/guard arch 파일 κ°μ‹œ λͺ¨λ“œ
77
+ --output <path> routes/openapi/doctor/contract/guard 좜λ ₯ 경둜
78
+ --verbose routes list/watch, contract validate, brain status 상세 좜λ ₯
79
+ --from <path> contract diff κΈ°μ€€ λ ˆμ§€μŠ€νŠΈλ¦¬ 경둜
80
+ --to <path> contract diff λŒ€μƒ λ ˆμ§€μŠ€νŠΈλ¦¬ 경둜
81
+ --json contract diff κ²°κ³Ό JSON 좜λ ₯
82
+ --title <title> openapi generate title
83
+ --version <ver> openapi generate version
84
+ --summary monitor μš”μ•½ 좜λ ₯ (JSON λ‘œκ·Έμ—μ„œλ§Œ)
85
+ --since <duration> monitor μš”μ•½ κΈ°κ°„ (예: 5m, 30s, 1h)
86
+ --follow <bool> monitor follow λͺ¨λ“œ (κΈ°λ³Έ: true)
87
+ --message <msg> change begin μ‹œ μ„€λͺ… λ©”μ‹œμ§€
88
+ --id <id> change rollback μ‹œ νŠΉμ • λ³€κ²½ ID
89
+ --keep <n> change prune μ‹œ μœ μ§€ν•  μŠ€λƒ…μƒ· 수 (κΈ°λ³Έ: 5)
90
+ --no-llm doctorμ—μ„œ LLM μ‚¬μš© μ•ˆ 함 (ν…œν”Œλ¦Ώ λͺ¨λ“œ)
91
+ --status watch μƒνƒœλ§Œ 좜λ ₯
92
+ --debounce <ms> watch debounce (ms)
93
+ --model <name> brain setup μ‹œ λͺ¨λΈ 이름 (κΈ°λ³Έ: llama3.2)
94
+ --url <url> brain setup μ‹œ Ollama URL
95
+ --skip-check brain setup μ‹œ λͺ¨λΈ/μ„œλ²„ 체크 κ±΄λ„ˆλœ€
96
+ --help, -h 도움말 ν‘œμ‹œ
97
+
98
+ Notes:
99
+ - 좜λ ₯ 포맷은 ν™˜κ²½μ— 따라 μžλ™ κ²°μ •λ©λ‹ˆλ‹€ (TTY/CI/MANDU_OUTPUT).
100
+ - doctor 좜λ ₯은 .json이면 JSON, κ·Έ μ™ΈλŠ” markdown으둜 μ €μž₯λ©λ‹ˆλ‹€.
101
+ - guard arch λ¦¬ν¬νŠΈλŠ” .json/.html/.md ν™•μž₯자λ₯Ό μžλ™ μΆ”λ‘ ν•©λ‹ˆλ‹€.
102
+ - ν¬νŠΈλŠ” PORT ν™˜κ²½λ³€μˆ˜ λ˜λŠ” mandu.config의 server.port둜 μ„€μ •ν•©λ‹ˆλ‹€.
103
+ - 포트 좩돌 μ‹œ λ‹€μŒ μ‚¬μš© κ°€λŠ₯ν•œ 포트둜 μžλ™ λ³€κ²½λ©λ‹ˆλ‹€.
114
104
 
115
105
  Examples:
116
106
  bunx mandu init --name my-app # Tailwind + shadcn/ui κΈ°λ³Έ
117
107
  bunx mandu init my-app --minimal # CSS/UI 없이 μ΅œμ†Œ ν…œν”Œλ¦Ώ
118
- bunx mandu init my-app --theme # 닀크λͺ¨λ“œ ν…Œλ§ˆ 포함
119
- bunx mandu init my-app --ui none # UI 라이브러리 없이
120
- bunx mandu check
121
- bunx mandu routes list
122
- bunx mandu routes generate
123
- bunx mandu dev --port 3000
124
- bunx mandu dev --no-guard
125
- bunx mandu build --minify
108
+ bunx mandu dev
109
+ bunx mandu build --watch
126
110
  bunx mandu guard
127
- bunx mandu guard arch --preset fsd
128
111
  bunx mandu guard arch --watch
129
- bunx mandu guard arch --ci --format json
130
- bunx mandu guard legacy
131
- bunx mandu monitor
112
+ bunx mandu guard arch --output guard-report.md
113
+ bunx mandu check
114
+ bunx mandu routes list --verbose
115
+ bunx mandu contract create users
116
+ bunx mandu contract validate --verbose
117
+ bunx mandu contract build --output .mandu/contracts.json
118
+ bunx mandu contract diff --json
119
+ bunx mandu openapi generate --output docs/openapi.json
120
+ bunx mandu openapi serve
132
121
  bunx mandu monitor --summary --since 5m
133
- bunx mandu doctor
122
+ bunx mandu doctor --output reports/doctor.json
134
123
  bunx mandu brain setup --model codellama
135
- bunx mandu contract create users
136
- bunx mandu contract build
137
- bunx mandu contract diff
138
- bunx mandu openapi generate --output docs/api.json
139
124
  bunx mandu change begin --message "Add new route"
140
125
 
141
126
  FS Routes Workflow (ꢌμž₯):
@@ -172,31 +157,6 @@ function parseArgs(args: string[]): { command: string; options: Record<string, s
172
157
  return { command, options };
173
158
  }
174
159
 
175
- /**
176
- * 포트 μ˜΅μ…˜ μ•ˆμ „ν•˜κ²Œ νŒŒμ‹±
177
- * - μˆ«μžκ°€ μ•„λ‹ˆλ©΄ undefined λ°˜ν™˜ (κΈ°λ³Έκ°’ μ‚¬μš©)
178
- * - 유효 λ²”μœ„: 1-65535
179
- */
180
- function parsePort(value: string | undefined, optionName = "port"): number | undefined {
181
- if (!value || value === "true") {
182
- return undefined; // κΈ°λ³Έκ°’ μ‚¬μš©
183
- }
184
-
185
- const port = Number(value);
186
-
187
- if (Number.isNaN(port)) {
188
- console.warn(`⚠️ Invalid --${optionName} value: "${value}" (using default)`);
189
- return undefined;
190
- }
191
-
192
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
193
- console.warn(`⚠️ Invalid --${optionName} range: ${port} (must be 1-65535, using default)`);
194
- return undefined;
195
- }
196
-
197
- return port;
198
- }
199
-
200
160
  async function main(): Promise<void> {
201
161
  const args = process.argv.slice(2);
202
162
  const { command, options } = parseArgs(args);
@@ -228,30 +188,15 @@ async function main(): Promise<void> {
228
188
  break;
229
189
 
230
190
  case "check":
231
- success = await check({
232
- preset: options.preset as any,
233
- format: options.format as any,
234
- ci: options.ci === "true",
235
- quiet: options.quiet === "true",
236
- legacy: options.legacy === "true",
237
- });
191
+ success = await check();
238
192
  break;
239
193
 
240
194
  case "guard": {
241
195
  const subCommand = args[1];
242
196
  const hasSubCommand = subCommand && !subCommand.startsWith("--");
243
197
  const guardArchOptions = {
244
- preset: options.preset as any,
245
198
  watch: options.watch === "true",
246
- ci: options.ci === "true",
247
- format: options.format as any,
248
- quiet: options.quiet === "true",
249
- srcDir: options["src-dir"],
250
- listPresets: options["list-presets"] === "true",
251
199
  output: options.output,
252
- reportFormat: (options["report-format"] as any) || "markdown",
253
- saveStats: options["save-stats"] === "true",
254
- showTrend: options["show-trend"] === "true",
255
200
  };
256
201
  switch (subCommand) {
257
202
  case "arch":
@@ -259,13 +204,14 @@ async function main(): Promise<void> {
259
204
  break;
260
205
  case "legacy":
261
206
  case "spec":
262
- success = await guardCheck({
263
- autoCorrect: options["no-auto-correct"] !== "true",
264
- });
207
+ success = await guardCheck();
265
208
  break;
266
209
  default:
267
210
  if (hasSubCommand) {
268
- console.error(`❌ Unknown guard subcommand: ${subCommand}`);
211
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
212
+ command: "guard",
213
+ subcommand,
214
+ });
269
215
  console.log("\nUsage: bunx mandu guard <arch|legacy>");
270
216
  process.exit(1);
271
217
  }
@@ -277,20 +223,12 @@ async function main(): Promise<void> {
277
223
 
278
224
  case "build":
279
225
  success = await build({
280
- minify: options.minify === "true",
281
- sourcemap: options.sourcemap === "true",
282
226
  watch: options.watch === "true",
283
227
  });
284
228
  break;
285
229
 
286
230
  case "dev":
287
- await dev({
288
- port: parsePort(options.port),
289
- guard: options["no-guard"] === "true" ? false : options.guard !== "false",
290
- guardPreset: options["guard-preset"] as any,
291
- guardFormat: options["guard-format"] as any,
292
- legacy: options.legacy === "true",
293
- });
231
+ await dev();
294
232
  break;
295
233
 
296
234
  case "routes": {
@@ -320,7 +258,10 @@ async function main(): Promise<void> {
320
258
  verbose: options.verbose === "true",
321
259
  });
322
260
  } else {
323
- console.error(`❌ Unknown routes subcommand: ${subCommand}`);
261
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
262
+ command: "routes",
263
+ subcommand,
264
+ });
324
265
  console.log("\nUsage: bunx mandu routes <generate|list|watch>");
325
266
  process.exit(1);
326
267
  }
@@ -334,7 +275,7 @@ async function main(): Promise<void> {
334
275
  case "create": {
335
276
  const routeId = args[2] || options._positional;
336
277
  if (!routeId) {
337
- console.error("❌ Route ID is required");
278
+ printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
338
279
  console.log("\nUsage: bunx mandu contract create <routeId>");
339
280
  process.exit(1);
340
281
  }
@@ -356,7 +297,10 @@ async function main(): Promise<void> {
356
297
  });
357
298
  break;
358
299
  default:
359
- console.error(`❌ Unknown contract subcommand: ${subCommand}`);
300
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
301
+ command: "contract",
302
+ subcommand,
303
+ });
360
304
  console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
361
305
  process.exit(1);
362
306
  }
@@ -374,12 +318,13 @@ async function main(): Promise<void> {
374
318
  });
375
319
  break;
376
320
  case "serve":
377
- success = await openAPIServe({
378
- port: parsePort(options.port),
379
- });
321
+ success = await openAPIServe();
380
322
  break;
381
323
  default:
382
- console.error(`❌ Unknown openapi subcommand: ${subCommand}`);
324
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
325
+ command: "openapi",
326
+ subcommand,
327
+ });
383
328
  console.log("\nUsage: bunx mandu openapi <generate|serve>");
384
329
  process.exit(1);
385
330
  }
@@ -410,7 +355,10 @@ async function main(): Promise<void> {
410
355
  });
411
356
  break;
412
357
  default:
413
- console.error(`❌ Unknown change subcommand: ${subCommand}`);
358
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
359
+ command: "change",
360
+ subcommand,
361
+ });
414
362
  console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
415
363
  process.exit(1);
416
364
  }
@@ -419,7 +367,6 @@ async function main(): Promise<void> {
419
367
 
420
368
  case "doctor":
421
369
  success = await doctor({
422
- format: (options.format as "console" | "json" | "markdown") || "console",
423
370
  useLLM: options["no-llm"] !== "true",
424
371
  output: options.output,
425
372
  });
@@ -434,7 +381,6 @@ async function main(): Promise<void> {
434
381
 
435
382
  case "monitor":
436
383
  success = await monitor({
437
- format: options.format as any,
438
384
  summary: options.summary === "true",
439
385
  since: options.since,
440
386
  follow: options.follow === "false" ? false : true,
@@ -458,7 +404,10 @@ async function main(): Promise<void> {
458
404
  });
459
405
  break;
460
406
  default:
461
- console.error(`❌ Unknown brain subcommand: ${subCommand}`);
407
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
408
+ command: "brain",
409
+ subcommand,
410
+ });
462
411
  console.log("\nUsage: bunx mandu brain <setup|status>");
463
412
  process.exit(1);
464
413
  }
@@ -466,7 +415,7 @@ async function main(): Promise<void> {
466
415
  }
467
416
 
468
417
  default:
469
- console.error(`❌ Unknown command: ${command}`);
418
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
470
419
  console.log(HELP_TEXT);
471
420
  process.exit(1);
472
421
  }
@@ -476,7 +425,4 @@ async function main(): Promise<void> {
476
425
  }
477
426
  }
478
427
 
479
- main().catch((error) => {
480
- console.error("❌ μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜:", error);
481
- process.exit(1);
482
- });
428
+ main().catch((error) => handleCLIError(error));
@@ -0,0 +1,6 @@
1
+ export function importFresh<T = unknown>(modulePath: string): Promise<T> {
2
+ const url = Bun.pathToFileURL(modulePath);
3
+ const cacheBusted = new URL(url.href);
4
+ cacheBusted.searchParams.set("t", Date.now().toString());
5
+ return import(cacheBusted.href) as Promise<T>;
6
+ }
@@ -0,0 +1,52 @@
1
+ import path from "path";
2
+ import {
3
+ generateManifest,
4
+ loadManifest,
5
+ type RoutesManifest,
6
+ type FSScannerConfig,
7
+ } from "@mandujs/core";
8
+ import { isDirectory } from "./fs";
9
+
10
+ export type ManifestSource = "fs" | "spec";
11
+
12
+ export interface ResolvedManifest {
13
+ manifest: RoutesManifest;
14
+ source: ManifestSource;
15
+ warnings: string[];
16
+ }
17
+
18
+ export async function resolveManifest(
19
+ rootDir: string,
20
+ options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
21
+ ): Promise<ResolvedManifest> {
22
+ const appDir = path.resolve(rootDir, "app");
23
+ const hasApp = await isDirectory(appDir);
24
+
25
+ if (hasApp) {
26
+ const result = await generateManifest(rootDir, {
27
+ scanner: options.fsRoutes,
28
+ outputPath: options.outputPath,
29
+ skipLegacy: true,
30
+ });
31
+ return {
32
+ manifest: result.manifest,
33
+ source: "fs",
34
+ warnings: result.warnings,
35
+ };
36
+ }
37
+
38
+ const specPath = path.join(rootDir, "spec", "routes.manifest.json");
39
+ if (await Bun.file(specPath).exists()) {
40
+ const result = await loadManifest(specPath);
41
+ if (!result.success) {
42
+ throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
43
+ }
44
+ return {
45
+ manifest: result.data!,
46
+ source: "spec",
47
+ warnings: [],
48
+ };
49
+ }
50
+
51
+ throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
52
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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