@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/src/main.ts CHANGED
@@ -1,35 +1,23 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { specUpsert } from "./commands/spec-upsert";
4
- import { generateApply } from "./commands/generate-apply";
5
- import { guardCheck } from "./commands/guard-check";
6
- import { guardArch } from "./commands/guard-arch";
7
- import { check } from "./commands/check";
8
- import { dev } from "./commands/dev";
9
- import { init } from "./commands/init";
10
- import { build } from "./commands/build";
11
- import { contractCreate, contractValidate, contractBuild, contractDiff } from "./commands/contract";
12
- import { openAPIGenerate, openAPIServe } from "./commands/openapi";
13
- import {
14
- changeBegin,
15
- changeCommit,
16
- changeRollback,
17
- changeStatus,
18
- changeList,
19
- changePrune,
20
- } from "./commands/change";
21
- import { doctor } from "./commands/doctor";
22
- import { watch } from "./commands/watch";
23
- import { brainSetup, brainStatus } from "./commands/brain";
24
- import { routesGenerate, routesList, routesWatch } from "./commands/routes";
25
- import { monitor } from "./commands/monitor";
26
- import { runLockCommand, lockHelp } from "./commands/lock";
3
+ /**
4
+ * Mandu CLI - Agent-Native Fullstack Framework
5
+ *
6
+ * DNA-010: Command Registry Pattern 적용
7
+ * - 선언적 명령어 등록
8
+ * - 레이지 로딩으로 시작 시간 최적화
9
+ */
10
+
11
+ import { commandRegistry, getCommand, type CommandContext } from "./commands/registry";
27
12
  import { CLI_ERROR_CODES, handleCLIError, printCLIError } from "./errors";
13
+ import { shouldShowBanner, renderHeroBanner, theme } from "./terminal";
14
+
15
+ const VERSION = "0.10.0";
28
16
 
29
17
  const HELP_TEXT = `
30
- 🥟 Mandu CLI - Agent-Native Fullstack Framework
18
+ ${theme.heading("🥟 Mandu CLI")} ${theme.muted(`v${VERSION}`)} - Agent-Native Fullstack Framework
31
19
 
32
- Usage: bunx mandu <command> [options]
20
+ ${theme.heading("Usage:")} ${theme.command("bunx mandu")} ${theme.option("<command>")} [options]
33
21
 
34
22
  Commands:
35
23
  init 새 프로젝트 생성 (Tailwind + shadcn/ui 기본 포함)
@@ -149,20 +137,42 @@ Brain (sLLM) Workflow:
149
137
  1. brain setup → 2. doctor (분석) → 3. watch (감시)
150
138
  `;
151
139
 
