@mandujs/cli 0.1.0 → 0.2.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/README.md +170 -0
- package/package.json +2 -2
- package/src/commands/dev.ts +1 -1
- package/src/commands/generate-apply.ts +1 -1
- package/src/commands/guard-check.ts +69 -6
- package/src/commands/spec-upsert.ts +1 -1
- package/src/main.ts +8 -5
- package/templates/default/apps/server/main.ts +1 -1
- package/templates/default/package.json +5 -3
- package/templates/default/tests/example.test.ts +58 -0
- package/templates/default/tests/helpers.ts +52 -0
- package/templates/default/tests/setup.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# @mandujs/cli
|
|
2
|
+
|
|
3
|
+
Agent-Native Fullstack Framework CLI - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Bun 필수
|
|
9
|
+
bun add -D @mandujs/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 빠른 시작
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# 새 프로젝트 생성
|
|
16
|
+
bunx @mandujs/cli init my-app
|
|
17
|
+
cd my-app
|
|
18
|
+
|
|
19
|
+
# 개발 서버 시작
|
|
20
|
+
bun run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 명령어
|
|
24
|
+
|
|
25
|
+
### `mandu init <project-name>`
|
|
26
|
+
|
|
27
|
+
새 Mandu 프로젝트를 생성합니다.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bunx @mandujs/cli init my-app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
생성되는 구조:
|
|
34
|
+
```
|
|
35
|
+
my-app/
|
|
36
|
+
├── apps/
|
|
37
|
+
│ ├── server/main.ts # 서버 진입점
|
|
38
|
+
│ └── web/entry.tsx # 클라이언트 진입점
|
|
39
|
+
├── spec/
|
|
40
|
+
│ └── routes.manifest.json # SSOT - 라우트 정의
|
|
41
|
+
├── tests/ # 테스트 템플릿
|
|
42
|
+
├── package.json
|
|
43
|
+
└── tsconfig.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `mandu dev`
|
|
47
|
+
|
|
48
|
+
개발 서버를 시작합니다 (HMR 지원).
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun run dev
|
|
52
|
+
# 또는
|
|
53
|
+
bunx mandu dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `mandu spec`
|
|
57
|
+
|
|
58
|
+
spec 파일을 검증하고 lock 파일을 갱신합니다.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bun run spec
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `mandu generate`
|
|
65
|
+
|
|
66
|
+
spec 기반으로 코드를 생성합니다.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bun run generate
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `mandu guard`
|
|
73
|
+
|
|
74
|
+
아키텍처 규칙을 검사하고 위반 사항을 자동 수정합니다.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun run guard
|
|
78
|
+
|
|
79
|
+
# 자동 수정 비활성화
|
|
80
|
+
bunx mandu guard --no-auto-correct
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
자동 수정 가능한 규칙:
|
|
84
|
+
- `SPEC_HASH_MISMATCH` → lock 파일 갱신
|
|
85
|
+
- `GENERATED_MANUAL_EDIT` → 코드 재생성
|
|
86
|
+
- `SLOT_NOT_FOUND` → slot 파일 생성
|
|
87
|
+
|
|
88
|
+
## Spec 파일 작성
|
|
89
|
+
|
|
90
|
+
`spec/routes.manifest.json`이 모든 라우트의 단일 진실 공급원(SSOT)입니다.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"version": "1.0.0",
|
|
95
|
+
"routes": [
|
|
96
|
+
{
|
|
97
|
+
"id": "getUsers",
|
|
98
|
+
"pattern": "/api/users",
|
|
99
|
+
"kind": "api",
|
|
100
|
+
"module": "apps/server/api/users.ts"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "homePage",
|
|
104
|
+
"pattern": "/",
|
|
105
|
+
"kind": "page",
|
|
106
|
+
"module": "apps/server/pages/home.ts",
|
|
107
|
+
"componentModule": "apps/web/pages/Home.tsx"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Slot 시스템 (v0.2.0+)
|
|
114
|
+
|
|
115
|
+
비즈니스 로직을 분리하려면 `slotModule`을 추가합니다:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"id": "getUsers",
|
|
120
|
+
"pattern": "/api/users",
|
|
121
|
+
"kind": "api",
|
|
122
|
+
"module": "apps/server/api/users.generated.ts",
|
|
123
|
+
"slotModule": "apps/server/api/users.slot.ts"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- `*.generated.ts` - 프레임워크가 관리 (수정 금지)
|
|
128
|
+
- `*.slot.ts` - 개발자가 작성하는 비즈니스 로직
|
|
129
|
+
|
|
130
|
+
## 개발 워크플로우
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# 1. spec 수정
|
|
134
|
+
# 2. spec 검증 및 lock 갱신
|
|
135
|
+
bun run spec
|
|
136
|
+
|
|
137
|
+
# 3. 코드 생성
|
|
138
|
+
bun run generate
|
|
139
|
+
|
|
140
|
+
# 4. 아키텍처 검사
|
|
141
|
+
bun run guard
|
|
142
|
+
|
|
143
|
+
# 5. 테스트
|
|
144
|
+
bun test
|
|
145
|
+
|
|
146
|
+
# 6. 개발 서버
|
|
147
|
+
bun run dev
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 테스트
|
|
151
|
+
|
|
152
|
+
Bun 테스트 프레임워크를 기본 지원합니다.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bun test # 테스트 실행
|
|
156
|
+
bun test --watch # 감시 모드
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 요구 사항
|
|
160
|
+
|
|
161
|
+
- Bun >= 1.0.0
|
|
162
|
+
- React >= 18.0.0
|
|
163
|
+
|
|
164
|
+
## 관련 패키지
|
|
165
|
+
|
|
166
|
+
- [@mandujs/core](https://www.npmjs.com/package/@mandujs/core) - 핵심 런타임
|
|
167
|
+
|
|
168
|
+
## 라이선스
|
|
169
|
+
|
|
170
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.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.
|
|
35
|
+
"@mandujs/core": "^0.2.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"bun": ">=1.0.0"
|
package/src/commands/dev.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@
|
|
1
|
+
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandujs/core";
|
|
2
2
|
import { resolveFromCwd } from "../util/fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@
|
|
1
|
+
import { loadManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@mandujs/core";
|
|
2
2
|
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
3
3
|
|
|
4
4
|
export async function generateApply(): Promise<boolean> {
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
loadManifest,
|
|
3
|
+
runGuardCheck,
|
|
4
|
+
buildGuardReport,
|
|
5
|
+
printReportSummary,
|
|
6
|
+
writeReport,
|
|
7
|
+
runAutoCorrect,
|
|
8
|
+
isAutoCorrectableViolation,
|
|
9
|
+
} from "@mandujs/core";
|
|
2
10
|
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
3
11
|
|
|
4
|
-
export
|
|
12
|
+
export interface GuardCheckOptions {
|
|
13
|
+
autoCorrect?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function guardCheck(options: GuardCheckOptions = {}): Promise<boolean> {
|
|
17
|
+
const { autoCorrect = true } = options;
|
|
18
|
+
|
|
5
19
|
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
6
20
|
const rootDir = getRootDir();
|
|
7
21
|
|
|
8
22
|
console.log(`🥟 Mandu Guard`);
|
|
9
|
-
console.log(`📄 Spec 파일: ${specPath}
|
|
23
|
+
console.log(`📄 Spec 파일: ${specPath}`);
|
|
24
|
+
console.log(`🔧 Auto-correct: ${autoCorrect ? "ON" : "OFF"}\n`);
|
|
10
25
|
|
|
11
26
|
const result = await loadManifest(specPath);
|
|
12
27
|
|
|
@@ -19,7 +34,55 @@ export async function guardCheck(): Promise<boolean> {
|
|
|
19
34
|
console.log(`✅ Spec 로드 완료`);
|
|
20
35
|
console.log(`🔍 Guard 검사 중...\n`);
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
let checkResult = await runGuardCheck(result.data, rootDir);
|
|
38
|
+
|
|
39
|
+
// Auto-correct 시도
|
|
40
|
+
if (!checkResult.passed && autoCorrect) {
|
|
41
|
+
const autoCorrectableCount = checkResult.violations.filter(isAutoCorrectableViolation).length;
|
|
42
|
+
|
|
43
|
+
if (autoCorrectableCount > 0) {
|
|
44
|
+
console.log(`⚠️ ${checkResult.violations.length}개 위반 감지 (자동 수정 가능: ${autoCorrectableCount}개)`);
|
|
45
|
+
console.log(`🔄 Auto-correct 실행 중...\n`);
|
|
46
|
+
|
|
47
|
+
const autoCorrectResult = await runAutoCorrect(
|
|
48
|
+
checkResult.violations,
|
|
49
|
+
result.data,
|
|
50
|
+
rootDir
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 수행된 단계 출력
|
|
54
|
+
for (const step of autoCorrectResult.steps) {
|
|
55
|
+
const icon = step.success ? "✅" : "❌";
|
|
56
|
+
console.log(` ${icon} [${step.action}] ${step.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (autoCorrectResult.fixed) {
|
|
60
|
+
console.log(`\n✅ Auto-correct 완료 (${autoCorrectResult.retriedCount}회 재시도)`);
|
|
61
|
+
|
|
62
|
+
// 최종 Guard 재검사
|
|
63
|
+
checkResult = await runGuardCheck(result.data, rootDir);
|
|
64
|
+
} else {
|
|
65
|
+
console.log(`\n⚠️ 일부 위반은 수동 수정이 필요합니다:`);
|
|
66
|
+
|
|
67
|
+
const manualViolations = autoCorrectResult.remainingViolations.filter(
|
|
68
|
+
(v) => !isAutoCorrectableViolation(v)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const v of manualViolations) {
|
|
72
|
+
console.log(` - [${v.ruleId}] ${v.file}`);
|
|
73
|
+
console.log(` 💡 ${v.suggestion}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 남은 위반으로 업데이트
|
|
77
|
+
checkResult = {
|
|
78
|
+
passed: autoCorrectResult.remainingViolations.length === 0,
|
|
79
|
+
violations: autoCorrectResult.remainingViolations,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
23
86
|
|
|
24
87
|
const report = buildGuardReport(checkResult);
|
|
25
88
|
printReportSummary(report);
|
|
@@ -29,11 +92,11 @@ export async function guardCheck(): Promise<boolean> {
|
|
|
29
92
|
console.log(`📋 Report 저장: ${reportPath}`);
|
|
30
93
|
|
|
31
94
|
if (!checkResult.passed) {
|
|
32
|
-
console.log(`\n❌
|
|
95
|
+
console.log(`\n❌ Guard 실패: ${checkResult.violations.length}개 위반 발견`);
|
|
33
96
|
return false;
|
|
34
97
|
}
|
|
35
98
|
|
|
36
|
-
console.log(`\n✅
|
|
99
|
+
console.log(`\n✅ Guard 통과`);
|
|
37
100
|
console.log(`💡 다음 단계: bunx mandu dev`);
|
|
38
101
|
|
|
39
102
|
return true;
|
package/src/main.ts
CHANGED
|
@@ -19,10 +19,11 @@ Commands:
|
|
|
19
19
|
dev 개발 서버 실행
|
|
20
20
|
|
|
21
21
|
Options:
|
|
22
|
-
--name <name>
|
|
23
|
-
--file <path>
|
|
24
|
-
--port <port>
|
|
25
|
-
--
|
|
22
|
+
--name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
|
|
23
|
+
--file <path> spec-upsert 시 사용할 spec 파일 경로
|
|
24
|
+
--port <port> dev 서버 포트 (기본: 3000)
|
|
25
|
+
--no-auto-correct guard 시 자동 수정 비활성화
|
|
26
|
+
--help, -h 도움말 표시
|
|
26
27
|
|
|
27
28
|
Examples:
|
|
28
29
|
bunx mandu init --name my-app
|
|
@@ -83,7 +84,9 @@ async function main(): Promise<void> {
|
|
|
83
84
|
break;
|
|
84
85
|
|
|
85
86
|
case "guard":
|
|
86
|
-
success = await guardCheck(
|
|
87
|
+
success = await guardCheck({
|
|
88
|
+
autoCorrect: options["no-auto-correct"] !== "true",
|
|
89
|
+
});
|
|
87
90
|
break;
|
|
88
91
|
|
|
89
92
|
case "dev":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@
|
|
1
|
+
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandujs/core";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
const SPEC_PATH = path.resolve(import.meta.dir, "../../spec/routes.manifest.json");
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
"dev": "mandu dev",
|
|
7
7
|
"generate": "mandu generate",
|
|
8
8
|
"guard": "mandu guard",
|
|
9
|
-
"spec": "mandu spec-upsert"
|
|
9
|
+
"spec": "mandu spec-upsert",
|
|
10
|
+
"test": "bun test",
|
|
11
|
+
"test:watch": "bun test --watch"
|
|
10
12
|
},
|
|
11
13
|
"dependencies": {
|
|
12
|
-
"@mandujs/core": "^0.
|
|
14
|
+
"@mandujs/core": "^0.2.0",
|
|
13
15
|
"react": "^18.2.0",
|
|
14
16
|
"react-dom": "^18.2.0"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
|
-
"@mandujs/cli": "^0.
|
|
19
|
+
"@mandujs/cli": "^0.2.0",
|
|
18
20
|
"@types/react": "^18.2.0",
|
|
19
21
|
"@types/react-dom": "^18.2.0",
|
|
20
22
|
"typescript": "^5.0.0"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Mandu Example Test
|
|
2
|
+
// 이 파일은 테스트 작성 방법을 보여주는 예제입니다.
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect } from "bun:test";
|
|
5
|
+
import { createTestRequest, parseJsonResponse, assertStatus } from "./helpers";
|
|
6
|
+
|
|
7
|
+
describe("Example Tests", () => {
|
|
8
|
+
describe("Basic Assertions", () => {
|
|
9
|
+
it("should pass basic equality test", () => {
|
|
10
|
+
expect(1 + 1).toBe(2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should pass object equality test", () => {
|
|
14
|
+
const obj = { status: "ok", data: { message: "hello" } };
|
|
15
|
+
expect(obj).toEqual({
|
|
16
|
+
status: "ok",
|
|
17
|
+
data: { message: "hello" },
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Test Helpers", () => {
|
|
23
|
+
it("should create test request", () => {
|
|
24
|
+
const req = createTestRequest("http://localhost:3000/api/test", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: { name: "test" },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(req.method).toBe("POST");
|
|
30
|
+
expect(req.url).toBe("http://localhost:3000/api/test");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should parse JSON response", async () => {
|
|
34
|
+
const mockResponse = new Response(
|
|
35
|
+
JSON.stringify({ status: "ok" }),
|
|
36
|
+
{ status: 200 }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const data = await parseJsonResponse<{ status: string }>(mockResponse);
|
|
40
|
+
expect(data.status).toBe("ok");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// API 핸들러 테스트 예제 (실제 핸들러 import 후 사용)
|
|
46
|
+
// import handler from "../apps/server/generated/routes/health.route";
|
|
47
|
+
//
|
|
48
|
+
// describe("API: GET /api/health", () => {
|
|
49
|
+
// it("should return 200 with status ok", async () => {
|
|
50
|
+
// const req = createTestRequest("http://localhost:3000/api/health");
|
|
51
|
+
// const response = handler(req, {});
|
|
52
|
+
//
|
|
53
|
+
// assertStatus(response, 200);
|
|
54
|
+
//
|
|
55
|
+
// const data = await parseJsonResponse<{ status: string }>(response);
|
|
56
|
+
// expect(data.status).toBe("ok");
|
|
57
|
+
// });
|
|
58
|
+
// });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Mandu Test Helpers
|
|
2
|
+
// 테스트에서 사용할 유틸리티 함수들
|
|
3
|
+
|
|
4
|
+
import type { Request } from "bun";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API 핸들러 테스트용 Request 생성
|
|
8
|
+
*/
|
|
9
|
+
export function createTestRequest(
|
|
10
|
+
url: string,
|
|
11
|
+
options?: {
|
|
12
|
+
method?: string;
|
|
13
|
+
body?: unknown;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
): Request {
|
|
17
|
+
const { method = "GET", body, headers = {} } = options || {};
|
|
18
|
+
|
|
19
|
+
return new Request(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
...headers,
|
|
24
|
+
},
|
|
25
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Response를 JSON으로 파싱
|
|
31
|
+
*/
|
|
32
|
+
export async function parseJsonResponse<T = unknown>(response: Response): Promise<T> {
|
|
33
|
+
return response.json() as Promise<T>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Response 상태 검증
|
|
38
|
+
*/
|
|
39
|
+
export function assertStatus(response: Response, expectedStatus: number): void {
|
|
40
|
+
if (response.status !== expectedStatus) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Expected status ${expectedStatus}, got ${response.status}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 테스트용 라우트 파라미터 생성
|
|
49
|
+
*/
|
|
50
|
+
export function createParams(params: Record<string, string>): Record<string, string> {
|
|
51
|
+
return params;
|
|
52
|
+
}
|