@mandujs/cli 0.12.2 → 0.13.0

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 (51) hide show
  1. package/README.ko.md +234 -234
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/contract.ts +173 -173
  5. package/src/commands/dev.ts +8 -68
  6. package/src/commands/doctor.ts +27 -27
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +367 -357
  12. package/src/commands/routes.ts +228 -228
  13. package/src/commands/start.ts +184 -0
  14. package/src/errors/codes.ts +35 -35
  15. package/src/errors/index.ts +2 -2
  16. package/src/errors/messages.ts +143 -143
  17. package/src/hooks/index.ts +17 -17
  18. package/src/hooks/preaction.ts +256 -256
  19. package/src/main.ts +37 -34
  20. package/src/terminal/banner.ts +166 -166
  21. package/src/terminal/help.ts +306 -306
  22. package/src/terminal/index.ts +71 -71
  23. package/src/terminal/output.ts +295 -295
  24. package/src/terminal/palette.ts +30 -30
  25. package/src/terminal/progress.ts +327 -327
  26. package/src/terminal/stream-writer.ts +214 -214
  27. package/src/terminal/table.ts +354 -354
  28. package/src/terminal/theme.ts +142 -142
  29. package/src/util/bun.ts +6 -6
  30. package/src/util/fs.ts +23 -23
  31. package/src/util/handlers.ts +96 -0
  32. package/src/util/manifest.ts +52 -52
  33. package/src/util/output.ts +22 -22
  34. package/src/util/port.ts +71 -71
  35. package/templates/default/AGENTS.md +96 -96
  36. package/templates/default/app/api/health/route.ts +13 -13
  37. package/templates/default/app/globals.css +49 -49
  38. package/templates/default/app/layout.tsx +27 -27
  39. package/templates/default/app/page.tsx +38 -38
  40. package/templates/default/package.json +1 -0
  41. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  42. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  43. package/templates/default/src/client/shared/ui/card.tsx +78 -78
  44. package/templates/default/src/client/shared/ui/index.ts +21 -21
  45. package/templates/default/src/client/shared/ui/input.tsx +24 -24
  46. package/templates/default/tests/example.test.ts +58 -58
  47. package/templates/default/tests/helpers.ts +52 -52
  48. package/templates/default/tests/setup.ts +9 -9
  49. package/templates/default/tsconfig.json +12 -14
  50. package/templates/default/apps/server/main.ts +0 -67
  51. package/templates/default/apps/web/entry.tsx +0 -35
