@mandujs/cli 0.12.2 → 0.13.1

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 (51) hide show
  1. package/README.ko.md +234 -234
  2. package/README.md +354 -354
  3. package/package.json +2 -2
  4. package/src/commands/contract.ts +173 -173
  5. package/src/commands/dev.ts +8 -68
  6. package/src/commands/doctor.ts +27 -27
  7. package/src/commands/guard-arch.ts +303 -303
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/monitor.ts +300 -300
  10. package/src/commands/openapi.ts +107 -107
  11. package/src/commands/registry.ts +367 -357
  12. package/src/commands/routes.ts +228 -228
  13. package/src/commands/start.ts +184 -0
  14. package/src/errors/codes.ts +35 -35
  15. package/src/errors/index.ts +2 -2
  16. package/src/errors/messages.ts +143 -143
  17. package/src/hooks/index.ts +17 -17
  18. package/src/hooks/preaction.ts +256 -256
  19. package/src/main.ts +37 -34
  20. package/src/terminal/banner.ts +166 -166
  21. package/src/terminal/help.ts +306 -306
  22. package/src/terminal/index.ts +71 -71
  23. package/src/terminal/output.ts +295 -295
  24. package/src/terminal/palette.ts +30 -30
  25. package/src/terminal/progress.ts +327 -327
  26. package/src/terminal/stream-writer.ts +214 -214
  27. package/src/terminal/table.ts +354 -354
  28. package/src/terminal/theme.ts +142 -142
  29. package/src/util/bun.ts +6 -6
  30. package/src/util/fs.ts +23 -23
  31. package/src/util/handlers.ts +96 -0
  32. package/src/util/manifest.ts +52 -52
  33. package/src/util/output.ts +22 -22
  34. package/src/util/port.ts +71 -71
  35. package/templates/default/AGENTS.md +96 -96
  36. package/templates/default/app/api/health/route.ts +13 -13
  37. package/templates/default/app/globals.css +49 -49
  38. package/templates/default/app/layout.tsx +27 -27
  39. package/templates/default/app/page.tsx +38 -38
  40. package/templates/default/package.json +1 -0
  41. package/templates/default/src/client/shared/lib/utils.ts +16 -16
  42. package/templates/default/src/client/shared/ui/button.tsx +57 -57
  43. package/templates/default/src/client/shared/ui/card.tsx +78 -78
  44. package/templates/default/src/client/shared/ui/index.ts +21 -21
  45. package/templates/default/src/client/shared/ui/input.tsx +24 -24
  46. package/templates/default/tests/example.test.ts +58 -58
  47. package/templates/default/tests/helpers.ts +52 -52
  48. package/templates/default/tests/setup.ts +9 -9
  49. package/templates/default/tsconfig.json +12 -14
  50. package/templates/default/apps/server/main.ts +0 -67
  51. package/templates/default/apps/web/entry.tsx +0 -35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
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.12.0",
35
+ "@mandujs/core": "0.12.2",
36
36
  "cfonts": "^3.3.0"
37
37
  },
