@mandujs/cli 0.10.0 → 0.12.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
5
5
  "type": "module",
6
6
  "main": "./src/main.ts",
@@ -32,7 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "workspace:*"
35
+ "@mandujs/core": "workspace:*",
36
+ "cfonts": "^3.3.0"
36
37
  },
37
38
  "engines": {
38
39
  "bun": ">=1.0.0"
@@ -0,0 +1,357 @@
1
+ /**
2
+ * DNA-010: Command Registry Pattern
3
+ *
4
+ * 선언적 명령어 등록 시스템
5
+ * - 각 명령어를 독립적으로 정의
6
+ * - 레이지 로딩으로 시작 시간 최적화
7
+ * - 서브커맨드 자동 라우팅
8
+ */
9
+
10
+ import type { CLI_ERROR_CODES } from "../errors";
11
+
12
+ /**
13
+ * 명령어 실행 컨텍스트
14
+ */
15
+ export interface CommandContext {
16
+ args: string[];
17
+ options: Record<string, string>;
18
+ }
19
+
20
+ /**
21
+ * 명령어 등록 정의
22
+ */
23
+ export interface CommandRegistration {
24
+ /** 명령어 ID (예: "dev", "build", "guard") */
25
+ id: string;
26
+ /** 명령어 설명 */
27
+ description: string;
28
+ /** 서브커맨드 목록 (예: guard의 "arch", "legacy") */
29
+ subcommands?: string[];
30
+ /** 기본 서브커맨드 (서브커맨드 없이 호출 시) */
31
+ defaultSubcommand?: string;
32
+ /** 명령어 실행 */
33
+ run: (ctx: CommandContext) => Promise<boolean>;
34
+ }
35
+
36
+ /**
37
+ * 명령어 레지스트리
38
+ */
39
+ export const commandRegistry = new Map<string, CommandRegistration>();
40
+
41
+ /**
42
+ * 명령어 등록
43
+ */
44
+ export function registerCommand(registration: CommandRegistration): void {
45
+ commandRegistry.set(registration.id, registration);
46
+ }
47
+
48
+ /**
49
+ * 명령어 조회
50
+ */
51
+ export function getCommand(id: string): CommandRegistration | undefined {
52
+ return commandRegistry.get(id);
53
+ }
54
+
55
+ /**
56
+ * 모든 명령어 ID 목록
57
+ */
58
+ export function getAllCommands(): string[] {
59
+ return Array.from(commandRegistry.keys());
60
+ }
61
+
62
+ // ============================================================================
63
+ // 명령어 등록 (레이지 로딩)
64
+ // ============================================================================
65
+
66
+ registerCommand({
67
+ id: "init",
68
+ description: "새 프로젝트 생성 (Tailwind + shadcn/ui 기본 포함)",
69
+ async run(ctx) {
70
+ const { init } = await import("./init");
71
+ return init({
72
+ name: ctx.options.name || ctx.options._positional,
73
+ css: ctx.options.css as any,
74
+ ui: ctx.options.ui as any,
75
+ theme: ctx.options.theme === "true",
76
+ minimal: ctx.options.minimal === "true",
77
+ });
78
+ },
79
+ });
80
+
81
+ registerCommand({
82
+ id: "dev",
83
+ description: "개발 서버 실행 (FS Routes + Guard 기본)",
84
+ async run() {
85
+ const { dev } = await import("./dev");
86
+ await dev();
87
+ return true;
88
+ },
89
+ });
90
+
91
+ registerCommand({
92
+ id: "build",
93
+ description: "클라이언트 번들 빌드 (Hydration)",
94
+ async run(ctx) {
95
+ const { build } = await import("./build");
96
+ return build({ watch: ctx.options.watch === "true" });
97
+ },
98
+ });
99
+
100
+ registerCommand({
101
+ id: "check",
102
+ description: "FS Routes + Guard 통합 검사",
103
+ async run() {
104
+ const { check } = await import("./check");
105
+ return check();
106
+ },
107
+ });
108
+
109
+ registerCommand({
110
+ id: "guard",
111
+ description: "아키텍처 위반 검사",
112
+ subcommands: ["arch", "legacy", "spec"],
113
+ defaultSubcommand: "arch",
114
+ async run(ctx) {
115
+ const subCommand = ctx.args[1];
116
+ const hasSubCommand = subCommand && !subCommand.startsWith("--");
117
+
118
+ const guardOptions = {
119
+ watch: ctx.options.watch === "true",
120
+ output: ctx.options.output,
121
+ };
122
+
123
+ switch (subCommand) {
124
+ case "arch": {
125
+ const { guardArch } = await import("./guard-arch");
126
+ return guardArch(guardOptions);
127
+ }
128
+ case "legacy":
129
+ case "spec": {
130
+ const { guardCheck } = await import("./guard-check");
131
+ return guardCheck();
132
+ }
133
+ default:
134
+ if (hasSubCommand) {
135
+ // 알 수 없는 서브커맨드는 main.ts에서 처리
136
+ return false;
137
+ }
138
+ // 기본값: architecture guard
139
+ const { guardArch } = await import("./guard-arch");
140
+ return guardArch(guardOptions);
141
+ }
142
+ },
143
+ });
144
+
145
+ registerCommand({
146
+ id: "routes",
147
+ description: "FS Routes 관리",
148
+ subcommands: ["generate", "list", "watch"],
149
+ defaultSubcommand: "list",
150
+ async run(ctx) {
151
+ const subCommand = ctx.args[1];
152
+ const { routesGenerate, routesList, routesWatch } = await import("./routes");
153
+
154
+ const routesOptions = {
155
+ output: ctx.options.output,
156
+ verbose: ctx.options.verbose === "true",
157
+ };
158
+
159
+ switch (subCommand) {
160
+ case "generate":
161
+ return routesGenerate(routesOptions);
162
+ case "list":
163
+ return routesList({ verbose: routesOptions.verbose });
164
+ case "watch":
165
+ return routesWatch(routesOptions);
166
+ default:
167
+ if (subCommand && !subCommand.startsWith("--")) {
168
+ return false; // 알 수 없는 서브커맨드
169
+ }
170
+ return routesList({ verbose: routesOptions.verbose });
171
+ }
172
+ },
173
+ });
174
+
175
+ registerCommand({
176
+ id: "contract",
177
+ description: "Contract-First API 개발",
178
+ subcommands: ["create", "validate", "build", "diff"],
179
+ async run(ctx) {
180
+ const subCommand = ctx.args[1];
181
+ const {
182
+ contractCreate,
183
+ contractValidate,
184
+ contractBuild,
185
+ contractDiff,
186
+ } = await import("./contract");
187
+
188
+ switch (subCommand) {
189
+ case "create": {
190
+ const routeId = ctx.args[2] || ctx.options._positional;
191
+ if (!routeId) return false;
192
+ return contractCreate({ routeId });
193
+ }
194
+ case "validate":
195
+ return contractValidate({ verbose: ctx.options.verbose === "true" });
196
+ case "build":
197
+ return contractBuild({ output: ctx.options.output });
198
+ case "diff":
199
+ return contractDiff({
200
+ from: ctx.options.from,
201
+ to: ctx.options.to,
202
+ output: ctx.options.output,
203
+ json: ctx.options.json === "true",
204
+ });
205
+ default:
206
+ return false;
207
+ }
208
+ },
209
+ });
210
+
211
+ registerCommand({
212
+ id: "openapi",
213
+ description: "OpenAPI 스펙 생성",
214
+ subcommands: ["generate", "serve"],
215
+ async run(ctx) {
216
+ const subCommand = ctx.args[1];
217
+ const { openAPIGenerate, openAPIServe } = await import("./openapi");
218
+
219
+ switch (subCommand) {
220
+ case "generate":
221
+ return openAPIGenerate({
222
+ output: ctx.options.output,
223
+ title: ctx.options.title,
224
+ version: ctx.options.version,
225
+ });
226
+ case "serve":
227
+ return openAPIServe();
228
+ default:
229
+ return false;
230
+ }
231
+ },
232
+ });
233
+
234
+ registerCommand({
235
+ id: "change",
236
+ description: "변경 트랜잭션 관리",
237
+ subcommands: ["begin", "commit", "rollback", "status", "list", "prune"],
238
+ async run(ctx) {
239
+ const subCommand = ctx.args[1];
240
+ const {
241
+ changeBegin,
242
+ changeCommit,
243
+ changeRollback,
244
+ changeStatus,
245
+ changeList,
246
+ changePrune,
247
+ } = await import("./change");
248
+
249
+ switch (subCommand) {
250
+ case "begin":
251
+ return changeBegin({ message: ctx.options.message });
252
+ case "commit":
253
+ return changeCommit();
254
+ case "rollback":
255
+ return changeRollback({ id: ctx.options.id });
256
+ case "status":
257
+ return changeStatus();
258
+ case "list":
259
+ return changeList();
260
+ case "prune":
261
+ return changePrune({
262
+ keep: ctx.options.keep ? Number(ctx.options.keep) : undefined,
263
+ });
264
+ default:
265
+ return false;
266
+ }
267
+ },
268
+ });
269
+
270
+ registerCommand({
271
+ id: "brain",
272
+ description: "Brain (sLLM) 관리",
273
+ subcommands: ["setup", "status"],
274
+ async run(ctx) {
275
+ const subCommand = ctx.args[1];
276
+ const { brainSetup, brainStatus } = await import("./brain");
277
+
278
+ switch (subCommand) {
279
+ case "setup":
280
+ return brainSetup({
281
+ model: ctx.options.model,
282
+ url: ctx.options.url,
283
+ skipCheck: ctx.options["skip-check"] === "true",
284
+ });
285
+ case "status":
286
+ return brainStatus({ verbose: ctx.options.verbose === "true" });
287
+ default:
288
+ return false;
289
+ }
290
+ },
291
+ });
292
+
293
+ registerCommand({
294
+ id: "doctor",
295
+ description: "Guard 실패 분석 + 패치 제안",
296
+ async run(ctx) {
297
+ const { doctor } = await import("./doctor");
298
+ return doctor({
299
+ useLLM: ctx.options["no-llm"] !== "true",
300
+ output: ctx.options.output,
301
+ });
302
+ },
303
+ });
304
+
305
+ registerCommand({
306
+ id: "watch",
307
+ description: "실시간 파일 감시",
308
+ async run(ctx) {
309
+ const { watch } = await import("./watch");
310
+ return watch({
311
+ status: ctx.options.status === "true",
312
+ debounce: ctx.options.debounce ? Number(ctx.options.debounce) : undefined,
313
+ });
314
+ },
315
+ });
316
+
317
+ registerCommand({
318
+ id: "monitor",
319
+ description: "MCP Activity Monitor",
320
+ async run(ctx) {
321
+ const { monitor } = await import("./monitor");
322
+ return monitor({
323
+ summary: ctx.options.summary === "true",
324
+ since: ctx.options.since,
325
+ follow: ctx.options.follow === "false" ? false : true,
326
+ file: ctx.options.file,
327
+ });
328
+ },
329
+ });
330
+
331
+ registerCommand({
332
+ id: "lock",
333
+ description: "Lockfile 관리",
334
+ async run(ctx) {
335
+ const { runLockCommand } = await import("./lock");
336
+ return runLockCommand(ctx.args.slice(1));
337
+ },
338
+ });
339
+
340
+ // 레거시 명령어
341
+ registerCommand({
342
+ id: "spec-upsert",
343
+ description: "Spec 파일 검증 및 lock 갱신 (레거시)",
344
+ async run(ctx) {
345
+ const { specUpsert } = await import("./spec-upsert");
346
+ return specUpsert({ file: ctx.options.file });
347
+ },
348
+ });
349
+
350
+ registerCommand({
351
+ id: "generate",
352
+ description: "Spec에서 코드 생성 (레거시)",
353
+ async run() {
354
+ const { generateApply } = await import("./generate-apply");
355
+ return generateApply();
356
+ },
357
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * DNA-016: CLI Hooks
3
+ *
4
+ * 명령어 라이프사이클 훅
5
+ */
6
+
7
+ export {
8
+ preActionRegistry,
9
+ runPreAction,
10
+ registerPreActionHook,
11
+ registerDefaultHooks,
12
+ setVerbose,
13
+ isVerbose,
14
+ setProcessTitle,
15
+ type PreActionContext,
16
+ type PreActionHook,
17
+ } from "./preaction.js";
@@ -0,0 +1,256 @@
1
+ /**
2
+ * DNA-016: Pre-Action Hooks
3
+ *
4
+ * 명령어 실행 전 공통 작업 수행
5
+ * - 프로세스 타이틀 설정
6
+ * - 조건부 배너 표시
7
+ * - Verbose 모드 설정
8
+ * - 설정 로드
9
+ */
10
+
11
+ import { shouldShowBanner, renderMiniBanner } from "../terminal/banner.js";
12
+ import { loadManduConfig, type ManduConfig } from "@mandujs/core";
13
+
14
+ /**
15
+ * Pre-Action 컨텍스트
16
+ */
17
+ export interface PreActionContext {
18
+ /** 현재 명령어 */
19
+ command: string;
20
+ /** 서브커맨드 */
21
+ subcommand?: string;
22
+ /** 명령어 옵션 */
23
+ options: Record<string, string>;
24
+ /** 로드된 설정 */
25
+ config?: ManduConfig;
26
+ /** verbose 모드 여부 */
27
+ verbose: boolean;
28
+ /** 작업 디렉토리 */
29
+ cwd: string;
30
+ }
31
+
32
+ /**
33
+ * Pre-Action 훅 타입
34
+ */
35
+ export type PreActionHook = (ctx: PreActionContext) => void | Promise<void>;
36
+
37
+ /**
38
+ * Pre-Action 훅 레지스트리
39
+ */
40
+ class PreActionRegistry {
41
+ private hooks: PreActionHook[] = [];
42
+
43
+ /**
44
+ * 훅 등록
45
+ */
46
+ register(hook: PreActionHook): void {
47
+ this.hooks.push(hook);
48
+ }
49
+
50
+ /**
51
+ * 훅 제거
52
+ */
53
+ unregister(hook: PreActionHook): boolean {
54
+ const index = this.hooks.indexOf(hook);
55
+ if (index >= 0) {
56
+ this.hooks.splice(index, 1);
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * 모든 훅 실행
64
+ */
65
+ async runAll(ctx: PreActionContext): Promise<void> {
66
+ for (const hook of this.hooks) {
67
+ await hook(ctx);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 훅 초기화
73
+ */
74
+ clear(): void {
75
+ this.hooks = [];
76
+ }
77
+
78
+ /**
79
+ * 등록된 훅 수
80
+ */
81
+ get size(): number {
82
+ return this.hooks.length;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 전역 Pre-Action 훅 레지스트리
88
+ */
89
+ export const preActionRegistry = new PreActionRegistry();
90
+
91
+ /**
92
+ * 설정 로드가 필요없는 명령어
93
+ */
94
+ const SKIP_CONFIG_COMMANDS = new Set([
95
+ "init",
96
+ "help",
97
+ "version",
98
+ "completion",
99
+ ]);
100
+
101
+ /**
102
+ * 배너 표시가 필요없는 명령어
103
+ */
104
+ const SKIP_BANNER_COMMANDS = new Set([
105
+ "completion",
106
+ "version",
107
+ ]);
108
+
109
+ /**
110
+ * verbose 전역 상태
111
+ */
112
+ let globalVerbose = false;
113
+
114
+ /**
115
+ * verbose 모드 설정
116
+ */
117
+ export function setVerbose(value: boolean): void {
118
+ globalVerbose = value;
119
+ }
120
+
121
+ /**
122
+ * verbose 모드 확인
123
+ */
124
+ export function isVerbose(): boolean {
125
+ return globalVerbose;
126
+ }
127
+
128
+ /**
129
+ * 프로세스 타이틀 설정
130
+ */
131
+ export function setProcessTitle(command: string, subcommand?: string): void {
132
+ const title = subcommand
133
+ ? `mandu ${command} ${subcommand}`
134
+ : `mandu ${command}`;
135
+
136
+ if (typeof process.title !== "undefined") {
137
+ process.title = title;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * 기본 Pre-Action 실행
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const ctx = await runPreAction({
147
+ * command: "dev",
148
+ * options: { port: "3000" },
149
+ * });
150
+ *
151
+ * // ctx.config 에서 로드된 설정 사용
152
+ * // ctx.verbose 로 verbose 모드 확인
153
+ * ```
154
+ */
155
+ export async function runPreAction(params: {
156
+ command: string;
157
+ subcommand?: string;
158
+ options: Record<string, string>;
159
+ cwd?: string;
160
+ version?: string;
161
+ }): Promise<PreActionContext> {
162
+ const {
163
+ command,
164
+ subcommand,
165
+ options,
166
+ cwd = process.cwd(),
167
+ version,
168
+ } = params;
169
+
170
+ // 1. verbose 모드 확인
171
+ const verbose = options.verbose === "true" || process.env.MANDU_VERBOSE === "true";
172
+ setVerbose(verbose);
173
+
174
+ // 2. 프로세스 타이틀 설정
175
+ setProcessTitle(command, subcommand);
176
+
177
+ // 3. 조건부 배너 표시
178
+ const showBanner =
179
+ !SKIP_BANNER_COMMANDS.has(command) &&
180
+ !isTruthyEnv("MANDU_HIDE_BANNER") &&
181
+ shouldShowBanner();
182
+
183
+ if (showBanner && version) {
184
+ console.log(renderMiniBanner(version));
185
+ console.log();
186
+ }
187
+
188
+ // 4. 설정 로드 (필요한 명령어만)
189
+ let config: ManduConfig | undefined;
190
+ if (!SKIP_CONFIG_COMMANDS.has(command)) {
191
+ try {
192
+ config = await loadManduConfig(cwd);
193
+ } catch {
194
+ // 설정 로드 실패 시 무시 (옵션 설정만 사용)
195
+ if (verbose) {
196
+ console.warn("[mandu] Config load failed, using defaults");
197
+ }
198
+ }
199
+ }
200
+
201
+ // Pre-Action 컨텍스트 생성
202
+ const ctx: PreActionContext = {
203
+ command,
204
+ subcommand,
205
+ options,
206
+ config,
207
+ verbose,
208
+ cwd,
209
+ };
210
+
211
+ // 5. 등록된 훅 실행
212
+ await preActionRegistry.runAll(ctx);
213
+
214
+ return ctx;
215
+ }
216
+
217
+ /**
218
+ * 환경변수가 truthy인지 확인
219
+ */
220
+ function isTruthyEnv(key: string): boolean {
221
+ const value = process.env[key];
222
+ if (!value) return false;
223
+ return !["0", "false", "no", ""].includes(value.toLowerCase());
224
+ }
225
+
226
+ /**
227
+ * Pre-Action 훅 등록 헬퍼
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * registerPreActionHook(async (ctx) => {
232
+ * if (ctx.command === "dev") {
233
+ * console.log("Starting development mode...");
234
+ * }
235
+ * });
236
+ * ```
237
+ */
238
+ export function registerPreActionHook(hook: PreActionHook): () => void {
239
+ preActionRegistry.register(hook);
240
+ return () => preActionRegistry.unregister(hook);
241
+ }
242
+
243
+ /**
244
+ * 기본 훅들 등록
245
+ */
246
+ export function registerDefaultHooks(): void {
247
+ // 예: 개발 모드에서 추가 정보 표시
248
+ registerPreActionHook((ctx) => {
249
+ if (ctx.verbose && ctx.config) {
250
+ console.log(`[mandu] Config loaded from ${ctx.cwd}`);
251
+ if (ctx.config.server?.port) {
252
+ console.log(`[mandu] Server port: ${ctx.config.server.port}`);
253
+ }
254
+ }
255
+ });
256
+ }