@mandujs/cli 0.9.22 → 0.9.24

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 (40) hide show
  1. package/README.ko.md +57 -4
  2. package/README.md +47 -15
  3. package/package.json +1 -1
  4. package/src/commands/check.ts +41 -5
  5. package/src/commands/contract.ts +135 -9
  6. package/src/commands/dev.ts +155 -95
  7. package/src/commands/guard-arch.ts +39 -9
  8. package/src/commands/guard-check.ts +3 -3
  9. package/src/commands/init.ts +264 -9
  10. package/src/commands/monitor.ts +21 -0
  11. package/src/main.ts +386 -344
  12. package/templates/default/app/globals.css +37 -0
  13. package/templates/default/app/layout.tsx +27 -0
  14. package/templates/default/app/page.tsx +27 -49
  15. package/templates/default/package.json +15 -6
  16. package/templates/default/postcss.config.js +6 -0
  17. package/templates/default/src/client/app/index.ts +1 -0
  18. package/templates/default/src/client/entities/index.ts +1 -0
  19. package/templates/default/src/client/features/index.ts +1 -0
  20. package/templates/default/src/client/pages/index.ts +1 -0
  21. package/templates/default/src/client/shared/index.ts +1 -0
  22. package/templates/default/src/client/shared/lib/utils.ts +16 -0
  23. package/templates/default/src/client/shared/ui/button.tsx +57 -0
  24. package/templates/default/src/client/shared/ui/card.tsx +78 -0
  25. package/templates/default/src/client/shared/ui/index.ts +21 -0
  26. package/templates/default/src/client/shared/ui/input.tsx +24 -0
  27. package/templates/default/src/client/widgets/index.ts +1 -0
  28. package/templates/default/src/server/api/index.ts +1 -0
  29. package/templates/default/src/server/application/index.ts +1 -0
  30. package/templates/default/src/server/core/index.ts +1 -0
  31. package/templates/default/src/server/domain/index.ts +1 -0
  32. package/templates/default/src/server/infra/index.ts +1 -0
  33. package/templates/default/src/shared/contracts/index.ts +1 -0
  34. package/templates/default/src/shared/env/index.ts +1 -0
  35. package/templates/default/src/shared/schema/index.ts +1 -0
  36. package/templates/default/src/shared/types/index.ts +1 -0
  37. package/templates/default/src/shared/utils/client/index.ts +1 -0
  38. package/templates/default/src/shared/utils/server/index.ts +1 -0
  39. package/templates/default/tailwind.config.ts +64 -0
  40. package/templates/default/tsconfig.json +14 -3
package/README.ko.md CHANGED
@@ -44,12 +44,35 @@ bunx @mandujs/cli init my-app
44
44
  생성되는 구조:
45
45
  ```
46
46
  my-app/
47
+ ├── app/ # FS Routes
48
+ │ └── page.tsx # /
49
+ ├── src/ # 아키텍처 레이어
50
+ │ ├── client/ # 클라이언트 (FSD)
51
+ │ │ ├── app/
52
+ │ │ ├── pages/
53
+ │ │ ├── widgets/
54
+ │ │ ├── features/
55
+ │ │ ├── entities/
56
+ │ │ └── shared/
57
+ │ ├── server/ # 서버 (Clean)
58
+ │ │ ├── api/
59
+ │ │ ├── application/
60
+ │ │ ├── domain/
61
+ │ │ ├── infra/
62
+ │ │ └── core/
63
+ │ └── shared/ # 공용
64
+ │ ├── contracts/ # client-safe 계약
65
+ │ ├── types/
66
+ │ ├── utils/
67
+ │ │ ├── client/ # 클라이언트 safe 유틸
68
+ │ │ └── server/ # 서버 전용 유틸
69
+ │ ├── schema/ # 서버 전용 스키마
70
+ │ └── env/ # 서버 전용 환경
47
71
  ├── apps/
48
72
  │ ├── server/main.ts # 서버 진입점
49
73
  │ └── web/entry.tsx # 클라이언트 진입점
50
74
  ├── spec/
51
75
  │ └── routes.manifest.json # SSOT - 라우트 정의
52
- ├── tests/ # 테스트 템플릿
53
76
  ├── package.json
54
77
  └── tsconfig.json
55
78
  ```
