@mandujs/cli 0.9.45 → 0.10.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/build.ts +27 -19
- package/src/commands/check.ts +290 -238
- package/src/commands/dev.ts +486 -440
- package/src/commands/init.ts +128 -21
- package/src/commands/lock.ts +434 -0
- package/src/main.ts +445 -428
package/src/commands/init.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import { CLI_ERROR_CODES, printCLIError } from "../errors";
|
|
4
|
+
import {
|
|
5
|
+
generateLockfile,
|
|
6
|
+
writeLockfile,
|
|
7
|
+
LOCKFILE_PATH,
|
|
8
|
+
} from "@mandujs/core";
|
|
4
9
|
|
|
5
10
|
export type CSSFramework = "tailwind" | "panda" | "none";
|
|
6
11
|
export type UILibrary = "shadcn" | "ark" | "none";
|
|
@@ -218,6 +223,9 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
218
223
|
// Setup .mcp.json for AI agent integration
|
|
219
224
|
const mcpResult = await setupMcpConfig(targetDir);
|
|
220
225
|
|
|
226
|
+
// Generate initial lockfile for config integrity
|
|
227
|
+
const lockfileResult = await setupLockfile(targetDir);
|
|
228
|
+
|
|
221
229
|
console.log(`\n✅ 프로젝트 생성 완료!\n`);
|
|
222
230
|
console.log(`📍 위치: ${targetDir}`);
|
|
223
231
|
console.log(`\n🚀 시작하기:`);
|
|
@@ -246,13 +254,31 @@ export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
|
246
254
|
|
|
247
255
|
// MCP 설정 안내
|
|
248
256
|
console.log(`\n🤖 AI 에이전트 통합:`);
|
|
249
|
-
if (mcpResult.created) {
|
|
257
|
+
if (mcpResult.status === "created") {
|
|
250
258
|
console.log(` .mcp.json 생성됨 (Claude Code 자동 연결)`);
|
|
251
|
-
} else if (mcpResult.updated) {
|
|
252
|
-
console.log(` .mcp.json에 mandu 서버
|
|
259
|
+
} else if (mcpResult.status === "updated") {
|
|
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}`);
|
|
253
270
|
}
|
|
254
271
|
console.log(` AGENTS.md → 에이전트 가이드 (Bun 사용 명시)`);
|
|
255
272
|
|
|
273
|
+
// Lockfile 안내
|
|
274
|
+
console.log(`\n🔒 설정 무결성:`);
|
|
275
|
+
if (lockfileResult.success) {
|
|
276
|
+
console.log(` ${LOCKFILE_PATH} 생성됨`);
|
|
277
|
+
console.log(` 해시: ${lockfileResult.hash}`);
|
|
278
|
+
} else {
|
|
279
|
+
console.log(` Lockfile 생성 건너뜀 (설정 없음)`);
|
|
280
|
+
}
|
|
281
|
+
|
|
256
282
|
return true;
|
|
257
283
|
}
|
|
258
284
|
|
|
@@ -356,9 +382,12 @@ async function updatePackageJson(
|
|
|
356
382
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
357
383
|
}
|
|
358
384
|
|
|
385
|
+
type McpConfigStatus = "created" | "updated" | "unchanged" | "backed-up" | "error";
|
|
386
|
+
|
|
359
387
|
interface McpConfigResult {
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
status: McpConfigStatus;
|
|
389
|
+
backupPath?: string;
|
|
390
|
+
error?: string;
|
|
362
391
|
}
|
|
363
392
|
|
|
364
393
|
/**
|
|
@@ -374,32 +403,110 @@ async function setupMcpConfig(targetDir: string): Promise<McpConfigResult> {
|
|
|
374
403
|
args: ["@mandujs/mcp"],
|
|
375
404
|
};
|
|
376
405
|
|
|
406
|
+
const writeConfig = async (data: Record<string, unknown>) => {
|
|
407
|
+
await fs.writeFile(mcpPath, JSON.stringify(data, null, 2) + "\n");
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const fileExists = async (filePath: string) => {
|
|
411
|
+
try {
|
|
412
|
+
await fs.access(filePath);
|
|
413
|
+
return true;
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const getBackupPath = async (basePath: string) => {
|
|
420
|
+
const base = `${basePath}.bak`;
|
|
421
|
+
if (!(await fileExists(base))) {
|
|
422
|
+
return base;
|
|
423
|
+
}
|
|
424
|
+
for (let i = 1; i <= 50; i++) {
|
|
425
|
+
const candidate = `${basePath}.bak.${i}`;
|
|
426
|
+
if (!(await fileExists(candidate))) {
|
|
427
|
+
return candidate;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return `${basePath}.bak.${Date.now()}`;
|
|
431
|
+
};
|
|
432
|
+
|
|
377
433
|
try {
|
|
378
|
-
// 기존 파일 확인
|
|
379
434
|
const existingContent = await fs.readFile(mcpPath, "utf-8");
|
|
380
|
-
|
|
435
|
+
let existing: Record<string, unknown>;
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
existing = JSON.parse(existingContent) as Record<string, unknown>;
|
|
439
|
+
} catch {
|
|
440
|
+
const backupPath = await getBackupPath(mcpPath);
|
|
441
|
+
await fs.writeFile(backupPath, existingContent);
|
|
442
|
+
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
443
|
+
return { status: "backed-up", backupPath };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!existing || typeof existing !== "object") {
|
|
447
|
+
existing = {};
|
|
448
|
+
}
|
|
381
449
|
|
|
382
|
-
|
|
383
|
-
if (!existing.mcpServers) {
|
|
450
|
+
if (!existing.mcpServers || typeof existing.mcpServers !== "object") {
|
|
384
451
|
existing.mcpServers = {};
|
|
385
452
|
}
|
|
386
453
|
|
|
387
|
-
const
|
|
388
|
-
|
|
454
|
+
const current = (existing.mcpServers as Record<string, unknown>).mandu;
|
|
455
|
+
const isSame =
|
|
456
|
+
current && JSON.stringify(current) === JSON.stringify(manduServer);
|
|
389
457
|
|
|
390
|
-
|
|
458
|
+
if (isSame) {
|
|
459
|
+
return { status: "unchanged" };
|
|
460
|
+
}
|
|
391
461
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
462
|
+
(existing.mcpServers as Record<string, unknown>).mandu = manduServer;
|
|
463
|
+
await writeConfig(existing);
|
|
464
|
+
return { status: "updated" };
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error && typeof error === "object" && "code" in error && (error as { code?: string }).code === "ENOENT") {
|
|
467
|
+
await writeConfig({ mcpServers: { mandu: manduServer } });
|
|
468
|
+
return { status: "created" };
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
status: "error",
|
|
472
|
+
error: error instanceof Error ? error.message : String(error),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
interface LockfileResult {
|
|
478
|
+
success: boolean;
|
|
479
|
+
hash?: string;
|
|
480
|
+
error?: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 초기 Lockfile 생성 (설정 무결성)
|
|
485
|
+
*/
|
|
486
|
+
async function setupLockfile(targetDir: string): Promise<LockfileResult> {
|
|
487
|
+
try {
|
|
488
|
+
// 초기 설정 (기본값)
|
|
489
|
+
const initialConfig = {
|
|
490
|
+
name: path.basename(targetDir),
|
|
491
|
+
version: "0.1.0",
|
|
492
|
+
createdAt: new Date().toISOString(),
|
|
399
493
|
};
|
|
400
494
|
|
|
401
|
-
|
|
495
|
+
const lockfile = generateLockfile(initialConfig, {
|
|
496
|
+
includeSnapshot: true,
|
|
497
|
+
includeMcpServerHashes: false,
|
|
498
|
+
});
|
|
402
499
|
|
|
403
|
-
|
|
500
|
+
await writeLockfile(targetDir, lockfile);
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
hash: lockfile.configHash,
|
|
505
|
+
};
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return {
|
|
508
|
+
success: false,
|
|
509
|
+
error: error instanceof Error ? error.message : String(error),
|
|
510
|
+
};
|
|
404
511
|
}
|
|
405
512
|
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mandu lock - Lockfile Management Command
|
|
3
|
+
*
|
|
4
|
+
* 설정 무결성을 위한 lockfile 생성, 검증, 비교
|
|
5
|
+
*
|
|
6
|
+
* @see docs/plans/08_ont-run_adoption_plan.md
|
|
7
|
+
*
|
|
8
|
+
* 사용법:
|
|
9
|
+
* mandu lock # lockfile 생성/갱신
|
|
10
|
+
* mandu lock --verify # lockfile 검증
|
|
11
|
+
* mandu lock --diff # 변경사항 표시
|
|
12
|
+
* mandu lock --show-secrets # 민감정보 출력 허용
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
validateAndReport,
|
|
17
|
+
generateLockfile,
|
|
18
|
+
readLockfile,
|
|
19
|
+
readMcpConfig,
|
|
20
|
+
writeLockfile,
|
|
21
|
+
lockfileExists,
|
|
22
|
+
validateLockfile,
|
|
23
|
+
validateWithPolicy,
|
|
24
|
+
formatValidationResult,
|
|
25
|
+
formatPolicyAction,
|
|
26
|
+
detectMode,
|
|
27
|
+
isBypassed,
|
|
28
|
+
diffConfig,
|
|
29
|
+
formatConfigDiff,
|
|
30
|
+
summarizeDiff,
|
|
31
|
+
resolveMcpSources,
|
|
32
|
+
type LockfileMode,
|
|
33
|
+
LOCKFILE_PATH,
|
|
34
|
+
} from "@mandujs/core";
|
|
35
|
+
import { resolveFromCwd } from "../util/fs";
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// CLI 옵션 타입
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
export interface LockOptions {
|
|
42
|
+
/** lockfile 검증만 수행 */
|
|
43
|
+
verify?: boolean;
|
|
44
|
+
/** 변경사항 표시 */
|
|
45
|
+
diff?: boolean;
|
|
46
|
+
/** 민감정보 출력 허용 */
|
|
47
|
+
showSecrets?: boolean;
|
|
48
|
+
/** 강제 모드 지정 */
|
|
49
|
+
mode?: LockfileMode;
|
|
50
|
+
/** 스냅샷 포함 */
|
|
51
|
+
includeSnapshot?: boolean;
|
|
52
|
+
/** 조용한 출력 */
|
|
53
|
+
quiet?: boolean;
|
|
54
|
+
/** JSON 출력 */
|
|
55
|
+
json?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// 메인 명령어
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* mandu lock 명령 실행
|
|
64
|
+
*/
|
|
65
|
+
export async function lock(options: LockOptions = {}): Promise<boolean> {
|
|
66
|
+
const rootDir = resolveFromCwd(".");
|
|
67
|
+
const {
|
|
68
|
+
verify = false,
|
|
69
|
+
diff = false,
|
|
70
|
+
showSecrets = false,
|
|
71
|
+
mode,
|
|
72
|
+
includeSnapshot = false,
|
|
73
|
+
quiet = false,
|
|
74
|
+
json = false,
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
// 설정 로드
|
|
78
|
+
const config = await validateAndReport(rootDir);
|
|
79
|
+
if (!config) {
|
|
80
|
+
if (!json) {
|
|
81
|
+
console.error("❌ mandu.config 로드 실패");
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MCP 설정 로드 (.mcp.json)
|
|
87
|
+
let mcpConfig: Record<string, unknown> | null = null;
|
|
88
|
+
try {
|
|
89
|
+
mcpConfig = await readMcpConfig(rootDir);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
if (json) {
|
|
93
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
94
|
+
} else {
|
|
95
|
+
console.error(`❌ .mcp.json 로드 실패: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const log = (msg: string) => {
|
|
101
|
+
if (!quiet && !json) {
|
|
102
|
+
console.log(msg);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// --verify: 검증만 수행
|
|
107
|
+
if (verify) {
|
|
108
|
+
return await verifyLockfile(rootDir, config, mcpConfig, { mode, quiet, json });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --diff: 변경사항 표시
|
|
112
|
+
if (diff) {
|
|
113
|
+
return await showDiff(rootDir, config, mcpConfig, { showSecrets, quiet, json });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 기본: lockfile 생성/갱신
|
|
117
|
+
return await createOrUpdateLockfile(rootDir, config, {
|
|
118
|
+
includeSnapshot,
|
|
119
|
+
quiet,
|
|
120
|
+
json,
|
|
121
|
+
mcpConfig,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// 서브 명령어
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* lockfile 생성 또는 갱신
|
|
131
|
+
*/
|
|
132
|
+
async function createOrUpdateLockfile(
|
|
133
|
+
rootDir: string,
|
|
134
|
+
config: Record<string, unknown>,
|
|
135
|
+
options: { includeSnapshot?: boolean; quiet?: boolean; json?: boolean; mcpConfig?: Record<string, unknown> | null }
|
|
136
|
+
): Promise<boolean> {
|
|
137
|
+
const { includeSnapshot = false, quiet = false, json = false, mcpConfig } = options;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const existingLockfile = await readLockfile(rootDir);
|
|
141
|
+
const isUpdate = existingLockfile !== null;
|
|
142
|
+
|
|
143
|
+
// lockfile 생성
|
|
144
|
+
const lockfile = generateLockfile(
|
|
145
|
+
config,
|
|
146
|
+
{
|
|
147
|
+
includeSnapshot,
|
|
148
|
+
includeMcpServerHashes: true,
|
|
149
|
+
},
|
|
150
|
+
mcpConfig
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// 쓰기
|
|
154
|
+
await writeLockfile(rootDir, lockfile);
|
|
155
|
+
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
success: true,
|
|
160
|
+
action: isUpdate ? "updated" : "created",
|
|
161
|
+
path: LOCKFILE_PATH,
|
|
162
|
+
hash: lockfile.configHash,
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
} else if (!quiet) {
|
|
166
|
+
if (isUpdate) {
|
|
167
|
+
console.log("✅ Lockfile 갱신 완료");
|
|
168
|
+
} else {
|
|
169
|
+
console.log("✅ Lockfile 생성 완료");
|
|
170
|
+
}
|
|
171
|
+
console.log(` 경로: ${LOCKFILE_PATH}`);
|
|
172
|
+
console.log(` 해시: ${lockfile.configHash}`);
|
|
173
|
+
console.log(` 시각: ${lockfile.generatedAt}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (json) {
|
|
179
|
+
console.log(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
success: false,
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
console.error("❌ Lockfile 생성 실패:", error);
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* lockfile 검증
|
|
194
|
+
*/
|
|
195
|
+
async function verifyLockfile(
|
|
196
|
+
rootDir: string,
|
|
197
|
+
config: Record<string, unknown>,
|
|
198
|
+
mcpConfig: Record<string, unknown> | null,
|
|
199
|
+
options: { mode?: LockfileMode; quiet?: boolean; json?: boolean }
|
|
200
|
+
): Promise<boolean> {
|
|
201
|
+
const { mode, quiet = false, json = false } = options;
|
|
202
|
+
|
|
203
|
+
const lockfile = await readLockfile(rootDir);
|
|
204
|
+
|
|
205
|
+
if (!lockfile) {
|
|
206
|
+
if (json) {
|
|
207
|
+
console.log(
|
|
208
|
+
JSON.stringify({
|
|
209
|
+
success: false,
|
|
210
|
+
error: "LOCKFILE_NOT_FOUND",
|
|
211
|
+
message: "Lockfile이 존재하지 않습니다. 'mandu lock'으로 생성하세요.",
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
console.error("❌ Lockfile이 존재하지 않습니다.");
|
|
216
|
+
console.error(" 'mandu lock' 명령으로 생성하세요.");
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 정책 기반 검증
|
|
222
|
+
const resolvedMode = mode ?? detectMode();
|
|
223
|
+
const { result, action, bypassed } = validateWithPolicy(
|
|
224
|
+
config,
|
|
225
|
+
lockfile,
|
|
226
|
+
resolvedMode,
|
|
227
|
+
mcpConfig
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (json) {
|
|
231
|
+
console.log(
|
|
232
|
+
JSON.stringify({
|
|
233
|
+
success: result?.valid ?? false,
|
|
234
|
+
action,
|
|
235
|
+
bypassed,
|
|
236
|
+
mode: resolvedMode,
|
|
237
|
+
currentHash: result?.currentHash,
|
|
238
|
+
lockedHash: result?.lockedHash,
|
|
239
|
+
errors: result?.errors ?? [],
|
|
240
|
+
warnings: result?.warnings ?? [],
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
return result?.valid ?? false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!quiet) {
|
|
247
|
+
console.log(formatPolicyAction(action, bypassed));
|
|
248
|
+
console.log(` 모드: ${resolvedMode}`);
|
|
249
|
+
|
|
250
|
+
if (result) {
|
|
251
|
+
console.log(formatValidationResult(result));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// action이 pass나 warn이면 성공으로 간주 (CI에서는 다르게 처리 가능)
|
|
256
|
+
return action === "pass" || action === "warn";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 변경사항 표시
|
|
261
|
+
*/
|
|
262
|
+
async function showDiff(
|
|
263
|
+
rootDir: string,
|
|
264
|
+
config: Record<string, unknown>,
|
|
265
|
+
mcpConfig: Record<string, unknown> | null,
|
|
266
|
+
options: { showSecrets?: boolean; quiet?: boolean; json?: boolean }
|
|
267
|
+
): Promise<boolean> {
|
|
268
|
+
const { showSecrets = false, quiet = false, json = false } = options;
|
|
269
|
+
|
|
270
|
+
const lockfile = await readLockfile(rootDir);
|
|
271
|
+
|
|
272
|
+
if (!lockfile) {
|
|
273
|
+
if (json) {
|
|
274
|
+
console.log(
|
|
275
|
+
JSON.stringify({
|
|
276
|
+
success: false,
|
|
277
|
+
error: "LOCKFILE_NOT_FOUND",
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
console.error("❌ Lockfile이 존재하지 않습니다.");
|
|
282
|
+
console.error(" 'mandu lock' 명령으로 생성하세요.");
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 스냅샷이 없으면 diff 불가
|
|
288
|
+
if (!lockfile.snapshot) {
|
|
289
|
+
if (json) {
|
|
290
|
+
console.log(
|
|
291
|
+
JSON.stringify({
|
|
292
|
+
success: false,
|
|
293
|
+
error: "SNAPSHOT_MISSING",
|
|
294
|
+
message:
|
|
295
|
+
"Lockfile에 스냅샷이 없습니다. '--include-snapshot' 옵션으로 다시 생성하세요.",
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
console.error("❌ Lockfile에 스냅샷이 없습니다.");
|
|
300
|
+
console.error(
|
|
301
|
+
" 'mandu lock --include-snapshot' 옵션으로 다시 생성하세요."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// diff 계산
|
|
308
|
+
const { mcpServers } = resolveMcpSources(config, mcpConfig);
|
|
309
|
+
const configForDiff = mcpServers ? { ...config, mcpServers } : config;
|
|
310
|
+
const diff = diffConfig(lockfile.snapshot.config, configForDiff);
|
|
311
|
+
|
|
312
|
+
if (json) {
|
|
313
|
+
console.log(
|
|
314
|
+
JSON.stringify({
|
|
315
|
+
success: true,
|
|
316
|
+
hasChanges: diff.hasChanges,
|
|
317
|
+
diff,
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!quiet) {
|
|
324
|
+
if (diff.hasChanges) {
|
|
325
|
+
console.log(
|
|
326
|
+
formatConfigDiff(diff, {
|
|
327
|
+
color: true,
|
|
328
|
+
verbose: true,
|
|
329
|
+
showSecrets,
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
console.log(`\n요약: ${summarizeDiff(diff)}`);
|
|
333
|
+
} else {
|
|
334
|
+
console.log("✅ 변경사항 없음");
|
|
335
|
+
console.log(` 현재 설정이 lockfile과 일치합니다.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================
|
|
343
|
+
// CLI 진입점 (main.ts에서 호출)
|
|
344
|
+
// ============================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* CLI 인자 파싱 및 실행
|
|
348
|
+
*/
|
|
349
|
+
export async function runLockCommand(args: string[]): Promise<boolean> {
|
|
350
|
+
const options: LockOptions = {};
|
|
351
|
+
|
|
352
|
+
const setMode = (value?: string) => {
|
|
353
|
+
switch (value) {
|
|
354
|
+
case "development":
|
|
355
|
+
case "build":
|
|
356
|
+
case "ci":
|
|
357
|
+
case "production":
|
|
358
|
+
options.mode = value;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < args.length; i++) {
|
|
364
|
+
const arg = args[i];
|
|
365
|
+
switch (arg) {
|
|
366
|
+
case "--verify":
|
|
367
|
+
case "-v":
|
|
368
|
+
options.verify = true;
|
|
369
|
+
break;
|
|
370
|
+
case "--diff":
|
|
371
|
+
case "-d":
|
|
372
|
+
options.diff = true;
|
|
373
|
+
break;
|
|
374
|
+
case "--show-secrets":
|
|
375
|
+
options.showSecrets = true;
|
|
376
|
+
break;
|
|
377
|
+
case "--include-snapshot":
|
|
378
|
+
options.includeSnapshot = true;
|
|
379
|
+
break;
|
|
380
|
+
case "--quiet":
|
|
381
|
+
case "-q":
|
|
382
|
+
options.quiet = true;
|
|
383
|
+
break;
|
|
384
|
+
case "--json":
|
|
385
|
+
options.json = true;
|
|
386
|
+
break;
|
|
387
|
+
case "--mode": {
|
|
388
|
+
const value = args[i + 1];
|
|
389
|
+
if (value) {
|
|
390
|
+
setMode(value);
|
|
391
|
+
i++;
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
default:
|
|
396
|
+
if (arg.startsWith("--mode=")) {
|
|
397
|
+
setMode(arg.split("=", 2)[1]);
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return lock(options);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============================================
|
|
407
|
+
// 도움말
|
|
408
|
+
// ============================================
|
|
409
|
+
|
|
410
|
+
export const lockHelp = `
|
|
411
|
+
mandu lock - Lockfile 관리
|
|
412
|
+
|
|
413
|
+
사용법:
|
|
414
|
+
mandu lock lockfile 생성/갱신
|
|
415
|
+
mandu lock --verify lockfile 검증
|
|
416
|
+
mandu lock --diff 변경사항 표시
|
|
417
|
+
|
|
418
|
+
옵션:
|
|
419
|
+
--verify, -v lockfile 검증만 수행
|
|
420
|
+
--diff, -d lockfile과 현재 설정 비교
|
|
421
|
+
--show-secrets 민감정보 출력 허용 (기본: 마스킹)
|
|
422
|
+
--include-snapshot 설정 스냅샷 포함 (diff 기능에 필요)
|
|
423
|
+
--mode=<mode> 검증 모드 지정 (development|build|ci|production)
|
|
424
|
+
--quiet, -q 조용한 출력
|
|
425
|
+
--json JSON 형식 출력
|
|
426
|
+
|
|
427
|
+
예시:
|
|
428
|
+
mandu lock # lockfile 생성
|
|
429
|
+
mandu lock --verify # 검증
|
|
430
|
+
mandu lock --diff --show-secrets # 민감정보 포함 diff
|
|
431
|
+
|
|
432
|
+
환경변수:
|
|
433
|
+
MANDU_LOCK_BYPASS=1 lockfile 검증 우회 (긴급 상황용)
|
|
434
|
+
`;
|