@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.
@@ -0,0 +1,306 @@
1
+ /**
2
+ * DNA-015: Semantic Help System
3
+ *
4
+ * 시맨틱 도움말 포맷팅
5
+ * - 예제 기반 도움말
6
+ * - 테마 적용 출력
7
+ * - 섹션별 구조화
8
+ */
9
+
10
+ import { theme, colorize, isRich } from "./theme.js";
11
+
12
+ /**
13
+ * 도움말 예제 타입
14
+ * [명령어, 설명]
15
+ */
16
+ export type HelpExample = readonly [command: string, description: string];
17
+
18
+ /**
19
+ * 명령어 옵션 정의
20
+ */
21
+ export interface HelpOption {
22
+ /** 플래그 (예: "--port", "-p, --port") */
23
+ flags: string;
24
+ /** 설명 */
25
+ description: string;
26
+ /** 기본값 */
27
+ default?: string;
28
+ /** 필수 여부 */
29
+ required?: boolean;
30
+ }
31
+
32
+ /**
33
+ * 서브커맨드 정의
34
+ */
35
+ export interface HelpSubcommand {
36
+ /** 서브커맨드 이름 */
37
+ name: string;
38
+ /** 설명 */
39
+ description: string;
40
+ /** 별칭 */
41
+ aliases?: string[];
42
+ }
43
+
44
+ /**
45
+ * 도움말 섹션
46
+ */
47
+ export interface HelpSection {
48
+ /** 섹션 제목 */
49
+ title: string;
50
+ /** 섹션 내용 */
51
+ content: string;
52
+ }
53
+
54
+ /**
55
+ * 도움말 정의
56
+ */
57
+ export interface HelpDefinition {
58
+ /** 명령어 이름 */
59
+ name: string;
60
+ /** 짧은 설명 */
61
+ description: string;
62
+ /** 사용법 */
63
+ usage?: string;
64
+ /** 옵션 목록 */
65
+ options?: HelpOption[];
66
+ /** 서브커맨드 목록 */
67
+ subcommands?: HelpSubcommand[];
68
+ /** 예제 목록 */
69
+ examples?: HelpExample[];
70
+ /** 추가 섹션 */
71
+ sections?: HelpSection[];
72
+ /** 참조 링크 */
73
+ seeAlso?: string[];
74
+ }
75
+
76
+ /**
77
+ * 예제 포맷팅
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * formatHelpExample("mandu dev", "Start development server");
82
+ * // " mandu dev"
83
+ * // " Start development server"
84
+ * ```
85
+ */
86
+ export function formatHelpExample(command: string, description: string): string {
87
+ const rich = isRich();
88
+ const cmd = rich ? theme.accent(command) : command;
89
+ const desc = rich ? theme.muted(description) : description;
90
+
91
+ return ` ${cmd}\n ${desc}`;
92
+ }
93
+
94
+ /**
95
+ * 예제 그룹 포맷팅
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * formatHelpExampleGroup("Examples:", [
100
+ * ["mandu dev", "Start development server"],
101
+ * ["mandu build --prod", "Build for production"],
102
+ * ]);
103
+ * ```
104
+ */
105
+ export function formatHelpExampleGroup(
106
+ label: string,
107
+ examples: ReadonlyArray<HelpExample>
108
+ ): string {
109
+ const rich = isRich();
110
+ const heading = rich ? theme.heading(label) : label;
111
+ const formatted = examples
112
+ .map(([cmd, desc]) => formatHelpExample(cmd, desc))
113
+ .join("\n\n");
114
+
115
+ return `${heading}\n${formatted}`;
116
+ }
117
+
118
+ /**
119
+ * 옵션 포맷팅
120
+ */
121
+ export function formatHelpOption(option: HelpOption): string {
122
+ const rich = isRich();
123
+ const flags = rich ? theme.option(option.flags) : option.flags;
124
+
125
+ let desc = option.description;
126
+ if (option.default) {
127
+ desc += rich
128
+ ? ` ${theme.muted(`(default: ${option.default})`)}`
129
+ : ` (default: ${option.default})`;
130
+ }
131
+ if (option.required) {
132
+ desc += rich ? ` ${theme.warn("[required]")}` : " [required]";
133
+ }
134
+
135
+ // 플래그와 설명 정렬
136
+ const padding = Math.max(0, 24 - option.flags.length);
137
+ return ` ${flags}${" ".repeat(padding)}${desc}`;
138
+ }
139
+
140
+ /**
141
+ * 서브커맨드 포맷팅
142
+ */
143
+ export function formatHelpSubcommand(subcommand: HelpSubcommand): string {
144
+ const rich = isRich();
145
+ let name = subcommand.name;
146
+
147
+ if (subcommand.aliases && subcommand.aliases.length > 0) {
148
+ name += `, ${subcommand.aliases.join(", ")}`;
149
+ }
150
+
151
+ const cmd = rich ? theme.command(name) : name;
152
+ const desc = rich ? subcommand.description : subcommand.description;
153
+
154
+ const padding = Math.max(0, 20 - name.length);
155
+ return ` ${cmd}${" ".repeat(padding)}${desc}`;
156
+ }
157
+
158
+ /**
159
+ * 섹션 제목 포맷팅
160
+ */
161
+ export function formatSectionTitle(title: string): string {
162
+ const rich = isRich();
163
+ return rich ? theme.heading(title) : title;
164
+ }
165
+
166
+ /**
167
+ * 전체 도움말 렌더링
168
+ */
169
+ export function renderHelp(def: HelpDefinition): string {
170
+ const lines: string[] = [];
171
+ const rich = isRich();
172
+
173
+ // 헤더
174
+ const name = rich ? theme.accent(def.name) : def.name;
175
+ lines.push(`${name} - ${def.description}`);
176
+ lines.push("");
177
+
178
+ // 사용법
179
+ if (def.usage) {
180
+ lines.push(formatSectionTitle("Usage:"));
181
+ lines.push(` ${def.usage}`);
182
+ lines.push("");
183
+ }
184
+
185
+ // 서브커맨드
186
+ if (def.subcommands && def.subcommands.length > 0) {
187
+ lines.push(formatSectionTitle("Commands:"));
188
+ for (const sub of def.subcommands) {
189
+ lines.push(formatHelpSubcommand(sub));
190
+ }
191
+ lines.push("");
192
+ }
193
+
194
+ // 옵션
195
+ if (def.options && def.options.length > 0) {
196
+ lines.push(formatSectionTitle("Options:"));
197
+ for (const opt of def.options) {
198
+ lines.push(formatHelpOption(opt));
199
+ }
200
+ lines.push("");
201
+ }
202
+
203
+ // 예제
204
+ if (def.examples && def.examples.length > 0) {
205
+ lines.push(formatHelpExampleGroup("Examples:", def.examples));
206
+ lines.push("");
207
+ }
208
+
209
+ // 추가 섹션
210
+ if (def.sections) {
211
+ for (const section of def.sections) {
212
+ lines.push(formatSectionTitle(section.title));
213
+ lines.push(section.content);
214
+ lines.push("");
215
+ }
216
+ }
217
+
218
+ // 참조
219
+ if (def.seeAlso && def.seeAlso.length > 0) {
220
+ lines.push(formatSectionTitle("See Also:"));
221
+ for (const ref of def.seeAlso) {
222
+ lines.push(` ${ref}`);
223
+ }
224
+ lines.push("");
225
+ }
226
+
227
+ return lines.join("\n");
228
+ }
229
+
230
+ /**
231
+ * Mandu CLI 기본 도움말 정의
232
+ */
233
+ export const MANDU_HELP: HelpDefinition = {
234
+ name: "mandu",
235
+ description: "Agent-Native Web Framework",
236
+ usage: "mandu <command> [options]",
237
+ subcommands: [
238
+ { name: "init", description: "Create a new Mandu project" },
239
+ { name: "dev", description: "Start development server with HMR" },
240
+ { name: "build", description: "Build for production" },
241
+ { name: "start", description: "Start production server" },
242
+ { name: "guard", description: "Check architecture violations", aliases: ["g"] },
243
+ { name: "routes", description: "Manage file-system routes" },
244
+ { name: "openapi", description: "Generate OpenAPI spec" },
245
+ { name: "brain", description: "Setup local AI with Ollama" },
246
+ ],
247
+ options: [
248
+ { flags: "--version, -v", description: "Show version number" },
249
+ { flags: "--help, -h", description: "Show help" },
250
+ { flags: "--json", description: "Output in JSON format" },
251
+ { flags: "--no-color", description: "Disable colored output" },
252
+ { flags: "--verbose", description: "Enable verbose logging" },
253
+ ],
254
+ examples: [
255
+ ["mandu init my-app", "Create a new project"],
256
+ ["mandu dev --port 4000", "Start dev server on port 4000"],
257
+ ["mandu build --prod", "Build for production"],
258
+ ["mandu guard --fix", "Check and auto-fix violations"],
259
+ ],
260
+ sections: [
261
+ {
262
+ title: "Environment Variables:",
263
+ content: ` MANDU_OUTPUT Output format (json|pretty|plain)
264
+ NO_COLOR Disable colors (set to any value)
265
+ FORCE_COLOR Force colors even in non-TTY`,
266
+ },
267
+ ],
268
+ seeAlso: [
269
+ "https://mandujs.com/docs",
270
+ "https://github.com/mandujs/mandu",
271
+ ],
272
+ };
273
+
274
+ /**
275
+ * 명령어별 도움말 렌더링
276
+ */
277
+ export function renderCommandHelp(
278
+ commandName: string,
279
+ def: Partial<HelpDefinition>
280
+ ): string {
281
+ return renderHelp({
282
+ name: `mandu ${commandName}`,
283
+ description: def.description ?? "",
284
+ ...def,
285
+ });
286
+ }
287
+
288
+ /**
289
+ * 간단한 사용법 힌트
290
+ */
291
+ export function formatUsageHint(command: string, hint: string): string {
292
+ const rich = isRich();
293
+ const cmd = rich ? theme.accent(command) : command;
294
+ const tip = rich ? theme.muted(hint) : hint;
295
+ return `${tip}\n ${cmd}`;
296
+ }
297
+
298
+ /**
299
+ * 에러 후 도움말 힌트
300
+ */
301
+ export function formatErrorHint(errorMessage: string, helpCommand: string): string {
302
+ const rich = isRich();
303
+ const error = rich ? theme.error(errorMessage) : errorMessage;
304
+ const help = rich ? theme.muted(`Run '${helpCommand}' for more information.`) : `Run '${helpCommand}' for more information.`;
305
+ return `${error}\n\n${help}`;
306
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Terminal UI module
3
+ *
4
+ * DNA-009: Color palette & theme
5
+ * DNA-013: Safe Stream Writer
6
+ * DNA-015: Semantic help system
7
+ * DNA-017: Hero banner
8
+ */
9
+
10
+ export { MANDU_PALETTE, type ManduColor } from "./palette.js";
11
+ export { theme, isRich, colorize, stripAnsi } from "./theme.js";
12
+ export {
13
+ shouldShowBanner,
14
+ renderHeroBanner,
15
+ renderMiniBanner,
16
+ renderBoxBanner,
17
+ } from "./banner.js";
18
+ export {
19
+ createSafeStreamWriter,
20
+ getSafeWriter,
21
+ safePrint,
22
+ safePrintln,
23
+ safePrintError,
24
+ type SafeStreamWriter,
25
+ type SafeStreamWriterOptions,
26
+ } from "./stream-writer.js";
27
+ export {
28
+ getOutputMode,
29
+ createFormatContext,
30
+ formatOutput,
31
+ formatError,
32
+ formatSuccess,
33
+ formatWarning,
34
+ formatInfo,
35
+ formatList,
36
+ type OutputMode,
37
+ type OutputOptions,
38
+ type FormatContext,
39
+ } from "./output.js";
40
+ export {
41
+ renderTable,
42
+ renderKeyValueTable,
43
+ type TableColumn,
44
+ type BorderStyle,
45
+ type RenderTableOptions,
46
+ } from "./table.js";
47
+ export {
48
+ createCliProgress,
49
+ withProgress,
50
+ startSpinner,
51
+ runSteps,
52
+ type ProgressOptions,
53
+ type ProgressReporter,
54
+ } from "./progress.js";
55
+ export {
56
+ formatHelpExample,
57
+ formatHelpExampleGroup,
58
+ formatHelpOption,
59
+ formatHelpSubcommand,
60
+ formatSectionTitle,
61
+ renderHelp,
62
+ renderCommandHelp,
63
+ formatUsageHint,
64
+ formatErrorHint,
65
+ MANDU_HELP,
66
+ type HelpExample,
67
+ type HelpOption,
68
+ type HelpSubcommand,
69
+ type HelpSection,
70
+ type HelpDefinition,
71
+ } from "./help.js";
@@ -0,0 +1,295 @@
1
+ /**
2
+ * DNA-014: Adaptive Output Format (JSON/Pretty/Plain)
3
+ *
4
+ * 환경에 따라 출력 형식을 자동 결정
5
+ * - TTY: Pretty (색상 + 포맷팅)
6
+ * - CI/pipe/agent: JSON (자동 처리/에이전트 친화)
7
+ * - --json 플래그: JSON
8
+ * - MANDU_OUTPUT 환경변수: 강제 지정
9
+ */
10
+
11
+ import { theme, isRich, stripAnsi } from "./theme.js";
12
+
13
+ /**
14
+ * 출력 모드
15
+ */
16
+ export type OutputMode = "json" | "pretty" | "plain";
17
+
18
+ /**
19
+ * 출력 옵션
20
+ */
21
+ export interface OutputOptions {
22
+ /** JSON 출력 강제 */
23
+ json?: boolean;
24
+ /** Plain 텍스트 강제 */
25
+ plain?: boolean;
26
+ }
27
+
28
+ /**
29
+ * 출력 모드 결정
30
+ *
31
+ * 우선순위:
32
+ * 1. --json 플래그 → "json"
33
+ * 2. --plain 플래그 → "plain"
34
+ * 3. MANDU_OUTPUT 환경변수 → 지정된 값
35
+ * 4. 에이전트 환경 → "json"
36
+ * 5. !TTY (파이프), CI → "json"
37
+ * 6. 기본값 → "pretty"
38
+ */
39
+ export function getOutputMode(opts: OutputOptions = {}): OutputMode {
40
+ // 플래그 우선
41
+ if (opts.json) return "json";
42
+ if (opts.plain) return "plain";
43
+
44
+ // 환경변수 체크
45
+ const envOutput = process.env.MANDU_OUTPUT?.toLowerCase();
46
+ if (envOutput === "json") return "json";
47
+ if (envOutput === "plain") return "plain";
48
+ if (envOutput === "pretty") return "pretty";
49
+ if (envOutput === "agent") return "json";
50
+
51
+ // 에이전트 환경이면 JSON
52
+ const agentSignals = [
53
+ "MANDU_AGENT",
54
+ "CODEX_AGENT",
55
+ "CODEX",
56
+ "CLAUDE_CODE",
57
+ "ANTHROPIC_CLAUDE_CODE",
58
+ ];
59
+ for (const key of agentSignals) {
60
+ const value = process.env[key];
61
+ if (value === "1" || value === "true") {
62
+ return "json";
63
+ }
64
+ }
65
+
66
+ // CI 환경이면 JSON
67
+ if (process.env.CI) return "json";
68
+
69
+ // TTY가 아니면 JSON
70
+ if (!process.stdout.isTTY) return "json";
71
+
72
+ return "pretty";
73
+ }
74
+
75
+ /**
76
+ * 출력 모드에 따른 포맷팅 컨텍스트
77
+ */
78
+ export interface FormatContext {
79
+ mode: OutputMode;
80
+ rich: boolean;
81
+ }
82
+
83
+ /**
84
+ * 포맷팅 컨텍스트 생성
85
+ */
86
+ export function createFormatContext(opts: OutputOptions = {}): FormatContext {
87
+ const mode = getOutputMode(opts);
88
+ return {
89
+ mode,
90
+ rich: mode === "pretty" && isRich(),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * 데이터를 모드에 맞게 포맷팅
96
+ */
97
+ export function formatOutput<T>(
98
+ data: T,
99
+ ctx: FormatContext,
100
+ formatters: {
101
+ json?: (data: T) => unknown;
102
+ pretty?: (data: T, rich: boolean) => string;
103
+ plain?: (data: T) => string;
104
+ }
105
+ ): string {
106
+ const { mode, rich } = ctx;
107
+
108
+ if (mode === "json") {
109
+ const jsonData = formatters.json ? formatters.json(data) : data;
110
+ return JSON.stringify(jsonData, null, 2);
111
+ }
112
+
113
+ if (mode === "plain") {
114
+ if (formatters.plain) {
115
+ return formatters.plain(data);
116
+ }
117
+ // Pretty 포맷터에서 ANSI 코드 제거
118
+ if (formatters.pretty) {
119
+ return stripAnsi(formatters.pretty(data, false));
120
+ }
121
+ return String(data);
122
+ }
123
+
124
+ // Pretty 모드
125
+ if (formatters.pretty) {
126
+ return formatters.pretty(data, rich);
127
+ }
128
+
129
+ return String(data);
130
+ }
131
+
132
+ /**
133
+ * 에러 출력 포맷팅
134
+ */
135
+ export interface ErrorOutput {
136
+ type: "error";
137
+ message: string;
138
+ error?: string;
139
+ hint?: string;
140
+ code?: string;
141
+ }
142
+
143
+ /**
144
+ * 에러를 모드에 맞게 포맷팅
145
+ */
146
+ export function formatError(
147
+ error: Error | string,
148
+ ctx: FormatContext,
149
+ options: {
150
+ hint?: string;
151
+ code?: string;
152
+ } = {}
153
+ ): string {
154
+ const { mode, rich } = ctx;
155
+ const message = error instanceof Error ? error.message : error;
156
+ const errorStack = error instanceof Error ? error.stack : undefined;
157
+
158
+ if (mode === "json") {
159
+ const output: ErrorOutput = {
160
+ type: "error",
161
+ message,
162
+ code: options.code,
163
+ hint: options.hint,
164
+ };
165
+ if (error instanceof Error && error.stack) {
166
+ output.error = error.stack;
167
+ }
168
+ return JSON.stringify(output, null, 2);
169
+ }
170
+
171
+ const lines: string[] = [];
172
+
173
+ // 에러 메시지
174
+ if (options.code) {
175
+ lines.push(
176
+ rich
177
+ ? `${theme.error("❌ Error")} [${theme.muted(options.code)}]`
178
+ : `Error [${options.code}]`
179
+ );
180
+ } else {
181
+ lines.push(rich ? theme.error("❌ Error") : "Error");
182
+ }
183
+ lines.push(rich ? ` ${message}` : ` ${message}`);
184
+
185
+ // 힌트
186
+ if (options.hint) {
187
+ lines.push("");
188
+ lines.push(rich ? theme.muted(`💡 ${options.hint}`) : `Hint: ${options.hint}`);
189
+ }
190
+
191
+ return lines.join("\n");
192
+ }
193
+
194
+ /**
195
+ * 성공 메시지 포맷팅
196
+ */
197
+ export function formatSuccess(
198
+ message: string,
199
+ ctx: FormatContext,
200
+ details?: Record<string, unknown>
201
+ ): string {
202
+ const { mode, rich } = ctx;
203
+
204
+ if (mode === "json") {
205
+ return JSON.stringify({ type: "success", message, ...details }, null, 2);
206
+ }
207
+
208
+ if (rich) {
209
+ return `${theme.success("✓")} ${message}`;
210
+ }
211
+
212
+ return `[OK] ${message}`;
213
+ }
214
+
215
+ /**
216
+ * 경고 메시지 포맷팅
217
+ */
218
+ export function formatWarning(
219
+ message: string,
220
+ ctx: FormatContext,
221
+ details?: Record<string, unknown>
222
+ ): string {
223
+ const { mode, rich } = ctx;
224
+
225
+ if (mode === "json") {
226
+ return JSON.stringify({ type: "warning", message, ...details }, null, 2);
227
+ }
228
+
229
+ if (rich) {
230
+ return `${theme.warn("⚠")} ${message}`;
231
+ }
232
+
233
+ return `[WARN] ${message}`;
234
+ }
235
+
236
+ /**
237
+ * 정보 메시지 포맷팅
238
+ */
239
+ export function formatInfo(
240
+ message: string,
241
+ ctx: FormatContext,
242
+ details?: Record<string, unknown>
243
+ ): string {
244
+ const { mode, rich } = ctx;
245
+
246
+ if (mode === "json") {
247
+ return JSON.stringify({ type: "info", message, ...details }, null, 2);
248
+ }
249
+
250
+ if (rich) {
251
+ return `${theme.info("ℹ")} ${message}`;
252
+ }
253
+
254
+ return `[INFO] ${message}`;
255
+ }
256
+
257
+ /**
258
+ * 리스트 출력 포맷팅
259
+ */
260
+ export function formatList<T>(
261
+ items: T[],
262
+ ctx: FormatContext,
263
+ options: {
264
+ title?: string;
265
+ itemFormatter?: (item: T, rich: boolean) => string;
266
+ emptyMessage?: string;
267
+ } = {}
268
+ ): string {
269
+ const { mode, rich } = ctx;
270
+ const { title, itemFormatter, emptyMessage = "No items" } = options;
271
+
272
+ if (mode === "json") {
273
+ return JSON.stringify({ title, items, count: items.length }, null, 2);
274
+ }
275
+
276
+ const lines: string[] = [];
277
+
278
+ if (title) {
279
+ lines.push(rich ? theme.heading(title) : title);
280
+ lines.push("");
281
+ }
282
+
283
+ if (items.length === 0) {
284
+ lines.push(rich ? theme.muted(emptyMessage) : emptyMessage);
285
+ } else {
286
+ for (const item of items) {
287
+ const formatted = itemFormatter
288
+ ? itemFormatter(item, rich)
289
+ : String(item);
290
+ lines.push(` ${formatted}`);
291
+ }
292
+ }
293
+
294
+ return lines.join("\n");
295
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * DNA-009: Mandu Color Palette
3
+ *
4
+ * Inspired by OpenClaw's "Lobster Seam" palette
5
+ * @see https://github.com/dominikwilkowski/cfonts
6
+ */
7
+
8
+ /**
9
+ * Mandu 브랜드 색상 팔레트
10
+ * 분홍색 기반의 따뜻한 톤
11
+ */
12
+ export const MANDU_PALETTE = {
13
+ // 브랜드 컬러 (만두 분홍)
14
+ accent: "#E8B4B8",
15
+ accentBright: "#F5D0D3",
16
+ accentDim: "#C9A0A4",
17
+
18
+ // 시맨틱 컬러
19
+ info: "#87CEEB", // 스카이 블루
20
+ success: "#90EE90", // 라이트 그린
21
+ warn: "#FFD700", // 골드
22
+ error: "#FF6B6B", // 코랄 레드
23
+
24
+ // 뉴트럴
25
+ muted: "#9CA3AF", // 그레이
26
+ dim: "#6B7280", // 다크 그레이
27
+ text: "#F9FAFB", // 화이트
28
+ } as const;
29
+
30
+ export type ManduColor = keyof typeof MANDU_PALETTE;