@mandujs/cli 0.4.4 → 0.5.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.
package/package.json CHANGED
@@ -1,40 +1,40 @@
1
- {
2
- "name": "@mandujs/cli",
3
- "version": "0.4.4",
4
- "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
- "type": "module",
6
- "main": "./src/main.ts",
7
- "bin": {
8
- "mandu": "./src/main.ts"
9
- },
10
- "files": [
11
- "src/**/*",
12
- "templates/**/*"
13
- ],
14
- "keywords": [
15
- "ai",
16
- "agent",
17
- "framework",
18
- "fullstack",
19
- "bun",
20
- "typescript",
21
- "react",
22
- "ssr",
23
- "code-generation"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/konamgil/mandu.git"
28
- },
29
- "author": "konamgil",
30
- "license": "MIT",
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "dependencies": {
35
- "@mandujs/core": "^0.4.3"
36
- },
37
- "engines": {
38
- "bun": ">=1.0.0"
39
- }
40
- }
1
+ {
2
+ "name": "@mandujs/cli",
3
+ "version": "0.5.1",
4
+ "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
+ "type": "module",
6
+ "main": "./src/main.ts",
7
+ "bin": {
8
+ "mandu": "./src/main.ts"
9
+ },
10
+ "files": [
11
+ "src/**/*",
12
+ "templates/**/*"
13
+ ],
14
+ "keywords": [
15
+ "ai",
16
+ "agent",
17
+ "framework",
18
+ "fullstack",
19
+ "bun",
20
+ "typescript",
21
+ "react",
22
+ "ssr",
23
+ "code-generation"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/konamgil/mandu.git"
28
+ },
29
+ "author": "konamgil",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@mandujs/core": "^0.5.1"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.0.0"
39
+ }
40
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Mandu CLI - Contract Commands
3
+ * Contract 생성 및 검증 명령어
4
+ */
5
+
6
+ import { loadManifest, runContractGuardCheck, generateContractTemplate } from "@mandujs/core";
7
+ import path from "path";
8
+ import fs from "fs/promises";
9
+
10
+ interface ContractCreateOptions {
11
+ routeId: string;
12
+ }
13
+
14
+ interface ContractValidateOptions {
15
+ verbose?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Create a new contract file for a route
20
+ */
21
+ export async function contractCreate(options: ContractCreateOptions): Promise<boolean> {
22
+ const rootDir = process.cwd();
23
+ const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
24
+
25
+ console.log(`\n📜 Creating contract for route: ${options.routeId}\n`);
26
+
27
+ // Load manifest
28
+ const manifestResult = await loadManifest(manifestPath);
29
+ if (!manifestResult.success) {
30
+ console.error("❌ Failed to load manifest:", manifestResult.errors);
31
+ return false;
32
+ }
33
+
34
+ const manifest = manifestResult.data!;
35
+
36
+ // Find the route
37
+ const route = manifest.routes.find((r) => r.id === options.routeId);
38
+ if (!route) {
39
+ console.error(`❌ Route not found: ${options.routeId}`);
40
+ console.log(`\nAvailable routes:`);
41
+ for (const r of manifest.routes) {
42
+ console.log(` - ${r.id} (${r.pattern})`);
43
+ }
44
+ return false;
45
+ }
46
+
47
+ // Check if contract already exists
48
+ const contractPath = route.contractModule || `spec/contracts/${options.routeId}.contract.ts`;
49
+ const fullContractPath = path.join(rootDir, contractPath);
50
+
51
+ try {
52
+ await fs.access(fullContractPath);
53
+ console.error(`❌ Contract file already exists: ${contractPath}`);
54
+ console.log(`\nTo regenerate, delete the file first.`);
55
+ return false;
56
+ } catch {
57
+ // File doesn't exist, we can create it
58
+ }
59
+
60
+ // Create directory if needed
61
+ const contractDir = path.dirname(fullContractPath);
62
+ await fs.mkdir(contractDir, { recursive: true });
63
+
64
+ // Generate contract content
65
+ const contractContent = generateContractTemplate(route);
66
+
67
+ // Write contract file
68
+ await Bun.write(fullContractPath, contractContent);
69
+ console.log(`✅ Created: ${contractPath}`);
70
+
71
+ // Suggest updating manifest
72
+ if (!route.contractModule) {
73
+ console.log(`\n💡 Don't forget to add contractModule to your manifest:`);
74
+ console.log(` "contractModule": "${contractPath}"`);
75
+ }
76
+
77
+ console.log(`\n📝 Next steps:`);
78
+ console.log(` 1. Edit ${contractPath} to define your API schema`);
79
+ console.log(` 2. Run \`mandu generate\` to regenerate handlers`);
80
+ console.log(` 3. Run \`mandu guard\` to validate contract-slot consistency`);
81
+
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Validate all contracts against their slot implementations
87
+ */
88
+ export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
89
+ const rootDir = process.cwd();
90
+ const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
91
+
92
+ console.log(`\n🔍 Validating contracts...\n`);
93
+
94
+ // Load manifest
95
+ const manifestResult = await loadManifest(manifestPath);
96
+ if (!manifestResult.success) {
97
+ console.error("❌ Failed to load manifest:", manifestResult.errors);
98
+ return false;
99
+ }
100
+
101
+ const manifest = manifestResult.data!;
102
+
103
+ // Run contract guard check
104
+ const violations = await runContractGuardCheck(manifest, rootDir);
105
+
106
+ if (violations.length === 0) {
107
+ console.log(`✅ All contracts are valid!\n`);
108
+
109
+ // Show summary
110
+ const contractCount = manifest.routes.filter((r) => r.contractModule).length;
111
+ console.log(`📊 Summary:`);
112
+ console.log(` Routes with contracts: ${contractCount}/${manifest.routes.length}`);
113
+
114
+ return true;
115
+ }
116
+
117
+ // Group violations by type
118
+ const byType: Record<string, typeof violations> = {};
119
+ for (const v of violations) {
120
+ byType[v.ruleId] = byType[v.ruleId] || [];
121
+ byType[v.ruleId].push(v);
122
+ }
123
+
124
+ // Display violations
125
+ console.log(`❌ Found ${violations.length} contract issues:\n`);
126
+
127
+ for (const [ruleId, ruleViolations] of Object.entries(byType)) {
128
+ const icon =
129
+ ruleId === "CONTRACT_METHOD_NOT_IMPLEMENTED"
130
+ ? "🔴"
131
+ : ruleId === "CONTRACT_METHOD_UNDOCUMENTED"
132
+ ? "🟡"
133
+ : "⚠️";
134
+
135
+ console.log(`${icon} ${ruleId} (${ruleViolations.length} issues)`);
136
+
137
+ for (const v of ruleViolations) {
138
+ console.log(` 📄 ${v.file}`);
139
+ console.log(` ${v.message}`);
140
+ if (options.verbose) {
141
+ console.log(` 💡 ${v.suggestion}`);
142
+ }
143
+ }
144
+ console.log();
145
+ }
146
+
147
+ if (!options.verbose) {
148
+ console.log(`💡 Use --verbose for fix suggestions\n`);
149
+ }
150
+
151
+ return false;
152
+ }
@@ -109,6 +109,7 @@ export async function dev(options: DevOptions = {}): Promise<void> {
109
109
  port,
110
110
  isDev: true,
111
111
  hmrPort: hmrServer ? port : undefined,
112
+ bundleManifest: devBundler?.manifest,
112
113
  });
