@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
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
204
|
-
|
|
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
|
|
291
|
+
const themeEnabled = options.theme || false;
|
|
218
292
|
const withCi = options.withCi || false;
|
|
219
293
|
|
|
220
|
-
console.log(
|
|
221
|
-
console.log(
|
|
222
|
-
console.log(
|
|
223
|
-
console.log(
|
|
224
|
-
console.log(
|
|
225
|
-
if (
|
|
226
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
387
|
+
console.error(`\n${theme.error("❌")} 프로젝트 생성 실패:`, error);
|
|
271
388
|
return false;
|
|
272
389
|
}
|
|
273
390
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
288
|
-
if (
|
|
289
|
-
|
|
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
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
335
|
-
logMcpConfigStatus(".claude.json", mcpResult
|
|
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
|
|
469
|
+
if (lockfileResult!.success) {
|
|
341
470
|
console.log(` ${LOCKFILE_PATH} 생성됨`);
|
|
342
|
-
console.log(` 해시: ${lockfileResult
|
|
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,
|
|
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
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
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
|
`;
|
package/src/commands/registry.ts
CHANGED
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 새 프로젝트 생성 (
|
|
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++) {
|
|
@@ -2,26 +2,19 @@
|
|
|
2
2
|
* Root Layout
|
|
3
3
|
*
|
|
4
4
|
* 모든 페이지의 공통 레이아웃
|
|
5
|
-
*
|
|
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
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
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
|
}
|