@mandujs/cli 0.15.1 → 0.15.2

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.
Files changed (90) hide show
  1. package/README.ko.md +33 -33
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/check.ts +71 -7
  5. package/src/commands/contract.ts +173 -173
  6. package/src/commands/dev.ts +9 -42
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/init.ts +50 -5
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +1 -0
  12. package/src/commands/start.ts +9 -42
  13. package/src/errors/codes.ts +35 -35
  14. package/src/errors/index.ts +2 -2
  15. package/src/errors/messages.ts +143 -143
  16. package/src/hooks/index.ts +17 -17
  17. package/src/hooks/preaction.ts +256 -256
  18. package/src/main.ts +9 -7
  19. package/src/terminal/banner.ts +166 -166
  20. package/src/terminal/help.ts +306 -306
  21. package/src/terminal/index.ts +71 -71
  22. package/src/terminal/output.ts +295 -295
  23. package/src/terminal/palette.ts +30 -30
  24. package/src/terminal/progress.ts +327 -327
  25. package/src/terminal/stream-writer.ts +214 -214
  26. package/src/terminal/table.ts +354 -354
  27. package/src/terminal/theme.ts +142 -142
  28. package/src/util/bun.ts +6 -6
  29. package/src/util/fs.ts +23 -23
  30. package/src/util/handlers.ts +49 -5
  31. package/src/util/lockfile.ts +66 -0
  32. package/src/util/output.ts +22 -22
  33. package/src/util/port.ts +71 -71
  34. package/templates/default/AGENTS.md +96 -96
  35. package/templates/default/app/api/health/route.ts +13 -13
  36. package/templates/default/app/globals.css +49 -49
  37. package/templates/default/app/layout.tsx +27 -27
  38. package/templates/default/app/page.tsx +38 -38
  39. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  40. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  41. package/templates/default/src/client/shared/ui/card.tsx +1 -1
  42. package/templates/default/src/client/shared/ui/index.ts +21 -21
  43. package/templates/default/src/client/shared/ui/input.tsx +5 -1
  44. package/templates/default/tests/example.test.ts +58 -58
  45. package/templates/default/tests/helpers.ts +52 -52
  46. package/templates/default/tests/setup.ts +9 -9
  47. package/templates/default/tsconfig.json +23 -23
  48. package/templates/realtime-chat/AGENTS.md +96 -0
  49. package/templates/realtime-chat/app/api/chat/messages/route.ts +63 -0
  50. package/templates/realtime-chat/app/api/chat/stream/route.ts +48 -0
  51. package/templates/realtime-chat/app/api/health/route.ts +13 -0
  52. package/templates/realtime-chat/app/globals.css +49 -0
  53. package/templates/realtime-chat/app/layout.tsx +27 -0
  54. package/templates/realtime-chat/app/page.tsx +16 -0
  55. package/templates/realtime-chat/package.json +34 -0
  56. package/templates/realtime-chat/src/client/app/index.ts +1 -0
  57. package/templates/realtime-chat/src/client/entities/index.ts +1 -0
  58. package/templates/realtime-chat/src/client/features/chat/chat-api.ts +177 -0
  59. package/templates/realtime-chat/src/client/features/chat/realtime-chat-starter.client.tsx +89 -0
  60. package/templates/realtime-chat/src/client/features/chat/use-realtime-chat.ts +73 -0
  61. package/templates/realtime-chat/src/client/features/index.ts +1 -0
  62. package/templates/realtime-chat/src/client/pages/index.ts +1 -0
  63. package/templates/realtime-chat/src/client/shared/index.ts +1 -0
  64. package/templates/realtime-chat/src/client/shared/lib/utils.ts +16 -0
  65. package/templates/realtime-chat/src/client/shared/ui/button.tsx +57 -0
  66. package/templates/realtime-chat/src/client/shared/ui/card.tsx +78 -0
  67. package/templates/realtime-chat/src/client/shared/ui/index.ts +21 -0
  68. package/templates/realtime-chat/src/client/shared/ui/input.tsx +28 -0
  69. package/templates/realtime-chat/src/client/widgets/index.ts +1 -0
  70. package/templates/realtime-chat/src/server/api/index.ts +1 -0
  71. package/templates/realtime-chat/src/server/application/ai-adapter.ts +24 -0
  72. package/templates/realtime-chat/src/server/application/chat-store.ts +88 -0
  73. package/templates/realtime-chat/src/server/application/index.ts +1 -0
  74. package/templates/realtime-chat/src/server/core/index.ts +1 -0
  75. package/templates/realtime-chat/src/server/domain/index.ts +1 -0
  76. package/templates/realtime-chat/src/server/infra/index.ts +1 -0
  77. package/templates/realtime-chat/src/shared/contracts/chat.ts +29 -0
  78. package/templates/realtime-chat/src/shared/contracts/index.ts +1 -0
  79. package/templates/realtime-chat/src/shared/env/index.ts +1 -0
  80. package/templates/realtime-chat/src/shared/schema/index.ts +1 -0
  81. package/templates/realtime-chat/src/shared/types/index.ts +1 -0
  82. package/templates/realtime-chat/src/shared/utils/client/index.ts +1 -0
  83. package/templates/realtime-chat/src/shared/utils/server/index.ts +1 -0
  84. package/templates/realtime-chat/tests/chat-api.sse.test.ts +151 -0
  85. package/templates/realtime-chat/tests/chat-starter.test.ts +149 -0
  86. package/templates/realtime-chat/tests/chat-store.concurrency.test.ts +39 -0
  87. package/templates/realtime-chat/tests/example.test.ts +58 -0
  88. package/templates/realtime-chat/tests/helpers.ts +52 -0
  89. package/templates/realtime-chat/tests/setup.ts +9 -0
  90. package/templates/realtime-chat/tsconfig.json +23 -0
@@ -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
+ }
@@ -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
- } else {
50
- console.log(
51
- ` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`
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
+ }
@@ -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
+ }