@mandujs/cli 0.15.1 β†’ 0.15.3

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.
Files changed (90) hide show
  1. package/README.ko.md +33 -33
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/check.ts +71 -7
  5. package/src/commands/contract.ts +173 -173
  6. package/src/commands/dev.ts +9 -42
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/init.ts +50 -5
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +1 -0
  12. package/src/commands/start.ts +9 -42
  13. package/src/errors/codes.ts +35 -35
  14. package/src/errors/index.ts +2 -2
  15. package/src/errors/messages.ts +143 -143
  16. package/src/hooks/index.ts +17 -17
  17. package/src/hooks/preaction.ts +256 -256
  18. package/src/main.ts +9 -7
  19. package/src/terminal/banner.ts +166 -166
  20. package/src/terminal/help.ts +306 -306
  21. package/src/terminal/index.ts +71 -71
  22. package/src/terminal/output.ts +295 -295
  23. package/src/terminal/palette.ts +30 -30
  24. package/src/terminal/progress.ts +327 -327
  25. package/src/terminal/stream-writer.ts +214 -214
  26. package/src/terminal/table.ts +354 -354
  27. package/src/terminal/theme.ts +142 -142
  28. package/src/util/bun.ts +6 -6
  29. package/src/util/fs.ts +23 -23
  30. package/src/util/handlers.ts +49 -5
  31. package/src/util/lockfile.ts +66 -0
  32. package/src/util/output.ts +22 -22
  33. package/src/util/port.ts +71 -71
  34. package/templates/default/AGENTS.md +96 -96
  35. package/templates/default/app/api/health/route.ts +13 -13
  36. package/templates/default/app/globals.css +49 -49
  37. package/templates/default/app/layout.tsx +27 -27
  38. package/templates/default/app/page.tsx +38 -38
  39. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  40. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  41. package/templates/default/src/client/shared/ui/card.tsx +1 -1
  42. package/templates/default/src/client/shared/ui/index.ts +21 -21
  43. package/templates/default/src/client/shared/ui/input.tsx +5 -1
  44. package/templates/default/tests/example.test.ts +58 -58
  45. package/templates/default/tests/helpers.ts +52 -52
  46. package/templates/default/tests/setup.ts +9 -9
  47. package/templates/default/tsconfig.json +23 -23
  48. package/templates/realtime-chat/AGENTS.md +96 -0
  49. package/templates/realtime-chat/app/api/chat/messages/route.ts +63 -0
  50. package/templates/realtime-chat/app/api/chat/stream/route.ts +85 -0
  51. package/templates/realtime-chat/app/api/health/route.ts +13 -0
  52. package/templates/realtime-chat/app/globals.css +49 -0
  53. package/templates/realtime-chat/app/layout.tsx +27 -0
  54. package/templates/realtime-chat/app/page.tsx +16 -0
  55. package/templates/realtime-chat/package.json +34 -0
  56. package/templates/realtime-chat/src/client/app/index.ts +1 -0
  57. package/templates/realtime-chat/src/client/entities/index.ts +1 -0
  58. package/templates/realtime-chat/src/client/features/chat/chat-api.ts +209 -0
  59. package/templates/realtime-chat/src/client/features/chat/realtime-chat-starter.client.tsx +89 -0
  60. package/templates/realtime-chat/src/client/features/chat/use-realtime-chat.ts +65 -0
  61. package/templates/realtime-chat/src/client/features/index.ts +1 -0
  62. package/templates/realtime-chat/src/client/pages/index.ts +1 -0
  63. package/templates/realtime-chat/src/client/shared/index.ts +1 -0
  64. package/templates/realtime-chat/src/client/shared/lib/utils.ts +16 -0
  65. package/templates/realtime-chat/src/client/shared/ui/button.tsx +57 -0
  66. package/templates/realtime-chat/src/client/shared/ui/card.tsx +78 -0
  67. package/templates/realtime-chat/src/client/shared/ui/index.ts +21 -0
  68. package/templates/realtime-chat/src/client/shared/ui/input.tsx +28 -0
  69. package/templates/realtime-chat/src/client/widgets/index.ts +1 -0
  70. package/templates/realtime-chat/src/server/api/index.ts +1 -0
  71. package/templates/realtime-chat/src/server/application/ai-adapter.ts +24 -0
  72. package/templates/realtime-chat/src/server/application/chat-store.ts +158 -0
  73. package/templates/realtime-chat/src/server/application/index.ts +1 -0
  74. package/templates/realtime-chat/src/server/core/index.ts +1 -0
  75. package/templates/realtime-chat/src/server/domain/index.ts +1 -0
  76. package/templates/realtime-chat/src/server/infra/index.ts +1 -0
  77. package/templates/realtime-chat/src/shared/contracts/chat.ts +29 -0
  78. package/templates/realtime-chat/src/shared/contracts/index.ts +1 -0
  79. package/templates/realtime-chat/src/shared/env/index.ts +1 -0
  80. package/templates/realtime-chat/src/shared/schema/index.ts +1 -0
  81. package/templates/realtime-chat/src/shared/types/index.ts +1 -0
  82. package/templates/realtime-chat/src/shared/utils/client/index.ts +1 -0
  83. package/templates/realtime-chat/src/shared/utils/server/index.ts +1 -0
  84. package/templates/realtime-chat/tests/chat-api.sse.test.ts +188 -0
  85. package/templates/realtime-chat/tests/chat-starter.test.ts +200 -0
  86. package/templates/realtime-chat/tests/chat-store.concurrency.test.ts +39 -0
  87. package/templates/realtime-chat/tests/example.test.ts +58 -0
  88. package/templates/realtime-chat/tests/helpers.ts +52 -0
  89. package/templates/realtime-chat/tests/setup.ts +9 -0
  90. package/templates/realtime-chat/tsconfig.json +23 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Agent-Native Fullstack Framework - μ—μ΄μ „νŠΈκ°€ 코딩해도 μ•„ν‚€ν…μ²˜κ°€ λ¬΄λ„ˆμ§€μ§€ μ•ŠλŠ” 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "^0.13.0",
