@mandujs/cli 0.13.1 → 0.14.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/check.ts +3 -5
- package/src/commands/dev.ts +0 -1
- package/src/commands/doctor.ts +1 -1
- package/src/commands/generate-apply.ts +9 -6
- package/src/commands/guard-check.ts +1 -1
- package/src/commands/init.ts +143 -108
- package/src/commands/registry.ts +3 -3
- package/src/commands/routes.ts +1 -2
- package/src/commands/spec-upsert.ts +6 -2
- package/src/util/manifest.ts +12 -26
package/package.json
CHANGED
package/src/commands/check.ts
CHANGED
|
@@ -36,7 +36,7 @@ export async function check(): Promise<boolean> {
|
|
|
36
36
|
const quiet = false;
|
|
37
37
|
const strictWarnings = process.env.CI === "true";
|
|
38
38
|
const enableFsRoutes = await isDirectory(path.resolve(rootDir, "app"));
|
|
39
|
-
const specPath = resolveFromCwd("
|
|
39
|
+
const specPath = resolveFromCwd(".mandu/routes.manifest.json");
|
|
40
40
|
const hasSpec = await pathExists(specPath);
|
|
41
41
|
|
|
42
42
|
let success = true;
|
|
@@ -71,8 +71,6 @@ export async function check(): Promise<boolean> {
|
|
|
71
71
|
if (format === "console") {
|
|
72
72
|
const result = await generateManifest(rootDir, {
|
|
73
73
|
scanner: config.fsRoutes,
|
|
74
|
-
outputPath: ".mandu/routes.manifest.json",
|
|
75
|
-
skipLegacy: true,
|
|
76
74
|
});
|
|
77
75
|
routesSummary.count = result.manifest.routes.length;
|
|
78
76
|
routesSummary.warnings = result.warnings;
|
|
@@ -217,9 +215,9 @@ export async function check(): Promise<boolean> {
|
|
|
217
215
|
}
|
|
218
216
|
} else {
|
|
219
217
|
if (quiet) {
|
|
220
|
-
print("ℹ️
|
|
218
|
+
print("ℹ️ .mandu/routes.manifest.json 없음 - Guard 스킵");
|
|
221
219
|
} else {
|
|
222
|
-
log("ℹ️
|
|
220
|
+
log("ℹ️ .mandu/routes.manifest.json 없음 - Guard 스킵");
|
|
223
221
|
}
|
|
224
222
|
}
|
|
225
223
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -344,7 +344,6 @@ export async function dev(options: DevOptions = {}): Promise<void> {
|
|
|
344
344
|
|
|
345
345
|
// FS Routes 실시간 감시
|
|
346
346
|
const routesWatcher = await watchFSRoutes(rootDir, {
|
|
347
|
-
skipLegacy: true,
|
|
348
347
|
onChange: async (result) => {
|
|
349
348
|
const timestamp = new Date().toLocaleTimeString();
|
|
350
349
|
console.log(`\n🔄 [${timestamp}] 라우트 변경 감지`);
|
package/src/commands/doctor.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
|
|
|
32
32
|
const inferredFormat = format ?? (output ? (path.extname(output).toLowerCase() === ".json" ? "json" : "markdown") : undefined);
|
|
33
33
|
const resolvedFormat = inferredFormat ?? "console";
|
|
34
34
|
|
|
35
|
-
const specPath = resolveFromCwd("
|
|
35
|
+
const specPath = resolveFromCwd(".mandu/routes.manifest.json");
|
|
36
36
|
const rootDir = getRootDir();
|
|
37
37
|
|
|
38
38
|
console.log(`🩺 Mandu Doctor`);
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { loadManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@mandujs/core";
|
|
1
|
+
import { loadManifest, generateManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@mandujs/core";
|
|
2
2
|
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
3
3
|
|
|
4
4
|
export async function generateApply(): Promise<boolean> {
|
|
5
|
-
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
6
5
|
const rootDir = getRootDir();
|
|
6
|
+
const manifestPath = resolveFromCwd(".mandu/routes.manifest.json");
|
|
7
7
|
|
|
8
8
|
console.log(`🥟 Mandu Generate`);
|
|
9
|
-
console.log(`📄
|
|
9
|
+
console.log(`📄 FS Routes 기반 코드 생성\n`);
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Regenerate manifest from FS Routes
|
|
12
|
+
const fsResult = await generateManifest(rootDir);
|
|
13
|
+
console.log(`✅ 매니페스트 생성 완료 (${fsResult.fsRoutesCount}개 라우트)`);
|
|
14
|
+
|
|
15
|
+
const result = await loadManifest(manifestPath);
|
|
12
16
|
|
|
13
17
|
if (!result.success || !result.data) {
|
|
14
|
-
console.error("❌
|
|
18
|
+
console.error("❌ 매니페스트 로드 실패:");
|
|
15
19
|
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
16
20
|
return false;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
console.log(`✅ Spec 로드 완료 (${result.data.routes.length}개 라우트)`);
|
|
20
23
|
console.log(`🔄 코드 생성 중...\n`);
|
|
21
24
|
|
|
22
25
|
const generateResult = await generateRoutes(result.data, rootDir);
|
|
@@ -16,7 +16,7 @@ export interface GuardCheckOptions {
|
|
|
16
16
|
export async function guardCheck(options: GuardCheckOptions = {}): Promise<boolean> {
|
|
17
17
|
const { autoCorrect = true } = options;
|
|
18
18
|
|
|
19
|
-
const specPath = resolveFromCwd("
|
|
19
|
+
const specPath = resolveFromCwd(".mandu/routes.manifest.json");
|
|
20
20
|
const rootDir = getRootDir();
|
|
21
21
|
|
|
22
22
|
console.log(`🥟 Mandu Guard (Legacy Spec)`);
|
package/src/commands/init.ts
CHANGED
|
@@ -252,23 +252,11 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
252
252
|
console.log(` src/client/shared/lib/utils.ts → 유틸리티 (cn 함수)`);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
// MCP 설정 안내
|
|
256
|
-
console.log(`\n🤖 AI 에이전트 통합:`);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
console.log(` .mcp.json에 mandu 서버 추가/업데이트됨`);
|
|
261
|
-
} else if (mcpResult.status === "unchanged") {
|
|
262
|
-
console.log(` .mcp.json 이미 최신`);
|
|
263
|
-
} else if (mcpResult.status === "backed-up") {
|
|
264
|
-
console.log(` .mcp.json 파싱 실패 → 백업 후 새로 생성됨`);
|
|
265
|
-
if (mcpResult.backupPath) {
|
|
266
|
-
console.log(` 백업: ${mcpResult.backupPath}`);
|
|
267
|
-
}
|
|
268
|
-
} else if (mcpResult.status === "error") {
|
|
269
|
-
console.log(` .mcp.json 설정 실패: ${mcpResult.error}`);
|
|
270
|
-
}
|
|
271
|
-
console.log(` AGENTS.md → 에이전트 가이드 (Bun 사용 명시)`);
|
|
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 사용 명시)`);
|
|
272
260
|
|
|
273
261
|
// Lockfile 안내
|
|
274
262
|
console.log(`\n🔒 설정 무결성:`);
|
|
@@ -382,97 +370,144 @@ async function updatePackageJson(
|
|
|
382
370
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
383
371
|
}
|
|
384
372
|
|
|
385
|
-
type McpConfigStatus = "created" | "updated" | "unchanged" | "backed-up" | "error";
|
|
386
|
-
|
|
387
|
-
interface
|
|
388
|
-
status: McpConfigStatus;
|
|
389
|
-
backupPath?: string;
|
|
390
|
-
error?: string;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
+
}
|
|
476
511
|
|
|
477
512
|
interface LockfileResult {
|
|
478
513
|
success: boolean;
|
package/src/commands/registry.ts
CHANGED
|
@@ -347,10 +347,10 @@ registerCommand({
|
|
|
347
347
|
},
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
-
// 레거시 명령어
|
|
350
|
+
// 레거시 명령어 (DEPRECATED)
|
|
351
351
|
registerCommand({
|
|
352
352
|
id: "spec-upsert",
|
|
353
|
-
description: "Spec 파일 검증 및 lock 갱신
|
|
353
|
+
description: "[DEPRECATED] Spec 파일 검증 및 lock 갱신 → routes generate 사용",
|
|
354
354
|
async run(ctx) {
|
|
355
355
|
const { specUpsert } = await import("./spec-upsert");
|
|
356
356
|
return specUpsert({ file: ctx.options.file });
|
|
@@ -359,7 +359,7 @@ registerCommand({
|
|
|
359
359
|
|
|
360
360
|
registerCommand({
|
|
361
361
|
id: "generate",
|
|
362
|
-
description: "
|
|
362
|
+
description: "FS Routes 기반 코드 생성",
|
|
363
363
|
async run() {
|
|
364
364
|
const { generateApply } = await import("./generate-apply");
|
|
365
365
|
return generateApply();
|
package/src/commands/routes.ts
CHANGED
|
@@ -55,8 +55,7 @@ export async function routesGenerate(options: RoutesGenerateOptions = {}): Promi
|
|
|
55
55
|
try {
|
|
56
56
|
const generateOptions: GenerateOptions = {
|
|
57
57
|
scanner: config.fsRoutes,
|
|
58
|
-
outputPath: options.output
|
|
59
|
-
skipLegacy: true, // 레거시 병합 비활성화
|
|
58
|
+
outputPath: options.output,
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
const result = await generateManifest(rootDir, generateOptions);
|
|
@@ -7,9 +7,13 @@ export interface SpecUpsertOptions {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export async function specUpsert(options: SpecUpsertOptions): Promise<boolean> {
|
|
10
|
+
console.warn("⚠️ DEPRECATED: spec-upsert는 더 이상 필요하지 않습니다.");
|
|
11
|
+
console.warn(" FS Routes(app/)가 유일한 라우트 소스입니다.");
|
|
12
|
+
console.warn(" 대신 'bunx mandu routes generate'를 사용하세요.\n");
|
|
13
|
+
|
|
10
14
|
const specPath = options.file
|
|
11
15
|
? resolveFromCwd(options.file)
|
|
12
|
-
: resolveFromCwd("
|
|
16
|
+
: resolveFromCwd(".mandu/routes.manifest.json");
|
|
13
17
|
|
|
14
18
|
console.log(`🥟 Mandu Spec Upsert`);
|
|
15
19
|
console.log(`📄 Spec 파일: ${specPath}\n`);
|
|
@@ -31,7 +35,7 @@ export async function specUpsert(options: SpecUpsertOptions): Promise<boolean> {
|
|
|
31
35
|
console.log(` ${kindIcon} ${route.id}: ${route.pattern} (${route.kind})`);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
const lockPath = resolveFromCwd("
|
|
38
|
+
const lockPath = resolveFromCwd(".mandu/spec.lock.json");
|
|
35
39
|
const previousLock = await readLock(lockPath);
|
|
36
40
|
const newLock = await writeLock(lockPath, result.data);
|
|
37
41
|
|
package/src/util/manifest.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@mandujs/core";
|
|
8
8
|
import { isDirectory } from "./fs";
|
|
9
9
|
|
|
10
|
-
export type ManifestSource = "fs"
|
|
10
|
+
export type ManifestSource = "fs";
|
|
11
11
|
|
|
12
12
|
export interface ResolvedManifest {
|
|
13
13
|
manifest: RoutesManifest;
|
|
@@ -22,31 +22,17 @@ export async function resolveManifest(
|
|
|
22
22
|
const appDir = path.resolve(rootDir, "app");
|
|
23
23
|
const hasApp = await isDirectory(appDir);
|
|
24
24
|
|
|
25
|
-
if (hasApp) {
|
|
26
|
-
|
|
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
|
-
};
|
|
25
|
+
if (!hasApp) {
|
|
26
|
+
throw new Error("No app/ directory found. Create app/ routes to get started.");
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
warnings: [],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
|
|
29
|
+
const result = await generateManifest(rootDir, {
|
|
30
|
+
scanner: options.fsRoutes,
|
|
31
|
+
outputPath: options.outputPath,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
manifest: result.manifest,
|
|
35
|
+
source: "fs",
|
|
36
|
+
warnings: result.warnings,
|
|
37
|
+
};
|
|
52
38
|
}
|