@mandujs/cli 0.9.45 โ†’ 0.10.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.
package/src/main.ts CHANGED
@@ -1,428 +1,445 @@
1
- #!/usr/bin/env bun
2
-
3
- import { specUpsert } from "./commands/spec-upsert";
4
- import { generateApply } from "./commands/generate-apply";
5
- import { guardCheck } from "./commands/guard-check";
6
- import { guardArch } from "./commands/guard-arch";
7
- import { check } from "./commands/check";
8
- import { dev } from "./commands/dev";
9
- import { init } from "./commands/init";
10
- import { build } from "./commands/build";
11
- import { contractCreate, contractValidate, contractBuild, contractDiff } from "./commands/contract";
12
- import { openAPIGenerate, openAPIServe } from "./commands/openapi";
13
- import {
14
- changeBegin,
15
- changeCommit,
16
- changeRollback,
17
- changeStatus,
18
- changeList,
19
- changePrune,
20
- } from "./commands/change";
21
- import { doctor } from "./commands/doctor";
22
- import { watch } from "./commands/watch";
23
- import { brainSetup, brainStatus } from "./commands/brain";
24
- import { routesGenerate, routesList, routesWatch } from "./commands/routes";
25
- import { monitor } from "./commands/monitor";
26
- import { CLI_ERROR_CODES, handleCLIError, printCLIError } from "./errors";
27
-
28
- const HELP_TEXT = `
29
- ๐ŸฅŸ Mandu CLI - Agent-Native Fullstack Framework
30
-
31
- Usage: bunx mandu <command> [options]
32
-
33
- Commands:
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 ์ƒํƒœ ํ™•์ธ
53
-
54
- contract create <routeId> ๋ผ์šฐํŠธ์— ๋Œ€ํ•œ Contract ์ƒ์„ฑ
55
- contract validate Contract-Slot ์ผ๊ด€์„ฑ ๊ฒ€์ฆ
56
- contract build Contract ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒ์„ฑ
57
- contract diff Contract ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋น„๊ต
58
-
59
- openapi generate OpenAPI 3.0 ์ŠคํŽ™ ์ƒ์„ฑ
60
- openapi serve Swagger UI ๋กœ์ปฌ ์„œ๋ฒ„ ์‹คํ–‰
61
-
62
- change begin ๋ณ€๊ฒฝ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ (์Šค๋ƒ…์ƒท ์ƒ์„ฑ)
63
- change commit ๋ณ€๊ฒฝ ํ™•์ •
64
- change rollback ์Šค๋ƒ…์ƒท์œผ๋กœ ๋ณต์›
65
- change status ํ˜„์žฌ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ
66
- change list ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ
67
- change prune ์˜ค๋ž˜๋œ ์Šค๋ƒ…์ƒท ์ •๋ฆฌ
68
-
69
- Options:
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
- - ํฌํŠธ ์ถฉ๋Œ ์‹œ ๋‹ค์Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํฌํŠธ๋กœ ์ž๋™ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.
104
-
105
- Examples:
106
- bunx mandu init --name my-app # Tailwind + shadcn/ui ๊ธฐ๋ณธ
107
- bunx mandu init my-app --minimal # CSS/UI ์—†์ด ์ตœ์†Œ ํ…œํ”Œ๋ฆฟ
108
- bunx mandu dev
109
- bunx mandu build --watch
110
- bunx mandu guard
111
- bunx mandu guard arch --watch
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
121
- bunx mandu monitor --summary --since 5m
122
- bunx mandu doctor --output reports/doctor.json
123
- bunx mandu brain setup --model codellama
124
- bunx mandu change begin --message "Add new route"
125
-
126
- FS Routes Workflow (๊ถŒ์žฅ):
127
- 1. init โ†’ 2. app/ ํด๋”์— page.tsx ์ƒ์„ฑ โ†’ 3. dev โ†’ 4. build
128
-
129
- Legacy Workflow:
130
- 1. init โ†’ 2. spec-upsert โ†’ 3. generate โ†’ 4. build โ†’ 5. guard โ†’ 6. dev
131
-
132
- Contract-first Workflow:
133
- 1. contract create โ†’ 2. Edit contract โ†’ 3. generate โ†’ 4. Edit slot โ†’ 5. contract validate
134
-
135
- Brain (sLLM) Workflow:
136
- 1. brain setup โ†’ 2. doctor (๋ถ„์„) โ†’ 3. watch (๊ฐ์‹œ)
137
- `;
138
-
139
- function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
140
- const command = args[0] || "";
141
- const options: Record<string, string> = {};
142
-
143
- for (let i = 1; i < args.length; i++) {
144
- const arg = args[i];
145
- if (arg.startsWith("--")) {
146
- const key = arg.slice(2);
147
- const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
148
- options[key] = value;
149
- } else if (arg === "-h") {
150
- options["help"] = "true";
151
- } else if (!options._positional) {
152
- // First non-flag argument after command is positional (e.g., project name)
153
- options._positional = arg;
154
- }
155
- }
156
-
157
- return { command, options };
158
- }
159
-
160
- async function main(): Promise<void> {
161
- const args = process.argv.slice(2);
162
- const { command, options } = parseArgs(args);
163
-
164
- if (options.help || command === "help" || !command) {
165
- console.log(HELP_TEXT);
166
- process.exit(0);
167
- }
168
-
169
- let success = true;
170
-
171
- switch (command) {
172
- case "init":
173
- success = await init({
174
- name: options.name || options._positional,
175
- css: options.css as any,
176
- ui: options.ui as any,
177
- theme: options.theme === "true",
178
- minimal: options.minimal === "true",
179
- });
180
- break;
181
-
182
- case "spec-upsert":
183
- success = await specUpsert({ file: options.file });
184
- break;
185
-
186
- case "generate":
187
- success = await generateApply();
188
- break;
189
-
190
- case "check":
191
- success = await check();
192
- break;
193
-
194
- case "guard": {
195
- const subCommand = args[1];
196
- const hasSubCommand = subCommand && !subCommand.startsWith("--");
197
- const guardArchOptions = {
198
- watch: options.watch === "true",
199
- output: options.output,
200
- };
201
- switch (subCommand) {
202
- case "arch":
203
- success = await guardArch(guardArchOptions);
204
- break;
205
- case "legacy":
206
- case "spec":
207
- success = await guardCheck();
208
- break;
209
- default:
210
- if (hasSubCommand) {
211
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
212
- command: "guard",
213
- subcommand,
214
- });
215
- console.log("\nUsage: bunx mandu guard <arch|legacy>");
216
- process.exit(1);
217
- }
218
- // ๊ธฐ๋ณธ๊ฐ’: architecture guard
219
- success = await guardArch(guardArchOptions);
220
- }
221
- break;
222
- }
223
-
224
- case "build":
225
- success = await build({
226
- watch: options.watch === "true",
227
- });
228
- break;
229
-
230
- case "dev":
231
- await dev();
232
- break;
233
-
234
- case "routes": {
235
- const subCommand = args[1];
236
- switch (subCommand) {
237
- case "generate":
238
- success = await routesGenerate({
239
- output: options.output,
240
- verbose: options.verbose === "true",
241
- });
242
- break;
243
- case "list":
244
- success = await routesList({
245
- verbose: options.verbose === "true",
246
- });
247
- break;
248
- case "watch":
249
- success = await routesWatch({
250
- output: options.output,
251
- verbose: options.verbose === "true",
252
- });
253
- break;
254
- default:
255
- // ๊ธฐ๋ณธ๊ฐ’: list
256
- if (!subCommand) {
257
- success = await routesList({
258
- verbose: options.verbose === "true",
259
- });
260
- } else {
261
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
262
- command: "routes",
263
- subcommand,
264
- });
265
- console.log("\nUsage: bunx mandu routes <generate|list|watch>");
266
- process.exit(1);
267
- }
268
- }
269
- break;
270
- }
271
-
272
- case "contract": {
273
- const subCommand = args[1];
274
- switch (subCommand) {
275
- case "create": {
276
- const routeId = args[2] || options._positional;
277
- if (!routeId) {
278
- printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
279
- console.log("\nUsage: bunx mandu contract create <routeId>");
280
- process.exit(1);
281
- }
282
- success = await contractCreate({ routeId });
283
- break;
284
- }
285
- case "validate":
286
- success = await contractValidate({ verbose: options.verbose === "true" });
287
- break;
288
- case "build":
289
- success = await contractBuild({ output: options.output });
290
- break;
291
- case "diff":
292
- success = await contractDiff({
293
- from: options.from,
294
- to: options.to,
295
- output: options.output,
296
- json: options.json === "true",
297
- });
298
- break;
299
- default:
300
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
301
- command: "contract",
302
- subcommand,
303
- });
304
- console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
305
- process.exit(1);
306
- }
307
- break;
308
- }
309
-
310
- case "openapi": {
311
- const subCommand = args[1];
312
- switch (subCommand) {
313
- case "generate":
314
- success = await openAPIGenerate({
315
- output: options.output,
316
- title: options.title,
317
- version: options.version,
318
- });
319
- break;
320
- case "serve":
321
- success = await openAPIServe();
322
- break;
323
- default:
324
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
325
- command: "openapi",
326
- subcommand,
327
- });
328
- console.log("\nUsage: bunx mandu openapi <generate|serve>");
329
- process.exit(1);
330
- }
331
- break;
332
- }
333
-
334
- case "change": {
335
- const subCommand = args[1];
336
- switch (subCommand) {
337
- case "begin":
338
- success = await changeBegin({ message: options.message });
339
- break;
340
- case "commit":
341
- success = await changeCommit();
342
- break;
343
- case "rollback":
344
- success = await changeRollback({ id: options.id });
345
- break;
346
- case "status":
347
- success = await changeStatus();
348
- break;
349
- case "list":
350
- success = await changeList();
351
- break;
352
- case "prune":
353
- success = await changePrune({
354
- keep: options.keep ? Number(options.keep) : undefined,
355
- });
356
- break;
357
- default:
358
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
359
- command: "change",
360
- subcommand,
361
- });
362
- console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
363
- process.exit(1);
364
- }
365
- break;
366
- }
367
-
368
- case "doctor":
369
- success = await doctor({
370
- useLLM: options["no-llm"] !== "true",
371
- output: options.output,
372
- });
373
- break;
374
-
375
- case "watch":
376
- success = await watch({
377
- status: options.status === "true",
378
- debounce: options.debounce ? Number(options.debounce) : undefined,
379
- });
380
- break;
381
-
382
- case "monitor":
383
- success = await monitor({
384
- summary: options.summary === "true",
385
- since: options.since,
386
- follow: options.follow === "false" ? false : true,
387
- file: options.file,
388
- });
389
- break;
390
-
391
- case "brain": {
392
- const subCommand = args[1];
393
- switch (subCommand) {
394
- case "setup":
395
- success = await brainSetup({
396
- model: options.model,
397
- url: options.url,
398
- skipCheck: options["skip-check"] === "true",
399
- });
400
- break;
401
- case "status":
402
- success = await brainStatus({
403
- verbose: options.verbose === "true",
404
- });
405
- break;
406
- default:
407
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
408
- command: "brain",
409
- subcommand,
410
- });
411
- console.log("\nUsage: bunx mandu brain <setup|status>");
412
- process.exit(1);
413
- }
414
- break;
415
- }
416
-
417
- default:
418
- printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
419
- console.log(HELP_TEXT);
420
- process.exit(1);
421
- }
422
-
423
- if (!success) {
424
- process.exit(1);
425
- }
426
- }
427
-
428
- main().catch((error) => handleCLIError(error));
1
+ #!/usr/bin/env bun
2
+
3
+ import { specUpsert } from "./commands/spec-upsert";
4
+ import { generateApply } from "./commands/generate-apply";
5
+ import { guardCheck } from "./commands/guard-check";
6
+ import { guardArch } from "./commands/guard-arch";
7
+ import { check } from "./commands/check";
8
+ import { dev } from "./commands/dev";
9
+ import { init } from "./commands/init";
10
+ import { build } from "./commands/build";
11
+ import { contractCreate, contractValidate, contractBuild, contractDiff } from "./commands/contract";
12
+ import { openAPIGenerate, openAPIServe } from "./commands/openapi";
13
+ import {
14
+ changeBegin,
15
+ changeCommit,
16
+ changeRollback,
17
+ changeStatus,
18
+ changeList,
19
+ changePrune,
20
+ } from "./commands/change";
21
+ import { doctor } from "./commands/doctor";
22
+ import { watch } from "./commands/watch";
23
+ import { brainSetup, brainStatus } from "./commands/brain";
24
+ import { routesGenerate, routesList, routesWatch } from "./commands/routes";
25
+ import { monitor } from "./commands/monitor";
26
+ import { runLockCommand, lockHelp } from "./commands/lock";
27
+ import { CLI_ERROR_CODES, handleCLIError, printCLIError } from "./errors";
28
+
29
+ const HELP_TEXT = `
30
+ ๐ŸฅŸ Mandu CLI - Agent-Native Fullstack Framework
31
+
32
+ Usage: bunx mandu <command> [options]
33
+
34
+ Commands:
35
+ init ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ (Tailwind + shadcn/ui ๊ธฐ๋ณธ ํฌํ•จ)
36
+ check FS Routes + Guard ํ†ตํ•ฉ ๊ฒ€์‚ฌ
37
+ routes generate FS Routes ์Šค์บ” ๋ฐ ๋งค๋‹ˆํŽ˜์ŠคํŠธ ์ƒ์„ฑ
38
+ routes list ํ˜„์žฌ ๋ผ์šฐํŠธ ๋ชฉ๋ก ์ถœ๋ ฅ
39
+ routes watch ์‹ค์‹œ๊ฐ„ ๋ผ์šฐํŠธ ๊ฐ์‹œ
40
+ dev ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ (FS Routes + Guard ๊ธฐ๋ณธ)
41
+ build ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค ๋นŒ๋“œ (Hydration)
42
+ guard ์•„ํ‚คํ…์ฒ˜ ์œ„๋ฐ˜ ๊ฒ€์‚ฌ (๊ธฐ๋ณธ)
43
+ guard arch ์•„ํ‚คํ…์ฒ˜ ์œ„๋ฐ˜ ๊ฒ€์‚ฌ (FSD/Clean/Hexagonal)
44
+ guard legacy ๋ ˆ๊ฑฐ์‹œ Spec Guard ๊ฒ€์‚ฌ
45
+ spec-upsert Spec ํŒŒ์ผ ๊ฒ€์ฆ ๋ฐ lock ๊ฐฑ์‹  (๋ ˆ๊ฑฐ์‹œ)
46
+ generate Spec์—์„œ ์ฝ”๋“œ ์ƒ์„ฑ (๋ ˆ๊ฑฐ์‹œ)
47
+
48
+ doctor Guard ์‹คํŒจ ๋ถ„์„ + ํŒจ์น˜ ์ œ์•ˆ (Brain)
49
+ watch ์‹ค์‹œ๊ฐ„ ํŒŒ์ผ ๊ฐ์‹œ - ๊ฒฝ๊ณ ๋งŒ (Brain)
50
+ monitor MCP Activity Monitor ๋กœ๊ทธ ์ŠคํŠธ๋ฆผ
51
+
52
+ brain setup sLLM ์„ค์ • (์„ ํƒ)
53
+ brain status Brain ์ƒํƒœ ํ™•์ธ
54
+
55
+ contract create <routeId> ๋ผ์šฐํŠธ์— ๋Œ€ํ•œ Contract ์ƒ์„ฑ
56
+ contract validate Contract-Slot ์ผ๊ด€์„ฑ ๊ฒ€์ฆ
57
+ contract build Contract ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ƒ์„ฑ
58
+ contract diff Contract ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋น„๊ต
59
+
60
+ openapi generate OpenAPI 3.0 ์ŠคํŽ™ ์ƒ์„ฑ
61
+ openapi serve Swagger UI ๋กœ์ปฌ ์„œ๋ฒ„ ์‹คํ–‰
62
+
63
+ change begin ๋ณ€๊ฒฝ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ (์Šค๋ƒ…์ƒท ์ƒ์„ฑ)
64
+ change commit ๋ณ€๊ฒฝ ํ™•์ •
65
+ change rollback ์Šค๋ƒ…์ƒท์œผ๋กœ ๋ณต์›
66
+ change status ํ˜„์žฌ ํŠธ๋žœ์žญ์…˜ ์ƒํƒœ
67
+ change list ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ
68
+ change prune ์˜ค๋ž˜๋œ ์Šค๋ƒ…์ƒท ์ •๋ฆฌ
69
+
70
+ lock Lockfile ์ƒ์„ฑ/๊ฐฑ์‹ 
71
+ lock --verify Lockfile ๊ฒ€์ฆ (์„ค์ • ๋ฌด๊ฒฐ์„ฑ ํ™•์ธ)
72
+ lock --diff Lockfile๊ณผ ํ˜„์žฌ ์„ค์ • ๋น„๊ต
73
+
74
+ Options:
75
+ --name <name> init ์‹œ ํ”„๋กœ์ ํŠธ ์ด๋ฆ„ (๊ธฐ๋ณธ: my-mandu-app)
76
+ --css <framework> init ์‹œ CSS ํ”„๋ ˆ์ž„์›Œํฌ: tailwind, panda, none (๊ธฐ๋ณธ: tailwind)
77
+ --ui <library> init ์‹œ UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: shadcn, ark, none (๊ธฐ๋ณธ: shadcn)
78
+ --theme init ์‹œ ๋‹คํฌ๋ชจ๋“œ ํ…Œ๋งˆ ์‹œ์Šคํ…œ ์ถ”๊ฐ€
79
+ --minimal init ์‹œ CSS/UI ์—†์ด ์ตœ์†Œ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ (--css none --ui none)
80
+ --file <path> spec-upsert spec ํŒŒ์ผ/monitor ๋กœ๊ทธ ํŒŒ์ผ ๊ฒฝ๋กœ
81
+ --watch build/guard arch ํŒŒ์ผ ๊ฐ์‹œ ๋ชจ๋“œ
82
+ --output <path> routes/openapi/doctor/contract/guard ์ถœ๋ ฅ ๊ฒฝ๋กœ
83
+ --verbose routes list/watch, contract validate, brain status ์ƒ์„ธ ์ถœ๋ ฅ
84
+ --from <path> contract diff ๊ธฐ์ค€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฒฝ๋กœ
85
+ --to <path> contract diff ๋Œ€์ƒ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ฒฝ๋กœ
86
+ --json contract diff ๊ฒฐ๊ณผ JSON ์ถœ๋ ฅ
87
+ --title <title> openapi generate title
88
+ --version <ver> openapi generate version
89
+ --summary monitor ์š”์•ฝ ์ถœ๋ ฅ (JSON ๋กœ๊ทธ์—์„œ๋งŒ)
90
+ --since <duration> monitor ์š”์•ฝ ๊ธฐ๊ฐ„ (์˜ˆ: 5m, 30s, 1h)
91
+ --follow <bool> monitor follow ๋ชจ๋“œ (๊ธฐ๋ณธ: true)
92
+ --message <msg> change begin ์‹œ ์„ค๋ช… ๋ฉ”์‹œ์ง€
93
+ --id <id> change rollback ์‹œ ํŠน์ • ๋ณ€๊ฒฝ ID
94
+ --keep <n> change prune ์‹œ ์œ ์ง€ํ•  ์Šค๋ƒ…์ƒท ์ˆ˜ (๊ธฐ๋ณธ: 5)
95
+ --verify, -v lock ์‹œ lockfile ๊ฒ€์ฆ๋งŒ ์ˆ˜ํ–‰
96
+ --diff, -d lock ์‹œ lockfile๊ณผ ํ˜„์žฌ ์„ค์ • ๋น„๊ต
97
+ --show-secrets lock diff ์‹œ ๋ฏผ๊ฐ์ •๋ณด ์ถœ๋ ฅ ํ—ˆ์šฉ
98
+ --include-snapshot lock ์‹œ ์„ค์ • ์Šค๋ƒ…์ƒท ํฌํ•จ (diff ๊ธฐ๋Šฅ์— ํ•„์š”)
99
+ --mode <mode> lock verify ์‹œ ๋ชจ๋“œ (development|build|ci|production)
100
+ --no-llm doctor์—์„œ LLM ์‚ฌ์šฉ ์•ˆ ํ•จ (ํ…œํ”Œ๋ฆฟ ๋ชจ๋“œ)
101
+ --status watch ์ƒํƒœ๋งŒ ์ถœ๋ ฅ
102
+ --debounce <ms> watch debounce (ms)
103
+ --model <name> brain setup ์‹œ ๋ชจ๋ธ ์ด๋ฆ„ (๊ธฐ๋ณธ: llama3.2)
104
+ --url <url> brain setup ์‹œ Ollama URL
105
+ --skip-check brain setup ์‹œ ๋ชจ๋ธ/์„œ๋ฒ„ ์ฒดํฌ ๊ฑด๋„ˆ๋œ€
106
+ --help, -h ๋„์›€๋ง ํ‘œ์‹œ
107
+
108
+ Notes:
109
+ - ์ถœ๋ ฅ ํฌ๋งท์€ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ž๋™ ๊ฒฐ์ •๋ฉ๋‹ˆ๋‹ค (TTY/CI/MANDU_OUTPUT).
110
+ - doctor ์ถœ๋ ฅ์€ .json์ด๋ฉด JSON, ๊ทธ ์™ธ๋Š” markdown์œผ๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
111
+ - guard arch ๋ฆฌํฌํŠธ๋Š” .json/.html/.md ํ™•์žฅ์ž๋ฅผ ์ž๋™ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค.
112
+ - ํฌํŠธ๋Š” PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋˜๋Š” mandu.config์˜ server.port๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
113
+ - ํฌํŠธ ์ถฉ๋Œ ์‹œ ๋‹ค์Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํฌํŠธ๋กœ ์ž๋™ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.
114
+
115
+ Examples:
116
+ bunx mandu init --name my-app # Tailwind + shadcn/ui ๊ธฐ๋ณธ
117
+ bunx mandu init my-app --minimal # CSS/UI ์—†์ด ์ตœ์†Œ ํ…œํ”Œ๋ฆฟ
118
+ bunx mandu dev
119
+ bunx mandu build --watch
120
+ bunx mandu guard
121
+ bunx mandu guard arch --watch
122
+ bunx mandu guard arch --output guard-report.md
123
+ bunx mandu check
124
+ bunx mandu routes list --verbose
125
+ bunx mandu contract create users
126
+ bunx mandu contract validate --verbose
127
+ bunx mandu contract build --output .mandu/contracts.json
128
+ bunx mandu contract diff --json
129
+ bunx mandu openapi generate --output docs/openapi.json
130
+ bunx mandu openapi serve
131
+ bunx mandu monitor --summary --since 5m
132
+ bunx mandu doctor --output reports/doctor.json
133
+ bunx mandu brain setup --model codellama
134
+ bunx mandu change begin --message "Add new route"
135
+ bunx mandu lock # Lockfile ์ƒ์„ฑ/๊ฐฑ์‹ 
136
+ bunx mandu lock --verify # ์„ค์ • ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ
137
+ bunx mandu lock --diff --show-secrets # ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ƒ์„ธ ๋น„๊ต
138
+
139
+ FS Routes Workflow (๊ถŒ์žฅ):
140
+ 1. init โ†’ 2. app/ ํด๋”์— page.tsx ์ƒ์„ฑ โ†’ 3. dev โ†’ 4. build
141
+
142
+ Legacy Workflow:
143
+ 1. init โ†’ 2. spec-upsert โ†’ 3. generate โ†’ 4. build โ†’ 5. guard โ†’ 6. dev
144
+
145
+ Contract-first Workflow:
146
+ 1. contract create โ†’ 2. Edit contract โ†’ 3. generate โ†’ 4. Edit slot โ†’ 5. contract validate
147
+
148
+ Brain (sLLM) Workflow:
149
+ 1. brain setup โ†’ 2. doctor (๋ถ„์„) โ†’ 3. watch (๊ฐ์‹œ)
150
+ `;
151
+
152
+ function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
153
+ const command = args[0] || "";
154
+ const options: Record<string, string> = {};
155
+
156
+ for (let i = 1; i < args.length; i++) {
157
+ const arg = args[i];
158
+ if (arg.startsWith("--")) {
159
+ const key = arg.slice(2);
160
+ const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
161
+ options[key] = value;
162
+ } else if (arg === "-h") {
163
+ options["help"] = "true";
164
+ } else if (!options._positional) {
165
+ // First non-flag argument after command is positional (e.g., project name)
166
+ options._positional = arg;
167
+ }
168
+ }
169
+
170
+ return { command, options };
171
+ }
172
+
173
+ async function main(): Promise<void> {
174
+ const args = process.argv.slice(2);
175
+ const { command, options } = parseArgs(args);
176
+
177
+ if (options.help || command === "help" || !command) {
178
+ console.log(HELP_TEXT);
179
+ process.exit(0);
180
+ }
181
+
182
+ let success = true;
183
+
184
+ switch (command) {
185
+ case "init":
186
+ success = await init({
187
+ name: options.name || options._positional,
188
+ css: options.css as any,
189
+ ui: options.ui as any,
190
+ theme: options.theme === "true",
191
+ minimal: options.minimal === "true",
192
+ });
193
+ break;
194
+
195
+ case "spec-upsert":
196
+ success = await specUpsert({ file: options.file });
197
+ break;
198
+
199
+ case "generate":
200
+ success = await generateApply();
201
+ break;
202
+
203
+ case "check":
204
+ success = await check();
205
+ break;
206
+
207
+ case "guard": {
208
+ const subCommand = args[1];
209
+ const hasSubCommand = subCommand && !subCommand.startsWith("--");
210
+ const guardArchOptions = {
211
+ watch: options.watch === "true",
212
+ output: options.output,
213
+ };
214
+ switch (subCommand) {
215
+ case "arch":
216
+ success = await guardArch(guardArchOptions);
217
+ break;
218
+ case "legacy":
219
+ case "spec":
220
+ success = await guardCheck();
221
+ break;
222
+ default:
223
+ if (hasSubCommand) {
224
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
225
+ command: "guard",
226
+ subcommand,
227
+ });
228
+ console.log("\nUsage: bunx mandu guard <arch|legacy>");
229
+ process.exit(1);
230
+ }
231
+ // ๊ธฐ๋ณธ๊ฐ’: architecture guard
232
+ success = await guardArch(guardArchOptions);
233
+ }
234
+ break;
235
+ }
236
+
237
+ case "build":
238
+ success = await build({
239
+ watch: options.watch === "true",
240
+ });
241
+ break;
242
+
243
+ case "dev":
244
+ await dev();
245
+ break;
246
+
247
+ case "routes": {
248
+ const subCommand = args[1];
249
+ switch (subCommand) {
250
+ case "generate":
251
+ success = await routesGenerate({
252
+ output: options.output,
253
+ verbose: options.verbose === "true",
254
+ });
255
+ break;
256
+ case "list":
257
+ success = await routesList({
258
+ verbose: options.verbose === "true",
259
+ });
260
+ break;
261
+ case "watch":
262
+ success = await routesWatch({
263
+ output: options.output,
264
+ verbose: options.verbose === "true",
265
+ });
266
+ break;
267
+ default:
268
+ // ๊ธฐ๋ณธ๊ฐ’: list
269
+ if (!subCommand) {
270
+ success = await routesList({
271
+ verbose: options.verbose === "true",
272
+ });
273
+ } else {
274
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
275
+ command: "routes",
276
+ subcommand,
277
+ });
278
+ console.log("\nUsage: bunx mandu routes <generate|list|watch>");
279
+ process.exit(1);
280
+ }
281
+ }
282
+ break;
283
+ }
284
+
285
+ case "contract": {
286
+ const subCommand = args[1];
287
+ switch (subCommand) {
288
+ case "create": {
289
+ const routeId = args[2] || options._positional;
290
+ if (!routeId) {
291
+ printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
292
+ console.log("\nUsage: bunx mandu contract create <routeId>");
293
+ process.exit(1);
294
+ }
295
+ success = await contractCreate({ routeId });
296
+ break;
297
+ }
298
+ case "validate":
299
+ success = await contractValidate({ verbose: options.verbose === "true" });
300
+ break;
301
+ case "build":
302
+ success = await contractBuild({ output: options.output });
303
+ break;
304
+ case "diff":
305
+ success = await contractDiff({
306
+ from: options.from,
307
+ to: options.to,
308
+ output: options.output,
309
+ json: options.json === "true",
310
+ });
311
+ break;
312
+ default:
313
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
314
+ command: "contract",
315
+ subcommand,
316
+ });
317
+ console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
318
+ process.exit(1);
319
+ }
320
+ break;
321
+ }
322
+
323
+ case "openapi": {
324
+ const subCommand = args[1];
325
+ switch (subCommand) {
326
+ case "generate":
327
+ success = await openAPIGenerate({
328
+ output: options.output,
329
+ title: options.title,
330
+ version: options.version,
331
+ });
332
+ break;
333
+ case "serve":
334
+ success = await openAPIServe();
335
+ break;
336
+ default:
337
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
338
+ command: "openapi",
339
+ subcommand,
340
+ });
341
+ console.log("\nUsage: bunx mandu openapi <generate|serve>");
342
+ process.exit(1);
343
+ }
344
+ break;
345
+ }
346
+
347
+ case "change": {
348
+ const subCommand = args[1];
349
+ switch (subCommand) {
350
+ case "begin":
351
+ success = await changeBegin({ message: options.message });
352
+ break;
353
+ case "commit":
354
+ success = await changeCommit();
355
+ break;
356
+ case "rollback":
357
+ success = await changeRollback({ id: options.id });
358
+ break;
359
+ case "status":
360
+ success = await changeStatus();
361
+ break;
362
+ case "list":
363
+ success = await changeList();
364
+ break;
365
+ case "prune":
366
+ success = await changePrune({
367
+ keep: options.keep ? Number(options.keep) : undefined,
368
+ });
369
+ break;
370
+ default:
371
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
372
+ command: "change",
373
+ subcommand,
374
+ });
375
+ console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
376
+ process.exit(1);
377
+ }
378
+ break;
379
+ }
380
+
381
+ case "doctor":
382
+ success = await doctor({
383
+ useLLM: options["no-llm"] !== "true",
384
+ output: options.output,
385
+ });
386
+ break;
387
+
388
+ case "watch":
389
+ success = await watch({
390
+ status: options.status === "true",
391
+ debounce: options.debounce ? Number(options.debounce) : undefined,
392
+ });
393
+ break;
394
+
395
+ case "monitor":
396
+ success = await monitor({
397
+ summary: options.summary === "true",
398
+ since: options.since,
399
+ follow: options.follow === "false" ? false : true,
400
+ file: options.file,
401
+ });
402
+ break;
403
+
404
+ case "brain": {
405
+ const subCommand = args[1];
406
+ switch (subCommand) {
407
+ case "setup":
408
+ success = await brainSetup({
409
+ model: options.model,
410
+ url: options.url,
411
+ skipCheck: options["skip-check"] === "true",
412
+ });
413
+ break;
414
+ case "status":
415
+ success = await brainStatus({
416
+ verbose: options.verbose === "true",
417
+ });
418
+ break;
419
+ default:
420
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
421
+ command: "brain",
422
+ subcommand,
423
+ });
424
+ console.log("\nUsage: bunx mandu brain <setup|status>");
425
+ process.exit(1);
426
+ }
427
+ break;
428
+ }
429
+
430
+ case "lock":
431
+ success = await runLockCommand(args.slice(1));
432
+ break;
433
+
434
+ default:
435
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
436
+ console.log(HELP_TEXT);
437
+ process.exit(1);
438
+ }
439
+
440
+ if (!success) {
441
+ process.exit(1);
442
+ }
443
+ }
444
+
445
+ main().catch((error) => handleCLIError(error));