@mandujs/cli 0.9.22 β†’ 0.9.23

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 (40) hide show
  1. package/README.ko.md +57 -4
  2. package/README.md +47 -15
  3. package/package.json +1 -1
  4. package/src/commands/check.ts +41 -5
  5. package/src/commands/contract.ts +135 -9
  6. package/src/commands/dev.ts +155 -95
  7. package/src/commands/guard-arch.ts +39 -9
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/init.ts +264 -9
  10. package/src/commands/monitor.ts +21 -0
  11. package/src/main.ts +386 -344
  12. package/templates/default/app/globals.css +37 -0
  13. package/templates/default/app/layout.tsx +27 -0
  14. package/templates/default/app/page.tsx +27 -49
  15. package/templates/default/package.json +15 -6
  16. package/templates/default/postcss.config.js +6 -0
  17. package/templates/default/src/client/app/index.ts +1 -0
  18. package/templates/default/src/client/entities/index.ts +1 -0
  19. package/templates/default/src/client/features/index.ts +1 -0
  20. package/templates/default/src/client/pages/index.ts +1 -0
  21. package/templates/default/src/client/shared/index.ts +1 -0
  22. package/templates/default/src/client/shared/lib/utils.ts +16 -0
  23. package/templates/default/src/client/shared/ui/button.tsx +57 -0
  24. package/templates/default/src/client/shared/ui/card.tsx +78 -0
  25. package/templates/default/src/client/shared/ui/index.ts +21 -0
  26. package/templates/default/src/client/shared/ui/input.tsx +24 -0
  27. package/templates/default/src/client/widgets/index.ts +1 -0
  28. package/templates/default/src/server/api/index.ts +1 -0
  29. package/templates/default/src/server/application/index.ts +1 -0
  30. package/templates/default/src/server/core/index.ts +1 -0
  31. package/templates/default/src/server/domain/index.ts +1 -0
  32. package/templates/default/src/server/infra/index.ts +1 -0
  33. package/templates/default/src/shared/contracts/index.ts +1 -0
  34. package/templates/default/src/shared/env/index.ts +1 -0
  35. package/templates/default/src/shared/schema/index.ts +1 -0
  36. package/templates/default/src/shared/types/index.ts +1 -0
  37. package/templates/default/src/shared/utils/client/index.ts +1 -0
  38. package/templates/default/src/shared/utils/server/index.ts +1 -0
  39. package/templates/default/tailwind.config.ts +64 -0
  40. package/templates/default/tsconfig.json +14 -3
@@ -1,30 +1,137 @@
1
1
  import path from "path";
2
2
  import fs from "fs/promises";
3
3
 
