@mandujs/cli 0.1.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 ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@mandujs/cli",
3
+ "version": "0.1.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.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "bun": ">=1.0.0"
39
+ }
40
+ }
@@ -0,0 +1,60 @@
1
+ import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandu/core";
2
+ import { resolveFromCwd } from "../util/fs";
3
+ import path from "path";
4
+
5
+ export interface DevOptions {
6
+ port?: number;
7
+ }
8
+
9
+ export async function dev(options: DevOptions = {}): Promise<void> {
10
+ const specPath = resolveFromCwd("spec/routes.manifest.json");
11
+ const rootDir = resolveFromCwd(".");
12
+
13
+ console.log(`🥟 Mandu Dev Server`);
14
+ console.log(`📄 Spec 파일: ${specPath}\n`);
15
+
16
+ const result = await loadManifest(specPath);
17
+
18
+ if (!result.success || !result.data) {
19
+ console.error("❌ Spec 로드 실패:");
20
+ result.errors?.forEach((e) => console.error(` - ${e}`));
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(`✅ Spec 로드 완료: ${result.data.routes.length}개 라우트`);
25
+
26
+ for (const route of result.data.routes) {
27
+ if (route.kind === "api") {
28
+ const modulePath = path.resolve(rootDir, route.module);
29
+ try {
30
+ const module = await import(modulePath);
31
+ registerApiHandler(route.id, module.default || module.handler);
32
+ console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
33
+ } catch (error) {
34
+ console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
35
+ }
36
+ } else if (route.kind === "page" && route.componentModule) {
37
+ const componentPath = path.resolve(rootDir, route.componentModule);
38
+ registerPageLoader(route.id, () => import(componentPath));
39
+ console.log(` 📄 Page: ${route.pattern} -> ${route.id}`);
40
+ }
41
+ }
42
+
43
+ console.log("");
44
+
45
+ const port = options.port || Number(process.env.PORT) || 3000;
46
+
47
+ const server = startServer(result.data, { port });
48
+
49
+ process.on("SIGINT", () => {
50
+ console.log("\n🛑 서버 종료 중...");
51
+ server.stop();
52
+ process.exit(0);
53
+ });
54
+
55
+ process.on("SIGTERM", () => {
56
+ console.log("\n🛑 서버 종료 중...");
57
+ server.stop();
58
+ process.exit(0);
59
+ });
60
+ }
@@ -0,0 +1,40 @@
1
+ import { loadManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@mandu/core";
2
+ import { resolveFromCwd, getRootDir } from "../util/fs";
3
+
4
+ export async function generateApply(): Promise<boolean> {
5
+ const specPath = resolveFromCwd("spec/routes.manifest.json");
6
+ const rootDir = getRootDir();
7
+
8
+ console.log(`🥟 Mandu Generate`);
9
+ console.log(`📄 Spec 파일: ${specPath}\n`);
10
+
11
+ const result = await loadManifest(specPath);
12
+
13
+ if (!result.success || !result.data) {
14
+ console.error("❌ Spec 로드 실패:");
15
+ result.errors?.forEach((e) => console.error(` - ${e}`));
16
+ return false;
17
+ }
18
+
19
+ console.log(`✅ Spec 로드 완료 (${result.data.routes.length}개 라우트)`);
20
+ console.log(`🔄 코드 생성 중...\n`);
21
+
22
+ const generateResult = await generateRoutes(result.data, rootDir);
23
+
24
+ const report = buildGenerateReport(generateResult);
25
+ printReportSummary(report);
26
+
27
+ const reportPath = resolveFromCwd("mandu-report.json");
28
+ await writeReport(report, reportPath);
29
+ console.log(`📋 Report 저장: ${reportPath}`);
30
+
31
+ if (!generateResult.success) {
32
+ console.log(`\n❌ generate 실패`);
33
+ return false;
34
+ }
35
+
36
+ console.log(`\n✅ generate 완료`);
37
+ console.log(`💡 다음 단계: bunx mandu guard`);
38
+
39
+ return true;
40
+ }
@@ -0,0 +1,40 @@
1
+ import { loadManifest, runGuardCheck, buildGuardReport, printReportSummary, writeReport } from "@mandu/core";
2
+ import { resolveFromCwd, getRootDir } from "../util/fs";
3
+
4
+ export async function guardCheck(): Promise<boolean> {
5
+ const specPath = resolveFromCwd("spec/routes.manifest.json");
6
+ const rootDir = getRootDir();
7
+
8
+ console.log(`🥟 Mandu Guard`);
9
+ console.log(`📄 Spec 파일: ${specPath}\n`);
10
+
11
+ const result = await loadManifest(specPath);
12
+
13
+ if (!result.success || !result.data) {
14
+ console.error("❌ Spec 로드 실패:");
15
+ result.errors?.forEach((e) => console.error(` - ${e}`));
16
+ return false;
17
+ }
18
+
19
+ console.log(`✅ Spec 로드 완료`);
20
+ console.log(`🔍 Guard 검사 중...\n`);
21
+
22
+ const checkResult = await runGuardCheck(result.data, rootDir);
23
+
24
+ const report = buildGuardReport(checkResult);
25
+ printReportSummary(report);
26
+
27
+ const reportPath = resolveFromCwd("mandu-report.json");
28
+ await writeReport(report, reportPath);
29
+ console.log(`📋 Report 저장: ${reportPath}`);
30
+
31
+ if (!checkResult.passed) {
32
+ console.log(`\n❌ guard 실패: ${checkResult.violations.length}개 위반 발견`);
33
+ return false;
34
+ }
35
+
36
+ console.log(`\n✅ guard 통과`);
37
+ console.log(`💡 다음 단계: bunx mandu dev`);
38
+
39
+ return true;
40
+ }
@@ -0,0 +1,88 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+
4
+ export interface InitOptions {
5
+ name?: string;
6
+ template?: string;
7
+ }
8
+
9
+ async function copyDir(src: string, dest: string, projectName: string): Promise<void> {
10
+ await fs.mkdir(dest, { recursive: true });
11
+ const entries = await fs.readdir(src, { withFileTypes: true });
12
+
13
+ for (const entry of entries) {
14
+ const srcPath = path.join(src, entry.name);
15
+ const destPath = path.join(dest, entry.name);
16
+
17
+ if (entry.isDirectory()) {
18
+ await copyDir(srcPath, destPath, projectName);
19
+ } else {
20
+ let content = await fs.readFile(srcPath, "utf-8");
21
+ // Replace template variables
22
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
23
+ await fs.writeFile(destPath, content);
24
+ }
25
+ }
26
+ }
27
+
28
+ function getTemplatesDir(): string {
29
+ // When installed via npm, templates are in the CLI package
30
+ const commandsDir = import.meta.dir;
31
+ // packages/cli/src/commands -> go up 2 levels to cli package root
32
+ return path.resolve(commandsDir, "../../templates");
33
+ }
34
+
35
+ export async function init(options: InitOptions = {}): Promise<boolean> {
36
+ const projectName = options.name || "my-mandu-app";
37
+ const template = options.template || "default";
38
+ const targetDir = path.resolve(process.cwd(), projectName);
39
+
40
+ console.log(`🥟 Mandu Init`);
41
+ console.log(`📁 프로젝트: ${projectName}`);
42
+ console.log(`📦 템플릿: ${template}\n`);
43
+
44
+ // Check if target directory exists
45
+ try {
46
+ await fs.access(targetDir);
47
+ console.error(`❌ 디렉토리가 이미 존재합니다: ${targetDir}`);
48
+ return false;
49
+ } catch {
50
+ // Directory doesn't exist, good to proceed
51
+ }
52
+
53
+ const templatesDir = getTemplatesDir();
54
+ const templateDir = path.join(templatesDir, template);
55
+
56
+ // Check if template exists
57
+ try {
58
+ await fs.access(templateDir);
59
+ } catch {
60
+ console.error(`❌ 템플릿을 찾을 수 없습니다: ${template}`);
61
+ console.error(` 사용 가능한 템플릿: default`);
62
+ return false;
63
+ }
64
+
65
+ console.log(`📋 템플릿 복사 중...`);
66
+
67
+ try {
68
+ await copyDir(templateDir, targetDir, projectName);
69
+ } catch (error) {
70
+ console.error(`❌ 템플릿 복사 실패:`, error);
71
+ return false;
72
+ }
73
+
74
+ // Create empty generated directories
75
+ await fs.mkdir(path.join(targetDir, "apps/server/generated/routes"), { recursive: true });
76
+ await fs.mkdir(path.join(targetDir, "apps/web/generated/routes"), { recursive: true });
77
+
78
+ console.log(`\n✅ 프로젝트 생성 완료!\n`);
79
+ console.log(`📍 위치: ${targetDir}`);
80
+ console.log(`\n🚀 시작하기:`);
81
+ console.log(` cd ${projectName}`);
82
+ console.log(` bun install`);
83
+ console.log(` bun run spec`);
84
+ console.log(` bun run generate`);
85
+ console.log(` bun run dev`);
86
+
87
+ return true;
88
+ }
@@ -0,0 +1,47 @@
1
+ import { loadManifest, writeLock, readLock } from "@mandu/core";
2
+ import { resolveFromCwd } from "../util/fs";
3
+ import path from "path";
4
+
5
+ export interface SpecUpsertOptions {
6
+ file?: string;
7
+ }
8
+
9
+ export async function specUpsert(options: SpecUpsertOptions): Promise<boolean> {
10
+ const specPath = options.file
11
+ ? resolveFromCwd(options.file)
12
+ : resolveFromCwd("spec/routes.manifest.json");
13
+
14
+ console.log(`🥟 Mandu Spec Upsert`);
15
+ console.log(`📄 Spec 파일: ${specPath}\n`);
16
+
17
+ const result = await loadManifest(specPath);
18
+
19
+ if (!result.success || !result.data) {
20
+ console.error("❌ Spec 검증 실패:");
21
+ result.errors?.forEach((e) => console.error(` - ${e}`));
22
+ return false;
23
+ }
24
+
25
+ console.log(`✅ Spec 검증 통과`);
26
+ console.log(` - 버전: ${result.data.version}`);
27
+ console.log(` - 라우트 수: ${result.data.routes.length}`);
28
+
29
+ for (const route of result.data.routes) {
30
+ const kindIcon = route.kind === "api" ? "📡" : "📄";
31
+ console.log(` ${kindIcon} ${route.id}: ${route.pattern} (${route.kind})`);
32
+ }
33
+
34
+ const lockPath = resolveFromCwd("spec/spec.lock.json");
35
+ const previousLock = await readLock(lockPath);
36
+ const newLock = await writeLock(lockPath, result.data);
37
+
38
+ console.log(`\n🔒 Lock 파일 갱신: ${lockPath}`);
39
+ console.log(` - 이전 해시: ${previousLock?.routesHash?.slice(0, 12) || "(없음)"}...`);
40
+ console.log(` - 새 해시: ${newLock.routesHash.slice(0, 12)}...`);
41
+ console.log(` - 갱신 시간: ${newLock.updatedAt}`);
42
+
43
+ console.log(`\n✅ spec-upsert 완료`);
44
+ console.log(`💡 다음 단계: bunx mandu generate`);
45
+
46
+ return true;
47
+ }
package/src/main.ts ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { specUpsert } from "./commands/spec-upsert";
4
+ import { generateApply } from "./commands/generate-apply";
5
+ import { guardCheck } from "./commands/guard-check";
6
+ import { dev } from "./commands/dev";
7
+ import { init } from "./commands/init";
8
+
9
+ const HELP_TEXT = `
10
+ 🥟 Mandu CLI - Agent-Native Fullstack Framework
11
+
12
+ Usage: bunx mandu <command> [options]
13
+
14
+ Commands:
15
+ init 새 프로젝트 생성
16
+ spec-upsert Spec 파일 검증 및 lock 갱신
17
+ generate Spec에서 코드 생성
18
+ guard Guard 규칙 검사
19
+ dev 개발 서버 실행
20
+
21
+ Options:
22
+ --name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
23
+ --file <path> spec-upsert 시 사용할 spec 파일 경로
24
+ --port <port> dev 서버 포트 (기본: 3000)
25
+ --help, -h 도움말 표시
26
+
27
+ Examples:
28
+ bunx mandu init --name my-app
29
+ bunx mandu spec-upsert
30
+ bunx mandu generate
31
+ bunx mandu guard
32
+ bunx mandu dev --port 3000
33
+
34
+ Workflow:
35
+ 1. init → 2. spec-upsert → 3. generate → 4. guard → 5. dev
36
+ `;
37
+
38
+ function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
39
+ const command = args[0] || "";
40
+ const options: Record<string, string> = {};
41
+
42
+ for (let i = 1; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg.startsWith("--")) {
45
+ const key = arg.slice(2);
46
+ const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
47
+ options[key] = value;
48
+ } else if (arg === "-h") {
49
+ options["help"] = "true";
50
+ } else if (!options._positional) {
51
+ // First non-flag argument after command is positional (e.g., project name)
52
+ options._positional = arg;
53
+ }
54
+ }
55
+
56
+ return { command, options };
57
+ }
58
+
59
+ async function main(): Promise<void> {
60
+ const args = process.argv.slice(2);
61
+ const { command, options } = parseArgs(args);
62
+
63
+ if (options.help || command === "help" || !command) {
64
+ console.log(HELP_TEXT);
65
+ process.exit(0);
66
+ }
67
+
68
+ let success = true;
69
+
70
+ switch (command) {
71
+ case "init":
72
+ success = await init({
73
+ name: options.name || options._positional
74
+ });
75
+ break;
76
+
77
+ case "spec-upsert":
78
+ success = await specUpsert({ file: options.file });
79
+ break;
80
+
81
+ case "generate":
82
+ success = await generateApply();
83
+ break;
84
+
85
+ case "guard":
86
+ success = await guardCheck();
87
+ break;
88
+
89
+ case "dev":
90
+ await dev({ port: options.port ? Number(options.port) : undefined });
91
+ break;
92
+
93
+ default:
94
+ console.error(`❌ Unknown command: ${command}`);
95
+ console.log(HELP_TEXT);
96
+ process.exit(1);
97
+ }
98
+
99
+ if (!success) {
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ main().catch((error) => {
105
+ console.error("❌ 예상치 못한 오류:", error);
106
+ process.exit(1);
107
+ });
package/src/util/fs.ts ADDED
@@ -0,0 +1,9 @@
1
+ import path from "path";
2
+
3
+ export function resolveFromCwd(...paths: string[]): string {
4
+ return path.resolve(process.cwd(), ...paths);
5
+ }
6
+
7
+ export function getRootDir(): string {
8
+ return process.cwd();
9
+ }
@@ -0,0 +1,49 @@
1
+ import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandu/core";
2
+ import path from "path";
3
+
4
+ const SPEC_PATH = path.resolve(import.meta.dir, "../../spec/routes.manifest.json");
5
+
6
+ async function main() {
7
+ console.log("🥟 Mandu Server Starting...\n");
8
+
9
+ const result = await loadManifest(SPEC_PATH);
10
+
11
+ if (!result.success || !result.data) {
12
+ console.error("❌ Spec 로드 실패:");
13
+ result.errors?.forEach((e) => console.error(` - ${e}`));
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(`✅ Spec 로드 완료: ${result.data.routes.length}개 라우트`);
18
+
19
+ for (const route of result.data.routes) {
20
+ if (route.kind === "api") {
21
+ const modulePath = path.resolve(import.meta.dir, "../../", route.module);
22
+ try {
23
+ const module = await import(modulePath);
24
+ registerApiHandler(route.id, module.default || module.handler);
25
+ console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
26
+ } catch (error) {
27
+ console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
28
+ }
29
+ } else if (route.kind === "page") {
30
+ const componentPath = path.resolve(import.meta.dir, "../../", route.componentModule!);
31
+ registerPageLoader(route.id, () => import(componentPath));
32
+ console.log(` 📄 Page: ${route.pattern} -> ${route.id}`);
33
+ }
34
+ }
35
+
36
+ console.log("");
37
+
38
+ const server = startServer(result.data, {
39
+ port: Number(process.env.PORT) || 3000,
40
+ });
41
+
42
+ process.on("SIGINT", () => {
43
+ console.log("\n🛑 서버 종료 중...");
44
+ server.stop();
45
+ process.exit(0);
46
+ });
47
+ }
48
+
49
+ main().catch(console.error);
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import type { ReactElement } from "react";
3
+
4
+ export interface AppContext {
5
+ routeId: string;
6
+ url: string;
7
+ params: Record<string, string>;
8
+ }
9
+
10
+ type RouteComponent = (props: { params: Record<string, string> }) => ReactElement;
11
+
12
+ const routeComponents: Record<string, RouteComponent> = {};
13
+
14
+ export function registerRoute(routeId: string, component: RouteComponent): void {
15
+ routeComponents[routeId] = component;
16
+ }
17
+
18
+ export function createApp(context: AppContext): ReactElement {
19
+ const Component = routeComponents[context.routeId];
20
+
21
+ if (!Component) {
22
+ return (
23
+ <div>
24
+ <h1>404 - Route Not Found</h1>
25
+ <p>Route ID: {context.routeId}</p>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return <Component params={context.params} />;
31
+ }
32
+
33
+ export function getRegisteredRoutes(): string[] {
34
+ return Object.keys(routeComponents);
35
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "mandu dev",
7
+ "generate": "mandu generate",
8
+ "guard": "mandu guard",
9
+ "spec": "mandu spec-upsert"
10
+ },
11
+ "dependencies": {
12
+ "@mandujs/core": "^0.1.0",
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0"
15
+ },
16
+ "devDependencies": {
17
+ "@mandujs/cli": "^0.1.0",
18
+ "@types/react": "^18.2.0",
19
+ "@types/react-dom": "^18.2.0",
20
+ "typescript": "^5.0.0"
21
+ }
22
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 1,
3
+ "routes": [
4
+ {
5
+ "id": "home",
6
+ "pattern": "/",
7
+ "kind": "page",
8
+ "module": "apps/server/generated/routes/home.route.ts",
9
+ "componentModule": "apps/web/generated/routes/home.route.tsx"
10
+ },
11
+ {
12
+ "id": "health",
13
+ "pattern": "/api/health",
14
+ "kind": "api",
15
+ "module": "apps/server/generated/routes/health.route.ts"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "jsx": "react-jsx",
10
+ "types": ["bun-types"]
11
+ },
12
+ "include": ["apps/**/*.ts", "apps/**/*.tsx"],
13
+ "exclude": ["node_modules"]
14
+ }