@mandujs/cli 0.15.1 → 0.15.3
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 +33 -33
- package/README.md +354 -354
- package/package.json +2 -2
- package/src/commands/check.ts +71 -7
- package/src/commands/contract.ts +173 -173
- package/src/commands/dev.ts +9 -42
- package/src/commands/guard-arch.ts +303 -303
- package/src/commands/init.ts +50 -5
- package/src/commands/monitor.ts +300 -300
- package/src/commands/openapi.ts +107 -107
- package/src/commands/registry.ts +1 -0
- package/src/commands/start.ts +9 -42
- package/src/errors/codes.ts +35 -35
- package/src/errors/index.ts +2 -2
- package/src/errors/messages.ts +143 -143
- package/src/hooks/index.ts +17 -17
- package/src/hooks/preaction.ts +256 -256
- package/src/main.ts +9 -7
- package/src/terminal/banner.ts +166 -166
- package/src/terminal/help.ts +306 -306
- package/src/terminal/index.ts +71 -71
- package/src/terminal/output.ts +295 -295
- package/src/terminal/palette.ts +30 -30
- package/src/terminal/progress.ts +327 -327
- package/src/terminal/stream-writer.ts +214 -214
- package/src/terminal/table.ts +354 -354
- package/src/terminal/theme.ts +142 -142
- package/src/util/bun.ts +6 -6
- package/src/util/fs.ts +23 -23
- package/src/util/handlers.ts +49 -5
- package/src/util/lockfile.ts +66 -0
- package/src/util/output.ts +22 -22
- package/src/util/port.ts +71 -71
- package/templates/default/AGENTS.md +96 -96
- package/templates/default/app/api/health/route.ts +13 -13
- package/templates/default/app/globals.css +49 -49
- package/templates/default/app/layout.tsx +27 -27
- package/templates/default/app/page.tsx +38 -38
- package/templates/default/src/client/shared/lib/utils.ts +16 -16
- package/templates/default/src/client/shared/ui/button.tsx +57 -57
- package/templates/default/src/client/shared/ui/card.tsx +1 -1
- package/templates/default/src/client/shared/ui/index.ts +21 -21
- package/templates/default/src/client/shared/ui/input.tsx +5 -1
- package/templates/default/tests/example.test.ts +58 -58
- package/templates/default/tests/helpers.ts +52 -52
- package/templates/default/tests/setup.ts +9 -9
- package/templates/default/tsconfig.json +23 -23
- package/templates/realtime-chat/AGENTS.md +96 -0
- package/templates/realtime-chat/app/api/chat/messages/route.ts +63 -0
- package/templates/realtime-chat/app/api/chat/stream/route.ts +85 -0
- package/templates/realtime-chat/app/api/health/route.ts +13 -0
- package/templates/realtime-chat/app/globals.css +49 -0
- package/templates/realtime-chat/app/layout.tsx +27 -0
- package/templates/realtime-chat/app/page.tsx +16 -0
- package/templates/realtime-chat/package.json +34 -0
- package/templates/realtime-chat/src/client/app/index.ts +1 -0
- package/templates/realtime-chat/src/client/entities/index.ts +1 -0
- package/templates/realtime-chat/src/client/features/chat/chat-api.ts +209 -0
- package/templates/realtime-chat/src/client/features/chat/realtime-chat-starter.client.tsx +89 -0
- package/templates/realtime-chat/src/client/features/chat/use-realtime-chat.ts +65 -0
- package/templates/realtime-chat/src/client/features/index.ts +1 -0
- package/templates/realtime-chat/src/client/pages/index.ts +1 -0
- package/templates/realtime-chat/src/client/shared/index.ts +1 -0
- package/templates/realtime-chat/src/client/shared/lib/utils.ts +16 -0
- package/templates/realtime-chat/src/client/shared/ui/button.tsx +57 -0
- package/templates/realtime-chat/src/client/shared/ui/card.tsx +78 -0
- package/templates/realtime-chat/src/client/shared/ui/index.ts +21 -0
- package/templates/realtime-chat/src/client/shared/ui/input.tsx +28 -0
- package/templates/realtime-chat/src/client/widgets/index.ts +1 -0
- package/templates/realtime-chat/src/server/api/index.ts +1 -0
- package/templates/realtime-chat/src/server/application/ai-adapter.ts +24 -0
- package/templates/realtime-chat/src/server/application/chat-store.ts +158 -0
- package/templates/realtime-chat/src/server/application/index.ts +1 -0
- package/templates/realtime-chat/src/server/core/index.ts +1 -0
- package/templates/realtime-chat/src/server/domain/index.ts +1 -0
- package/templates/realtime-chat/src/server/infra/index.ts +1 -0
- package/templates/realtime-chat/src/shared/contracts/chat.ts +29 -0
- package/templates/realtime-chat/src/shared/contracts/index.ts +1 -0
- package/templates/realtime-chat/src/shared/env/index.ts +1 -0
- package/templates/realtime-chat/src/shared/schema/index.ts +1 -0
- package/templates/realtime-chat/src/shared/types/index.ts +1 -0
- package/templates/realtime-chat/src/shared/utils/client/index.ts +1 -0
- package/templates/realtime-chat/src/shared/utils/server/index.ts +1 -0
- package/templates/realtime-chat/tests/chat-api.sse.test.ts +188 -0
- package/templates/realtime-chat/tests/chat-starter.test.ts +200 -0
- package/templates/realtime-chat/tests/chat-store.concurrency.test.ts +39 -0
- package/templates/realtime-chat/tests/example.test.ts +58 -0
- package/templates/realtime-chat/tests/helpers.ts +52 -0
- package/templates/realtime-chat/tests/setup.ts +9 -0
- package/templates/realtime-chat/tsconfig.json +23 -0
package/src/terminal/theme.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DNA-009: Mandu CLI Theme System
|
|
3
|
-
*
|
|
4
|
-
* Chalk-based dynamic color theme with NO_COLOR/FORCE_COLOR support
|
|
5
|
-
* Inspired by OpenClaw's terminal/theme.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { MANDU_PALETTE } from "./palette.js";
|
|
9
|
-
|
|
10
|
-
// Bun's native console supports colors, but we need a simple wrapper
|
|
11
|
-
// for consistent theming across the CLI
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Check if rich output (colors) is supported
|
|
15
|
-
*/
|
|
16
|
-
function checkRichSupport(): boolean {
|
|
17
|
-
// NO_COLOR takes precedence (accessibility standard)
|
|
18
|
-
if (process.env.NO_COLOR) {
|
|
19
|
-
const forceColor = process.env.FORCE_COLOR?.trim();
|
|
20
|
-
if (forceColor !== "1" && forceColor !== "true") {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check TTY
|
|
26
|
-
if (!process.stdout.isTTY) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Check TERM
|
|
31
|
-
const term = process.env.TERM;
|
|
32
|
-
if (term === "dumb") {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const richSupported = checkRichSupport();
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* ANSI escape code wrapper
|
|
43
|
-
*/
|
|
44
|
-
function ansi(code: string) {
|
|
45
|
-
return richSupported ? `\x1b[${code}m` : "";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Convert hex to ANSI 256 color (approximation)
|
|
50
|
-
*/
|
|
51
|
-
function hexToAnsi256(hex: string): number {
|
|
52
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
53
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
54
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
55
|
-
|
|
56
|
-
// Convert to 6x6x6 color cube
|
|
57
|
-
const ri = Math.round((r / 255) * 5);
|
|
58
|
-
const gi = Math.round((g / 255) * 5);
|
|
59
|
-
const bi = Math.round((b / 255) * 5);
|
|
60
|
-
|
|
61
|
-
return 16 + 36 * ri + 6 * gi + bi;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a color function from hex
|
|
66
|
-
*/
|
|
67
|
-
function hex(hexColor: string): (text: string) => string {
|
|
68
|
-
if (!richSupported) return (text) => text;
|
|
69
|
-
|
|
70
|
-
const colorCode = hexToAnsi256(hexColor);
|
|
71
|
-
return (text) => `\x1b[38;5;${colorCode}m${text}\x1b[0m`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create a bold color function
|
|
76
|
-
*/
|
|
77
|
-
function boldHex(hexColor: string): (text: string) => string {
|
|
78
|
-
if (!richSupported) return (text) => text;
|
|
79
|
-
|
|
80
|
-
const colorCode = hexToAnsi256(hexColor);
|
|
81
|
-
return (text) => `\x1b[1;38;5;${colorCode}m${text}\x1b[0m`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Mandu CLI Theme
|
|
86
|
-
*/
|
|
87
|
-
export const theme = {
|
|
88
|
-
// Brand colors
|
|
89
|
-
accent: hex(MANDU_PALETTE.accent),
|
|
90
|
-
accentBright: hex(MANDU_PALETTE.accentBright),
|
|
91
|
-
accentDim: hex(MANDU_PALETTE.accentDim),
|
|
92
|
-
|
|
93
|
-
// Semantic colors
|
|
94
|
-
info: hex(MANDU_PALETTE.info),
|
|
95
|
-
success: hex(MANDU_PALETTE.success),
|
|
96
|
-
warn: hex(MANDU_PALETTE.warn),
|
|
97
|
-
error: hex(MANDU_PALETTE.error),
|
|
98
|
-
|
|
99
|
-
// Neutral
|
|
100
|
-
muted: hex(MANDU_PALETTE.muted),
|
|
101
|
-
dim: hex(MANDU_PALETTE.dim),
|
|
102
|
-
|
|
103
|
-
// Composite styles
|
|
104
|
-
heading: boldHex(MANDU_PALETTE.accent),
|
|
105
|
-
command: hex(MANDU_PALETTE.accentBright),
|
|
106
|
-
option: hex(MANDU_PALETTE.warn),
|
|
107
|
-
path: hex(MANDU_PALETTE.info),
|
|
108
|
-
|
|
109
|
-
// Basic styles
|
|
110
|
-
bold: richSupported ? (text: string) => `\x1b[1m${text}\x1b[0m` : (text: string) => text,
|
|
111
|
-
italic: richSupported ? (text: string) => `\x1b[3m${text}\x1b[0m` : (text: string) => text,
|
|
112
|
-
underline: richSupported ? (text: string) => `\x1b[4m${text}\x1b[0m` : (text: string) => text,
|
|
113
|
-
|
|
114
|
-
// Reset
|
|
115
|
-
reset: richSupported ? "\x1b[0m" : "",
|
|
116
|
-
} as const;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check if rich output is available
|
|
120
|
-
*/
|
|
121
|
-
export function isRich(): boolean {
|
|
122
|
-
return richSupported;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Conditionally apply color based on rich mode
|
|
127
|
-
*/
|
|
128
|
-
export function colorize(
|
|
129
|
-
rich: boolean,
|
|
130
|
-
colorFn: (text: string) => string,
|
|
131
|
-
text: string
|
|
132
|
-
): string {
|
|
133
|
-
return rich ? colorFn(text) : text;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Strip ANSI codes from string (for width calculation)
|
|
138
|
-
*/
|
|
139
|
-
export function stripAnsi(text: string): string {
|
|
140
|
-
// eslint-disable-next-line no-control-regex
|
|
141
|
-
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DNA-009: Mandu CLI Theme System
|
|
3
|
+
*
|
|
4
|
+
* Chalk-based dynamic color theme with NO_COLOR/FORCE_COLOR support
|
|
5
|
+
* Inspired by OpenClaw's terminal/theme.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MANDU_PALETTE } from "./palette.js";
|
|
9
|
+
|
|
10
|
+
// Bun's native console supports colors, but we need a simple wrapper
|
|
11
|
+
// for consistent theming across the CLI
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if rich output (colors) is supported
|
|
15
|
+
*/
|
|
16
|
+
function checkRichSupport(): boolean {
|
|
17
|
+
// NO_COLOR takes precedence (accessibility standard)
|
|
18
|
+
if (process.env.NO_COLOR) {
|
|
19
|
+
const forceColor = process.env.FORCE_COLOR?.trim();
|
|
20
|
+
if (forceColor !== "1" && forceColor !== "true") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check TTY
|
|
26
|
+
if (!process.stdout.isTTY) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check TERM
|
|
31
|
+
const term = process.env.TERM;
|
|
32
|
+
if (term === "dumb") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const richSupported = checkRichSupport();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ANSI escape code wrapper
|
|
43
|
+
*/
|
|
44
|
+
function ansi(code: string) {
|
|
45
|
+
return richSupported ? `\x1b[${code}m` : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert hex to ANSI 256 color (approximation)
|
|
50
|
+
*/
|
|
51
|
+
function hexToAnsi256(hex: string): number {
|
|
52
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
53
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
54
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
55
|
+
|
|
56
|
+
// Convert to 6x6x6 color cube
|
|
57
|
+
const ri = Math.round((r / 255) * 5);
|
|
58
|
+
const gi = Math.round((g / 255) * 5);
|
|
59
|
+
const bi = Math.round((b / 255) * 5);
|
|
60
|
+
|
|
61
|
+
return 16 + 36 * ri + 6 * gi + bi;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a color function from hex
|
|
66
|
+
*/
|
|
67
|
+
function hex(hexColor: string): (text: string) => string {
|
|
68
|
+
if (!richSupported) return (text) => text;
|
|
69
|
+
|
|
70
|
+
const colorCode = hexToAnsi256(hexColor);
|
|
71
|
+
return (text) => `\x1b[38;5;${colorCode}m${text}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a bold color function
|
|
76
|
+
*/
|
|
77
|
+
function boldHex(hexColor: string): (text: string) => string {
|
|
78
|
+
if (!richSupported) return (text) => text;
|
|
79
|
+
|
|
80
|
+
const colorCode = hexToAnsi256(hexColor);
|
|
81
|
+
return (text) => `\x1b[1;38;5;${colorCode}m${text}\x1b[0m`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mandu CLI Theme
|
|
86
|
+
*/
|
|
87
|
+
export const theme = {
|
|
88
|
+
// Brand colors
|
|
89
|
+
accent: hex(MANDU_PALETTE.accent),
|
|
90
|
+
accentBright: hex(MANDU_PALETTE.accentBright),
|
|
91
|
+
accentDim: hex(MANDU_PALETTE.accentDim),
|
|
92
|
+
|
|
93
|
+
// Semantic colors
|
|
94
|
+
info: hex(MANDU_PALETTE.info),
|
|
95
|
+
success: hex(MANDU_PALETTE.success),
|
|
96
|
+
warn: hex(MANDU_PALETTE.warn),
|
|
97
|
+
error: hex(MANDU_PALETTE.error),
|
|
98
|
+
|
|
99
|
+
// Neutral
|
|
100
|
+
muted: hex(MANDU_PALETTE.muted),
|
|
101
|
+
dim: hex(MANDU_PALETTE.dim),
|
|
102
|
+
|
|
103
|
+
// Composite styles
|
|
104
|
+
heading: boldHex(MANDU_PALETTE.accent),
|
|
105
|
+
command: hex(MANDU_PALETTE.accentBright),
|
|
106
|
+
option: hex(MANDU_PALETTE.warn),
|
|
107
|
+
path: hex(MANDU_PALETTE.info),
|
|
108
|
+
|
|
109
|
+
// Basic styles
|
|
110
|
+
bold: richSupported ? (text: string) => `\x1b[1m${text}\x1b[0m` : (text: string) => text,
|
|
111
|
+
italic: richSupported ? (text: string) => `\x1b[3m${text}\x1b[0m` : (text: string) => text,
|
|
112
|
+
underline: richSupported ? (text: string) => `\x1b[4m${text}\x1b[0m` : (text: string) => text,
|
|
113
|
+
|
|
114
|
+
// Reset
|
|
115
|
+
reset: richSupported ? "\x1b[0m" : "",
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if rich output is available
|
|
120
|
+
*/
|
|
121
|
+
export function isRich(): boolean {
|
|
122
|
+
return richSupported;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Conditionally apply color based on rich mode
|
|
127
|
+
*/
|
|
128
|
+
export function colorize(
|
|
129
|
+
rich: boolean,
|
|
130
|
+
colorFn: (text: string) => string,
|
|
131
|
+
text: string
|
|
132
|
+
): string {
|
|
133
|
+
return rich ? colorFn(text) : text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Strip ANSI codes from string (for width calculation)
|
|
138
|
+
*/
|
|
139
|
+
export function stripAnsi(text: string): string {
|
|
140
|
+
// eslint-disable-next-line no-control-regex
|
|
141
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
+
}
|
package/src/util/bun.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export function importFresh<T = unknown>(modulePath: string): Promise<T> {
|
|
2
|
-
const url = Bun.pathToFileURL(modulePath);
|
|
3
|
-
const cacheBusted = new URL(url.href);
|
|
4
|
-
cacheBusted.searchParams.set("t", Date.now().toString());
|
|
5
|
-
return import(cacheBusted.href) as Promise<T>;
|
|
6
|
-
}
|
|
1
|
+
export function importFresh<T = unknown>(modulePath: string): Promise<T> {
|
|
2
|
+
const url = Bun.pathToFileURL(modulePath);
|
|
3
|
+
const cacheBusted = new URL(url.href);
|
|
4
|
+
cacheBusted.searchParams.set("t", Date.now().toString());
|
|
5
|
+
return import(cacheBusted.href) as Promise<T>;
|
|
6
|
+
}
|
package/src/util/fs.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
3
|
|
|
4
4
|
export function resolveFromCwd(...paths: string[]): string {
|
|
5
5
|
return path.resolve(process.cwd(), ...paths);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function getRootDir(): string {
|
|
9
|
-
return process.cwd();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function pathExists(targetPath: string): Promise<boolean> {
|
|
13
|
-
try {
|
|
14
|
-
await fs.access(targetPath);
|
|
15
|
-
return true;
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function isDirectory(targetPath: string): Promise<boolean> {
|
|
22
|
-
try {
|
|
23
|
-
const stat = await fs.stat(targetPath);
|
|
24
|
-
return stat.isDirectory();
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
8
|
+
export function getRootDir(): string {
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function pathExists(targetPath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(targetPath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function isDirectory(targetPath: string): Promise<boolean> {
|
|
22
|
+
try {
|
|
23
|
+
const stat = await fs.stat(targetPath);
|
|
24
|
+
return stat.isDirectory();
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/util/handlers.ts
CHANGED
|
@@ -8,6 +8,45 @@ import {
|
|
|
8
8
|
} from "@mandujs/core";
|
|
9
9
|
import path from "path";
|
|
10
10
|
|
|
11
|
+
type RouteModule = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const;
|
|
14
|
+
|
|
15
|
+
type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
16
|
+
|
|
17
|
+
function isHttpMethod(method: string): method is HttpMethod {
|
|
18
|
+
return (HTTP_METHODS as readonly string[]).includes(method);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasHttpMethodHandlers(module: RouteModule): boolean {
|
|
22
|
+
return HTTP_METHODS.some((method) => typeof module[method] === "function");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createMethodDispatcher(module: RouteModule, routeId: string) {
|
|
26
|
+
return async (req: Request, params: Record<string, string> = {}) => {
|
|
27
|
+
const method = req.method.toUpperCase();
|
|
28
|
+
const handler = (isHttpMethod(method) ? module[method] : undefined) as
|
|
29
|
+
| ((request: Request, context?: { params: Record<string, string> }) => Response | Promise<Response>)
|
|
30
|
+
| undefined;
|
|
31
|
+
|
|
32
|
+
if (!handler) {
|
|
33
|
+
return Response.json(
|
|
34
|
+
{
|
|
35
|
+
error: `Method ${method} not allowed for route ${routeId}`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
status: 405,
|
|
39
|
+
headers: {
|
|
40
|
+
Allow: HTTP_METHODS.filter((m) => typeof module[m] === "function").join(", "),
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return handler(req, { params });
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
11
50
|
export interface RegisterHandlersOptions {
|
|
12
51
|
/** 모듈 import 함수 (dev: importFresh, start: 표준 import) */
|
|
13
52
|
importFn: (modulePath: string) => Promise<any>;
|
|
@@ -39,17 +78,22 @@ export async function registerManifestHandlers(
|
|
|
39
78
|
const module = await importFn(modulePath);
|
|
40
79
|
let handler = module.default || module.handler || module;
|
|
41
80
|
|
|
42
|
-
// ManduFilling
|
|
81
|
+
// 1) ManduFilling 인스턴스
|
|
43
82
|
if (handler && typeof handler.handle === "function") {
|
|
44
83
|
console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
|
|
45
84
|
const filling = handler;
|
|
46
85
|
handler = async (req: Request, params?: Record<string, string>) => {
|
|
47
86
|
return filling.handle(req, params);
|
|
48
87
|
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
88
|
+
}
|
|
89
|
+
// 2) Route module with HTTP method exports (GET/POST/...)
|
|
90
|
+
else if (handler && typeof handler === "object" && hasHttpMethodHandlers(handler as RouteModule)) {
|
|
91
|
+
handler = createMethodDispatcher(handler as RouteModule, route.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof handler !== "function") {
|
|
95
|
+
console.warn(` ⚠️ API 핸들러 변환 실패: ${route.id} (type: ${typeof handler})`);
|
|
96
|
+
continue;
|
|
53
97
|
}
|
|
54
98
|
|
|
55
99
|
registerApiHandler(route.id, handler);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readLockfile,
|
|
3
|
+
readMcpConfig,
|
|
4
|
+
validateWithPolicy,
|
|
5
|
+
detectMode,
|
|
6
|
+
formatPolicyAction,
|
|
7
|
+
formatValidationResult,
|
|
8
|
+
type LockfileValidationResult,
|
|
9
|
+
} from "@mandujs/core";
|
|
10
|
+
|
|
11
|
+
export async function validateRuntimeLockfile(config: Record<string, unknown>, rootDir: string) {
|
|
12
|
+
const lockfile = await readLockfile(rootDir);
|
|
13
|
+
|
|
14
|
+
let mcpConfig: Record<string, unknown> | null = null;
|
|
15
|
+
try {
|
|
16
|
+
mcpConfig = await readMcpConfig(rootDir);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.warn(
|
|
19
|
+
`⚠️ MCP 설정 로드 실패: ${error instanceof Error ? error.message : String(error)}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { result: lockResult, action, bypassed } = validateWithPolicy(
|
|
24
|
+
config,
|
|
25
|
+
lockfile,
|
|
26
|
+
detectMode(),
|
|
27
|
+
mcpConfig
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return { lockfile, lockResult, action, bypassed };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function handleBlockedLockfile(action: "pass" | "warn" | "error" | "block", lockResult: LockfileValidationResult | null): void {
|
|
34
|
+
if (action !== "block") return;
|
|
35
|
+
|
|
36
|
+
console.error("🛑 서버 시작 차단: Lockfile 불일치");
|
|
37
|
+
console.error(" 설정이 변경되었습니다. 의도한 변경이라면 아래 중 하나를 실행하세요:");
|
|
38
|
+
console.error(" $ mandu lock");
|
|
39
|
+
console.error(" $ bunx mandu lock");
|
|
40
|
+
console.error("");
|
|
41
|
+
console.error(" 변경 사항 확인:");
|
|
42
|
+
console.error(" $ mandu lock --diff");
|
|
43
|
+
console.error(" $ bunx mandu lock --diff");
|
|
44
|
+
if (lockResult) {
|
|
45
|
+
console.error("");
|
|
46
|
+
console.error(formatValidationResult(lockResult));
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function printRuntimeLockfileStatus(
|
|
52
|
+
action: "pass" | "warn" | "error" | "block",
|
|
53
|
+
bypassed: boolean,
|
|
54
|
+
lockfile: unknown,
|
|
55
|
+
lockResult: LockfileValidationResult | null
|
|
56
|
+
): void {
|
|
57
|
+
if (action === "warn") {
|
|
58
|
+
console.log(`⚠️ ${formatPolicyAction(action, bypassed)}`);
|
|
59
|
+
console.log(` ↳ lock 갱신: mandu lock (or bunx mandu lock)`);
|
|
60
|
+
console.log(` ↳ 변경 확인: mandu lock --diff (or bunx mandu lock --diff)`);
|
|
61
|
+
} else if (lockfile && lockResult?.valid) {
|
|
62
|
+
console.log(`🔒 설정 무결성 확인됨 (${lockResult.currentHash?.slice(0, 8)})`);
|
|
63
|
+
} else if (!lockfile) {
|
|
64
|
+
console.log(`💡 Lockfile 없음 - 'mandu lock' 또는 'bunx mandu lock'으로 생성 권장`);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/util/output.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { getOutputMode } from "../terminal/output";
|
|
2
|
-
|
|
3
|
-
export type OutputFormat = "console" | "agent" | "json";
|
|
4
|
-
|
|
5
|
-
function normalizeFormat(value?: string): OutputFormat | undefined {
|
|
6
|
-
if (!value) return undefined;
|
|
7
|
-
const normalized = value.toLowerCase();
|
|
8
|
-
if (normalized === "console" || normalized === "agent" || normalized === "json") {
|
|
9
|
-
return normalized;
|
|
10
|
-
}
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function resolveOutputFormat(explicit?: OutputFormat): OutputFormat {
|
|
15
|
-
const env = process.env;
|
|
16
|
-
|
|
17
|
-
const direct = normalizeFormat(explicit) ?? normalizeFormat(env.MANDU_OUTPUT);
|
|
18
|
-
if (direct) return direct;
|
|
19
|
-
|
|
20
|
-
const mode = getOutputMode();
|
|
21
|
-
return mode === "json" ? "json" : "console";
|
|
22
|
-
}
|
|
1
|
+
import { getOutputMode } from "../terminal/output";
|
|
2
|
+
|
|
3
|
+
export type OutputFormat = "console" | "agent" | "json";
|
|
4
|
+
|
|
5
|
+
function normalizeFormat(value?: string): OutputFormat | undefined {
|
|
6
|
+
if (!value) return undefined;
|
|
7
|
+
const normalized = value.toLowerCase();
|
|
8
|
+
if (normalized === "console" || normalized === "agent" || normalized === "json") {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveOutputFormat(explicit?: OutputFormat): OutputFormat {
|
|
15
|
+
const env = process.env;
|
|
16
|
+
|
|
17
|
+
const direct = normalizeFormat(explicit) ?? normalizeFormat(env.MANDU_OUTPUT);
|
|
18
|
+
if (direct) return direct;
|
|
19
|
+
|
|
20
|
+
const mode = getOutputMode();
|
|
21
|
+
return mode === "json" ? "json" : "console";
|
|
22
|
+
}
|
package/src/util/port.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { createServer } from "net";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
4
|
-
|
|
5
|
-
function isPortUsable(error: unknown): boolean {
|
|
6
|
-
if (!error || typeof error !== "object") return false;
|
|
7
|
-
const code = (error as { code?: string }).code;
|
|
8
|
-
return code === "EADDRINUSE" || code === "EACCES";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
const server = createServer();
|
|
14
|
-
|
|
15
|
-
server.once("error", (error) => {
|
|
16
|
-
if (isPortUsable(error)) {
|
|
17
|
-
resolve(false);
|
|
18
|
-
} else {
|
|
19
|
-
resolve(false);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
server.once("listening", () => {
|
|
24
|
-
server.close(() => resolve(true));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
server.listen(port, hostname);
|
|
29
|
-
server.unref();
|
|
30
|
-
} catch {
|
|
31
|
-
resolve(false);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function resolveAvailablePort(
|
|
37
|
-
startPort: number,
|
|
38
|
-
options: {
|
|
39
|
-
hostname?: string;
|
|
40
|
-
offsets?: number[];
|
|
41
|
-
maxAttempts?: number;
|
|
42
|
-
} = {}
|
|
43
|
-
): Promise<{ port: number; attempts: number }> {
|
|
44
|
-
const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
|
|
45
|
-
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
46
|
-
|
|
47
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
48
|
-
const candidate = startPort + attempt;
|
|
49
|
-
if (candidate < 1 || candidate > 65535) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const targets = offsets
|
|
54
|
-
.map((offset) => candidate + offset)
|
|
55
|
-
.filter((port) => port >= 1 && port <= 65535);
|
|
56
|
-
|
|
57
|
-
if (targets.length !== offsets.length) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const results = await Promise.all(
|
|
62
|
-
targets.map((port) => isPortAvailable(port, options.hostname))
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (results.every(Boolean)) {
|
|
66
|
-
return { port: candidate, attempts: attempt };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
throw new Error(`No available port found starting at ${startPort}`);
|
|
71
|
-
}
|
|
1
|
+
import { createServer } from "net";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
4
|
+
|
|
5
|
+
function isPortUsable(error: unknown): boolean {
|
|
6
|
+
if (!error || typeof error !== "object") return false;
|
|
7
|
+
const code = (error as { code?: string }).code;
|
|
8
|
+
return code === "EADDRINUSE" || code === "EACCES";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function isPortAvailable(port: number, hostname?: string): Promise<boolean> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = createServer();
|
|
14
|
+
|
|
15
|
+
server.once("error", (error) => {
|
|
16
|
+
if (isPortUsable(error)) {
|
|
17
|
+
resolve(false);
|
|
18
|
+
} else {
|
|
19
|
+
resolve(false);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
server.once("listening", () => {
|
|
24
|
+
server.close(() => resolve(true));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
server.listen(port, hostname);
|
|
29
|
+
server.unref();
|
|
30
|
+
} catch {
|
|
31
|
+
resolve(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function resolveAvailablePort(
|
|
37
|
+
startPort: number,
|
|
38
|
+
options: {
|
|
39
|
+
hostname?: string;
|
|
40
|
+
offsets?: number[];
|
|
41
|
+
maxAttempts?: number;
|
|
42
|
+
} = {}
|
|
43
|
+
): Promise<{ port: number; attempts: number }> {
|
|
44
|
+
const offsets = options.offsets && options.offsets.length > 0 ? options.offsets : [0];
|
|
45
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
48
|
+
const candidate = startPort + attempt;
|
|
49
|
+
if (candidate < 1 || candidate > 65535) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const targets = offsets
|
|
54
|
+
.map((offset) => candidate + offset)
|
|
55
|
+
.filter((port) => port >= 1 && port <= 65535);
|
|
56
|
+
|
|
57
|
+
if (targets.length !== offsets.length) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = await Promise.all(
|
|
62
|
+
targets.map((port) => isPortAvailable(port, options.hostname))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (results.every(Boolean)) {
|
|
66
|
+
return { port: candidate, attempts: attempt };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`No available port found starting at ${startPort}`);
|
|
71
|
+
}
|