@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 +3 -2
- package/src/commands/registry.ts +357 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/preaction.ts +256 -0
- package/src/main.ts +83 -291
- package/src/terminal/banner.ts +166 -0
- package/src/terminal/help.ts +306 -0
- package/src/terminal/index.ts +71 -0
- package/src/terminal/output.ts +295 -0
- package/src/terminal/palette.ts +30 -0
- package/src/terminal/progress.ts +327 -0
- package/src/terminal/stream-writer.ts +214 -0
- package/src/terminal/table.ts +354 -0
- package/src/terminal/theme.ts +142 -0
- package/src/util/output.ts +7 -26
package/src/main.ts
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import {
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
useLLM: options["no-llm"] !== "true",
|
|
384
|
-
output: options.output,
|
|
385
|
-
});
|
|
386
|
-
break;
|
|
213
|
+
// 명령어 실행
|
|
214
|
+
const success = await registration.run(ctx);
|
|
387
215
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
+
}
|