4
+ export type CSSFramework = "tailwind" | "panda" | "none";
5
+ export type UILibrary = "shadcn" | "ark" | "none";
6
+
4
7
  export interface InitOptions {
5
8
  name?: string;
6
9
  template?: string;
10
+ css?: CSSFramework;
11
+ ui?: UILibrary;
12
+ theme?: boolean;
13
+ minimal?: boolean;
14
+ }
15
+
16
+ // Files to skip based on CSS/UI options
17
+ const CSS_FILES = [
18
+ "tailwind.config.ts",
19
+ "postcss.config.js",
20
+ "app/globals.css",
21
+ ];
22
+
23
+ const UI_FILES = [
24
+ "src/client/shared/ui/button.tsx",
25
+ "src/client/shared/ui/card.tsx",
26
+ "src/client/shared/ui/input.tsx",
27
+ "src/client/shared/ui/index.ts",
28
+ "src/client/shared/lib/utils.ts",
29
+ ];
30
+
31
+ interface CopyOptions {
32
+ projectName: string;
33
+ css: CSSFramework;
34
+ ui: UILibrary;
35
+ theme: boolean;
36
+ }
37
+
38
+ function shouldSkipFile(relativePath: string, options: CopyOptions): boolean {
39
+ const normalizedPath = relativePath.replace(/\\/g, "/");
40
+
41
+ // Skip CSS files if css is none
42
+ if (options.css === "none") {
43
+ if (CSS_FILES.some((f) => normalizedPath.endsWith(f))) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ // Skip UI files if ui is none
49
+ if (options.ui === "none") {
50
+ if (UI_FILES.some((f) => normalizedPath.endsWith(f))) {
51
+ return true;
52
+ }
53
+ // Skip UI/shared directories
54
+ if (normalizedPath.includes("src/client/shared/ui/")) return true;
55
+ if (normalizedPath.includes("src/client/shared/lib/")) return true;
56
+ }
57
+
58
+ return false;
7
59
  }
8
60
 
9
- async function copyDir(src: string, dest: string, projectName: string): Promise<void> {
61
+ async function copyDir(
62
+ src: string,
63
+ dest: string,
64
+ options: CopyOptions,
65
+ relativePath = ""
66
+ ): Promise<void> {
10
67
  await fs.mkdir(dest, { recursive: true });
11
68
  const entries = await fs.readdir(src, { withFileTypes: true });
12
69
 
13
70
  for (const entry of entries) {
14
71
  const srcPath = path.join(src, entry.name);
15
72
  const destPath = path.join(dest, entry.name);
73
+ const currentRelativePath = relativePath
74
+ ? `${relativePath}/${entry.name}`
75
+ : entry.name;
16
76
 
17
77
  if (entry.isDirectory()) {
18
- await copyDir(srcPath, destPath, projectName);
19
- } else {
78
+ // Skip directories that would be empty when ui=none
79
+ if (options.ui === "none") {
80
+ if (entry.name === "ui" && relativePath === "src/client/shared") continue;
81
+ if (entry.name === "lib" && relativePath === "src/client/shared") continue;
82
+ }
83
+ await copyDir(srcPath, destPath, options, currentRelativePath);
84
+ } else {
85
+ // Check if file should be skipped
86
+ if (shouldSkipFile(currentRelativePath, options)) {
87
+ continue;
88
+ }
89
+
20
90
  let content = await fs.readFile(srcPath, "utf-8");
21
91
  // Replace template variables
22
- content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
92
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
93
+
94
+ // Add dark mode CSS variables if theme is enabled
95
+ if (options.theme && currentRelativePath === "app/globals.css") {
96
+ content = addDarkModeCSS(content);
97
+ }
98
+
23
99
  await fs.writeFile(destPath, content);
24
100
  }
25
101
  }
26
102
  }
27
103
 
104
+ function addDarkModeCSS(content: string): string {
105
+ const darkModeCSS = `
106
+ .dark {
107
+ --background: 222.2 84% 4.9%;
108
+ --foreground: 210 40% 98%;
109
+ --card: 222.2 84% 4.9%;
110
+ --card-foreground: 210 40% 98%;
111
+ --popover: 222.2 84% 4.9%;
112
+ --popover-foreground: 210 40% 98%;
113
+ --primary: 210 40% 98%;
114
+ --primary-foreground: 222.2 47.4% 11.2%;
115
+ --secondary: 217.2 32.6% 17.5%;
116
+ --secondary-foreground: 210 40% 98%;
117
+ --muted: 217.2 32.6% 17.5%;
118
+ --muted-foreground: 215 20.2% 65.1%;
119
+ --accent: 217.2 32.6% 17.5%;
120
+ --accent-foreground: 210 40% 98%;
121
+ --destructive: 0 62.8% 30.6%;
122
+ --destructive-foreground: 210 40% 98%;
123
+ --border: 217.2 32.6% 17.5%;
124
+ --input: 217.2 32.6% 17.5%;
125
+ --ring: 212.7 26.8% 83.9%;
126
+ }`;
127
+
128
+ // Insert dark mode after :root block
129
+ return content.replace(
130
+ /(:root\s*\{[^}]+\})/,
131
+ `$1\n${darkModeCSS}`
132
+ );
133
+ }
134
+
28
135
  function getTemplatesDir(): string {
29
136
  // When installed via npm, templates are in the CLI package
30
137
  const commandsDir = import.meta.dir;
@@ -37,9 +144,20 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
37
144
  const template = options.template || "default";
38
145
  const targetDir = path.resolve(process.cwd(), projectName);
39
146
 
147
+ // Handle minimal flag (shortcut for --css none --ui none)
148
+ const css: CSSFramework = options.minimal ? "none" : (options.css || "tailwind");
149
+ const ui: UILibrary = options.minimal ? "none" : (options.ui || "shadcn");
150
+ const theme = options.theme || false;
151
+
40
152
  console.log(`πŸ₯Ÿ Mandu Init`);
41
153
  console.log(`πŸ“ ν”„λ‘œμ νŠΈ: ${projectName}`);
42
- console.log(`πŸ“¦ ν…œν”Œλ¦Ώ: ${template}\n`);
154
+ console.log(`πŸ“¦ ν…œν”Œλ¦Ώ: ${template}`);
155
+ console.log(`🎨 CSS: ${css}${css !== "none" ? " (Tailwind CSS)" : ""}`);
156
+ console.log(`🧩 UI: ${ui}${ui !== "none" ? " (shadcn/ui)" : ""}`);
157
+ if (theme) {
158
+ console.log(`πŸŒ™ ν…Œλ§ˆ: Dark mode 지원`);
159
+ }
160
+ console.log();
43
161
 
44
162
  // Check if target directory exists
45
163
  try {
@@ -64,8 +182,15 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
64
182
 
65
183
  console.log(`πŸ“‹ ν…œν”Œλ¦Ώ 볡사 쀑...`);
66
184
 
185
+ const copyOptions: CopyOptions = {
186
+ projectName,
187
+ css,
188
+ ui,
189
+ theme,
190
+ };
191
+
67
192
  try {
68
- await copyDir(templateDir, targetDir, projectName);
193
+ await copyDir(templateDir, targetDir, copyOptions);
69
194
  } catch (error) {
70
195
  console.error(`❌ ν…œν”Œλ¦Ώ 볡사 μ‹€νŒ¨:`, error);
71
196
  return false;
@@ -74,15 +199,145 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
74
199
  // Create .mandu directory for build output
75
200
  await fs.mkdir(path.join(targetDir, ".mandu/client"), { recursive: true });
76
201
 
202
+ // Create minimal layout.tsx if css=none (without globals.css import)
203
+ if (css === "none") {
204
+ await createMinimalLayout(targetDir, projectName);
205
+ }
206
+
207
+ // Create minimal page.tsx if ui=none (without UI components)
208
+ if (ui === "none") {
209
+ await createMinimalPage(targetDir);
210
+ }
211
+
212
+ // Update package.json to remove unused dependencies
213
+ if (css === "none" || ui === "none") {
214
+ await updatePackageJson(targetDir, css, ui);
215
+ }
216
+
77
217
  console.log(`\nβœ… ν”„λ‘œμ νŠΈ 생성 μ™„λ£Œ!\n`);
78
218
  console.log(`πŸ“ μœ„μΉ˜: ${targetDir}`);
79
219
  console.log(`\nπŸš€ μ‹œμž‘ν•˜κΈ°:`);
80
220
  console.log(` cd ${projectName}`);
81
221
  console.log(` bun install`);
82
222
  console.log(` bun run dev`);
83
- console.log(`\nπŸ“‚ 파일 ꡬ쑰:`);
84
- console.log(` app/page.tsx β†’ http://localhost:3000/`);
85
- console.log(` app/api/*/route.ts β†’ API endpoints`);
223
+ console.log(`\nπŸ“‚ 파일 ꡬ쑰:`);
224
+ console.log(` app/layout.tsx β†’ 루트 λ ˆμ΄μ•„μ›ƒ`);
225
+ console.log(` app/page.tsx β†’ http://localhost:3000/`);
226
+ console.log(` app/api/*/route.ts β†’ API endpoints`);
227
+ console.log(` src/client/* β†’ ν΄λΌμ΄μ–ΈνŠΈ λ ˆμ΄μ–΄`);
228
+ console.log(` src/server/* β†’ μ„œλ²„ λ ˆμ΄μ–΄`);
229
+ console.log(` src/shared/contracts β†’ 계약 (client-safe)`);
230
+ console.log(` src/shared/types β†’ 곡용 νƒ€μž…`);
231
+ console.log(` src/shared/utils/client β†’ ν΄λΌμ΄μ–ΈνŠΈ safe μœ ν‹Έ`);
232
+ console.log(` src/shared/utils/server β†’ μ„œλ²„ μ „μš© μœ ν‹Έ`);
233
+ console.log(` src/shared/schema β†’ μ„œλ²„ μ „μš© μŠ€ν‚€λ§ˆ`);
234
+ console.log(` src/shared/env β†’ μ„œλ²„ μ „μš© ν™˜κ²½`);
235
+ if (css !== "none") {
236
+ console.log(` app/globals.css β†’ μ „μ—­ CSS (Tailwind)`);
237
+ console.log(` tailwind.config.ts β†’ Tailwind μ„€μ •`);
238
+ }
239
+ if (ui !== "none") {
240
+ console.log(` src/client/shared/ui/ β†’ UI μ»΄ν¬λ„ŒνŠΈ (shadcn)`);
241
+ console.log(` src/client/shared/lib/utils.ts β†’ μœ ν‹Έλ¦¬ν‹° (cn ν•¨μˆ˜)`);
242
+ }
86
243
 
87
244
  return true;
88
245
  }
246
+
247
+ async function createMinimalLayout(targetDir: string, projectName: string): Promise<void> {
248
+ const layoutContent = `/**
249
+ * Root Layout (Minimal)
250
+ */
251
+
252
+ interface RootLayoutProps {
253
+ children: React.ReactNode;
254
+ }
255
+
256
+ export default function RootLayout({ children }: RootLayoutProps) {
257
+ return (
258
+ <html lang="ko">
259
+ <head>
260
+ <meta charSet="UTF-8" />
261
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
262
+ <title>${projectName}</title>
263
+ </head>
264
+ <body>
265
+ {children}
266
+ </body>
267
+ </html>
268
+ );
269
+ }
270
+ `;
271
+ await fs.writeFile(path.join(targetDir, "app/layout.tsx"), layoutContent);
272
+ }
273
+
274
+ async function createMinimalPage(targetDir: string): Promise<void> {
275
+ const pageContent = `/**
276
+ * Home Page (Minimal)
277
+ *
278
+ * Edit this file and see changes at http://localhost:3000
279
+ */
280
+
281
+ export default function HomePage() {
282
+ return (
283
+ <main style={{
284
+ display: "flex",
285
+ minHeight: "100vh",
286
+ flexDirection: "column",
287
+ alignItems: "center",
288
+ justifyContent: "center",
289
+ background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
290
+ padding: "2rem",
291
+ }}>
292
+ <div style={{
293
+ textAlign: "center",
294
+ color: "white",
295
+ }}>
296
+ <h1 style={{ fontSize: "3rem", marginBottom: "1rem" }}>πŸ₯Ÿ Mandu</h1>
297
+ <p style={{ fontSize: "1.2rem", opacity: 0.9 }}>
298
+ Welcome to your new Mandu project!
299
+ </p>
300
+ <p style={{ fontSize: "1rem", opacity: 0.8, marginTop: "0.5rem" }}>
301
+ Edit <code style={{
302
+ background: "rgba(255,255,255,0.2)",
303
+ padding: "0.2rem 0.5rem",
304
+ borderRadius: "4px",
305
+ }}>app/page.tsx</code> to get started.
306
+ </p>
307
+ <p style={{ marginTop: "1rem" }}>
308
+ <a href="/api/health" style={{ color: "white" }}>API Health β†’</a>
309
+ </p>
310
+ </div>
311
+ </main>
312
+ );
313
+ }
314
+ `;
315
+ await fs.writeFile(path.join(targetDir, "app/page.tsx"), pageContent);
316
+ }
317
+
318
+ async function updatePackageJson(
319
+ targetDir: string,
320
+ css: CSSFramework,
321
+ ui: UILibrary
322
+ ): Promise<void> {
323
+ const pkgPath = path.join(targetDir, "package.json");
324
+ const content = await fs.readFile(pkgPath, "utf-8");
325
+ const pkg = JSON.parse(content);
326
+
327
+ if (css === "none") {
328
+ // Remove Tailwind dependencies
329
+ delete pkg.devDependencies?.tailwindcss;
330
+ delete pkg.devDependencies?.postcss;
331
+ delete pkg.devDependencies?.autoprefixer;
332
+ }
333
+
334
+ if (ui === "none") {
335
+ // Remove UI library dependencies
336
+ delete pkg.dependencies?.["@radix-ui/react-slot"];
337
+ delete pkg.dependencies?.["class-variance-authority"];
338
+ delete pkg.dependencies?.clsx;
339
+ delete pkg.dependencies?.["tailwind-merge"];
340
+ }
341
+
342
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
343
+ }
@@ -80,6 +80,27 @@ function formatEventForConsole(event: MonitorEvent): string {
80
80
  const icon = event.severity === "info" ? "β„Ή" : "⚠";
81
81
  return `${time} ${icon} [WATCH:${ruleId ?? "UNKNOWN"}] ${file ?? ""}${countSuffix}\n ${message}`;
82
82
  }
83
+ if (type === "guard.violation") {
84
+ const ruleId = event.data?.ruleId as string | undefined;
85
+ const file = event.data?.file as string | undefined;
86
+ const line = event.data?.line as number | undefined;
87
+ const message = event.message ?? (event.data?.message as string | undefined) ?? "";
88
+ const location = line ? `${file}:${line}` : file ?? "";
89
+ return `${time} 🚨 [GUARD:${ruleId ?? "UNKNOWN"}] ${location}${countSuffix}\n ${message}`;
90
+ }
91
+ if (type === "guard.summary") {
92
+ const count = event.data?.count as number | undefined;
93
+ const passed = event.data?.passed as boolean | undefined;
94
+ return `${time} 🧱 [GUARD] ${passed ? "PASSED" : "FAILED"} (${count ?? 0} violations)`;
95
+ }
96
+ if (type === "routes.change") {
97
+ const action = event.data?.action as string | undefined;
98
+ const routeId = event.data?.routeId as string | undefined;
99
+ const pattern = event.data?.pattern as string | undefined;
100
+ const kind = event.data?.kind as string | undefined;
101
+ const detail = [routeId, pattern, kind].filter(Boolean).join(" ");
102
+ return `${time} πŸ›£οΈ [ROUTES:${action ?? "change"}] ${detail}${countSuffix}`;
103
+ }
83
104
  if (type === "monitor.summary") {
84
105
  return `${time} Β· SUMMARY ${event.message ?? ""}`;
85
106
  }