@mandujs/cli 0.12.2 → 0.13.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/README.ko.md +234 -234
- package/README.md +354 -354
- package/package.json +2 -2
- package/src/commands/contract.ts +173 -173
- package/src/commands/dev.ts +8 -68
- package/src/commands/doctor.ts +27 -27
- package/src/commands/guard-arch.ts +303 -303
- package/src/commands/guard-check.ts +3 -3
- package/src/commands/monitor.ts +300 -300
- package/src/commands/openapi.ts +107 -107
- package/src/commands/registry.ts +367 -357
- package/src/commands/routes.ts +228 -228
- package/src/commands/start.ts +184 -0
- package/src/errors/codes.ts +35 -35
- package/src/errors/index.ts +2 -2
- package/src/errors/messages.ts +143 -143
- package/src/hooks/index.ts +17 -17
- package/src/hooks/preaction.ts +256 -256
- package/src/main.ts +37 -34
- package/src/terminal/banner.ts +166 -166
- package/src/terminal/help.ts +306 -306
- package/src/terminal/index.ts +71 -71
- package/src/terminal/output.ts +295 -295
- package/src/terminal/palette.ts +30 -30
- package/src/terminal/progress.ts +327 -327
- package/src/terminal/stream-writer.ts +214 -214
- package/src/terminal/table.ts +354 -354
- package/src/terminal/theme.ts +142 -142
- package/src/util/bun.ts +6 -6
- package/src/util/fs.ts +23 -23
- package/src/util/handlers.ts +96 -0
- package/src/util/manifest.ts +52 -52
- package/src/util/output.ts +22 -22
- package/src/util/port.ts +71 -71
- package/templates/default/AGENTS.md +96 -96
- package/templates/default/app/api/health/route.ts +13 -13
- package/templates/default/app/globals.css +49 -49
- package/templates/default/app/layout.tsx +27 -27
- package/templates/default/app/page.tsx +38 -38
- package/templates/default/package.json +1 -0
- package/templates/default/src/client/shared/lib/utils.ts +16 -16
- package/templates/default/src/client/shared/ui/button.tsx +57 -57
- package/templates/default/src/client/shared/ui/card.tsx +78 -78
- package/templates/default/src/client/shared/ui/index.ts +21 -21
- package/templates/default/src/client/shared/ui/input.tsx +24 -24
- package/templates/default/tests/example.test.ts +58 -58
- package/templates/default/tests/helpers.ts +52 -52
- package/templates/default/tests/setup.ts +9 -9
- package/templates/default/tsconfig.json +12 -14
- package/templates/default/apps/server/main.ts +0 -67
- package/templates/default/apps/web/entry.tsx +0 -35
package/src/terminal/output.ts
CHANGED
|
@@ -1,295 +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
|
-
}
|
|
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
|
+
}
|
package/src/terminal/palette.ts
CHANGED
|
@@ -1,30 +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;
|
|
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;
|