@@ -82,20 +105,50 @@ bun run generate
82
105
 
83
106
  ### `mandu guard`
84
107
 
85
- 아키텍처 규칙을 검사하고 위반 사항을 자동 수정합니다.
108
+ 아키텍처 규칙을 검사합니다. (기본: mandu 프리셋)
86
109
 
87
110
  ```bash
88
111
  bun run guard
89
112
 
113
+ # 프리셋 변경
114
+ bunx mandu guard --preset fsd
115
+
116
+ # CI 모드 (warning도 실패 처리)
117
+ bunx mandu guard --ci
118
+ ```
119
+
120
+ ### `mandu guard legacy`
121
+
122
+ 레거시 Spec Guard 검사 + 자동 수정입니다.
123
+
124
+ ```bash
125
+ bunx mandu guard legacy
126
+
90
127
  # 자동 수정 비활성화
91
- bunx mandu guard --no-auto-correct
128
+ bunx mandu guard legacy --no-auto-correct
92
129
  ```
93
130
 
94
- 자동 수정 가능한 규칙:
131
+ 자동 수정 가능한 규칙(legacy):
95
132
  - `SPEC_HASH_MISMATCH` → lock 파일 갱신
96
133
  - `GENERATED_MANUAL_EDIT` → 코드 재생성
97
134
  - `SLOT_NOT_FOUND` → slot 파일 생성
98
135
 
136
+ ### `mandu contract build`
137
+
138
+ 계약 레지스트리(`.mandu/contracts.json`)를 생성합니다.
139
+
140
+ ```bash
141
+ bunx mandu contract build
142
+ ```
143
+
144
+ ### `mandu contract diff`
145
+
146
+ 계약 변경사항(major/minor/patch)을 비교합니다.
147
+
148
+ ```bash
149
+ bunx mandu contract diff
150
+ ```
151
+
99
152
  ## Spec 파일 작성
100
153
 
101
154
  `spec/routes.manifest.json`이 모든 라우트의 단일 진실 공급원(SSOT)입니다.
package/README.md CHANGED
@@ -61,6 +61,34 @@ app/
61
61
  bunx mandu build
62
62
  ```
63
63
 
64
+ ### Default Architecture Layout
65
+
66
+ ```
67
+ app/ # FS Routes
68
+ src/
69
+ client/ # Client (FSD)
70
+ app/
71
+ pages/
72
+ widgets/
73
+ features/
74
+ entities/
75
+ shared/
76
+ server/ # Server (Clean)
77
+ api/
78
+ application/
79
+ domain/
80
+ infra/
81
+ core/
82
+ shared/ # Universal shared
83
+ contracts/ # Client-safe contracts
84
+ types/
85
+ utils/
86
+ client/ # Client-safe utils
87
+ server/ # Server-only utils
88
+ schema/ # Server-only schema
89
+ env/ # Server-only env
90
+ ```
91
+
64
92
  That's it!
65
93
 
66
94
  ---
@@ -87,11 +115,12 @@ That's it!
87
115
 
88
116
  | Command | Description |
89
117
  |---------|-------------|
90
- | `mandu guard arch` | Run architecture check (default: mandu preset) |
91
- | `mandu guard arch --watch` | Watch mode |
92
- | `mandu guard arch --ci` | CI mode (exit 1 on errors) |
93
- | `mandu guard arch --preset fsd` | Use specific preset |
94
- | `mandu guard arch --output report.md` | Generate report |
118
+ | `mandu guard` | Run architecture check (default: mandu preset) |
119
+ | `mandu guard --watch` | Watch mode |
120
+ | `mandu guard --ci` | CI mode (exit 1 on errors/warnings) |
121
+ | `mandu guard --preset fsd` | Use specific preset |
122
+ | `mandu guard --output report.md` | Generate report |
123
+ | `mandu guard legacy` | Legacy Spec guard (auto-correct) |
95
124
 
96
125
  ### Transaction Commands
97
126
 
@@ -119,6 +148,8 @@ That's it!
119
148
  |---------|-------------|
120
149
  | `mandu contract create <routeId>` | Create contract for route |
121
150
  | `mandu contract validate` | Validate contract-slot consistency |
151
+ | `mandu contract build` | Build contract registry |
152
+ | `mandu contract diff` | Diff contracts against registry |
122
153
  | `mandu openapi generate` | Generate OpenAPI 3.0 spec |
123
154
  | `mandu openapi serve` | Start Swagger UI server |
124
155
 
@@ -149,7 +180,7 @@ bun run dev
149
180
  bunx mandu dev --guard
150
181
 
151
182
  # Or run Guard separately
152
- bunx mandu guard arch --watch
183
+ bunx mandu guard --watch
153
184
  ```
