@mandujs/cli 0.14.1 → 0.15.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 +1 -1
- package/src/commands/init.ts +183 -143
- package/templates/default/package.json +2 -2
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -39,6 +39,8 @@ interface CopyOptions {
|
|
|
39
39
|
css: CSSFramework;
|
|
40
40
|
ui: UILibrary;
|
|
41
41
|
theme: boolean;
|
|
42
|
+
coreVersion: string;
|
|
43
|
+
cliVersion: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function shouldSkipFile(relativePath: string, options: CopyOptions): boolean {
|
|
@@ -96,6 +98,8 @@ async function copyDir(
|
|
|
96
98
|
let content = await fs.readFile(srcPath, "utf-8");
|
|
97
99
|
// Replace template variables
|
|
98
100
|
content = content.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName);
|
|
101
|
+
content = content.replace(/\{\{CORE_VERSION\}\}/g, options.coreVersion);
|
|
102
|
+
content = content.replace(/\{\{CLI_VERSION\}\}/g, options.cliVersion);
|
|
99
103
|
|
|
100
104
|
// Add dark mode CSS variables if theme is enabled
|
|
101
105
|
if (options.theme && currentRelativePath === "app/globals.css") {
|
|
@@ -145,6 +149,38 @@ function getTemplatesDir(): string {
|
|
|
145
149
|
return path.resolve(commandsDir, "../../templates");
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* CLI/Core 패키지 버전을 런타임에 읽어서 ^major.minor.0 형태로 반환
|
|
154
|
+
* 템플릿 package.json의 {{CORE_VERSION}}, {{CLI_VERSION}} 치환에 사용
|
|
155
|
+
*/
|
|
156
|
+
async function resolvePackageVersions(): Promise<{ coreVersion: string; cliVersion: string }> {
|
|
157
|
+
const cliPkgPath = path.resolve(import.meta.dir, "../../package.json");
|
|
158
|
+
const cliPkg = JSON.parse(await fs.readFile(cliPkgPath, "utf-8"));
|
|
159
|
+
const cliVersion = cliPkg.version ?? "0.0.0";
|
|
160
|
+
|
|
161
|
+
// core는 CLI의 node_modules 또는 workspace에서 읽기
|
|
162
|
+
let coreVersion = cliVersion; // fallback: CLI 버전과 동일
|
|
163
|
+
try {
|
|
164
|
+
const corePkgPath = require.resolve("@mandujs/core/package.json", { paths: [path.resolve(import.meta.dir, "../..")] });
|
|
165
|
+
const corePkg = JSON.parse(await fs.readFile(corePkgPath, "utf-8"));
|
|
166
|
+
coreVersion = corePkg.version ?? coreVersion;
|
|
167
|
+
} catch {
|
|
168
|
+
// workspace 환경: 직접 경로로 시도
|
|
169
|
+
try {
|
|
170
|
+
const workspacePath = path.resolve(import.meta.dir, "../../../core/package.json");
|
|
171
|
+
const corePkg = JSON.parse(await fs.readFile(workspacePath, "utf-8"));
|
|
172
|
+
coreVersion = corePkg.version ?? coreVersion;
|
|
173
|
+
} catch {
|
|
174
|
+
// fallback 유지
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
coreVersion: `^${coreVersion}`,
|
|
180
|
+
cliVersion: `^${cliVersion}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
148
184
|
export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
149
185
|
const projectName = options.name || "my-mandu-app";
|
|
150
186
|
const template = options.template || "default";
|
|
@@ -188,11 +224,15 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
188
224
|
|
|
189
225
|
console.log(`📋 템플릿 복사 중...`);
|
|
190
226
|
|
|
227
|
+
const { coreVersion, cliVersion } = await resolvePackageVersions();
|
|
228
|
+
|
|
191
229
|
const copyOptions: CopyOptions = {
|
|
192
230
|
projectName,
|
|
193
231
|
css,
|
|
194
232
|
ui,
|
|
195
233
|
theme,
|
|
234
|
+
coreVersion,
|
|
235
|
+
cliVersion,
|
|
196
236
|
};
|
|
197
237
|
|
|
198
238
|
try {
|
|
@@ -252,11 +292,11 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
252
292
|
console.log(` src/client/shared/lib/utils.ts → 유틸리티 (cn 함수)`);
|
|
253
293
|
}
|
|
254
294
|
|
|
255
|
-
// MCP 설정 안내
|
|
256
|
-
console.log(`\n🤖 AI 에이전트 통합:`);
|
|
257
|
-
logMcpConfigStatus(".mcp.json", mcpResult.mcpJson, "Claude Code 자동 연결");
|
|
258
|
-
logMcpConfigStatus(".claude.json", mcpResult.claudeJson, "Claude MCP 로컬 범위");
|
|
259
|
-
console.log(` AGENTS.md → 에이전트 가이드 (Bun 사용 명시)`);
|
|
295
|
+
// MCP 설정 안내
|
|
296
|
+
console.log(`\n🤖 AI 에이전트 통합:`);
|
|
297
|
+
logMcpConfigStatus(".mcp.json", mcpResult.mcpJson, "Claude Code 자동 연결");
|
|
298
|
+
logMcpConfigStatus(".claude.json", mcpResult.claudeJson, "Claude MCP 로컬 범위");
|
|
299
|
+
console.log(` AGENTS.md → 에이전트 가이드 (Bun 사용 명시)`);
|
|
260
300
|
|
|
261
301
|
// Lockfile 안내
|
|
262
302
|
console.log(`\n🔒 설정 무결성:`);
|
|
@@ -370,144 +410,144 @@ async function updatePackageJson(
|
|
|
370
410
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
371
411
|
}
|
|
372
412
|
|
|
373
|
-
type McpConfigStatus = "created" | "updated" | "unchanged" | "backed-up" | "error";
|
|
374
|
-
|
|
375
|
-
interface McpConfigFileResult {
|
|
376
|
-
status: McpConfigStatus;
|
|
377
|
-
backupPath?: string;
|
|
378
|
-
error?: string;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
interface McpConfigResult {
|
|
382
|
-
mcpJson: McpConfigFileResult;
|
|
383
|
-
claudeJson: McpConfigFileResult;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function logMcpConfigStatus(
|
|
387
|
-
label: string,
|
|
388
|
-
result: McpConfigFileResult,
|
|
389
|
-
createdNote?: string
|
|
390
|
-
): void {
|
|
391
|
-
if (result.status === "created") {
|
|
392
|
-
console.log(` ${label} 생성됨${createdNote ? ` (${createdNote})` : ""}`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (result.status === "updated") {
|
|
397
|
-
console.log(` ${label}에 mandu 서버 추가/업데이트됨`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (result.status === "unchanged") {
|
|
402
|
-
console.log(` ${label} 이미 최신`);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (result.status === "backed-up") {
|
|
407
|
-
console.log(` ${label} 파싱 실패 → 백업 후 새로 생성됨`);
|
|
408
|
-
if (result.backupPath) {
|
|
409
|
-
console.log(` 백업: ${result.backupPath}`);
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (result.status === "error") {
|
|
415
|
-
console.log(` ${label} 설정 실패: ${result.error}`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* .mcp.json / .claude.json 설정 (AI 에이전트 통합)
|
|
421
|
-
* - 파일 없으면 새로 생성
|
|
422
|
-
* - 파일 있으면 mandu 서버만 추가/업데이트 (다른 설정 유지)
|
|
423
|
-
*/
|
|
424
|
-
async function setupMcpConfig(targetDir: string): Promise<McpConfigResult> {
|
|
425
|
-
const mcpPath = path.join(targetDir, ".mcp.json");
|
|
426
|
-
const claudePath = path.join(targetDir, ".claude.json");
|
|
427
|
-
|
|
428
|
-
const manduServer = {
|
|
429
|
-
command: "bunx",
|
|
430
|
-
args: ["@mandujs/mcp"],
|
|
431
|
-
cwd: ".",
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const updateMcpFile = async (filePath: string): Promise<McpConfigFileResult> => {
|
|
435
|
-
const writeConfig = async (data: Record<string, unknown>) => {
|
|
436
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const fileExists = async (candidatePath: string) => {
|
|
440
|
-
try {
|
|
441
|
-
await fs.access(candidatePath);
|
|
442
|
-
return true;
|
|
443
|
-
} catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const getBackupPath = async (basePath: string) => {
|
|
449
|
-
const base = `${basePath}.bak`;
|
|
450
|
-
if (!(await fileExists(base))) {
|
|
451
|
-
return base;
|
|
452
|
-
}
|
|
453
|
-
for (let i = 1; i <= 50; i++) {
|
|
454
|
-
const candidate = `${basePath}.bak.${i}`;
|
|
455
|
-
if (!(await fileExists(candidate))) {
|
|
456
|
-
return candidate;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return `${basePath}.bak.${Date.now()}`;
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
const existingContent = await fs.readFile(filePath, "utf-8");
|
|
464
|
-
let existing: Record<string, unknown>;
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
existing = JSON.parse(existingContent) as Record<string, unknown>;
|
|
468
|
-
} catch {
|
|
469
|
-
const backupPath = await getBackupPath(filePath);
|
|
470
|
-
await fs.writeFile(backupPath, existingContent);
|
|
471
|
-
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
472
|
-
return { status: "backed-up", backupPath };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (!existing || typeof existing !== "object") {
|
|
476
|
-
existing = {};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (!existing.mcpServers || typeof existing.mcpServers !== "object") {
|
|
480
|
-
existing.mcpServers = {};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const current = (existing.mcpServers as Record<string, unknown>).mandu;
|
|
484
|
-
const isSame =
|
|
485
|
-
current && JSON.stringify(current) === JSON.stringify(manduServer);
|
|
486
|
-
|
|
487
|
-
if (isSame) {
|
|
488
|
-
return { status: "unchanged" };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
(existing.mcpServers as Record<string, unknown>).mandu = manduServer;
|
|
492
|
-
await writeConfig(existing);
|
|
493
|
-
return { status: "updated" };
|
|
494
|
-
} catch (error) {
|
|
495
|
-
if (error && typeof error === "object" && "code" in error && (error as { code?: string }).code === "ENOENT") {
|
|
496
|
-
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
497
|
-
return { status: "created" };
|
|
498
|
-
}
|
|
499
|
-
return {
|
|
500
|
-
status: "error",
|
|
501
|
-
error: error instanceof Error ? error.message : String(error),
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
const mcpJson = await updateMcpFile(mcpPath);
|
|
507
|
-
const claudeJson = await updateMcpFile(claudePath);
|
|
508
|
-
|
|
509
|
-
return { mcpJson, claudeJson };
|
|
510
|
-
}
|
|
413
|
+
type McpConfigStatus = "created" | "updated" | "unchanged" | "backed-up" | "error";
|
|
414
|
+
|
|
415
|
+
interface McpConfigFileResult {
|
|
416
|
+
status: McpConfigStatus;
|
|
417
|
+
backupPath?: string;
|
|
418
|
+
error?: string;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
interface McpConfigResult {
|
|
422
|
+
mcpJson: McpConfigFileResult;
|
|
423
|
+
claudeJson: McpConfigFileResult;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function logMcpConfigStatus(
|
|
427
|
+
label: string,
|
|
428
|
+
result: McpConfigFileResult,
|
|
429
|
+
createdNote?: string
|
|
430
|
+
): void {
|
|
431
|
+
if (result.status === "created") {
|
|
432
|
+
console.log(` ${label} 생성됨${createdNote ? ` (${createdNote})` : ""}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (result.status === "updated") {
|
|
437
|
+
console.log(` ${label}에 mandu 서버 추가/업데이트됨`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (result.status === "unchanged") {
|
|
442
|
+
console.log(` ${label} 이미 최신`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (result.status === "backed-up") {
|
|
447
|
+
console.log(` ${label} 파싱 실패 → 백업 후 새로 생성됨`);
|
|
448
|
+
if (result.backupPath) {
|
|
449
|
+
console.log(` 백업: ${result.backupPath}`);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (result.status === "error") {
|
|
455
|
+
console.log(` ${label} 설정 실패: ${result.error}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* .mcp.json / .claude.json 설정 (AI 에이전트 통합)
|
|
461
|
+
* - 파일 없으면 새로 생성
|
|
462
|
+
* - 파일 있으면 mandu 서버만 추가/업데이트 (다른 설정 유지)
|
|
463
|
+
*/
|
|
464
|
+
async function setupMcpConfig(targetDir: string): Promise<McpConfigResult> {
|
|
465
|
+
const mcpPath = path.join(targetDir, ".mcp.json");
|
|
466
|
+
const claudePath = path.join(targetDir, ".claude.json");
|
|
467
|
+
|
|
468
|
+
const manduServer = {
|
|
469
|
+
command: "bunx",
|
|
470
|
+
args: ["@mandujs/mcp"],
|
|
471
|
+
cwd: ".",
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const updateMcpFile = async (filePath: string): Promise<McpConfigFileResult> => {
|
|
475
|
+
const writeConfig = async (data: Record<string, unknown>) => {
|
|
476
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const fileExists = async (candidatePath: string) => {
|
|
480
|
+
try {
|
|
481
|
+
await fs.access(candidatePath);
|
|
482
|
+
return true;
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const getBackupPath = async (basePath: string) => {
|
|
489
|
+
const base = `${basePath}.bak`;
|
|
490
|
+
if (!(await fileExists(base))) {
|
|
491
|
+
return base;
|
|
492
|
+
}
|
|
493
|
+
for (let i = 1; i <= 50; i++) {
|
|
494
|
+
const candidate = `${basePath}.bak.${i}`;
|
|
495
|
+
if (!(await fileExists(candidate))) {
|
|
496
|
+
return candidate;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return `${basePath}.bak.${Date.now()}`;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const existingContent = await fs.readFile(filePath, "utf-8");
|
|
504
|
+
let existing: Record<string, unknown>;
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
existing = JSON.parse(existingContent) as Record<string, unknown>;
|
|
508
|
+
} catch {
|
|
509
|
+
const backupPath = await getBackupPath(filePath);
|
|
510
|
+
await fs.writeFile(backupPath, existingContent);
|
|
511
|
+
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
512
|
+
return { status: "backed-up", backupPath };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!existing || typeof existing !== "object") {
|
|
516
|
+
existing = {};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!existing.mcpServers || typeof existing.mcpServers !== "object") {
|
|
520
|
+
existing.mcpServers = {};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const current = (existing.mcpServers as Record<string, unknown>).mandu;
|
|
524
|
+
const isSame =
|
|
525
|
+
current && JSON.stringify(current) === JSON.stringify(manduServer);
|
|
526
|
+
|
|
527
|
+
if (isSame) {
|
|
528
|
+
return { status: "unchanged" };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
(existing.mcpServers as Record<string, unknown>).mandu = manduServer;
|
|
532
|
+
await writeConfig(existing);
|
|
533
|
+
return { status: "updated" };
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (error && typeof error === "object" && "code" in error && (error as { code?: string }).code === "ENOENT") {
|
|
536
|
+
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
537
|
+
return { status: "created" };
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
status: "error",
|
|
541
|
+
error: error instanceof Error ? error.message : String(error),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const mcpJson = await updateMcpFile(mcpPath);
|
|
547
|
+
const claudeJson = await updateMcpFile(claudePath);
|
|
548
|
+
|
|
549
|
+
return { mcpJson, claudeJson };
|
|
550
|
+
}
|
|
511
551
|
|
|
512
552
|
interface LockfileResult {
|
|
513
553
|
success: boolean;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@mandujs/core": "
|
|
18
|
+
"@mandujs/core": "{{CORE_VERSION}}",
|
|
19
19
|
"@radix-ui/react-slot": "^1.1.0",
|
|
20
20
|
"class-variance-authority": "^0.7.0",
|
|
21
21
|
"clsx": "^2.1.1",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"tailwind-merge": "^2.5.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@mandujs/cli": "
|
|
27
|
+
"@mandujs/cli": "{{CLI_VERSION}}",
|
|
28
28
|
"@tailwindcss/cli": "^4.1.0",
|
|
29
29
|
"@types/react": "^19.2.0",
|
|
30
30
|
"@types/react-dom": "^19.2.0",
|