@mandujs/cli 0.4.4 → 0.5.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 +40 -40
- package/src/commands/contract.ts +152 -0
- package/src/commands/openapi.ts +183 -0
- package/src/main.ts +65 -1
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"bun": ">=1.0.0"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandujs/cli",
|
|
3
|
+
"version": "0.5.0",
|
|
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.0"
|
|
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
|
+
}
|
|
@@ -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
|
|
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) {
|