113
114
 
114
115
  // 정리 함수
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Mandu CLI - OpenAPI Commands
3
+ * OpenAPI 스펙 생성 명령어
4
+ */
5
+
6
+ import { loadManifest, generateOpenAPIDocument, openAPIToJSON } from "@mandujs/core";
7
+ import path from "path";
8
+ import fs from "fs/promises";
9
+
10
+ interface OpenAPIGenerateOptions {
11
+ output?: string;
12
+ title?: string;
13
+ version?: string;
14
+ }
15
+
16
+ interface OpenAPIServeOptions {
17
+ port?: number;
18
+ }
19
+
20
+ /**
21
+ * Generate OpenAPI specification from contracts
22
+ */
23
+ export async function openAPIGenerate(options: OpenAPIGenerateOptions = {}): Promise<boolean> {
24
+ const rootDir = process.cwd();
25
+ const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
26
+
27
+ console.log(`\n📄 Generating OpenAPI specification...\n`);
28
+
29
+ // Load manifest
30
+ const manifestResult = await loadManifest(manifestPath);
31
+ if (!manifestResult.success) {
32
+ console.error("❌ Failed to load manifest:", manifestResult.errors);
33
+ return false;
34
+ }
35
+
36
+ const manifest = manifestResult.data!;
37
+
38
+ // Count routes with contracts
39
+ const contractRoutes = manifest.routes.filter((r) => r.contractModule);
40
+ if (contractRoutes.length === 0) {
41
+ console.log(`⚠️ No routes with contracts found.`);
42
+ console.log(`\nTo generate OpenAPI docs, add contractModule to your routes.`);
43
+ console.log(`Example:`);
44
+ console.log(` {`);
45
+ console.log(` "id": "users",`);
46
+ console.log(` "pattern": "/api/users",`);
47
+ console.log(` "contractModule": "spec/contracts/users.contract.ts"`);
48
+ console.log(` }`);
49
+ return true;
50
+ }
51
+
52
+ console.log(`📝 Found ${contractRoutes.length} routes with contracts`);
53
+
54
+ // Generate OpenAPI document
55
+ try {
56
+ const doc = await generateOpenAPIDocument(manifest, rootDir, {
57
+ title: options.title,
58
+ version: options.version,
59
+ });
60
+
61
+ const json = openAPIToJSON(doc);
62
+
63
+ // Determine output path
64
+ const outputPath = options.output || path.join(rootDir, "openapi.json");
65
+ const outputDir = path.dirname(outputPath);
66
+
67
+ // Ensure directory exists
68
+ await fs.mkdir(outputDir, { recursive: true });
69
+
70
+ // Write file
71
+ await Bun.write(outputPath, json);
72
+
73
+ console.log(`\n✅ Generated: ${path.relative(rootDir, outputPath)}`);
74
+
75
+ // Show summary
76
+ const pathCount = Object.keys(doc.paths).length;
77
+ const tagCount = doc.tags?.length || 0;
78
+
79
+ console.log(`\n📊 Summary:`);
80
+ console.log(` Paths: ${pathCount}`);
81
+ console.log(` Tags: ${tagCount}`);
82
+ console.log(` Version: ${doc.info.version}`);
83
+
84
+ console.log(`\n💡 View your API docs:`);
85
+ console.log(` - Import into Swagger Editor: https://editor.swagger.io`);
86
+ console.log(` - Import into Postman`);
87
+ console.log(` - Run \`mandu openapi serve\` for local Swagger UI`);
88
+
89
+ return true;
90
+ } catch (error) {
91
+ console.error(`❌ Failed to generate OpenAPI:`, error);
92
+ return false;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Serve Swagger UI for OpenAPI documentation
98
+ */
99
+ export async function openAPIServe(options: OpenAPIServeOptions = {}): Promise<boolean> {
100
+ const rootDir = process.cwd();
101
+ const port = options.port || 8080;
102
+ const openAPIPath = path.join(rootDir, "openapi.json");
103
+
104
+ console.log(`\n🌐 Starting OpenAPI documentation server...\n`);
105
+
106
+ // Check if openapi.json exists
107
+ try {
108
+ await fs.access(openAPIPath);
109
+ } catch {
110
+ console.log(`⚠️ openapi.json not found. Generating...`);
111
+ const generated = await openAPIGenerate({});
112
+ if (!generated) {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // Read OpenAPI spec
118
+ const specContent = await Bun.file(openAPIPath).text();
119
+
120
+ // Simple HTML for Swagger UI
121
+ const swaggerHTML = `
122
+ <!DOCTYPE html>
123
+ <html lang="en">
124
+ <head>
125
+ <meta charset="UTF-8">
126
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
127
+ <title>Mandu API Documentation</title>
128
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css">
129
+ <style>
130
+ body { margin: 0; padding: 0; }
131
+ .swagger-ui .topbar { display: none; }
132
+ .swagger-ui .info .title { font-size: 28px; }
133
+ .swagger-ui .info { margin: 20px 0; }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div id="swagger-ui"></div>
138
+ <script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
139
+ <script>
140
+ window.onload = () => {
141
+ SwaggerUIBundle({
142
+ spec: ${specContent},
143
+ dom_id: '#swagger-ui',
144
+ deepLinking: true,
145
+ presets: [
146
+ SwaggerUIBundle.presets.apis,
147
+ SwaggerUIBundle.SwaggerUIStandalonePreset
148
+ ],
149
+ layout: "BaseLayout"
150
+ });
151
+ };
152
+ </script>
153
+ </body>
154
+ </html>
155
+ `.trim();
156
+
157
+ // Start server
158
+ const server = Bun.serve({
159
+ port,
160
+ fetch(req) {
161
+ const url = new URL(req.url);
162
+
163
+ if (url.pathname === "/openapi.json") {
164
+ return new Response(specContent, {
165
+ headers: { "Content-Type": "application/json" },
166
+ });
167
+ }
168
+
169
+ return new Response(swaggerHTML, {
170
+ headers: { "Content-Type": "text/html" },
171
+ });
172
+ },
173
+ });
174
+
175
+ console.log(`✅ Swagger UI is running at http://localhost:${port}`);
176
+ console.log(` OpenAPI spec: http://localhost:${port}/openapi.json`);
177
+ console.log(`\nPress Ctrl+C to stop.\n`);
178
+
179
+ // Keep server running
180
+ await new Promise(() => {});
181
+
182
+ return true;
183
+ }
package/src/main.ts CHANGED
@@ -6,6 +6,8 @@ import { guardCheck } from "./commands/guard-check";
6
6
  import { dev } from "./commands/dev";
7
7
  import { init } from "./commands/init";
8
8
  import { build } from "./commands/build";
9
+ import { contractCreate, contractValidate } from "./commands/contract";
10
+ import { openAPIGenerate, openAPIServe } from "./commands/openapi";
9
11
  import {
10
12
  changeBegin,
11
13
  changeCommit,
@@ -28,6 +30,12 @@ Commands:
28
30
  build 클라이언트 번들 빌드 (Hydration)
29
31
  dev 개발 서버 실행
30
32
 
33
+ contract create <routeId> 라우트에 대한 Contract 생성
34
+ contract validate Contract-Slot 일관성 검증
35
+
36
+ openapi generate OpenAPI 3.0 스펙 생성
37
+ openapi serve Swagger UI 로컬 서버 실행
38
+
31
39
  change begin 변경 트랜잭션 시작 (스냅샷 생성)
32
40
  change commit 변경 확정
33
41
  change rollback 스냅샷으로 복원
@@ -38,7 +46,7 @@ Commands:
38
46
  Options:
39
47
  --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
40
48
  --file <path> spec-upsert 시 사용할 spec 파일 경로
41
- --port <port> dev 서버 포트 (기본: 3000)
49
+ --port <port> dev/openapi serve 포트 (기본: 3000/8080)
42
50
  --no-auto-correct guard 시 자동 수정 비활성화
43
51
  --minify build 시 코드 압축
44
52
  --sourcemap build 시 소스맵 생성
@@ -46,6 +54,8 @@ Options:
46
54
  --message <msg> change begin 시 설명 메시지
47
55
  --id <id> change rollback 시 특정 변경 ID
48
56
  --keep <n> change prune 시 유지할 스냅샷 수 (기본: 5)
57
+ --output <path> openapi generate 시 출력 경로 (기본: openapi.json)
58
+ --verbose contract validate 시 상세 출력
49
59
  --help, -h 도움말 표시
50
60
 
51
61
  Examples:
@@ -56,12 +66,19 @@ Examples:
56
66
  bunx mandu build --minify
57
67
  bunx mandu build --watch
58
68
  bunx mandu dev --port 3000
69
+ bunx mandu contract create users
70
+ bunx mandu contract validate --verbose
71
+ bunx mandu openapi generate --output docs/api.json
72
+ bunx mandu openapi serve --port 8080
59
73
  bunx mandu change begin --message "Add new route"
60
74
  bunx mandu change commit
61
75
  bunx mandu change rollback
62
76
 
63
77
  Workflow:
64
78
  1. init → 2. spec-upsert → 3. generate → 4. build → 5. guard → 6. dev
79
+
80
+ Contract-first Workflow:
81
+ 1. contract create → 2. Edit contract → 3. generate → 4. Edit slot → 5. contract validate
65
82
  `;
66
83
 
67
84
  function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
@@ -129,6 +146,53 @@ async function main(): Promise<void> {
129
146
  await dev({ port: options.port ? Number(options.port) : undefined });
130
147
  break;
131
148
 
149
+ case "contract": {
150
+ const subCommand = args[1];
151
+ switch (subCommand) {
152
+ case "create": {
153
+ const routeId = args[2] || options._positional;
154
+ if (!routeId) {
155
+ console.error("❌ Route ID is required");
156
+ console.log("\nUsage: bunx mandu contract create <routeId>");
157
+ process.exit(1);
158
+ }
159
+ success = await contractCreate({ routeId });
160
+ break;
161
+ }
162
+ case "validate":
163
+ success = await contractValidate({ verbose: options.verbose === "true" });
164
+ break;
165
+ default:
166
+ console.error(`❌ Unknown contract subcommand: ${subCommand}`);
167
+ console.log("\nUsage: bunx mandu contract <create|validate>");
168
+ process.exit(1);
169
+ }
170
+ break;
171
+ }
172
+
173
+ case "openapi": {
174
+ const subCommand = args[1];
175
+ switch (subCommand) {
176
+ case "generate":
177
+ success = await openAPIGenerate({
178
+ output: options.output,
179
+ title: options.title,
180
+ version: options.version,
181
+ });
182
+ break;
183
+ case "serve":
184
+ success = await openAPIServe({
185
+ port: options.port ? Number(options.port) : undefined,
186
+ });
187
+ break;
188
+ default:
189
+ console.error(`❌ Unknown openapi subcommand: ${subCommand}`);
190
+ console.log("\nUsage: bunx mandu openapi <generate|serve>");
191
+ process.exit(1);
192
+ }
193
+ break;
194
+ }
195
+
132
196
  case "change": {
133
197
  const subCommand = args[1];
134
198
  switch (subCommand) {