152
- function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
153
- const command = args[0] || "";
154
- const options: Record<string, string> = {};
155
-
156
- for (let i = 1; i < args.length; i++) {
157
- const arg = args[i];
158
- if (arg.startsWith("--")) {
159
- const key = arg.slice(2);
160
- const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
161
- options[key] = value;
162
- } else if (arg === "-h") {
163
- options["help"] = "true";
164
- } else if (!options._positional) {
165
- // First non-flag argument after command is positional (e.g., project name)
140
+ /**
141
+ * 인자 파싱
142
+ */
143
+ function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
144
+ const options: Record<string, string> = {};
145
+ let command = "";
146
+ const shortFlags: Record<string, string> = {
147
+ h: "help",
148
+ q: "quiet",
149
+ v: "verify",
150
+ d: "diff",
151
+ };
152
+
153
+ for (let i = 0; i < args.length; i++) {
154
+ const arg = args[i];
155
+
156
+ // 플래그 처리
157
+ if (arg.startsWith("--")) {
158
+ const key = arg.slice(2);
159
+ const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
160
+ options[key] = value;
161
+ } else if (arg.startsWith("-") && arg.length > 1) {
162
+ const flags = arg.slice(1).split("");
163
+ for (const flag of flags) {
164
+ const mapped = shortFlags[flag];
165
+ if (mapped) {
166
+ options[mapped] = "true";
167
+ } else {
168
+ options[flag] = "true";
169
+ }
170
+ }
171
+ } else if (!command) {
172
+ // 첫 번째 비플래그 인자가 명령어
173
+ command = arg;
174
+ } else if (!options._positional) {
175
+ // 두 번째 비플래그 인자가 positional
166
176
  options._positional = arg;
167
177
  }
168
178
  }
@@ -170,274 +180,56 @@ function parseArgs(args: string[]): { command: string; options: Record<string, s
170
180
  return { command, options };
171
181
  }
172
182
 
183
+ /**
184
+ * 메인 함수
185
+ */
173
186
  async function main(): Promise<void> {
174
187
  const args = process.argv.slice(2);
175
188
  const { command, options } = parseArgs(args);
176
189
 
190
+ // 도움말 처리
177
191
  if (options.help || command === "help" || !command) {
178
192
  console.log(HELP_TEXT);
179
193
  process.exit(0);
180
194
  }
181
195
 
182
- let success = true;
183
-
184
- switch (command) {
185
- case "init":
186
- success = await init({
187
- name: options.name || options._positional,
188
- css: options.css as any,
189
- ui: options.ui as any,
190
- theme: options.theme === "true",
191
- minimal: options.minimal === "true",
192
- });
193
- break;
194
-
195
- case "spec-upsert":
196
- success = await specUpsert({ file: options.file });
197
- break;
198
-
199
- case "generate":
200
- success = await generateApply();
201
- break;
202
-
203
- case "check":
204
- success = await check();
205
- break;
206
-
207
- case "guard": {
208
- const subCommand = args[1];
209
- const hasSubCommand = subCommand && !subCommand.startsWith("--");
210
- const guardArchOptions = {
211
- watch: options.watch === "true",
212
- output: options.output,
213
- };
214
- switch (subCommand) {
215
- case "arch":
216
- success = await guardArch(guardArchOptions);
217
- break;
218
- case "legacy":
219
- case "spec":
220
- success = await guardCheck();
221
- break;
222
- default:
223
- if (hasSubCommand) {
224
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
225
- command: "guard",
226
- subcommand,
227
- });
228
- console.log("\nUsage: bunx mandu guard <arch|legacy>");
229
- process.exit(1);
230
- }
231
- // 기본값: architecture guard
232
- success = await guardArch(guardArchOptions);
233
- }
234
- break;
235
- }
236
-
237
- case "build":
238
- success = await build({
239
- watch: options.watch === "true",
240
- });
241
- break;
242
-
243
- case "dev":
244
- await dev();
245
- break;
246
-
247
- case "routes": {
248
- const subCommand = args[1];
249
- switch (subCommand) {
250
- case "generate":
251
- success = await routesGenerate({
252
- output: options.output,
253
- verbose: options.verbose === "true",
254
- });
255
- break;
256
- case "list":
257
- success = await routesList({
258
- verbose: options.verbose === "true",
259
- });
260
- break;
261
- case "watch":
262
- success = await routesWatch({
263
- output: options.output,
264
- verbose: options.verbose === "true",
265
- });
266
- break;
267
- default:
268
- // 기본값: list
269
- if (!subCommand) {
270
- success = await routesList({
271
- verbose: options.verbose === "true",
272
- });
273
- } else {
274
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
275
- command: "routes",
276
- subcommand,
277
- });
278
- console.log("\nUsage: bunx mandu routes <generate|list|watch>");
279
- process.exit(1);
280
- }
281
- }
282
- break;
283
- }
196
+ // 히어로 배너 표시
197
+ if (shouldShowBanner(args)) {
198
+ await renderHeroBanner(VERSION);
199
+ }
284
200
 