@@ -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
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ registerApiHandler,
3
+ registerPageLoader,
4
+ registerPageHandler,
5
+ registerLayoutLoader,
6
+ needsHydration,
7
+ type RoutesManifest,
8
+ } from "@mandujs/core";
9
+ import path from "path";
10
+
11
+ export interface RegisterHandlersOptions {
12
+ /** 모듈 import 함수 (dev: importFresh, start: 표준 import) */
13
+ importFn: (modulePath: string) => Promise<any>;
14
+ /** 이미 등록된 layout 경로 추적용 Set */
15
+ registeredLayouts: Set<string>;
16
+ /** 리로드 시 layout 캐시 클리어 */
17
+ isReload?: boolean;
18
+ }
19
+
20
+ /**
21
+ * 매니페스트 라우트를 서버 핸들러로 등록
22
+ * dev.ts와 start.ts에서 공유
23
+ */
24
+ export async function registerManifestHandlers(
25
+ manifest: RoutesManifest,
26
+ rootDir: string,
27
+ options: RegisterHandlersOptions
28
+ ): Promise<void> {
29
+ const { importFn, registeredLayouts, isReload = false } = options;
30
+
31
+ if (isReload) {
32
+ registeredLayouts.clear();
33
+ }
34
+
35
+ for (const route of manifest.routes) {
36
+ if (route.kind === "api") {
37
+ const modulePath = path.resolve(rootDir, route.module);
38
+ try {
39
+ const module = await importFn(modulePath);
40
+ let handler = module.default || module.handler || module;
41
+
42
+ // ManduFilling 인스턴스를 핸들러 함수로 래핑
43
+ if (handler && typeof handler.handle === "function") {
44
+ console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
45
+ const filling = handler;
46
+ handler = async (req: Request, params?: Record<string, string>) => {
47
+ return filling.handle(req, params);
48
+ };
49
+ } else {
50
+ console.log(
51
+ ` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`
52
+ );
53
+ }
54
+
55
+ registerApiHandler(route.id, handler);
56
+ console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
57
+ } catch (error) {
58
+ console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
59
+ }
60
+ } else if (route.kind === "page" && route.componentModule) {
61
+ const componentPath = path.resolve(rootDir, route.componentModule);
62
+ const isIsland = needsHydration(route);
63
+ const hasLayout = route.layoutChain && route.layoutChain.length > 0;
64
+
65
+ // Layout 로더 등록
66
+ if (route.layoutChain) {
67
+ for (const layoutPath of route.layoutChain) {
68
+ if (!registeredLayouts.has(layoutPath)) {
69
+ const absLayoutPath = path.resolve(rootDir, layoutPath);
70
+ registerLayoutLoader(layoutPath, async () => {
71
+ return importFn(absLayoutPath);
72
+ });
73
+ registeredLayouts.add(layoutPath);
74
+ console.log(` 🎨 Layout: ${layoutPath}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ // slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
80
+ if (route.slotModule) {
81
+ registerPageHandler(route.id, async () => {
82
+ const module = await importFn(componentPath);
83
+ return module.default;
84
+ });
85
+ console.log(
86
+ ` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`
87
+ );
88
+ } else {
89
+ registerPageLoader(route.id, () => importFn(componentPath));
90
+ console.log(
91
+ ` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`
92
+ );
93
+ }
94
+ }
95
+ }
96
+ }
@@ -1,52 +1,52 @@
1
- import path from "path";
2
- import {
3
- generateManifest,
4
- loadManifest,
5
- type RoutesManifest,
6
- type FSScannerConfig,
7
- } from "@mandujs/core";
8
- import { isDirectory } from "./fs";
9
-
10
- export type ManifestSource = "fs" | "spec";
11
-
12
- export interface ResolvedManifest {
13
- manifest: RoutesManifest;
14
- source: ManifestSource;
15
- warnings: string[];
16
- }
17
-
18
- export async function resolveManifest(
19
- rootDir: string,
20
- options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
21
- ): Promise<ResolvedManifest> {
22
- const appDir = path.resolve(rootDir, "app");
23
- const hasApp = await isDirectory(appDir);
24
-
25
- if (hasApp) {
26
- const result = await generateManifest(rootDir, {
27
- scanner: options.fsRoutes,
28
- outputPath: options.outputPath,
29
- skipLegacy: true,
30
- });
31
- return {
32
- manifest: result.manifest,
33
- source: "fs",
34
- warnings: result.warnings,
35
- };
36
- }
37
-
38
- const specPath = path.join(rootDir, "spec", "routes.manifest.json");
39
- if (await Bun.file(specPath).exists()) {
40
- const result = await loadManifest(specPath);
41
- if (!result.success) {
42
- throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
43
- }
44
- return {
45
- manifest: result.data!,
46
- source: "spec",
47
- warnings: [],
48
- };
49
- }
50
-
51
- throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
52
- }
1
+ import path from "path";
2
+ import {
3
+ generateManifest,
4
+ loadManifest,
5
+ type RoutesManifest,
6
+ type FSScannerConfig,
7
+ } from "@mandujs/core";
8
+ import { isDirectory } from "./fs";
9
+
10
+ export type ManifestSource = "fs" | "spec";
11
+
12
+ export interface ResolvedManifest {
13
+ manifest: RoutesManifest;
14
+ source: ManifestSource;
15
+ warnings: string[];
16
+ }
17
+
18
+ export async function resolveManifest(
19
+ rootDir: string,
20
+ options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
21
+ ): Promise<ResolvedManifest> {
22
+ const appDir = path.resolve(rootDir, "app");
23
+ const hasApp = await isDirectory(appDir);
24
+
25
+ if (hasApp) {
26
+ const result = await generateManifest(rootDir, {
27
+ scanner: options.fsRoutes,
28
+ outputPath: options.outputPath,
29
+ skipLegacy: true,
30
+ });
31
+ return {
32
+ manifest: result.manifest,
33
+ source: "fs",
34
+ warnings: result.warnings,
35
+ };
36
+ }
37
+
38
+ const specPath = path.join(rootDir, "spec", "routes.manifest.json");
39
+ if (await Bun.file(specPath).exists()) {
40
+ const result = await loadManifest(specPath);
41
+ if (!result.success) {
42
+ throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
43
+ }
44
+ return {
45
+ manifest: result.data!,
46
+ source: "spec",
47
+ warnings: [],
48
+ };
49
+ }
50
+
51
+ throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
52
+ }
@@ -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
+ }