@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/README.ko.md +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/commands/build.ts +50 -22
- package/src/commands/check.ts +16 -18
- package/src/commands/contract.ts +50 -42
- package/src/commands/dev.ts +294 -212
- package/src/commands/doctor.ts +27 -25
- package/src/commands/guard-arch.ts +25 -10
- package/src/commands/init.ts +8 -7
- package/src/commands/monitor.ts +2 -3
- package/src/commands/openapi.ts +107 -48
- package/src/commands/routes.ts +11 -1
- package/src/errors/codes.ts +35 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/messages.ts +143 -0
- package/src/main.ts +103 -157
- package/src/util/bun.ts +6 -0
- 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
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
calculateLayerStatistics,
|
|
20
20
|
generateGuardMarkdownReport,
|
|
21
21
|
generateHTMLReport,
|
|
22
|
+
validateAndReport,
|
|
22
23
|
type GuardConfig,
|
|
23
24
|
type GuardPreset,
|
|
24
25
|
} from "@mandujs/core";
|
|
@@ -52,22 +53,28 @@ export interface GuardArchOptions {
|
|
|
52
53
|
showTrend?: boolean;
|
|
53
54
|
}
|
|
54
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
|
+
|
|
55
65
|
export async function guardArch(options: GuardArchOptions = {}): Promise<boolean> {
|
|
66
|
+
const rootDir = resolveFromCwd(".");
|
|
56
67
|
const {
|
|
57
|
-
preset = "mandu",
|
|
58
68
|
watch = false,
|
|
59
|
-
ci =
|
|
69
|
+
ci = process.env.CI === "true",
|
|
60
70
|
format,
|
|
61
71
|
quiet = false,
|
|
62
|
-
srcDir = "src",
|
|
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;
|
|
69
|
-
|
|
70
|
-
const rootDir = resolveFromCwd(".");
|
|
71
78
|
const resolvedFormat = resolveOutputFormat(format);
|
|
72
79
|
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
73
80
|
|
|
@@ -86,10 +93,17 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
86
93
|
console.log("");
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
console.log("Usage:
|
|
96
|
+
console.log("Usage: set guard.preset in mandu.config to choose a preset");
|
|
90
97
|
return true;
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
const fileConfig = await validateAndReport(rootDir);
|
|
101
|
+
if (!fileConfig) return false;
|
|
102
|
+
const guardConfigFromFile = fileConfig.guard ?? {};
|
|
103
|
+
|
|
104
|
+
const preset = options.preset ?? guardConfigFromFile.preset ?? "mandu";
|
|
105
|
+
const srcDir = options.srcDir ?? guardConfigFromFile.srcDir ?? "src";
|
|
106
|
+
|
|
93
107
|
if (resolvedFormat === "console") {
|
|
94
108
|
console.log("");
|
|
95
109
|
console.log("π‘οΈ Mandu Guard - Architecture Checker");
|
|
@@ -101,11 +115,12 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
// Guard μ€μ
|
|
104
|
-
const
|
|
118
|
+
const guardConfig: GuardConfig = {
|
|
105
119
|
preset,
|
|
106
120
|
srcDir,
|
|
107
121
|
realtime: watch,
|
|
108
122
|
realtimeOutput: resolvedFormat,
|
|
123
|
+
exclude: guardConfigFromFile.exclude,
|
|
109
124
|
fsRoutes: enableFsRoutes
|
|
110
125
|
? {
|
|
111
126
|
noPageToPage: true,
|
|
@@ -152,7 +167,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
const watcher = createGuardWatcher({
|
|
155
|
-
config,
|
|
170
|
+
config: guardConfig,
|
|
156
171
|
rootDir,
|
|
157
172
|
onViolation: (violation) => {
|
|
158
173
|
// μ€μκ° μλ° μΆλ ₯μ watcher λ΄λΆμμ μ²λ¦¬λ¨
|
|
@@ -185,7 +200,7 @@ export async function guardArch(options: GuardArchOptions = {}): Promise<boolean
|
|
|
185
200
|
console.log("π Scanning for architecture violations...\n");
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
const report = await checkDirectory(
|
|
203
|
+
const report = await checkDirectory(guardConfig, rootDir);
|
|
189
204
|
const presetDef = getPreset(preset);
|
|
190
205
|
|
|
191
206
|
// μΆλ ₯ νμμ λ°λ₯Έ 리ν¬νΈ μΆλ ₯
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
3
4
|
|
|
4
5
|
export type CSSFramework = "tailwind" | "panda" | "none";
|
|
5
6
|
export type UILibrary = "shadcn" | "ark" | "none";
|
|
@@ -162,8 +163,8 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
162
163
|
// Check if target directory exists
|
|
163
164
|
try {
|
|
164
165
|
await fs.access(targetDir);
|
|
165
|
-
|
|
166
|
-
return false;
|
|
166
|
+
printCLIError(CLI_ERROR_CODES.INIT_DIR_EXISTS, { path: targetDir });
|
|
167
|
+
return false;
|
|
167
168
|
} catch {
|
|
168
169
|
// Directory doesn't exist, good to proceed
|
|
169
170
|
}
|
|
@@ -175,9 +176,9 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
175
176
|
try {
|
|
176
177
|
await fs.access(templateDir);
|
|
177
178
|
} catch {
|
|
178
|
-
|
|
179
|
-
console.error(` μ¬μ© κ°λ₯ν ν
νλ¦Ώ: default`);
|
|
180
|
-
return false;
|
|
179
|
+
printCLIError(CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND, { template });
|
|
180
|
+
console.error(` μ¬μ© κ°λ₯ν ν
νλ¦Ώ: default`);
|
|
181
|
+
return false;
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
console.log(`π ν
νλ¦Ώ λ³΅μ¬ μ€...`);
|
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/commands/routes.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateManifest,
|
|
10
10
|
formatRoutesForCLI,
|
|
11
11
|
watchFSRoutes,
|
|
12
|
+
validateAndReport,
|
|
12
13
|
type GenerateOptions,
|
|
13
14
|
type FSScannerConfig,
|
|
14
15
|
} from "@mandujs/core";
|
|
@@ -46,11 +47,14 @@ export interface RoutesWatchOptions {
|
|
|
46
47
|
*/
|
|
47
48
|
export async function routesGenerate(options: RoutesGenerateOptions = {}): Promise<boolean> {
|
|
48
49
|
const rootDir = resolveFromCwd(".");
|
|
50
|
+
const config = await validateAndReport(rootDir);
|
|
51
|
+
if (!config) return false;
|
|
49
52
|
|
|
50
53
|
console.log("π₯ Mandu FS Routes Generate\n");
|
|
51
54
|
|
|
52
55
|
try {
|
|
53
56
|
const generateOptions: GenerateOptions = {
|
|
57
|
+
scanner: config.fsRoutes,
|
|
54
58
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
55
59
|
skipLegacy: true, // λ κ±°μ λ³ν© λΉνμ±ν
|
|
56
60
|
};
|
|
@@ -93,11 +97,13 @@ export async function routesGenerate(options: RoutesGenerateOptions = {}): Promi
|
|
|
93
97
|
*/
|
|
94
98
|
export async function routesList(options: RoutesListOptions = {}): Promise<boolean> {
|
|
95
99
|
const rootDir = resolveFromCwd(".");
|
|
100
|
+
const config = await validateAndReport(rootDir);
|
|
101
|
+
if (!config) return false;
|
|
96
102
|
|
|
97
103
|
console.log("π₯ Mandu Routes List\n");
|
|
98
104
|
|
|
99
105
|
try {
|
|
100
|
-
const result = await scanRoutes(rootDir);
|
|
106
|
+
const result = await scanRoutes(rootDir, config.fsRoutes);
|
|
101
107
|
|
|
102
108
|
if (result.errors.length > 0) {
|
|
103
109
|
console.log("β οΈ μ€μΊ κ²½κ³ :");
|
|
@@ -164,6 +170,8 @@ export async function routesList(options: RoutesListOptions = {}): Promise<boole
|
|
|
164
170
|
*/
|
|
165
171
|
export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boolean> {
|
|
166
172
|
const rootDir = resolveFromCwd(".");
|
|
173
|
+
const config = await validateAndReport(rootDir);
|
|
174
|
+
if (!config) return false;
|
|
167
175
|
|
|
168
176
|
console.log("π₯ Mandu FS Routes Watch\n");
|
|
169
177
|
console.log("π λΌμ°νΈ λ³κ²½ κ°μ μ€... (Ctrl+Cλ‘ μ’
λ£)\n");
|
|
@@ -171,6 +179,7 @@ export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boo
|
|
|
171
179
|
try {
|
|
172
180
|
// μ΄κΈ° μ€μΊ
|
|
173
181
|
const initialResult = await generateManifest(rootDir, {
|
|
182
|
+
scanner: config.fsRoutes,
|
|
174
183
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
175
184
|
});
|
|
176
185
|
|
|
@@ -178,6 +187,7 @@ export async function routesWatch(options: RoutesWatchOptions = {}): Promise<boo
|
|
|
178
187
|
|
|
179
188
|
// κ°μ μμ
|
|
180
189
|
const watcher = await watchFSRoutes(rootDir, {
|
|
190
|
+
scanner: config.fsRoutes,
|
|
181
191
|
outputPath: options.output ?? ".mandu/routes.manifest.json",
|
|
182
192
|
onChange: (result) => {
|
|
183
193
|
const timestamp = new Date().toLocaleTimeString();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI error codes
|
|
3
|
+
*/
|
|
4
|
+
export const CLI_ERROR_CODES = {
|
|
5
|
+
// Init errors (E001-E009)
|
|
6
|
+
INIT_DIR_EXISTS: "CLI_E001",
|
|
7
|
+
INIT_BUN_NOT_FOUND: "CLI_E002",
|
|
8
|
+
INIT_TEMPLATE_NOT_FOUND: "CLI_E003",
|
|
9
|
+
|
|
10
|
+
// Dev errors (E010-E019)
|
|
11
|
+
DEV_PORT_IN_USE: "CLI_E010",
|
|
12
|
+
DEV_MANIFEST_NOT_FOUND: "CLI_E011",
|
|
13
|
+
DEV_NO_ROUTES: "CLI_E012",
|
|
14
|
+
|
|
15
|
+
// Guard errors (E020-E029)
|
|
16
|
+
GUARD_CONFIG_INVALID: "CLI_E020",
|
|
17
|
+
GUARD_PRESET_NOT_FOUND: "CLI_E021",
|
|
18
|
+
GUARD_VIOLATION_FOUND: "CLI_E022",
|
|
19
|
+
|
|
20
|
+
// Build errors (E030-E039)
|
|
21
|
+
BUILD_ENTRY_NOT_FOUND: "CLI_E030",
|
|
22
|
+
BUILD_BUNDLE_FAILED: "CLI_E031",
|
|
23
|
+
BUILD_OUTDIR_NOT_WRITABLE: "CLI_E032",
|
|
24
|
+
|
|
25
|
+
// Config errors (E040-E049)
|
|
26
|
+
CONFIG_PARSE_FAILED: "CLI_E040",
|
|
27
|
+
CONFIG_VALIDATION_FAILED: "CLI_E041",
|
|
28
|
+
|
|
29
|
+
// CLI usage errors (E100+)
|
|
30
|
+
UNKNOWN_COMMAND: "CLI_E100",
|
|
31
|
+
UNKNOWN_SUBCOMMAND: "CLI_E101",
|
|
32
|
+
MISSING_ARGUMENT: "CLI_E102",
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export type CLIErrorCode = typeof CLI_ERROR_CODES[keyof typeof CLI_ERROR_CODES];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CLI_ERROR_CODES, type CLIErrorCode } from "./codes";
|
|
2
|
+
|
|
3
|
+
interface ErrorInfo {
|
|
4
|
+
message: string;
|
|
5
|
+
suggestion?: string;
|
|
6
|
+
docLink?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ERROR_MESSAGES: Record<CLIErrorCode, ErrorInfo> = {
|
|
10
|
+
[CLI_ERROR_CODES.INIT_DIR_EXISTS]: {
|
|
11
|
+
message: "Directory already exists: {path}",
|
|
12
|
+
suggestion: "Choose a different project name or remove the existing directory.",
|
|
13
|
+
},
|
|
14
|
+
[CLI_ERROR_CODES.INIT_BUN_NOT_FOUND]: {
|
|
15
|
+
message: "Bun runtime not found.",
|
|
16
|
+
suggestion: "Install Bun and ensure it is available in your PATH.",
|
|
17
|
+
},
|
|
18
|
+
[CLI_ERROR_CODES.INIT_TEMPLATE_NOT_FOUND]: {
|
|
19
|
+
message: "Template not found: {template}",
|
|
20
|
+
suggestion: "Use a valid template name (default).",
|
|
21
|
+
},
|
|
22
|
+
[CLI_ERROR_CODES.DEV_PORT_IN_USE]: {
|
|
23
|
+
message: "Port {port} is already in use.",
|
|
24
|
+
suggestion: "Set PORT or mandu.config server.port to pick a different port, or stop the process using this port.",
|
|
25
|
+
},
|
|
26
|
+
[CLI_ERROR_CODES.DEV_MANIFEST_NOT_FOUND]: {
|
|
27
|
+
message: "Routes manifest not found.",
|
|
28
|
+
suggestion: "Run `mandu routes generate` or create app/ routes before dev.",
|
|
29
|
+
},
|
|
30
|
+
[CLI_ERROR_CODES.DEV_NO_ROUTES]: {
|
|
31
|
+
message: "No routes were found in app/.",
|
|
32
|
+
suggestion: "Create app/page.tsx or app/api/*/route.ts to get started.",
|
|
33
|
+
},
|
|
34
|
+
[CLI_ERROR_CODES.GUARD_CONFIG_INVALID]: {
|
|
35
|
+
message: "Invalid guard configuration.",
|
|
36
|
+
suggestion: "Check your mandu.config and guard settings.",
|
|
37
|
+
},
|
|
38
|
+
[CLI_ERROR_CODES.GUARD_PRESET_NOT_FOUND]: {
|
|
39
|
+
message: "Unknown architecture preset: {preset}",
|
|
40
|
+
suggestion: "Available presets: mandu, fsd, clean, hexagonal, atomic.",
|
|
41
|
+
},
|
|
42
|
+
[CLI_ERROR_CODES.GUARD_VIOLATION_FOUND]: {
|
|
43
|
+
message: "{count} architecture violation(s) found.",
|
|
44
|
+
suggestion: "Fix violations above or set MANDU_OUTPUT=agent for AI-friendly output.",
|
|
45
|
+
},
|
|
46
|
+
[CLI_ERROR_CODES.BUILD_ENTRY_NOT_FOUND]: {
|
|
47
|
+
message: "Build entry not found: {entry}",
|
|
48
|
+
suggestion: "Check your routes manifest or build inputs.",
|
|
49
|
+
},
|
|
50
|
+
[CLI_ERROR_CODES.BUILD_BUNDLE_FAILED]: {
|
|
51
|
+
message: "Bundle build failed for '{target}'.",
|
|
52
|
+
suggestion: "Review build errors above for missing deps or syntax errors.",
|
|
53
|
+
},
|
|
54
|
+
[CLI_ERROR_CODES.BUILD_OUTDIR_NOT_WRITABLE]: {
|
|
55
|
+
message: "Output directory is not writable: {path}",
|
|
56
|
+
suggestion: "Ensure the directory exists and you have write permissions.",
|
|
57
|
+
},
|
|
58
|
+
[CLI_ERROR_CODES.CONFIG_PARSE_FAILED]: {
|
|
59
|
+
message: "Failed to parse mandu.config.",
|
|
60
|
+
suggestion: "Fix syntax errors in the config file.",
|
|
61
|
+
},
|
|
62
|
+
[CLI_ERROR_CODES.CONFIG_VALIDATION_FAILED]: {
|
|
63
|
+
message: "Configuration validation failed.",
|
|
64
|
+
suggestion: "Review validation errors above and fix your config.",
|
|
65
|
+
},
|
|
66
|
+
[CLI_ERROR_CODES.UNKNOWN_COMMAND]: {
|
|
67
|
+
message: "Unknown command: {command}",
|
|
68
|
+
suggestion: "Run with --help to see available commands.",
|
|
69
|
+
},
|
|
70
|
+
[CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND]: {
|
|
71
|
+
message: "Unknown subcommand '{subcommand}' for {command}.",
|
|
72
|
+
suggestion: "Run the command with --help to see available subcommands.",
|
|
73
|
+
},
|
|
74
|
+
[CLI_ERROR_CODES.MISSING_ARGUMENT]: {
|
|
75
|
+
message: "Missing required argument: {argument}",
|
|
76
|
+
suggestion: "Provide the required argument and try again.",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function interpolate(text: string, context?: Record<string, string | number>): string {
|
|
81
|
+
if (!context) return text;
|
|
82
|
+
let result = text;
|
|
83
|
+
for (const [key, value] of Object.entries(context)) {
|
|
84
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), String(value));
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatCLIError(
|
|
90
|
+
code: CLIErrorCode,
|
|
91
|
+
context?: Record<string, string | number>
|
|
92
|
+
): string {
|
|
93
|
+
const info = ERROR_MESSAGES[code];
|
|
94
|
+
const message = interpolate(info?.message ?? "Unknown error", context);
|
|
95
|
+
const suggestion = info?.suggestion ? interpolate(info.suggestion, context) : undefined;
|
|
96
|
+
|
|
97
|
+
const lines = ["", `β Error [${code}]`, ` ${message}`];
|
|
98
|
+
if (suggestion) {
|
|
99
|
+
lines.push("", `π‘ ${suggestion}`);
|
|
100
|
+
}
|
|
101
|
+
if (info?.docLink) {
|
|
102
|
+
lines.push(`π ${info.docLink}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class CLIError extends Error {
|
|
109
|
+
readonly code: CLIErrorCode;
|
|
110
|
+
readonly context?: Record<string, string | number>;
|
|
111
|
+
|
|
112
|
+
constructor(code: CLIErrorCode, context?: Record<string, string | number>) {
|
|
113
|
+
super(formatCLIError(code, context));
|
|
114
|
+
this.code = code;
|
|
115
|
+
this.context = context;
|
|
116
|
+
this.name = "CLIError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function printCLIError(
|
|
121
|
+
code: CLIErrorCode,
|
|
122
|
+
context?: Record<string, string | number>
|
|
123
|
+
): void {
|
|
124
|
+
console.error(formatCLIError(code, context));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function handleCLIError(error: unknown): never {
|
|
128
|
+
if (error instanceof CLIError) {
|
|
129
|
+
console.error(error.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
console.error(`\nβ Unexpected error: ${error.message}\n`);
|
|
135
|
+
if (process.env.DEBUG) {
|
|
136
|
+
console.error(error.stack);
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.error("\nβ Unknown error occurred\n");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|