285
- case "contract": {
286
- const subCommand = args[1];
287
- switch (subCommand) {
288
- case "create": {
289
- const routeId = args[2] || options._positional;
290
- if (!routeId) {
291
- printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, { argument: "routeId" });
292
- console.log("\nUsage: bunx mandu contract create <routeId>");
293
- process.exit(1);
294
- }
295
- success = await contractCreate({ routeId });
296
- break;
297
- }
298
- case "validate":
299
- success = await contractValidate({ verbose: options.verbose === "true" });
300
- break;
301
- case "build":
302
- success = await contractBuild({ output: options.output });
303
- break;
304
- case "diff":
305
- success = await contractDiff({
306
- from: options.from,
307
- to: options.to,
308
- output: options.output,
309
- json: options.json === "true",
310
- });
311
- break;
312
- default:
313
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
314
- command: "contract",
315
- subcommand,
316
- });
317
- console.log("\nUsage: bunx mandu contract <create|validate|build|diff>");
318
- process.exit(1);
319
- }
320
- break;
321
- }
201
+ // DNA-010: 레지스트리에서 명령어 조회
202
+ const registration = getCommand(command);
322
203
 
323
- case "openapi": {
324
- const subCommand = args[1];
325
- switch (subCommand) {
326
- case "generate":
327
- success = await openAPIGenerate({
328
- output: options.output,
329
- title: options.title,
330
- version: options.version,
331
- });
332
- break;
333
- case "serve":
334
- success = await openAPIServe();
335
- break;
336
- default:
337
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
338
- command: "openapi",
339
- subcommand,
340
- });
341
- console.log("\nUsage: bunx mandu openapi <generate|serve>");
342
- process.exit(1);
343
- }
344
- break;
345
- }
204
+ if (!registration) {
205
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
206
+ console.log(HELP_TEXT);
207
+ process.exit(1);
208
+ }
346
209
 
347
- case "change": {
348
- const subCommand = args[1];
349
- switch (subCommand) {
350
- case "begin":
351
- success = await changeBegin({ message: options.message });
352
- break;
353
- case "commit":
354
- success = await changeCommit();
355
- break;
356
- case "rollback":
357
- success = await changeRollback({ id: options.id });
358
- break;
359
- case "status":
360
- success = await changeStatus();
361
- break;
362
- case "list":
363
- success = await changeList();
364
- break;
365
- case "prune":
366
- success = await changePrune({
367
- keep: options.keep ? Number(options.keep) : undefined,
368
- });
369
- break;
370
- default:
371
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
372
- command: "change",
373
- subcommand,
374
- });
375
- console.log(`\nUsage: bunx mandu change <begin|commit|rollback|status|list|prune>`);
376
- process.exit(1);
377
- }
378
- break;
379
- }
210
+ // 명령어 실행 컨텍스트
211
+ const ctx: CommandContext = { args, options };
380
212
 
381
- case "doctor":
382
- success = await doctor({
383
- useLLM: options["no-llm"] !== "true",
384
- output: options.output,
385
- });
386
- break;
213
+ // 명령어 실행
214
+ const success = await registration.run(ctx);
387
215
 