154
185
 
155
186
  ### CI/CD Integration
@@ -157,7 +188,7 @@ bunx mandu guard arch --watch
157
188
  ```bash
158
189
  # Build and check
159
190
  bunx mandu build --minify
160
- bunx mandu guard arch --ci --format json
191
+ bunx mandu guard --ci --format json
161
192
  ```
162
193
 
163
194
  ---
@@ -209,10 +240,10 @@ app/
209
240
 
210
241
  ```bash
211
242
  # List all presets
212
- bunx mandu guard arch --list-presets
243
+ bunx mandu guard --list-presets
213
244
 
214
245
  # Use specific preset
215
- bunx mandu guard arch --preset fsd
246
+ bunx mandu guard --preset fsd
216
247
  ```
217
248
 
218
249
  ---
@@ -235,13 +266,13 @@ bunx mandu guard arch --preset fsd
235
266
  | `--sourcemap` | Generate sourcemaps |
236
267
  | `--watch` | Watch mode |
237
268
 
238
- ### `mandu guard arch`
269
+ ### `mandu guard`
239
270
 
240
271
  | Option | Description |
241
272
  |--------|-------------|
242
273
  | `--preset <p>` | Preset: fsd, clean, hexagonal, atomic, mandu |
243
274
  | `--watch` | Watch mode |
244
- | `--ci` | CI mode (exit 1 on errors) |
275
+ | `--ci` | CI mode (exit 1 on errors/warnings) |
245
276
  | `--quiet` | Summary only |
246
277
  | `--format <f>` | Output: console, agent, json |
247
278
  | `--output <path>` | Report file path |
@@ -284,10 +315,11 @@ bunx mandu routes list
284
315
  bunx mandu routes generate
285
316
 
286
317
  # Guard
287
- bunx mandu guard arch
288
- bunx mandu guard arch --watch
289
- bunx mandu guard arch --ci --format json
290
- bunx mandu guard arch --output report.md
318
+ bunx mandu guard
319
+ bunx mandu guard --watch
320
+ bunx mandu guard --ci --format json
321
+ bunx mandu guard --output report.md
322
+ bunx mandu guard legacy
291
323
 
292
324
  # Transactions
293
325
  bunx mandu change begin --message "Add users API"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.9.22",
3
+ "version": "0.9.24",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -34,6 +34,7 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
34
34
  const preset = options.preset ?? "mandu";
35
35
  const format = resolveOutputFormat(options.format);
36
36
  const quiet = options.quiet === true;
37
+ const strictWarnings = options.ci === true;
37
38
  const enableFsRoutes = !options.legacy && await isDirectory(path.resolve(rootDir, "app"));
38
39
  const specPath = resolveFromCwd("spec/routes.manifest.json");
39
40
  const hasSpec = await pathExists(specPath);
@@ -122,14 +123,45 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
122
123
  fsRoutes: enableFsRoutes
