@mandujs/cli 0.9.42 → 0.9.44
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/init.ts +53 -52
- 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/init.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
4
4
|
|
|
5
5
|
export type CSSFramework = "tailwind" | "panda" | "none";
|
|
6
6
|
export type UILibrary = "shadcn" | "ark" | "none";
|
|
@@ -21,13 +21,13 @@ const CSS_FILES = [
|
|
|
21
21
|
"app/globals.css",
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
const UI_FILES = [
|
|
25
|
-
"src/client/shared/ui/button.tsx",
|
|
26
|
-
"src/client/shared/ui/card.tsx",
|
|
27
|
-
"src/client/shared/ui/input.tsx",
|
|
28
|
-
"src/client/shared/ui/index.ts",
|
|
29
|
-
"src/client/shared/lib/utils.ts",
|
|
30
|
-
];
|
|
24
|
+
const UI_FILES = [
|
|
25
|
+
"src/client/shared/ui/button.tsx",
|
|
26
|
+
"src/client/shared/ui/card.tsx",
|
|
27
|
+
"src/client/shared/ui/input.tsx",
|
|
28
|
+
"src/client/shared/ui/index.ts",
|
|
29
|
+
"src/client/shared/lib/utils.ts",
|
|
30
|
+
];
|
|
31
31
|
|
|
32
32
|
interface CopyOptions {
|
|
33
33
|
projectName: string;
|
|
@@ -46,15 +46,15 @@ function shouldSkipFile(relativePath: string, options: CopyOptions): boolean {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Skip UI files if ui is none
|
|
50
|
-
if (options.ui === "none") {
|
|
51
|
-
if (UI_FILES.some((f) => normalizedPath.endsWith(f))) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
// Skip UI/shared directories
|
|
55
|
-
if (normalizedPath.includes("src/client/shared/ui/")) return true;
|
|
56
|
-
if (normalizedPath.includes("src/client/shared/lib/")) return true;
|
|
57
|
-
}
|
|
49
|
+
// Skip UI files if ui is none
|
|
50
|
+
if (options.ui === "none") {
|
|
51
|
+
if (UI_FILES.some((f) => normalizedPath.endsWith(f))) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// Skip UI/shared directories
|
|
55
|
+
if (normalizedPath.includes("src/client/shared/ui/")) return true;
|
|
56
|
+
if (normalizedPath.includes("src/client/shared/lib/")) return true;
|
|
57
|
+
}
|
|
58
58
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
@@ -76,13 +76,13 @@ async function copyDir(
|
|
|
76
76
|
: entry.name;
|
|
77
77
|
|
|
78
78
|
if (entry.isDirectory()) {
|
|
79
|
-
// Skip directories that would be empty when ui=none
|
|
80
|
-
if (options.ui === "none") {
|
|
81
|
-
if (entry.name === "ui" && relativePath === "src/client/shared") continue;
|
|
82
|
-
if (entry.name === "lib" && relativePath === "src/client/shared") continue;
|
|
83
|
-
}
|
|
84
|
-
await copyDir(srcPath, destPath, options, currentRelativePath);
|
|
85
|
-
} else {
|
|
79
|
+
// Skip directories that would be empty when ui=none
|
|
80
|
+
if (options.ui === "none") {
|
|
81
|
+
if (entry.name === "ui" && relativePath === "src/client/shared") continue;
|
|
82
|
+
if (entry.name === "lib" && relativePath === "src/client/shared") continue;
|
|
83
|
+
}
|
|
84
|
+
await copyDir(srcPath, destPath, options, currentRelativePath);
|
|
85
|
+
} else {
|
|
86
86
|
// Check if file should be skipped
|
|
87
87
|
if (shouldSkipFile(currentRelativePath, options)) {
|
|
88
88
|
continue;
|
|
@@ -163,8 +163,8 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
163
163
|
// Check if target directory exists
|
|
164
164
|
try {
|
|
165
165
|
await fs.access(targetDir);
|
|
166
|
-
printCLIError(CLI_ERROR_CODES.INIT_DIR_EXISTS, { path: targetDir });
|
|
167
|
-
return false;
|
|
166
|
+
printCLIError(CLI_ERROR_CODES.INIT_DIR_EXISTS, { path: targetDir });
|
|
167
|
+
return false;
|
|
168
168
|
} catch {
|
|
169
169
|
// Directory doesn't exist, good to proceed
|
|
170
170
|
}
|
|
@@ -176,9 +176,9 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
176
176
|
try {
|
|
177
177
|
await fs.access(templateDir);
|
|
178
178
|
} catch {
|
|
179
|
-
printCLIError(CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND, { template });
|
|
180
|
-
console.error(` 사용 가능한 템플릿: default`);
|
|
181
|
-
return false;
|
|
179
|
+
printCLIError(CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND, { template });
|
|
180
|
+
console.error(` 사용 가능한 템플릿: default`);
|
|
181
|
+
return false;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
console.log(`📋 템플릿 복사 중...`);
|
|
@@ -221,26 +221,25 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
221
221
|
console.log(` cd ${projectName}`);
|
|
222
222
|
console.log(` bun install`);
|
|
223
223
|
console.log(` bun run dev`);
|
|
224
|
-
console.log(`\n📂 파일 구조:`);
|
|
225
|
-
console.log(` app/layout.tsx → 루트 레이아웃`);
|
|
226
|
-
console.log(` app/page.tsx → http://localhost:3000/`);
|
|
227
|
-
console.log(` app/api/*/route.ts → API endpoints`);
|
|
228
|
-
console.log(` src/client/* → 클라이언트 레이어`);
|
|
229
|
-
console.log(` src/server/* → 서버 레이어`);
|
|
230
|
-
console.log(` src/shared/contracts → 계약 (client-safe)`);
|
|
231
|
-
console.log(` src/shared/types → 공용 타입`);
|
|
232
|
-
console.log(` src/shared/utils/client → 클라이언트 safe 유틸`);
|
|
233
|
-
console.log(` src/shared/utils/server → 서버 전용 유틸`);
|
|
234
|
-
console.log(` src/shared/schema → 서버 전용 스키마`);
|
|
235
|
-
console.log(` src/shared/env → 서버 전용 환경`);
|
|
236
|
-
if (css !== "none") {
|
|
237
|
-
console.log(` app/globals.css → 전역 CSS (Tailwind)`);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(` src/client/shared/
|
|
242
|
-
|
|
243
|
-
}
|
|
224
|
+
console.log(`\n📂 파일 구조:`);
|
|
225
|
+
console.log(` app/layout.tsx → 루트 레이아웃`);
|
|
226
|
+
console.log(` app/page.tsx → http://localhost:3000/`);
|
|
227
|
+
console.log(` app/api/*/route.ts → API endpoints`);
|
|
228
|
+
console.log(` src/client/* → 클라이언트 레이어`);
|
|
229
|
+
console.log(` src/server/* → 서버 레이어`);
|
|
230
|
+
console.log(` src/shared/contracts → 계약 (client-safe)`);
|
|
231
|
+
console.log(` src/shared/types → 공용 타입`);
|
|
232
|
+
console.log(` src/shared/utils/client → 클라이언트 safe 유틸`);
|
|
233
|
+
console.log(` src/shared/utils/server → 서버 전용 유틸`);
|
|
234
|
+
console.log(` src/shared/schema → 서버 전용 스키마`);
|
|
235
|
+
console.log(` src/shared/env → 서버 전용 환경`);
|
|
236
|
+
if (css !== "none") {
|
|
237
|
+
console.log(` app/globals.css → 전역 CSS (Tailwind v4)`);
|
|
238
|
+
}
|
|
239
|
+
if (ui !== "none") {
|
|
240
|
+
console.log(` src/client/shared/ui/ → UI 컴포넌트 (shadcn)`);
|
|
241
|
+
console.log(` src/client/shared/lib/utils.ts → 유틸리티 (cn 함수)`);
|
|
242
|
+
}
|
|
244
243
|
|
|
245
244
|
return true;
|
|
246
245
|
}
|
|
@@ -326,8 +325,10 @@ async function updatePackageJson(
|
|
|
326
325
|
const pkg = JSON.parse(content);
|
|
327
326
|
|
|
328
327
|
if (css === "none") {
|
|
329
|
-
// Remove Tailwind dependencies
|
|
328
|
+
// Remove Tailwind dependencies (v4)
|
|
330
329
|
delete pkg.devDependencies?.tailwindcss;
|
|
330
|
+
delete pkg.devDependencies?.["@tailwindcss/cli"];
|
|
331
|
+
// Legacy v3 (just in case)
|
|
331
332
|
delete pkg.devDependencies?.postcss;
|
|
332
333
|
delete pkg.devDependencies?.autoprefixer;
|
|
333
334
|
}
|
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}",
|