@mandujs/cli 0.9.42 → 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/README.ko.md +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +55 -33
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +95 -42
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +12 -3
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- package/src/errors/messages.ts +2 -2
- package/src/main.ts +75 -145
- package/src/util/manifest.ts +52 -0
- package/src/util/port.ts +71 -0
- package/templates/default/AGENTS.md +96 -0
- package/templates/default/app/globals.css +45 -33
- package/templates/default/package.json +15 -12
- package/templates/default/postcss.config.js +0 -6
- package/templates/default/tailwind.config.ts +0 -64
package/src/commands/doctor.ts
CHANGED
|
@@ -15,20 +15,22 @@ import {
|
|
|
15
15
|
getBrain,
|
|
16
16
|
} from "../../../core/src/index";
|
|
17
17
|
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
18
|
-
import path from "path";
|
|
18
|
+
import path from "path";
|
|
19
19
|
import fs from "fs/promises";
|
|
20
20
|
|
|
21
|
-
export interface DoctorOptions {
|
|
22
|
-
/** Output format: console, json, or markdown */
|
|
23
|
-
format?: "console" | "json" | "markdown";
|
|
21
|
+
export interface DoctorOptions {
|
|
22
|
+
/** Output format: console, json, or markdown */
|
|
23
|
+
format?: "console" | "json" | "markdown";
|
|
24
24
|
/** Whether to use LLM for enhanced analysis */
|
|
25
25
|
useLLM?: boolean;
|
|
26
26
|
/** Output file path (for json/markdown formats) */
|
|
27
|
-
output?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
31
|
-
const { format
|
|
27
|
+
output?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
31
|
+
const { format, useLLM = true, output } = options;
|
|
32
|
+
const inferredFormat = format ?? (output ? (path.extname(output).toLowerCase() === ".json" ? "json" : "markdown") : undefined);
|
|
33
|
+
const resolvedFormat = inferredFormat ?? "console";
|
|
32
34
|
|
|
33
35
|
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
34
36
|
const rootDir = getRootDir();
|
|
@@ -80,12 +82,12 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
// Output based on format
|
|
83
|
-
switch (
|
|
84
|
-
case "console":
|
|
85
|
-
printDoctorReport(analysis);
|
|
86
|
-
break;
|
|
87
|
-
|
|
88
|
-
case "json": {
|
|
85
|
+
switch (resolvedFormat) {
|
|
86
|
+
case "console":
|
|
87
|
+
printDoctorReport(analysis);
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case "json": {
|
|
89
91
|
const json = JSON.stringify(
|
|
90
92
|
{
|
|
91
93
|
summary: analysis.summary,
|
|
@@ -104,10 +106,10 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
104
106
|
} else {
|
|
105
107
|
console.log(json);
|
|
106
108
|
}
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case "markdown": {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "markdown": {
|
|
111
113
|
const md = generateDoctorMarkdownReport(analysis);
|
|
112
114
|
|
|
113
115
|
if (output) {
|
|
@@ -116,9 +118,9 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
116
118
|
} else {
|
|
117
119
|
console.log(md);
|
|
118
120
|
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
@@ -53,16 +53,25 @@ export interface GuardArchOptions {
|
|
|
53
53
|
showTrend?: boolean;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function inferReportFormat(output?: string): "json" | "markdown" | "html" | undefined {
|
|
57
|
+
if (!output) return undefined;
|
|
58
|
+
const ext = path.extname(output).toLowerCase();
|
|
59
|
+
if (ext === ".json") return "json";
|
|
60
|
+
if (ext === ".html" || ext === ".htm") return "html";
|
|
61
|
+
if (ext === ".md" || ext === ".markdown") return "markdown";
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
57
66
|
const rootDir = resolveFromCwd(".");
|
|
58
67
|
const {
|
|
59
68
|
watch = false,
|
|
60
|
-
ci =
|
|
69
|
+
ci = process.env.CI === "true",
|
|
61
70
|
format,
|
|
62
71
|
quiet = false,
|
|
63
72
|
listPresets: showPresets = false,
|
|
64
73
|
output,
|
|
65
|
-
reportFormat = "markdown",
|
|
74
|
+
reportFormat = inferReportFormat(options.output) ?? "markdown",
|
|
66
75
|
saveStats = false,
|
|
67
76
|
showTrend = false,
|
|
68
77
|
} = options;
|
|
@@ -84,7 +93,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
84
93
|
console.log("");
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
console.log("Usage:
|
|
96
|
+
console.log("Usage: set guard.preset in mandu.config to choose a preset");
|
|
88
97
|
return true;
|
|
89
98
|
}
|
|
90
99
|
|
package/src/commands/monitor.ts
CHANGED
|
@@ -2,12 +2,11 @@ import fs from "fs/promises";
|
|
|
2
2
|
import fsSync from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { resolveFromCwd, pathExists } from "../util/fs";
|
|
5
|
-
import { resolveOutputFormat
|
|
5
|
+
import { resolveOutputFormat } from "../util/output";
|
|
6
6
|
|
|
7
7
|
type MonitorOutput = "console" | "json";
|
|
8
8
|
|
|
9
9
|
export interface MonitorOptions {
|
|
10
|
-
format?: OutputFormat;
|
|
11
10
|
follow?: boolean;
|
|
12
11
|
summary?: boolean;
|
|
13
12
|
since?: string;
|
|
@@ -263,7 +262,7 @@ async function followFile(
|
|
|
263
262
|
|
|
264
263
|
export async function monitor(options: MonitorOptions = {}): Promise<boolean> {
|
|
265
264
|
const rootDir = resolveFromCwd(".");
|
|
266
|
-
const resolved = resolveOutputFormat(
|
|
265
|
+
const resolved = resolveOutputFormat();
|
|
267
266
|
const output: MonitorOutput = resolved === "json" || resolved === "agent" ? "json" : "console";
|
|
268
267
|
const filePath = await resolveLogFile(rootDir, output, options.file);
|
|
269
268
|
|
package/src/commands/openapi.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* OpenAPI 스펙 생성 명령어
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import path from "path";
|
|
8
|
-
import fs from "fs/promises";
|
|
6
|
+
import { generateOpenAPIDocument, openAPIToJSON, validateAndReport } from "@mandujs/core";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs/promises";
|
|
9
|
+
import { resolveManifest } from "../util/manifest";
|
|
9
10
|
|
|
10
11
|
interface OpenAPIGenerateOptions {
|
|
11
12
|
output?: string;
|
|
@@ -13,27 +14,78 @@ interface OpenAPIGenerateOptions {
|
|
|
13
14
|
version?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
interface OpenAPIServeOptions {
|
|
17
|
-
port?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
interface OpenAPIServeOptions {
|
|
18
|
+
port?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePort(value: string | number | undefined, label: string): number | undefined {
|
|
22
|
+
if (value === undefined || value === null || value === "") {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const port = typeof value === "string" ? Number(value) : value;
|
|
26
|
+
if (!Number.isFinite(port) || !Number.isInteger(port)) {
|
|
27
|
+
console.warn(`⚠️ Invalid ${label} value: "${value}" (using default)`);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (port < 1 || port > 65535) {
|
|
31
|
+
console.warn(`⚠️ Invalid ${label} range: ${port} (must be 1-65535, using default)`);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return port;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPortInUse(error: unknown): boolean {
|
|
38
|
+
if (!error || typeof error !== "object") return false;
|
|
39
|
+
const code = (error as { code?: string }).code;
|
|
40
|
+
const message = (error as { message?: string }).message ?? "";
|
|
41
|
+
return code === "EADDRINUSE" || message.includes("EADDRINUSE") || message.includes("address already in use");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function serveWithAutoPort(
|
|
45
|
+
startPort: number,
|
|
46
|
+
fetch: (req: Request) => Response
|
|
47
|
+
): { server: ReturnType<typeof Bun.serve>; port: number; attempts: number } {
|
|
48
|
+
const maxAttempts = 10;
|
|
49
|
+
let lastError: unknown = null;
|
|
50
|
+
|
|
51
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
52
|
+
const candidate = startPort + attempt;
|
|
53
|
+
if (candidate < 1 || candidate > 65535) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const server = Bun.serve({ port: candidate, fetch });
|
|
58
|
+
return { server, port: server.port ?? candidate, attempts: attempt };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isPortInUse(error)) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
lastError = error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw lastError ?? new Error(`No available port found starting at ${startPort}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate OpenAPI specification from contracts
|
|
72
|
+
*/
|
|
73
|
+
export async function openAPIGenerate(options: OpenAPIGenerateOptions = {}): Promise<boolean> {
|
|
74
|
+
const rootDir = process.cwd();
|
|
75
|
+
|
|
76
|
+
console.log(`\n📄 Generating OpenAPI specification...\n`);
|
|
77
|
+
|
|
78
|
+
// Load manifest (FS Routes 우선)
|
|
79
|
+
let manifest: Awaited<ReturnType<typeof resolveManifest>>["manifest"];
|
|
80
|
+
try {
|
|
81
|
+
const config = await validateAndReport(rootDir);
|
|
82
|
+
if (!config) return false;
|
|
83
|
+
const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
|
|
84
|
+
manifest = resolved.manifest;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
37
89
|
|
|
38
90
|
// Count routes with contracts
|
|
39
91
|
const contractRoutes = manifest.routes.filter((r) => r.contractModule);
|
|
@@ -96,10 +148,16 @@ export async function openAPIGenerate(options: OpenAPIGenerateOptions = {}): Pro
|
|
|
96
148
|
/**
|
|
97
149
|
* Serve Swagger UI for OpenAPI documentation
|
|
98
150
|
*/
|
|
99
|
-
export async function openAPIServe(options: OpenAPIServeOptions = {}): Promise<boolean> {
|
|
100
|
-
const rootDir = process.cwd();
|
|
101
|
-
const
|
|
102
|
-
|
|
151
|
+
export async function openAPIServe(options: OpenAPIServeOptions = {}): Promise<boolean> {
|
|
152
|
+
const rootDir = process.cwd();
|
|
153
|
+
const config = await validateAndReport(rootDir);
|
|
154
|
+
if (!config) return false;
|
|
155
|
+
|
|
156
|
+
const optionPort = normalizePort(options.port, "openapi.port");
|
|
157
|
+
const envPort = normalizePort(process.env.PORT, "PORT");
|
|
158
|
+
const configPort = normalizePort(config.server?.port, "mandu.config server.port");
|
|
159
|
+
const desiredPort = optionPort ?? envPort ?? configPort ?? 8080;
|
|
160
|
+
const openAPIPath = path.join(rootDir, "openapi.json");
|
|
103
161
|
|
|
104
162
|
console.log(`\n🌐 Starting OpenAPI documentation server...\n`);
|
|
105
163
|
|
|
@@ -154,26 +212,27 @@ export async function openAPIServe(options: OpenAPIServeOptions = {}): Promise<b
|
|
|
154
212
|
</html>
|
|
155
213
|
`.trim();
|
|
156
214
|
|
|
157
|
-
// Start server
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.log(
|
|
215
|
+
// Start server (auto port fallback)
|
|
216
|
+
const { port, attempts } = serveWithAutoPort(desiredPort, (req) => {
|
|
217
|
+
const url = new URL(req.url);
|
|
218
|
+
|
|
219
|
+
if (url.pathname === "/openapi.json") {
|
|
220
|
+
return new Response(specContent, {
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return new Response(swaggerHTML, {
|
|
226
|
+
headers: { "Content-Type": "text/html" },
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (attempts > 0) {
|
|
231
|
+
console.warn(`⚠️ Port ${desiredPort} is in use. Using ${port} instead.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`✅ Swagger UI is running at http://localhost:${port}`);
|
|
235
|
+
console.log(` OpenAPI spec: http://localhost:${port}/openapi.json`);
|
|
177
236
|
console.log(`\nPress Ctrl+C to stop.\n`);
|
|
178
237
|
|
|
179
238
|
// Keep server running
|
package/src/errors/messages.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
|
|
|
21
21
|
},
|
|
22
22
|
[CLI_ERROR_CODES.DEV_PORT_IN_USE]: {
|
|
23
23
|
message: "Port {port} is already in use.",
|
|
24
|
-
suggestion: "
|
|
24
|
+
suggestion: "Set PORT or mandu.config server.port to pick a different port, or stop the process using this port.",
|
|
25
25
|
},
|
|
26
26
|
[CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND]: {
|
|
27
27
|
message: "Routes manifest not found.",
|
|
@@ -41,7 +41,7 @@ export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
|
|
|
41
41
|
},
|
|
42
42
|
[CLI_ERROR_CODES.GUARD_VIOLATION_FOUND]: {
|
|
43
43
|
message: "{count} architecture violation(s) found.",
|
|
44
|
-
suggestion: "Fix violations above or
|
|
44
|
+
suggestion: "Fix violations above or set MANDU_OUTPUT=agent for AI-friendly output.",
|
|
45
45
|
},
|
|
46
46
|
[CLI_ERROR_CODES.BUILD_ENTRY_NOT_FOUND]: {
|
|
47
47
|
message: "Build entry not found: {entry}",
|
package/src/main.ts
CHANGED
|
@@ -31,30 +31,25 @@ const HELP_TEXT = `
|
|
|
31
31
|
Usage: bunx mandu <command> [options]
|
|
32
32
|
|
|
33
33
|
Commands:
|
|
34
|
-
init
|
|
35
|
-
check
|
|
36
|
-
routes generate
|
|
37
|
-
routes list
|
|
38
|
-
routes watch
|
|
39
|
-
dev
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
guard
|
|
43
|
-
guard
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
watch 실시간 파일 감시 - 경고만 (Brain)
|
|
54
|
-
monitor MCP Activity Monitor 로그 스트림
|
|
55
|
-
|
|
56
|
-
brain setup sLLM 설정 (선택)
|
|
57
|
-
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 상태 확인
|
|
58
53
|
|
|
59
54
|
contract create <routeId> 라우트에 대한 Contract 생성
|
|
60
55
|
contract validate Contract-Slot 일관성 검증
|
|
@@ -64,79 +59,68 @@ Commands:
|
|
|
64
59
|
openapi generate OpenAPI 3.0 스펙 생성
|
|
65
60
|
openapi serve Swagger UI 로컬 서버 실행
|
|
66
61
|
|
|
67
|
-
change begin
|
|
68
|
-
change commit
|
|
69
|
-
change rollback
|
|
70
|
-
change status
|
|
71
|
-
change list
|
|
72
|
-
change prune
|
|
62
|
+
change begin 변경 트랜잭션 시작 (스냅샷 생성)
|
|
63
|
+
change commit 변경 확정
|
|
64
|
+
change rollback 스냅샷으로 복원
|
|
65
|
+
change status 현재 트랜잭션 상태
|
|
66
|
+
change list 변경 이력 조회
|
|
67
|
+
change prune 오래된 스냅샷 정리
|
|
73
68
|
|
|
74
69
|
Options:
|
|
75
|
-
--name <name>
|
|
76
|
-
--css <framework>
|
|
77
|
-
--ui <library>
|
|
78
|
-
--theme
|
|
79
|
-
--minimal
|
|
80
|
-
--file <path>
|
|
81
|
-
--
|
|
82
|
-
--guard
|
|
83
|
-
--
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
--
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--watch
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
--format <fmt> doctor 출력 형식: console, json, markdown (기본: console)
|
|
110
|
-
--no-llm doctor에서 LLM 사용 안 함 (템플릿 모드)
|
|
111
|
-
--model <name> brain setup 시 모델 이름 (기본: llama3.2)
|
|
112
|
-
--url <url> brain setup 시 Ollama URL
|
|
113
|
-
--verbose 상세 출력
|
|
114
|
-
--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
|
+
- 포트 충돌 시 다음 사용 가능한 포트로 자동 변경됩니다.
|
|
115
104
|
|
|
116
105
|
Examples:
|
|
117
106
|
bunx mandu init --name my-app # Tailwind + shadcn/ui 기본
|
|
118
107
|
bunx mandu init my-app --minimal # CSS/UI 없이 최소 템플릿
|
|
119
|
-
bunx mandu
|
|
120
|
-
bunx mandu
|
|
121
|
-
bunx mandu check
|
|
122
|
-
bunx mandu routes list
|
|
123
|
-
bunx mandu routes generate
|
|
124
|
-
bunx mandu dev --port 3000
|
|
125
|
-
bunx mandu dev --no-guard
|
|
126
|
-
bunx mandu build --minify
|
|
108
|
+
bunx mandu dev
|
|
109
|
+
bunx mandu build --watch
|
|
127
110
|
bunx mandu guard
|
|
128
|
-
bunx mandu guard arch --preset fsd
|
|
129
111
|
bunx mandu guard arch --watch
|
|
130
|
-
bunx mandu guard arch --
|
|
131
|
-
bunx mandu
|
|
132
|
-
bunx mandu
|
|
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
|
|
133
121
|
bunx mandu monitor --summary --since 5m
|
|
134
|
-
bunx mandu doctor
|
|
122
|
+
bunx mandu doctor --output reports/doctor.json
|
|
135
123
|
bunx mandu brain setup --model codellama
|
|
136
|
-
bunx mandu contract create users
|
|
137
|
-
bunx mandu contract build
|
|
138
|
-
bunx mandu contract diff
|
|
139
|
-
bunx mandu openapi generate --output docs/api.json
|
|
140
124
|
bunx mandu change begin --message "Add new route"
|
|
141
125
|
|
|
142
126
|
FS Routes Workflow (권장):
|
|
@@ -173,31 +157,6 @@ function parseArgs(args: string[]): { command: string; options: Record<string, s
|
|
|
173
157
|
return { command, options };
|
|
174
158
|
}
|
|
175
159
|
|
|
176
|
-
/**
|
|
177
|
-
* 포트 옵션 안전하게 파싱
|
|
178
|
-
* - 숫자가 아니면 undefined 반환 (기본값 사용)
|
|
179
|
-
* - 유효 범위: 1-65535
|
|
180
|
-
*/
|
|
181
|
-
function parsePort(value: string | undefined, optionName = "port"): number | undefined {
|
|
182
|
-
if (!value || value === "true") {
|
|
183
|
-
return undefined; // 기본값 사용
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const port = Number(value);
|
|
187
|
-
|
|
188
|
-
if (Number.isNaN(port)) {
|
|
189
|
-
console.warn(`⚠️ Invalid --${optionName} value: "${value}" (using default)`);
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
194
|
-
console.warn(`⚠️ Invalid --${optionName} range: ${port} (must be 1-65535, using default)`);
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return port;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
160
|
async function main(): Promise<void> {
|
|
202
161
|
const args = process.argv.slice(2);
|
|
203
162
|
const { command, options } = parseArgs(args);
|
|
@@ -229,30 +188,15 @@ async function main(): Promise<void> {
|
|
|
229
188
|
break;
|
|
230
189
|
|
|
231
190
|
case "check":
|
|
232
|
-
success = await check(
|
|
233
|
-
preset: options.preset as any,
|
|
234
|
-
format: options.format as any,
|
|
235
|
-
ci: options.ci === "true",
|
|
236
|
-
quiet: options.quiet === "true",
|
|
237
|
-
legacy: options.legacy === "true",
|
|
238
|
-
});
|
|
191
|
+
success = await check();
|
|
239
192
|
break;
|
|
240
193
|
|
|
241
194
|
case "guard": {
|
|
242
195
|
const subCommand = args[1];
|
|
243
196
|
const hasSubCommand = subCommand && !subCommand.startsWith("--");
|
|
244
197
|
const guardArchOptions = {
|
|
245
|
-
preset: options.preset as any,
|
|
246
198
|
watch: options.watch === "true",
|
|
247
|
-
ci: options.ci === "true",
|
|
248
|
-
format: options.format as any,
|
|
249
|
-
quiet: options.quiet === "true",
|
|
250
|
-
srcDir: options["src-dir"],
|
|
251
|
-
listPresets: options["list-presets"] === "true",
|
|
252
199
|
output: options.output,
|
|
253
|
-
reportFormat: (options["report-format"] as any) || "markdown",
|
|
254
|
-
saveStats: options["save-stats"] === "true",
|
|
255
|
-
showTrend: options["show-trend"] === "true",
|
|
256
200
|
};
|
|
257
201
|
switch (subCommand) {
|
|
258
202
|
case "arch":
|
|
@@ -260,9 +204,7 @@ async function main(): Promise<void> {
|
|
|
260
204
|
break;
|
|
261
205
|
case "legacy":
|
|
262
206
|
case "spec":
|
|
263
|
-
success = await guardCheck(
|
|
264
|
-
autoCorrect: options["no-auto-correct"] !== "true",
|
|
265
|
-
});
|
|
207
|
+
success = await guardCheck();
|
|
266
208
|
break;
|
|
267
209
|
default:
|
|
268
210
|
if (hasSubCommand) {
|
|
@@ -281,20 +223,12 @@ async function main(): Promise<void> {
|
|
|
281
223
|
|
|
282
224
|
case "build":
|
|
283
225
|
success = await build({
|
|
284
|
-
minify: options.minify === "true",
|
|
285
|
-
sourcemap: options.sourcemap === "true",
|
|
286
226
|
watch: options.watch === "true",
|
|
287
227
|
});
|
|
288
228
|
break;
|
|
289
229
|
|
|
290
230
|
case "dev":
|
|
291
|
-
await dev(
|
|
292
|
-
port: parsePort(options.port),
|
|
293
|
-
guard: options["no-guard"] === "true" ? false : options.guard !== "false",
|
|
294
|
-
guardPreset: options["guard-preset"] as any,
|
|
295
|
-
guardFormat: options["guard-format"] as any,
|
|
296
|
-
legacy: options.legacy === "true",
|
|
297
|
-
});
|
|
231
|
+
await dev();
|
|
298
232
|
break;
|
|
299
233
|
|
|
300
234
|
case "routes": {
|
|
@@ -384,9 +318,7 @@ async function main(): Promise<void> {
|
|
|
384
318
|
});
|
|
385
319
|
break;
|
|
386
320
|
case "serve":
|
|
387
|
-
success = await openAPIServe(
|
|
388
|
-
port: parsePort(options.port),
|
|
389
|
-
});
|
|
321
|
+
success = await openAPIServe();
|
|
390
322
|
break;
|
|
391
323
|
default:
|
|
392
324
|
printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
|
|
@@ -435,7 +367,6 @@ async function main(): Promise<void> {
|
|
|
435
367
|
|
|
436
368
|
case "doctor":
|
|
437
369
|
success = await doctor({
|
|
438
|
-
format: (options.format as "console" | "json" | "markdown") || "console",
|
|
439
370
|
useLLM: options["no-llm"] !== "true",
|
|
440
371
|
output: options.output,
|
|
441
372
|
});
|
|
@@ -450,7 +381,6 @@ async function main(): Promise<void> {
|
|
|
450
381
|
|
|
451
382
|
case "monitor":
|
|
452
383
|
success = await monitor({
|
|
453
|
-
format: options.format as any,
|
|
454
384
|
summary: options.summary === "true",
|
|
455
385
|
since: options.since,
|
|
456
386
|
follow: options.follow === "false" ? false : true,
|