@mandujs/cli 0.18.10 → 0.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.18.10",
3
+ "version": "0.19.0",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -1,6 +1,9 @@
1
1
  import path from "path";
2
2
  import fs from "fs/promises";
3
+ import { createInterface } from "readline/promises";
3
4
  import { CLI_ERROR_CODES, printCLIError } from "../errors";
5
+ import { startSpinner, runSteps } from "../terminal/progress";
6
+ import { theme } from "../terminal/theme";
4
7
  import {
5
8
  generateLockfile,
6
9
  writeLockfile,
@@ -18,6 +21,8 @@ export interface InitOptions {
18
21
  theme?: boolean;
19
22
  minimal?: boolean;
20
23
  withCi?: boolean;
24
+ yes?: boolean;
25
+ noInstall?: boolean;
21
26
  }
22
27
 
23
28
  const ALLOWED_TEMPLATES = ["default", "realtime-chat"] as const;
@@ -199,9 +204,78 @@ async function resolvePackageVersions(): Promise<{ coreVersion: string; cliVersi
199
204
  };
200
205
  }
201
206
 
207
+ interface InteractiveAnswers {
208
+ name: string;
209
+ template: AllowedTemplate;
210
+ install: boolean;
211
+ }
212
+
213
+ async function runInteractivePrompts(defaults: {
214
+ name: string;
215
+ template: string;
216
+ }): Promise<InteractiveAnswers> {
217
+ const rl = createInterface({
218
+ input: process.stdin,
219
+ output: process.stdout,
220
+ });
221
+
222
+ console.log(`\n${theme.heading("🥟 Mandu Init")}\n`);
223
+
224
+ // 1. Project name
225
+ const nameInput = await rl.question(
226
+ ` 프로젝트 이름 ${theme.muted(`(${defaults.name})`)} : `
227
+ );
228
+ const name = nameInput.trim() || defaults.name;
229
+
230
+ // 2. Template selection
231
+ console.log(`\n 템플릿 선택:`);
232
+ for (let i = 0; i < ALLOWED_TEMPLATES.length; i++) {
233
+ const t = ALLOWED_TEMPLATES[i];
234
+ const label = t === "default" ? "default (권장)" : t;
235
+ console.log(` ${theme.accent(`${i + 1})`)} ${label}`);
236
+ }
237
+ const templateInput = await rl.question(
238
+ `\n 번호 입력 ${theme.muted("(1)")} : `
239
+ );
240
+ const templateIndex = parseInt(templateInput.trim(), 10) - 1;
241
+ const template: AllowedTemplate =
242
+ templateIndex >= 0 && templateIndex < ALLOWED_TEMPLATES.length
243
+ ? ALLOWED_TEMPLATES[templateIndex]
244
+ : (resolveTemplateName(defaults.template) as AllowedTemplate) ?? "default";
245
+
246
+ // 3. Install dependencies?
247
+ const installInput = await rl.question(
248
+ `\n 의존성 설치 (bun install)? ${theme.muted("(Y/n)")} : `
249
+ );
250
+ const install = installInput.trim().toLowerCase() !== "n";
251
+
252
+ rl.close();
253
+ console.log();
254
+
255
+ return { name, template, install };
256
+ }
257
+
202
258
  export async function init(options: InitOptions = {}): Promise<boolean> {
203
- const projectName = options.name || "my-mandu-app";
204
- const requestedTemplate = options.template || "default";
259
+ const isInteractive = process.stdin.isTTY && !options.yes;
260
+
261
+ let projectName: string;
262
+ let requestedTemplate: string;
263
+ let shouldInstall: boolean;
264
+
265
+ if (isInteractive) {
266
+ const answers = await runInteractivePrompts({
267
+ name: options.name || "my-mandu-app",
268
+ template: options.template || "default",
269
+ });
270
+ projectName = answers.name;
271
+ requestedTemplate = answers.template;
272
+ shouldInstall = options.noInstall ? false : answers.install;
273
+ } else {
274
+ projectName = options.name || "my-mandu-app";
275
+ requestedTemplate = options.template || "default";
276
+ shouldInstall = !options.noInstall;
277
+ }
278
+
205
279
  const template = resolveTemplateName(requestedTemplate);
206
280
  const targetDir = path.resolve(process.cwd(), projectName);
207
281
 
@@ -214,19 +288,19 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
214
288
  // Handle minimal flag (shortcut for --css none --ui none)
215
289
  const css: CSSFramework = options.minimal ? "none" : (options.css || "tailwind");
216
290
  const ui: UILibrary = options.minimal ? "none" : (options.ui || "shadcn");
217
- const theme = options.theme || false;
291
+ const themeEnabled = options.theme || false;
218
292
  const withCi = options.withCi || false;
219
293
 
220
- console.log(`🥟 Mandu Init`);
221
- console.log(`📁 프로젝트: ${projectName}`);
222
- console.log(`📦 템플릿: ${template}`);
223
- console.log(`🎨 CSS: ${css}${css !== "none" ? " (Tailwind CSS)" : ""}`);
224
- console.log(`🧩 UI: ${ui}${ui !== "none" ? " (shadcn/ui)" : ""}`);
225
- if (theme) {
226
- console.log(`🌙 테마: Dark mode 지원`);
294
+ console.log(`${theme.heading("🥟 Mandu Init")}`);
295
+ console.log(`${theme.info("📁")} 프로젝트: ${theme.accent(projectName)}`);
296
+ console.log(`${theme.info("📦")} 템플릿: ${theme.accent(template)}`);
297
+ console.log(`${theme.info("🎨")} CSS: ${css}${css !== "none" ? " (Tailwind CSS)" : ""}`);
298
+ console.log(`${theme.info("🧩")} UI: ${ui}${ui !== "none" ? " (shadcn/ui)" : ""}`);
299
+ if (themeEnabled) {
300
+ console.log(`${theme.info("🌙")} 테마: Dark mode 지원`);
227
301
  }
228
302
  if (withCi) {
229
- console.log(`🔄 CI/CD: GitHub Actions 워크플로우 포함`);
303
+ console.log(`${theme.info("🔄")} CI/CD: GitHub Actions 워크플로우 포함`);
230
304
  }
231
305
  console.log();
232
306
 
@@ -251,64 +325,119 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
251
325
  return false;
252
326
  }
253
327
 
254
- console.log(`📋 템플릿 복사 중...`);
255
-
256
328
  const { coreVersion, cliVersion } = await resolvePackageVersions();
257
329
 
258
330
  const copyOptions: CopyOptions = {
259
331
  projectName,
260
332
  css,
261
333
  ui,
262
- theme,
334
+ theme: themeEnabled,
263
335
  coreVersion,
264
336
  cliVersion,
265
337
  };
266
338
 
339
+ // Run structured steps with progress
340
+ let mcpResult: McpConfigResult;
341
+ let lockfileResult: LockfileResult;
342
+
267
343
  try {
268
- await copyDir(templateDir, targetDir, copyOptions);
344
+ await runSteps([
345
+ {
346
+ label: "디렉토리 생성",
347
+ fn: async () => {
348
+ await fs.mkdir(targetDir, { recursive: true });
349
+ await fs.mkdir(path.join(targetDir, ".mandu/client"), { recursive: true });
350
+ },
351
+ },
352
+ {
353
+ label: "템플릿 복사",
354
+ fn: () => copyDir(templateDir, targetDir, copyOptions),
355
+ },
356
+ {
357
+ label: "설정 파일 생성",
358
+ fn: async () => {
359
+ if (withCi) {
360
+ await setupCiWorkflows(targetDir);
361
+ }
362
+ if (css === "none") {
363
+ await createMinimalLayout(targetDir, projectName);
364
+ }
365
+ if (ui === "none") {
366
+ await createMinimalPage(targetDir);
367
+ }
368
+ if (css === "none" || ui === "none") {
369
+ await updatePackageJson(targetDir, css, ui);
370
+ }
371
+ },
372
+ },
373
+ {
374
+ label: "MCP 설정",
375
+ fn: async () => {
376
+ mcpResult = await setupMcpConfig(targetDir);
377
+ },
378
+ },
379
+ {
380
+ label: "Lockfile 생성",
381
+ fn: async () => {
382
+ lockfileResult = await setupLockfile(targetDir);
383
+ },
384
+ },
385
+ ]);
269
386
  } catch (error) {
270
- console.error(`❌ 템플릿 복사 실패:`, error);
387
+ console.error(`\n${theme.error("❌")} 프로젝트 생성 실패:`, error);
271
388
  return false;
272
389
  }
273
390
 
274
- // Create .mandu directory for build output
275
- await fs.mkdir(path.join(targetDir, ".mandu/client"), { recursive: true });
276
-
277
- // Setup CI/CD workflows if requested
278
- if (withCi) {
279
- await setupCiWorkflows(targetDir);
391
+ // Validate project files
392
+ const requiredFiles = ["app/page.tsx", "package.json", "tsconfig.json"];
393
+ const missingFiles: string[] = [];
394
+ for (const file of requiredFiles) {
395
+ try {
396
+ await fs.access(path.join(targetDir, file));
397
+ } catch {
398
+ missingFiles.push(file);
399
+ }
280
400
  }
281
-
282
- // Create minimal layout.tsx if css=none (without globals.css import)
283
- if (css === "none") {
284
- await createMinimalLayout(targetDir, projectName);
401
+ if (missingFiles.length > 0) {
402
+ console.log(`\n${theme.warn("⚠")} 누락된 파일: ${missingFiles.join(", ")}`);
285
403
  }
286
404
 
287
- // Create minimal page.tsx if ui=none (without UI components)
288
- if (ui === "none") {
289
- await createMinimalPage(targetDir);
405
+ // Auto install dependencies
406
+ if (shouldInstall) {
407
+ const stopSpinner = startSpinner("패키지 설치 중 (bun install)...");
408
+ try {
409
+ const proc = Bun.spawn(["bun", "install"], {
410
+ cwd: targetDir,
411
+ stdout: "inherit",
412
+ stderr: "inherit",
413
+ });
414
+ const exitCode = await proc.exited;
415
+ if (exitCode === 0) {
416
+ stopSpinner("패키지 설치 완료");
417
+ } else {
418
+ stopSpinner();
419
+ console.log(`${theme.warn("⚠")} 패키지 설치 실패 (exit code: ${exitCode})`);
420
+ console.log(` ${theme.muted("프로젝트 디렉토리에서 직접 'bun install'을 실행해주세요.")}`);
421
+ }
422
+ } catch {
423
+ stopSpinner();
424
+ console.log(`${theme.warn("⚠")} 패키지 설치를 건너뛰었습니다.`);
425
+ console.log(` ${theme.muted("프로젝트 디렉토리에서 직접 'bun install'을 실행해주세요.")}`);
426
+ }
290
427
  }
291
428
 
292
- // Update package.json to remove unused dependencies
293
- if (css === "none" || ui === "none") {
294
- await updatePackageJson(targetDir, css, ui);
429
+ // Success message
430
+ console.log(`\n${theme.success("")} ${theme.heading("프로젝트 생성 완료!")}\n`);
431
+ console.log(`📍 위치: ${theme.path(targetDir)}`);
432
+ console.log(`\n${theme.heading("🚀 시작하기:")}`);
433
+ console.log(` ${theme.command(`cd ${projectName}`)}`);
434
+ if (!shouldInstall) {
435
+ console.log(` ${theme.command("bun install")}`);
295
436
  }
296
-
297
- // Setup .mcp.json for AI agent integration
298
- const mcpResult = await setupMcpConfig(targetDir);
299
-
300
- // Generate initial lockfile for config integrity
301
- const lockfileResult = await setupLockfile(targetDir);
302
-
303
- console.log(`\n✅ 프로젝트 생성 완료!\n`);
304
- console.log(`📍 위치: ${targetDir}`);
305
- console.log(`\n🚀 시작하기:`);
306
- console.log(` cd ${projectName}`);
307
- console.log(` bun install`);
308
- console.log(` bun run dev`);
437
+ console.log(` ${theme.command("bun run dev")}`);
309
438
  console.log(`\n💡 CLI 실행 참고 (환경별):`);
310
- console.log(` bun run dev # 권장 (로컬 스크립트)`);
311
- console.log(` bunx mandu dev # PATH에 mandu가 없을 때 대안`);
439
+ console.log(` ${theme.command("bun run dev")} ${theme.muted("# 권장 (로컬 스크립트)")}`);
440
+ console.log(` ${theme.command("bunx mandu dev")} ${theme.muted("# PATH에 mandu가 없을 때 대안")}`);
312
441
  console.log(`\n📂 파일 구조:`);
313
442
  console.log(` app/layout.tsx → 루트 레이아웃`);
314
443
  console.log(` app/page.tsx → http://localhost:3000/`);
@@ -331,15 +460,15 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
331
460
 
332
461
  // MCP 설정 안내
333
462
  console.log(`\n🤖 AI 에이전트 통합:`);
334
- logMcpConfigStatus(".mcp.json", mcpResult.mcpJson, "Claude Code 자동 연결");
335
- logMcpConfigStatus(".claude.json", mcpResult.claudeJson, "Claude MCP 로컬 범위");
463
+ logMcpConfigStatus(".mcp.json", mcpResult!.mcpJson, "Claude Code 자동 연결");
464
+ logMcpConfigStatus(".claude.json", mcpResult!.claudeJson, "Claude MCP 로컬 범위");
336
465
  console.log(` AGENTS.md → 에이전트 가이드 (Bun 사용 명시)`);
337
466
 
338
467
  // Lockfile 안내
339
468
  console.log(`\n🔒 설정 무결성:`);
340
- if (lockfileResult.success) {
469
+ if (lockfileResult!.success) {
341
470
  console.log(` ${LOCKFILE_PATH} 생성됨`);
342
- console.log(` 해시: ${lockfileResult.hash}`);
471
+ console.log(` 해시: ${lockfileResult!.hash}`);
343
472
  } else {
344
473
  console.log(` Lockfile 생성 건너뜀 (설정 없음)`);
345
474
  }
@@ -347,9 +476,12 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
347
476
  return true;
348
477
  }
349
478
 
350
- async function createMinimalLayout(targetDir: string, projectName: string): Promise<void> {
479
+ async function createMinimalLayout(targetDir: string, _projectName: string): Promise<void> {
351
480
  const layoutContent = `/**
352
481
  * Root Layout (Minimal)
482
+ *
483
+ * - html/head/body 태그는 Mandu SSR이 자동으로 생성합니다
484
+ * - 여기서는 body 내부의 공통 래퍼만 정의합니다
353
485
  */
354
486
 
355
487
  interface RootLayoutProps {
@@ -358,16 +490,9 @@ interface RootLayoutProps {
358
490
 
359
491
  export default function RootLayout({ children }: RootLayoutProps) {
360
492
  return (
361
- <html lang="ko">
362
- <head>
363
- <meta charSet="UTF-8" />
364
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
365
- <title>${projectName}</title>
366
- </head>
367
- <body>
368
- {children}
369
- </body>
370
- </html>
493
+ <div className="min-h-screen">
494
+ {children}
495
+ </div>
371
496
  );
372
497
  }
373
498
  `;
@@ -95,6 +95,8 @@ registerCommand({
95
95
  theme: ctx.options.theme === "true",
96
96
  minimal: ctx.options.minimal === "true",
97
97
  withCi: ctx.options["with-ci"] === "true",
98
+ yes: ctx.options.yes === "true",
99
+ noInstall: ctx.options["no-install"] === "true",
98
100
  });
99
101
  },
100
102
  });
package/src/main.ts CHANGED
@@ -20,7 +20,7 @@ ${theme.heading("🥟 Mandu CLI")} ${theme.muted(`v${VERSION}`)} - Agent-Native
20
20
  ${theme.heading("Usage:")} ${theme.command("bunx mandu")} ${theme.option("<command>")} [options]
21
21
 
22
22
  Commands:
23
- init 새 프로젝트 생성 (Tailwind + shadcn/ui 기본 포함)
23
+ init 새 프로젝트 생성 (대화형 / --yes로 비대화형)
24
24
  check FS Routes + Guard 통합 검사
25
25
  routes generate FS Routes 스캔 및 매니페스트 생성
26
26
  routes list 현재 라우트 목록 출력
@@ -76,6 +76,8 @@ Options:
76
76
  --theme init 시 다크모드 테마 시스템 추가
77
77
  --minimal init 시 CSS/UI 없이 최소 템플릿 생성 (--css none --ui none)
78
78
  --with-ci init 시 GitHub Actions CI/CD 워크플로우 포함 (ATE E2E 테스트)
79
+ --yes, -y init 시 대화형 프롬프트 건너뛰기 (기존 비대화형 동작)
80
+ --no-install init 시 패키지 설치 건너뛰기
79
81
  --file <path> spec-upsert spec 파일/monitor 로그 파일 경로
80
82
  --watch build/guard arch 파일 감시 모드
81
83
  --output <path> routes/openapi/doctor/contract/guard 출력 경로
@@ -173,6 +175,7 @@ export function parseArgs(args: string[]): { command: string; options: Record<st
173
175
  q: "quiet",
174
176
  v: "verify",
175
177
  d: "diff",
178
+ y: "yes",
176
179
  };
177
180
 
178
181
  for (let i = 0; i < args.length; i++) {
@@ -83,7 +83,7 @@ import { Button } from "@/client/shared/ui/button";
83
83
 
84
84
  ```bash
85
85
  bun install # 최초 설치
86
- bun run dev # 개발 서버 (http://localhost:4000)
86
+ bun run dev # 개발 서버 (http://localhost:3333)
87
87
  bun run build # 프로덕션 빌드
88
88
  bun run guard # 아키텍처 검증
89
89
  ```
@@ -2,26 +2,19 @@
2
2
  * Root Layout
3
3
  *
4
4
  * 모든 페이지의 공통 레이아웃
5
- * globals.css를 여기서 임포트
5
+ * - html/head/body 태그는 Mandu SSR이 자동으로 생성합니다
6
+ * - 여기서는 body 내부의 공통 래퍼만 정의합니다
7
+ * - CSS는 Mandu가 자동으로 주입합니다: /.mandu/client/globals.css
6
8
  */
7
9
 
8
- import "./globals.css";
9
-
10
10
  interface RootLayoutProps {
11
11
  children: React.ReactNode;
12
12
  }
13
13
 
14
14
  export default function RootLayout({ children }: RootLayoutProps) {
15
15
  return (
16
- <html lang="ko">
17
- <head>
18
- <meta charSet="UTF-8" />
19
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
- <title>{{PROJECT_NAME}}</title>
21
- </head>
22
- <body className="min-h-screen bg-background font-sans antialiased">
23
- {children}
24
- </body>
25
- </html>
16
+ <div className="min-h-screen bg-background font-sans antialiased">
17
+ {children}
18
+ </div>
26
19
  );
27
20
  }