35
+ "@mandujs/core": "^0.13.2",
36
36
  "cfonts": "^3.3.0"
37
37
  },
38
38
  "engines": {
@@ -15,6 +15,8 @@ import {
15
15
  runGuardCheck,
16
16
  buildGuardReport,
17
17
  printReportSummary,
18
+ runAutoCorrect,
19
+ isAutoCorrectableViolation,
18
20
  guardConfig,
19
21
  formatConfigGuardResult,
20
22
  calculateHealthScore,
@@ -25,6 +27,51 @@ import path from "path";
25
27
  import { resolveFromCwd, isDirectory, pathExists } from "../util/fs";
26
28
  import { resolveOutputFormat } from "../util/output";
27
29
 
30
+ interface LegacyCheckDeps {
31
+ runGuardCheck: typeof runGuardCheck;
32
+ runAutoCorrect: typeof runAutoCorrect;
33
+ isAutoCorrectableViolation: typeof isAutoCorrectableViolation;
34
+ }
35
+
36
+ export async function runLegacyGuardWithAutoHeal(
37
+ manifest: Parameters<typeof runGuardCheck>[0],
38
+ rootDir: string,
39
+ deps: LegacyCheckDeps = { runGuardCheck, runAutoCorrect, isAutoCorrectableViolation }
40
+ ): Promise<{
41
+ passed: boolean;
42
+ violations: number;
43
+ autoHealed: boolean;
44
+ nextAction?: string;
45
+ checkResult: Awaited<ReturnType<typeof runGuardCheck>>;
46
+ }> {
47
+ let checkResult = await deps.runGuardCheck(manifest, rootDir);
48
+ let autoHealed = false;
49
+
50
+ if (!checkResult.passed) {
51
+ const hasAutoCorrectableViolation = checkResult.violations.some(
52
+ deps.isAutoCorrectableViolation
53
+ );
54
+
55
+ if (hasAutoCorrectableViolation) {
56
+ const autoCorrectResult = await deps.runAutoCorrect(
57
+ checkResult.violations,
58
+ manifest,
59
+ rootDir
60
+ );
61
+ autoHealed = autoCorrectResult.fixed;
62
+ checkResult = await deps.runGuardCheck(manifest, rootDir);
63
+ }
64
+ }
65
+
66
+ return {
67
+ passed: checkResult.passed,
68
+ violations: checkResult.violations.length,
69
+ autoHealed,
70
+ nextAction: checkResult.passed ? undefined : "mandu guard legacy",
71
+ checkResult,
72
+ };
73
+ }
74
+
28
75
  export async function check(): Promise<boolean> {
29
76
  const rootDir = resolveFromCwd(".");
30
77
  const config = await validateAndReport(rootDir);
@@ -175,7 +222,14 @@ export async function check(): Promise<boolean> {
175
222
  }
176
223
 
177
224
  // 3) Legacy Guard 검사 (spec 파일이 μžˆμ„ λ•Œλ§Œ)
178
- let legacySummary: { enabled: boolean; passed: boolean; violations: number; errors?: string[] } = {
225
+ let legacySummary: {
226
+ enabled: boolean;
227
+ passed: boolean;
228
+ violations: number;
229
+ errors?: string[];
230
+ autoHealed?: boolean;
231
+ nextAction?: string;
232
+ } = {
179
233
  enabled: false,
180
234
  passed: true,
181
235
  violations: 0,
@@ -195,17 +249,27 @@ export async function check(): Promise<boolean> {
195
249
  manifestResult.errors?.forEach((e) => console.error(` - ${e}`));
196
250
  }
197
251
  } else {
198
- const checkResult = await runGuardCheck(manifestResult.data, rootDir);
199
- legacySummary.passed = checkResult.passed;
200
- legacySummary.violations = checkResult.violations.length;
201
- if (strictWarnings && checkResult.violations.length > 0) {
252
+ const legacyResult = await runLegacyGuardWithAutoHeal(manifestResult.data, rootDir);
253
+ legacySummary.passed = legacyResult.passed;
254
+ legacySummary.violations = legacyResult.violations;
255
+ legacySummary.autoHealed = legacyResult.autoHealed;
256
+ legacySummary.nextAction = legacyResult.nextAction;
257
+
258
+ if (format === "console" && legacyResult.autoHealed) {
259
+ log("βœ… Legacy spec drift μžλ™ 볡ꡬ μ™„λ£Œ");
260
+ }
261
+ if (format === "console" && legacyResult.nextAction) {
262
+ log("πŸ’‘ Legacy guard μœ„λ°˜μ΄ 남아 μžˆμŠ΅λ‹ˆλ‹€. `mandu guard legacy`둜 상세 점검/볡ꡬλ₯Ό μ§„ν–‰ν•˜μ„Έμš”.");
263
+ }
264
+
265
+ if (strictWarnings && legacyResult.violations > 0) {
202
266
  success = false;
203
267
  } else {
204
- success = success && checkResult.passed;
268
+ success = success && legacyResult.passed;
205
269
  }
206
270
 
207
271
  if (format === "console") {
208
- const legacyReport = buildGuardReport(checkResult);
272
+ const legacyReport = buildGuardReport(legacyResult.checkResult);
209
273
  if (quiet) {
210
274
  print(`πŸ“Š Legacy Guard: ${legacySummary.violations}개 μœ„λ°˜`);
211
275
  } else {
@@ -3,63 +3,63 @@
3
3
  * Contract 생성 및 검증 λͺ…λ Ήμ–΄
4
4
  */
5
5
 
6
- import {
7
- runContractGuardCheck,
8
- generateContractTemplate,
9
- buildContractRegistry,
10
- writeContractRegistry,
11
- readContractRegistry,
12
- diffContractRegistry,
13
- validateAndReport,
14
- } from "@mandujs/core";
15
- import path from "path";
16
- import fs from "fs/promises";
17
- import { resolveManifest } from "../util/manifest";
6
+ import {
7
+ runContractGuardCheck,
8
+ generateContractTemplate,
9
+ buildContractRegistry,
10
+ writeContractRegistry,
11
+ readContractRegistry,
12
+ diffContractRegistry,
13
+ validateAndReport,
14
+ } from "@mandujs/core";
15
+ import path from "path";
16
+ import fs from "fs/promises";
17
+ import { resolveManifest } from "../util/manifest";
18
18
 
19
19
  interface ContractCreateOptions {
20
20
  routeId: string;
21
21
  }
22
22
 
23
- interface ContractValidateOptions {
24
- verbose?: boolean;
25
- }
26
-
27
- interface ContractBuildOptions {
28
- output?: string;
29
- }
30
-
31
- interface ContractDiffOptions {
32
- from?: string;
33
- to?: string;
34
- output?: string;
35
- json?: boolean;
36
- }
37
-
38
- async function loadRoutesManifest(rootDir: string) {
39
- const config = await validateAndReport(rootDir);
40
- if (!config) {
41
- throw new Error("Invalid mandu.config");
42
- }
43
- const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
44
- return resolved.manifest;
45
- }
23
+ interface ContractValidateOptions {
24
+ verbose?: boolean;
25
+ }
26
+
27
+ interface ContractBuildOptions {
28
+ output?: string;
29
+ }
30
+
31
+ interface ContractDiffOptions {
32
+ from?: string;
33
+ to?: string;
34
+ output?: string;
35
+ json?: boolean;
36
+ }
37
+
38
+ async function loadRoutesManifest(rootDir: string) {
39
+ const config = await validateAndReport(rootDir);
40
+ if (!config) {
41
+ throw new Error("Invalid mandu.config");
42
+ }
43
+ const resolved = await resolveManifest(rootDir, { fsRoutes: config.fsRoutes });
44
+ return resolved.manifest;
45
+ }
46
46
 
47
47
  /**
48
48
  * Create a new contract file for a route
49
49
  */
50
- export async function contractCreate(options: ContractCreateOptions): Promise<boolean> {
51
- const rootDir = process.cwd();
52
-
53
- console.log(`\nπŸ“œ Creating contract for route: ${options.routeId}\n`);
54
-
55
- // Load manifest
56
- let manifest;
57
- try {
58
- manifest = await loadRoutesManifest(rootDir);
59
- } catch (error) {
60
- console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
61
- return false;
62
- }
50
+ export async function contractCreate(options: ContractCreateOptions): Promise<boolean> {
51
+ const rootDir = process.cwd();
52
+
53
+ console.log(`\nπŸ“œ Creating contract for route: ${options.routeId}\n`);
54
+
55
+ // Load manifest
56
+ let manifest;
57
+ try {
58
+ manifest = await loadRoutesManifest(rootDir);
59
+ } catch (error) {
60
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
61
+ return false;
62
+ }
63
63
 
64
64
  // Find the route
65
65
  const route = manifest.routes.find((r) => r.id === options.routeId);
@@ -113,22 +113,22 @@ export async function contractCreate(options: ContractCreateOptions): Promise<bo
113
113
  /**
114
114
  * Validate all contracts against their slot implementations
115
115
  */
116
- export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
117
- const rootDir = process.cwd();
118
-
119
- console.log(`\nπŸ” Validating contracts...\n`);
120
-
121
- // Load manifest
122
- let manifest;
123
- try {
124
- manifest = await loadRoutesManifest(rootDir);
125
- } catch (error) {
126
- console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
127
- return false;
128
- }
129
-
130
- // Run contract guard check
131
- const violations = await runContractGuardCheck(manifest, rootDir);
116
+ export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
117
+ const rootDir = process.cwd();
118
+
119
+ console.log(`\nπŸ” Validating contracts...\n`);
120
+
121
+ // Load manifest
122
+ let manifest;
123
+ try {
124
+ manifest = await loadRoutesManifest(rootDir);
125
+ } catch (error) {
126
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
127
+ return false;
128
+ }
129
+
130
+ // Run contract guard check
131
+ const violations = await runContractGuardCheck(manifest, rootDir);
132
132
 
133
133
  if (violations.length === 0) {
134
134
  console.log(`βœ… All contracts are valid!\n`);
@@ -175,112 +175,112 @@ export async function contractValidate(options: ContractValidateOptions = {}): P
175
175
  console.log(`πŸ’‘ Use --verbose for fix suggestions\n`);
176
176
  }
177
177
 
178
- return false;
179
- }
180
-
181
- /**
182
- * Build contract registry (.mandu/contracts.json)
183
- */
184
- export async function contractBuild(options: ContractBuildOptions = {}): Promise<boolean> {
185
- const rootDir = process.cwd();
186
- const outputPath = options.output || path.join(rootDir, ".mandu", "contracts.json");
187
-
188
- console.log(`\nπŸ“¦ Building contract registry...\n`);
189
-
190
- let manifest;
191
- try {
192
- manifest = await loadRoutesManifest(rootDir);
193
- } catch (error) {
194
- console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
195
- return false;
196
- }
197
- const { registry, warnings } = await buildContractRegistry(manifest, rootDir);
198
-
199
- if (warnings.length > 0) {
200
- console.log(`⚠️ ${warnings.length} warning(s):`);
201
- for (const warning of warnings) {
202
- console.log(` - ${warning}`);
203
- }
204
- console.log();
205
- }
206
-
207
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
208
- await writeContractRegistry(outputPath, registry);
209
-
210
- console.log(`βœ… Registry generated: ${path.relative(rootDir, outputPath)}`);
211
- console.log(`πŸ“Š Contracts: ${registry.contracts.length}`);
212
-
213
- return true;
214
- }
215
-
216
- /**
217
- * Diff current contracts against a registry
218
- */
219
- export async function contractDiff(options: ContractDiffOptions = {}): Promise<boolean> {
220
- const rootDir = process.cwd();
221
- const fromPath = options.from || path.join(rootDir, ".mandu", "contracts.json");
222
-
223
- console.log(`\nπŸ” Diffing contracts...\n`);
224
-
225
- const fromRegistry = await readContractRegistry(fromPath);
226
- if (!fromRegistry) {
227
- console.error(`❌ Registry not found: ${path.relative(rootDir, fromPath)}`);
228
- console.log(`πŸ’‘ Run \`mandu contract build\` first.`);
229
- return false;
230
- }
231
-
232
- let toRegistry = options.to ? await readContractRegistry(options.to) : null;
233
-
234
- if (!toRegistry) {
235
- let manifest;
236
- try {
237
- manifest = await loadRoutesManifest(rootDir);
238
- } catch (error) {
239
- console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
240
- return false;
241
- }
242
- const { registry } = await buildContractRegistry(manifest, rootDir);
243
- toRegistry = registry;
244
- }
245
-
246
- const diff = diffContractRegistry(fromRegistry, toRegistry);
247
-
248
- if (options.output) {
249
- await fs.mkdir(path.dirname(options.output), { recursive: true });
250
- await Bun.write(options.output, JSON.stringify(diff, null, 2));
251
- console.log(`βœ… Diff saved: ${path.relative(rootDir, options.output)}`);
252
- }
253
-
254
- if (options.json) {
255
- console.log(JSON.stringify(diff, null, 2));
256
- return diff.summary.major === 0;
257
- }
258
-
259
- console.log(`πŸ“Š Summary: major ${diff.summary.major}, minor ${diff.summary.minor}, patch ${diff.summary.patch}`);
260
-
261
- if (diff.added.length > 0) {
262
- console.log(`\n🟒 Added (${diff.added.length})`);
263
- for (const entry of diff.added) {
264
- console.log(` - ${entry.id} (${entry.routeId})`);
265
- }
266
- }
267
-
268
- if (diff.removed.length > 0) {
269
- console.log(`\nπŸ”΄ Removed (${diff.removed.length})`);
270
- for (const entry of diff.removed) {
271
- console.log(` - ${entry.id} (${entry.routeId})`);
272
- }
273
- }
274
-
275
- if (diff.changed.length > 0) {
276
- console.log(`\n🟑 Changed (${diff.changed.length})`);
277
- for (const change of diff.changed) {
278
- console.log(` - ${change.id} (${change.routeId}) [${change.severity}]`);
279
- for (const detail of change.changes) {
280
- console.log(` β€’ ${detail}`);
281
- }
282
- }
283
- }
284
-
285
- return diff.summary.major === 0;
286
- }
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Build contract registry (.mandu/contracts.json)
183
+ */
184
+ export async function contractBuild(options: ContractBuildOptions = {}): Promise<boolean> {
185
+ const rootDir = process.cwd();
186
+ const outputPath = options.output || path.join(rootDir, ".mandu", "contracts.json");
187
+
188
+ console.log(`\nπŸ“¦ Building contract registry...\n`);
189
+
190
+ let manifest;
191
+ try {
192
+ manifest = await loadRoutesManifest(rootDir);
193
+ } catch (error) {
194
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
195
+ return false;
196
+ }
197
+ const { registry, warnings } = await buildContractRegistry(manifest, rootDir);
198
+
199
+ if (warnings.length > 0) {
200
+ console.log(`⚠️ ${warnings.length} warning(s):`);
201
+ for (const warning of warnings) {
202
+ console.log(` - ${warning}`);
203
+ }
204
+ console.log();
205
+ }
206
+
207
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
208
+ await writeContractRegistry(outputPath, registry);
209
+
210
+ console.log(`βœ… Registry generated: ${path.relative(rootDir, outputPath)}`);
211
+ console.log(`πŸ“Š Contracts: ${registry.contracts.length}`);
212
+
213
+ return true;
214
+ }
215
+
216
+ /**
217
+ * Diff current contracts against a registry
218
+ */
219
+ export async function contractDiff(options: ContractDiffOptions = {}): Promise<boolean> {
220
+ const rootDir = process.cwd();
221
+ const fromPath = options.from || path.join(rootDir, ".mandu", "contracts.json");
222
+
223
+ console.log(`\nπŸ” Diffing contracts...\n`);
224
+
225
+ const fromRegistry = await readContractRegistry(fromPath);
226
+ if (!fromRegistry) {
227
+ console.error(`❌ Registry not found: ${path.relative(rootDir, fromPath)}`);
228
+ console.log(`πŸ’‘ Run \`mandu contract build\` first.`);
229
+ return false;
230
+ }
231
+
232
+ let toRegistry = options.to ? await readContractRegistry(options.to) : null;
233
+
234
+ if (!toRegistry) {
235
+ let manifest;
236
+ try {
237
+ manifest = await loadRoutesManifest(rootDir);
238
+ } catch (error) {
239
+ console.error("❌ Failed to load manifest:", error instanceof Error ? error.message : error);
240
+ return false;
241
+ }
242
+ const { registry } = await buildContractRegistry(manifest, rootDir);
243
+ toRegistry = registry;
244
+ }
245
+
246
+ const diff = diffContractRegistry(fromRegistry, toRegistry);
247
+
248
+ if (options.output) {
249
+ await fs.mkdir(path.dirname(options.output), { recursive: true });
250
+ await Bun.write(options.output, JSON.stringify(diff, null, 2));
251
+ console.log(`βœ… Diff saved: ${path.relative(rootDir, options.output)}`);
252
+ }
253
+
254
+ if (options.json) {
255
+ console.log(JSON.stringify(diff, null, 2));
256
+ return diff.summary.major === 0;
257
+ }
258
+
259
+ console.log(`πŸ“Š Summary: major ${diff.summary.major}, minor ${diff.summary.minor}, patch ${diff.summary.patch}`);
260
+
261
+ if (diff.added.length > 0) {
262
+ console.log(`\n🟒 Added (${diff.added.length})`);
263
+ for (const entry of diff.added) {
264
+ console.log(` - ${entry.id} (${entry.routeId})`);
265
+ }
266
+ }
267
+
268
+ if (diff.removed.length > 0) {
269
+ console.log(`\nπŸ”΄ Removed (${diff.removed.length})`);
270
+ for (const entry of diff.removed) {
271
+ console.log(` - ${entry.id} (${entry.routeId})`);
272
+ }
273
+ }
274
+
275
+ if (diff.changed.length > 0) {
276
+ console.log(`\n🟑 Changed (${diff.changed.length})`);
277
+ for (const change of diff.changed) {
278
+ console.log(` - ${change.id} (${change.routeId}) [${change.severity}]`);
279
+ for (const detail of change.changes) {
280
+ console.log(` β€’ ${detail}`);
281
+ }
282
+ }
283
+ }
284
+
285
+ return diff.summary.major === 0;
286
+ }
@@ -15,12 +15,6 @@ import {
15
15
  validateAndReport,
16
16
  isTailwindProject,
17
17
  startCSSWatch,
18
- readLockfile,
19
- readMcpConfig,
20
- validateWithPolicy,
21
- detectMode,
22
- formatPolicyAction,
23
- formatValidationResult,
24
18
  type RoutesManifest,
25
19
  type GuardConfig,
26
20
  type Violation,
@@ -32,6 +26,11 @@ import { CLI_ERROR_CODES, printCLIError } from "../errors";
32
26
  import { importFresh } from "../util/bun";
33
27
  import { resolveManifest } from "../util/manifest";
34
28
  import { resolveAvailablePort } from "../util/port";
29
+ import {
30
+ validateRuntimeLockfile,
31
+ handleBlockedLockfile,
32
+ printRuntimeLockfileStatus,
33
+ } from "../util/lockfile";
35
34
  import { registerManifestHandlers } from "../util/handlers";
36
35
  import path from "path";
37
36
 
@@ -49,35 +48,8 @@ export async function dev(options: DevOptions = {}): Promise<void> {
49
48
  }
50
49
 
51
50
  // Lockfile 검증 (μ„€μ • 무결성)
52
- const lockfile = await readLockfile(rootDir);
53
- let mcpConfig: Record<string, unknown> | null = null;
54
- try {
55
- mcpConfig = await readMcpConfig(rootDir);
56
- } catch (error) {
57
- console.warn(
58
- `⚠️ MCP μ„€μ • λ‘œλ“œ μ‹€νŒ¨: ${error instanceof Error ? error.message : String(error)}`
59
- );
60
- }
61
- const { result: lockResult, action, bypassed } = validateWithPolicy(
62
- config,
63
- lockfile,
64
- detectMode(),
65
- mcpConfig
66
- );
67
-
68
- if (action === "block") {
69
- console.error("πŸ›‘ μ„œλ²„ μ‹œμž‘ 차단: Lockfile 뢈일치");
70
- console.error(" 섀정이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ˜λ„ν•œ 변경이라면:");
71
- console.error(" $ mandu lock");
72
- console.error("");
73
- console.error(" λ³€κ²½ 사항 확인:");
74
- console.error(" $ mandu lock --diff");
75
- if (lockResult) {
76
- console.error("");
77
- console.error(formatValidationResult(lockResult));
78
- }
79
- process.exit(1);
80
- }
51
+ const { lockfile, lockResult, action, bypassed } = await validateRuntimeLockfile(config, rootDir);
52
+ handleBlockedLockfile(action, lockResult);
81
53
 
82
54
  const serverConfig = config.server ?? {};
83
55
  const devConfig = config.dev ?? {};
@@ -87,13 +59,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
87
59
  console.log(`πŸ₯Ÿ Mandu Dev Server`);
88
60
 
89
61
  // Lockfile μƒνƒœ 좜λ ₯
90
- if (action === "warn") {
91
- console.log(`⚠️ ${formatPolicyAction(action, bypassed)}`);
92
- } else if (lockfile && lockResult?.valid) {
93
- console.log(`πŸ”’ μ„€μ • 무결성 확인됨 (${lockResult.currentHash?.slice(0, 8)})`);
94
- } else if (!lockfile) {
95
- console.log(`πŸ’‘ Lockfile μ—†μŒ - 'mandu lock'으둜 생성 ꢌμž₯`);
96
- }
62
+ printRuntimeLockfileStatus(action, bypassed, lockfile, lockResult);
97
63
 
98
64
  // .env 파일 λ‘œλ“œ
99
65
  const envResult = await loadEnv({
@@ -328,6 +294,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
328
294
  bundleManifest: devBundler?.initialBuild.manifest,
329
295
  cors: serverConfig.cors,
330
296
  streaming: serverConfig.streaming,
297
+ rateLimit: serverConfig.rateLimit,
331
298
  // Tailwind 감지 μ‹œμ—λ§Œ CSS 링크 μ£Όμž…
332
299
  cssPath: hasTailwind ? cssWatcher?.serverPath : false,
333
300
  });