388
- case "watch":
389
- success = await watch({
390
- status: options.status === "true",
391
- debounce: options.debounce ? Number(options.debounce) : undefined,
216
+ // 서브커맨드 에러 처리
217
+ if (!success) {
218
+ const subCommand = args[1];
219
+ if (registration.subcommands && subCommand && !subCommand.startsWith("--")) {
220
+ // 알 수 없는 서브커맨드
221
+ printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
222
+ command,
223
+ subcommand: subCommand,
392
224
  });
393
- break;
394
-
395
- case "monitor":
396
- success = await monitor({
397
- summary: options.summary === "true",
398
- since: options.since,
399
- follow: options.follow === "false" ? false : true,
400
- file: options.file,
225
+ console.log(`\nUsage: bunx mandu ${command} <${registration.subcommands.join("|")}>`);
226
+ } else if (registration.subcommands) {
227
+ // 서브커맨드 필요
228
+ printCLIError(CLI_ERROR_CODES.MISSING_ARGUMENT, {
229
+ argument: "subcommand",
401
230
  });
402
- break;
403
-
404
- case "brain": {
405
- const subCommand = args[1];
406
- switch (subCommand) {
407
- case "setup":
408
- success = await brainSetup({
409
- model: options.model,
410
- url: options.url,
411
- skipCheck: options["skip-check"] === "true",
412
- });
413
- break;
414
- case "status":
415
- success = await brainStatus({
416
- verbose: options.verbose === "true",
417
- });
418
- break;
419
- default:
420
- printCLIError(CLI_ERROR_CODES.UNKNOWN_SUBCOMMAND, {
421
- command: "brain",
422
- subcommand,
423
- });
424
- console.log("\nUsage: bunx mandu brain <setup|status>");
425
- process.exit(1);
426
- }
427
- break;
231
+ console.log(`\nUsage: bunx mandu ${command} <${registration.subcommands.join("|")}>`);
428
232
  }
429
-
430
- case "lock":
431
- success = await runLockCommand(args.slice(1));
432
- break;
433
-
434
- default:
435
- printCLIError(CLI_ERROR_CODES.UNKNOWN_COMMAND, { command });
436
- console.log(HELP_TEXT);
437
- process.exit(1);
438
- }
439
-
440
- if (!success) {
441
233
  process.exit(1);
442
234
  }
443
235
  }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * DNA-017: Hero Banner with cfonts + gradient
3
+ *
4
+ * Sexy ASCII art banner for CLI startup
5
+ * Inspired by Claude Code, Vite, Astro CLI screens
6
+ *
7
+ * @see https://github.com/dominikwilkowski/cfonts
8
+ */
9
+
10
+ import { theme, isRich, stripAnsi } from "./theme.js";
11
+ import { MANDU_PALETTE } from "./palette.js";
12
+
13
+ /**
14
+ * Check if banner should be displayed
15
+ */
16
+ export function shouldShowBanner(argv: string[]): boolean {
17
+ // Environment-based skip
18
+ if (process.env.MANDU_NO_BANNER) return false;
19
+ if (process.env.CI) return false;
20
+ if (process.env.CLAUDE_CODE) return false;
21
+ if (process.env.CODEX_AGENT) return false;
22
+ if (process.env.MANDU_AGENT) return false;
23
+
24
+ // TTY check
25
+ if (!process.stdout.isTTY) return false;
26
+
27
+ // Flag-based skip
28
+ const hasJsonFlag = argv.includes("--json");
29
+ const hasQuietFlag = argv.includes("--quiet") || argv.includes("-q");
30
+ const hasHelpFlag = argv.includes("--help") || argv.includes("-h");
31
+
32
+ if (hasJsonFlag || hasQuietFlag || hasHelpFlag) return false;
33
+
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * Fallback ASCII art banner (no dependencies)
39
+ */
40
+ const MANDU_ASCII_SMALL = `
41
+ ╔╦╗╔═╗╔╗╔╔╦╗╦ ╦
42
+ ║║║╠═╣║║║ ║║║ ║
43
+ ╩ ╩╩ ╩╝╚╝═╩╝╚═╝
44
+ `;
45
+
46
+ const MANDU_ASCII_LARGE = `
47
+ ███╗ ███╗ █████╗ ███╗ ██╗██████╗ ██╗ ██╗
48
+ ████╗ ████║██╔══██╗████╗ ██║██╔══██╗██║ ██║
49
+ ██╔████╔██║███████║██╔██╗ ██║██║ ██║██║ ██║
50
+ ██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║██║ ██║
51
+ ██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝╚██████╔╝
52
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝
53
+ `;
54
+
55
+ /**
56
+ * Apply gradient effect to text (simple version)
57
+ */
58
+ function applyGradient(text: string): string {
59
+ if (!isRich()) return text;
60
+
61
+ const lines = text.split("\n");
62
+ const colorFns = [
63
+ theme.accentDim,
64
+ theme.accent,
65
+ theme.accentBright,
66
+ theme.accent,
67
+ theme.accentDim,
68
+ ];
69
+
70
+ return lines
71
+ .map((line, i) => {
72
+ const colorIndex = Math.min(i, colorFns.length - 1);
73
+ const colorFn = colorFns[colorIndex];
74
+ return colorFn(line);
75
+ })
76
+ .join("\n");
77
+ }
78
+
79
+ /**
80
+ * Render hero banner with cfonts (if available) or fallback
81
+ */
82
+ export async function renderHeroBanner(version: string): Promise<void> {
83
+ const cols = process.stdout.columns ?? 80;
84
+
85
+ // Very narrow terminal: minimal output
86
+ if (cols < 40) {
87
+ console.log(`\n 🥟 Mandu v${version}\n`);
88
+ return;
89
+ }
90
+
91
+ // Try cfonts first
92
+ try {
93
+ const cfonts = await import("cfonts");
94
+
95
+ cfonts.say("MANDU", {
96
+ font: "block",
97
+ gradient: [MANDU_PALETTE.accent, MANDU_PALETTE.accentBright],
98
+ transitionGradient: true,
99
+ align: "center",
100
+ space: true,
101
+ maxLength: Math.min(cols - 4, 80),
102
+ });
103
+ } catch {
104
+ // cfonts not available, use fallback
105
+ const ascii = cols >= 60 ? MANDU_ASCII_LARGE : MANDU_ASCII_SMALL;
106
+ console.log(applyGradient(ascii));
107
+ }
108
+
109
+ // Tagline
110
+ const tagline = `🥟 Agent-Native Web Framework v${version}`;
111
+ const taglineWidth = stripAnsi(tagline).length;
112
+ const padding = Math.max(0, Math.floor((cols - taglineWidth) / 2));
113
+
114
+ console.log(" ".repeat(padding) + theme.muted(tagline));
115
+ console.log();
116
+ }
117
+
118
+ /**
119
+ * Render minimal banner (for narrow terminals or quick commands)
120
+ */
121
+ export function renderMiniBanner(version: string): void {
122
+ if (!isRich()) {
123
+ console.log(`\nMandu v${version}\n`);
124
+ return;
125
+ }
126
+
127
+ console.log();
128
+ console.log(` ${theme.heading("🥟 Mandu")} ${theme.muted(`v${version}`)}`);
129
+ console.log(` ${theme.muted("Agent-Native Web Framework")}`);
130
+ console.log();
131
+ }
132
+
133
+ /**
134
+ * Render box banner (alternative style)
135
+ */
136
+ export function renderBoxBanner(version: string): void {
137
+ if (!isRich()) {
138
+ console.log(`\nMandu v${version}\n`);
139
+ return;
140
+ }
141
+
142
+ const width = 30;
143
+ const top = theme.accent(" ╭" + "─".repeat(width) + "╮");
144
+ const mid1 =
145
+ theme.accent(" │") +
146
+ " " +
147
+ theme.heading("🥟 Mandu") +
148
+ " " +
149
+ theme.muted(`v${version}`) +
150
+ " ".repeat(width - 18 - version.length) +
151
+ theme.accent("│");
152
+ const mid2 =
153
+ theme.accent(" │") +
154
+ " " +
155
+ theme.muted("Agent-Native Framework") +
156
+ " ".repeat(width - 24) +
157
+ theme.accent("│");
158
+ const bottom = theme.accent(" ╰" + "─".repeat(width) + "╯");
159
+
160
+ console.log();
161
+ console.log(top);
162
+ console.log(mid1);
163
+ console.log(mid2);
164
+ console.log(bottom);
165
+ console.log();
166
+ }