123
124
  ? {
124
125
  noPageToPage: true,
125
- pageCanImport: ["widgets", "features", "entities", "shared"],
126
- layoutCanImport: ["widgets", "shared"],
126
+ pageCanImport: [
127
+ "client/pages",
128
+ "client/widgets",
129
+ "client/features",
130
+ "client/entities",
131
+ "client/shared",
132
+ "shared/contracts",
133
+ "shared/types",
134
+ "shared/utils/client",
135
+ ],
136
+ layoutCanImport: [
137
+ "client/app",
138
+ "client/widgets",
139
+ "client/shared",
140
+ "shared/contracts",
141
+ "shared/types",
142
+ "shared/utils/client",
143
+ ],
144
+ routeCanImport: [
145
+ "server/api",
146
+ "server/application",
147
+ "server/domain",
148
+ "server/infra",
149
+ "server/core",
150
+ "shared/contracts",
151
+ "shared/schema",
152
+ "shared/types",
153
+ "shared/utils/client",
154
+ "shared/utils/server",
155
+ "shared/env",
156
+ ],
127
157
  }
128
158
  : undefined,
129
159
  };
130
160
 
131
161
  const report = await checkDirectory(guardConfig, rootDir);
132
- if (report.bySeverity.error > 0) {
162
+ const hasArchErrors = report.bySeverity.error > 0;
163
+ const hasArchWarnings = report.bySeverity.warn > 0;
164
+ if (hasArchErrors || (strictWarnings && hasArchWarnings)) {
133
165
  success = false;
134
166
  }
135
167
 
@@ -166,7 +198,11 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
166
198
  const checkResult = await runGuardCheck(manifestResult.data, rootDir);
167
199
  legacySummary.passed = checkResult.passed;
168
200
  legacySummary.violations = checkResult.violations.length;
169
- success = success && checkResult.passed;
201
+ if (strictWarnings && checkResult.violations.length > 0) {
202
+ success = false;
203
+ } else {
204
+ success = success && checkResult.passed;
205
+ }
170
206
 
171
207
  if (format === "console") {
172
208
  const legacyReport = buildGuardReport(checkResult);
@@ -200,5 +236,5 @@ export async function check(options: CheckOptions = {}): Promise<boolean> {
200
236
  console.log(JSON.stringify(summary, null, 2));
201
237
  }
202
238
 
203
- return options.ci ? success : true;
239
+ return success;
204
240
  }
@@ -3,17 +3,36 @@
3
3
  * Contract 생성 및 검증 명령어
4
4
  */
5
5
 
6
- import { loadManifest, runContractGuardCheck, generateContractTemplate } from "@mandujs/core";
7
- import path from "path";
8
- import fs from "fs/promises";
6
+ import {
7
+ loadManifest,
8
+ runContractGuardCheck,
9
+ generateContractTemplate,
10
+ buildContractRegistry,
11
+ writeContractRegistry,
12
+ readContractRegistry,
13
+ diffContractRegistry,
14
+ } from "@mandujs/core";
15
+ import path from "path";
16
+ import fs from "fs/promises";
9
17
 
10
18
  interface ContractCreateOptions {
11
19
  routeId: string;
12
20
  }
13
21
 
14
- interface ContractValidateOptions {
15
- verbose?: boolean;
16
- }
22
+ interface ContractValidateOptions {
23
+ verbose?: boolean;
24
+ }
25
+
26
+ interface ContractBuildOptions {
27
+ output?: string;
28
+ }
29
+
30
+ interface ContractDiffOptions {
31
+ from?: string;
32
+ to?: string;
33
+ output?: string;
34
+ json?: boolean;
35
+ }
17
36
 
18
37
  /**
19
38
  * Create a new contract file for a route
@@ -85,7 +104,7 @@ export async function contractCreate(options: ContractCreateOptions): Promise<bo
85
104
  /**
86
105
  * Validate all contracts against their slot implementations
87
106
  */
88
- export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
107
+ export async function contractValidate(options: ContractValidateOptions = {}): Promise<boolean> {
89
108
  const rootDir = process.cwd();
90
109
  const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
91
110
 
@@ -148,5 +167,112 @@ export async function contractValidate(options: ContractValidateOptions = {}): P
148
167
  console.log(`💡 Use --verbose for fix suggestions\n`);
149
168
  }
150
169
 
151
- return false;
152
- }
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Build contract registry (.mandu/contracts.json)
175
+ */
176
+ export async function contractBuild(options: ContractBuildOptions = {}): Promise<boolean> {
177
+ const rootDir = process.cwd();
178
+ const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
179
+ const outputPath = options.output || path.join(rootDir, ".mandu", "contracts.json");
180
+
181
+ console.log(`\n📦 Building contract registry...\n`);
182
+
183
+ const manifestResult = await loadManifest(manifestPath);
184
+ if (!manifestResult.success) {
185
+ console.error("❌ Failed to load manifest:", manifestResult.errors);
186
+ return false;
187
+ }
188
+
189
+ const manifest = manifestResult.data!;
190
+ const { registry, warnings } = await buildContractRegistry(manifest, rootDir);
191
+
192
+ if (warnings.length > 0) {
193
+ console.log(`⚠️ ${warnings.length} warning(s):`);
194
+ for (const warning of warnings) {
195
+ console.log(` - ${warning}`);
196
+ }
197
+ console.log();
198
+ }
199
+
200
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
201
+ await writeContractRegistry(outputPath, registry);
202
+
203
+ console.log(`✅ Registry generated: ${path.relative(rootDir, outputPath)}`);
204
+ console.log(`📊 Contracts: ${registry.contracts.length}`);
205
+
206
+ return true;
207
+ }
208
+
209
+ /**
210
+ * Diff current contracts against a registry
211
+ */
212
+ export async function contractDiff(options: ContractDiffOptions = {}): Promise<boolean> {
213
+ const rootDir = process.cwd();
214
+ const manifestPath = path.join(rootDir, "spec/routes.manifest.json");
215
+ const fromPath = options.from || path.join(rootDir, ".mandu", "contracts.json");
216
+
217
+ console.log(`\n🔍 Diffing contracts...\n`);
218
+
219
+ const fromRegistry = await readContractRegistry(fromPath);
220
+ if (!fromRegistry) {
221
+ console.error(`❌ Registry not found: ${path.relative(rootDir, fromPath)}`);
222
+ console.log(`💡 Run \`mandu contract build\` first.`);
223
+ return false;
224
+ }
225
+
226
+ let toRegistry = options.to ? await readContractRegistry(options.to) : null;
227
+
228
+ if (!toRegistry) {
229
+ const manifestResult = await loadManifest(manifestPath);
230
+ if (!manifestResult.success) {
231
+ console.error("❌ Failed to load manifest:", manifestResult.errors);
232
+ return false;
233
+ }
234
+ const { registry } = await buildContractRegistry(manifestResult.data!, rootDir);
235
+ toRegistry = registry;
236
+ }
237
+
238
+ const diff = diffContractRegistry(fromRegistry, toRegistry);
239
+
240
+ if (options.output) {
241
+ await fs.mkdir(path.dirname(options.output), { recursive: true });
242
+ await Bun.write(options.output, JSON.stringify(diff, null, 2));
243
+ console.log(`✅ Diff saved: ${path.relative(rootDir, options.output)}`);
244
+ }
245
+
246
+ if (options.json) {
247
+ console.log(JSON.stringify(diff, null, 2));
248
+ return diff.summary.major === 0;
249
+ }
250
+
251
+ console.log(`📊 Summary: major ${diff.summary.major}, minor ${diff.summary.minor}, patch ${diff.summary.patch}`);
252
+
253
+ if (diff.added.length > 0) {
254
+ console.log(`\n🟢 Added (${diff.added.length})`);
255
+ for (const entry of diff.added) {
256
+ console.log(` - ${entry.id} (${entry.routeId})`);
257
+ }
258
+ }
259
+
260
+ if (diff.removed.length > 0) {
261
+ console.log(`\n🔴 Removed (${diff.removed.length})`);
262
+ for (const entry of diff.removed) {
263
+ console.log(` - ${entry.id} (${entry.routeId})`);
264
+ }
265
+ }
266
+
267
+ if (diff.changed.length > 0) {
268
+ console.log(`\n🟡 Changed (${diff.changed.length})`);
269
+ for (const change of diff.changed) {
270
+ console.log(` - ${change.id} (${change.routeId}) [${change.severity}]`);
271
+ for (const detail of change.changes) {
272
+ console.log(` • ${detail}`);
273
+ }
274
+ }
275
+ }
276
+
277
+ return diff.summary.major === 0;
278
+ }