38
38
  "engines": {
@@ -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
+ }
@@ -1,9 +1,5 @@
1
1
  import {
2
2
  startServer,
3
- registerApiHandler,
4
- registerPageLoader,
5
- registerPageHandler,
6
- registerLayoutLoader,
7
3
  startDevBundler,
8
4
  createHMRServer,
9
5
  needsHydration,
@@ -36,6 +32,7 @@ import { CLI_ERROR_CODES, printCLIError } from "../errors";
36
32
  import { importFresh } from "../util/bun";
37
33
  import { resolveManifest } from "../util/manifest";
38
34
  import { resolveAvailablePort } from "../util/port";
35
+ import { registerManifestHandlers } from "../util/handlers";
39
36
  import path from "path";
40
37
 
41
38
  export interface DevOptions {
@@ -202,70 +199,13 @@ export async function dev(options: DevOptions = {}): Promise<void> {
202
199
  // Layout 경로 추적 (중복 등록 방지)
203
200
  const registeredLayouts = new Set<string>();
204
201
 
205
- // 핸들러 등록 함수
206
- const registerHandlers = async (manifest: RoutesManifest, isReload = false) => {
207
- // 리로드 레이아웃 캐시 클리어
208
- if (isReload) {
209
- registeredLayouts.clear();
210
- }
211
-
212
- for (const route of manifest.routes) {
213
- if (route.kind === "api") {
214
- const modulePath = path.resolve(rootDir, route.module);
215
- try {
216
- // 캐시 무효화 (HMR용)
217
- const module = await importFresh(modulePath);
218
- let handler = module.default || module.handler || module;
219
-
220
- // ManduFilling 인스턴스를 핸들러 함수로 래핑
221
- if (handler && typeof handler.handle === 'function') {
222
- console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
223
- const filling = handler;
224
- handler = async (req: Request, params?: Record<string, string>) => {
225
- return filling.handle(req, params);
226
- };
227
- } else {
228
- console.log(` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`);
229
- }
230
-
231
- registerApiHandler(route.id, handler);
232
- console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
233
- } catch (error) {
234
- console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
235
- }
236
- } else if (route.kind === "page" && route.componentModule) {
237
- const componentPath = path.resolve(rootDir, route.componentModule);
238
- const isIsland = needsHydration(route);
239
- const hasLayout = route.layoutChain && route.layoutChain.length > 0;
240
-
241
- // Layout 로더 등록
242
- if (route.layoutChain) {
243
- for (const layoutPath of route.layoutChain) {
244
- if (!registeredLayouts.has(layoutPath)) {
245
- const absLayoutPath = path.resolve(rootDir, layoutPath);
246
- registerLayoutLoader(layoutPath, async () => {
247
- // 캐시 무효화 (HMR용)
248
- return importFresh(absLayoutPath);
249
- });
250
- registeredLayouts.add(layoutPath);
251
- console.log(` 🎨 Layout: ${layoutPath}`);
252
- }
253
- }
254
- }
255
-
256
- // slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
257
- if (route.slotModule) {
258
- registerPageHandler(route.id, async () => {
259
- const module = await importFresh(componentPath);
260
- return module.default;
261
- });
262
- console.log(` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
263
- } else {
264
- registerPageLoader(route.id, () => importFresh(componentPath));
265
- console.log(` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`);
266
- }
267
- }
268
- }
202
+ // 핸들러 등록 함수 (공유 유틸 사용)
203
+ const registerHandlers = async (m: RoutesManifest, isReload = false) => {
204
+ await registerManifestHandlers(m, rootDir, {
205
+ importFn: importFresh,
206
+ registeredLayouts,
207
+ isReload,
208
+ });
269
209
  };
270
210
 
271
211
  // 초기 핸들러 등록
@@ -15,22 +15,22 @@ import {
15
15
  getBrain,
16
16
  } from "../../../core/src/index";
17
17
  import { resolveFromCwd, getRootDir } from "../util/fs";
18
- import path from "path";
18
+ import path from "path";
19
19
  import fs from "fs/promises";
20
20
 
21
- export interface DoctorOptions {
22
- /** Output format: console, json, or markdown */
23
- format?: "console" | "json" | "markdown";
21
+ export interface DoctorOptions {
22
+ /** Output format: console, json, or markdown */
23
+ format?: "console" | "json" | "markdown";
24
24
  /** Whether to use LLM for enhanced analysis */
25
25
  useLLM?: boolean;
26
26
  /** Output file path (for json/markdown formats) */
27
- output?: string;
28
- }
29
-
30
- export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
31
- const { format, useLLM = true, output } = options;
32
- const inferredFormat = format ?? (output ? (path.extname(output).toLowerCase() === ".json" ? "json" : "markdown") : undefined);
33
- const resolvedFormat = inferredFormat ?? "console";
27
+ output?: string;
28
+ }
29
+
30
+ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
31
+ const { format, useLLM = true, output } = options;
32
+ const inferredFormat = format ?? (output ? (path.extname(output).toLowerCase() === ".json" ? "json" : "markdown") : undefined);
33
+ const resolvedFormat = inferredFormat ?? "console";
34
34
 
35
35
  const specPath = resolveFromCwd("spec/routes.manifest.json");
36
36
  const rootDir = getRootDir();
@@ -82,12 +82,12 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
82
82
  });
83
83
 
84
84
  // Output based on format
85
- switch (resolvedFormat) {
86
- case "console":
87
- printDoctorReport(analysis);
88
- break;
89
-
90
- case "json": {
85
+ switch (resolvedFormat) {
86
+ case "console":
87
+ printDoctorReport(analysis);
88
+ break;
89
+
90
+ case "json": {
91
91
  const json = JSON.stringify(
92
92
  {
93
93
  summary: analysis.summary,
@@ -106,10 +106,10 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
106
106
  } else {
107
107
  console.log(json);
108
108
  }
109
- break;
110
- }
111
-
112
- case "markdown": {
109
+ break;
110
+ }
111
+
112
+ case "markdown": {
113
113
  const md = generateDoctorMarkdownReport(analysis);
114
114
 
115
115
  if (output) {
@@ -118,9 +118,9 @@ export async function doctor(options: DoctorOptions = {}): Promise<boolean> {
118
118
  } else {
119
119
  console.log(md);
120
120
  }
121
- break;
122
- }
123
- }
124
-
125
- return false;
126
- }
121
+ break;
122
+ }
123
+ }
124
+
125
+ return false;
126
+ }