@seed-design/cli 1.2.2 → 1.3.1
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/bin/index.mjs +23 -9
- package/package.json +1 -1
- package/src/commands/docs.ts +450 -0
- package/src/commands/upgrade.ts +387 -0
- package/src/index.ts +4 -0
- package/src/schema.ts +46 -10
- package/src/tests/resolve-dependencies.test.ts +13 -13
- package/src/utils/analytics.ts +1 -2
- package/src/utils/fetch.ts +82 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { fetchDocsIndex } from "@/src/utils/fetch";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import type { CAC } from "cac";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { BASE_URL } from "../constants";
|
|
6
|
+
import { analytics } from "../utils/analytics";
|
|
7
|
+
import { highlight } from "../utils/color";
|
|
8
|
+
import {
|
|
9
|
+
CliCancelError,
|
|
10
|
+
CliError,
|
|
11
|
+
handleCliError,
|
|
12
|
+
isCliCancelError,
|
|
13
|
+
isVerboseMode,
|
|
14
|
+
} from "../utils/error";
|
|
15
|
+
import type { DocsCategory, DocsItem, DocsSection } from "../schema";
|
|
16
|
+
|
|
17
|
+
const GITHUB_SNIPPET_BASE =
|
|
18
|
+
"https://raw.githubusercontent.com/daangn/seed-design/refs/heads/dev/docs/registry";
|
|
19
|
+
|
|
20
|
+
const docsOptionsSchema = z.object({
|
|
21
|
+
query: z.string().optional(),
|
|
22
|
+
baseUrl: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function buildSnippetUrl(registryId: string, snippetPath: string): string {
|
|
26
|
+
return `${GITHUB_SNIPPET_BASE}/${registryId}/${snippetPath}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printDocsResult(item: DocsItem, baseUrl: string) {
|
|
30
|
+
const docLink = `${baseUrl}${item.docUrl}`;
|
|
31
|
+
const llmsLink = `${baseUrl}/llms${item.docUrl}.txt`;
|
|
32
|
+
|
|
33
|
+
const lines = [item.id, `- docs: ${docLink}`, `- llms.txt: ${llmsLink}`];
|
|
34
|
+
|
|
35
|
+
if (item.snippetKey && item.snippets && item.snippets.length > 0) {
|
|
36
|
+
const [registryId] = item.snippetKey.split(":");
|
|
37
|
+
if (registryId === "ui" || registryId === "breeze") {
|
|
38
|
+
if (item.snippets.length === 1) {
|
|
39
|
+
lines.push(`- snippet: ${buildSnippetUrl(registryId, item.snippets[0].path)}`);
|
|
40
|
+
} else {
|
|
41
|
+
lines.push("- snippet:");
|
|
42
|
+
for (const snippet of item.snippets) {
|
|
43
|
+
lines.push(` - ${snippet.label}: ${buildSnippetUrl(registryId, snippet.path)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
p.log.message(lines.join("\n"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute the Levenshtein (edit) distance between two strings.
|
|
54
|
+
* Used to suggest similar valid paths when users make typos.
|
|
55
|
+
*/
|
|
56
|
+
function levenshtein(a: string, b: string): number {
|
|
57
|
+
const m = a.length;
|
|
58
|
+
const n = b.length;
|
|
59
|
+
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
|
|
60
|
+
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
61
|
+
);
|
|
62
|
+
for (let i = 1; i <= m; i++) {
|
|
63
|
+
for (let j = 1; j <= n; j++) {
|
|
64
|
+
dp[i][j] = Math.min(
|
|
65
|
+
dp[i - 1][j] + 1,
|
|
66
|
+
dp[i][j - 1] + 1,
|
|
67
|
+
dp[i - 1][j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return dp[m][n];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Find candidates similar to `input` within `maxDistance` edits, sorted by distance.
|
|
76
|
+
*/
|
|
77
|
+
function findSimilar(input: string, candidates: string[], maxDistance = 3): string[] {
|
|
78
|
+
const q = input.toLowerCase();
|
|
79
|
+
return candidates
|
|
80
|
+
.map((c) => ({ value: c, dist: levenshtein(q, c.toLowerCase()) }))
|
|
81
|
+
.filter(({ dist }) => dist > 0 && dist <= maxDistance)
|
|
82
|
+
.sort((a, b) => a.dist - b.dist)
|
|
83
|
+
.map(({ value }) => value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a suggestion hint from a path query by fuzzy-matching each segment
|
|
88
|
+
* against the docs index hierarchy.
|
|
89
|
+
*/
|
|
90
|
+
function buildSuggestionHint(segments: string[], categories: DocsCategory[]): string | undefined {
|
|
91
|
+
if (segments.length === 0) return undefined;
|
|
92
|
+
|
|
93
|
+
const suggestions: string[] = [];
|
|
94
|
+
|
|
95
|
+
// Try to fuzzy-match the first segment against category IDs
|
|
96
|
+
const categoryIds = categories.map((c) => c.id);
|
|
97
|
+
const similarCategories = findSimilar(segments[0], categoryIds);
|
|
98
|
+
|
|
99
|
+
if (similarCategories.length === 0) {
|
|
100
|
+
// No similar category — try to find similar full paths across everything
|
|
101
|
+
const allPaths = categories.flatMap((cat) =>
|
|
102
|
+
cat.sections.flatMap((sec) => sec.items.map((item) => `${cat.id}/${sec.id}/${item.id}`)),
|
|
103
|
+
);
|
|
104
|
+
const fullQuery = segments.join("/");
|
|
105
|
+
const similarPaths = findSimilar(fullQuery, allPaths, 5);
|
|
106
|
+
if (similarPaths.length > 0) {
|
|
107
|
+
suggestions.push(...similarPaths.slice(0, 3));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const bestCat = categories.find((c) => c.id === similarCategories[0]);
|
|
111
|
+
if (bestCat && segments.length >= 2) {
|
|
112
|
+
const sectionIds = bestCat.sections.map((s) => s.id);
|
|
113
|
+
const similarSections = findSimilar(segments[1], sectionIds);
|
|
114
|
+
|
|
115
|
+
if (similarSections.length > 0) {
|
|
116
|
+
const bestSec = bestCat.sections.find((s) => s.id === similarSections[0]);
|
|
117
|
+
if (bestSec && segments.length >= 3) {
|
|
118
|
+
const itemIds = bestSec.items.map((i) => i.id);
|
|
119
|
+
const similarItems = findSimilar(segments[2], itemIds);
|
|
120
|
+
for (const item of similarItems.slice(0, 3)) {
|
|
121
|
+
suggestions.push(`${bestCat.id}/${bestSec.id}/${item}`);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
for (const sec of similarSections.slice(0, 3)) {
|
|
125
|
+
suggestions.push(`${bestCat.id}/${sec}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Section not found, search items within category
|
|
130
|
+
const allItemIds = bestCat.sections.flatMap((s) =>
|
|
131
|
+
s.items.map((i) => ({ path: `${bestCat.id}/${s.id}/${i.id}`, id: i.id })),
|
|
132
|
+
);
|
|
133
|
+
const similarItems = findSimilar(
|
|
134
|
+
segments[1],
|
|
135
|
+
allItemIds.map((x) => x.id),
|
|
136
|
+
);
|
|
137
|
+
for (const itemId of similarItems.slice(0, 3)) {
|
|
138
|
+
const found = allItemIds.find((x) => x.id === itemId);
|
|
139
|
+
if (found) suggestions.push(found.path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
for (const cat of similarCategories.slice(0, 3)) {
|
|
144
|
+
suggestions.push(cat);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (suggestions.length === 0) return undefined;
|
|
150
|
+
|
|
151
|
+
const lines = ["", "💡 이것을 의미했나요?"];
|
|
152
|
+
for (const s of suggestions) {
|
|
153
|
+
lines.push(` - ${s}`);
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse a path-style query into segments.
|
|
160
|
+
* e.g. "react/components/action-button" → ["react", "components", "action-button"]
|
|
161
|
+
*/
|
|
162
|
+
function parseQueryPath(query: string): string[] {
|
|
163
|
+
return query
|
|
164
|
+
.split("/")
|
|
165
|
+
.map((s) => s.trim())
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Search all items across all categories/sections.
|
|
171
|
+
*/
|
|
172
|
+
function searchAllItems(
|
|
173
|
+
categories: DocsCategory[],
|
|
174
|
+
query: string,
|
|
175
|
+
): { item: DocsItem; categoryLabel: string; sectionLabel: string }[] {
|
|
176
|
+
const q = query.toLowerCase();
|
|
177
|
+
return categories.flatMap((cat) =>
|
|
178
|
+
cat.sections.flatMap((sec) =>
|
|
179
|
+
sec.items
|
|
180
|
+
.filter((item) => item.id.toLowerCase().includes(q) || item.title.toLowerCase().includes(q))
|
|
181
|
+
.map((item) => ({
|
|
182
|
+
item,
|
|
183
|
+
categoryLabel: cat.label,
|
|
184
|
+
sectionLabel: sec.label,
|
|
185
|
+
})),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function selectItem(items: DocsItem[]): Promise<DocsItem> {
|
|
191
|
+
const selected = await p.select({
|
|
192
|
+
message: "항목을 선택해주세요",
|
|
193
|
+
options: items.map((item) => ({
|
|
194
|
+
label: `${item.deprecated ? "(deprecated) " : ""}${item.title}`,
|
|
195
|
+
value: item,
|
|
196
|
+
hint: item.description,
|
|
197
|
+
})),
|
|
198
|
+
});
|
|
199
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
200
|
+
return selected;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function selectSection(sections: DocsSection[]): Promise<DocsSection> {
|
|
204
|
+
const selected = await p.select({
|
|
205
|
+
message: "섹션을 선택해주세요",
|
|
206
|
+
options: sections.map((sec) => ({
|
|
207
|
+
label: sec.label,
|
|
208
|
+
value: sec,
|
|
209
|
+
hint: `${sec.items.length}개 항목`,
|
|
210
|
+
})),
|
|
211
|
+
});
|
|
212
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
213
|
+
return selected;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function selectCategory(categories: DocsCategory[]): Promise<DocsCategory> {
|
|
217
|
+
const selected = await p.select({
|
|
218
|
+
message: "카테고리를 선택해주세요",
|
|
219
|
+
options: categories.map((cat) => ({
|
|
220
|
+
label: cat.label,
|
|
221
|
+
value: cat,
|
|
222
|
+
hint: `${cat.sections.reduce((sum, s) => sum + s.items.length, 0)}개 항목`,
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
226
|
+
return selected;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const docsCommand = (cli: CAC) => {
|
|
230
|
+
cli
|
|
231
|
+
.command("docs [query]", "문서 링크, llms.txt 링크, 스니펫 링크를 조회합니다")
|
|
232
|
+
.option("-u, --baseUrl <baseUrl>", `레지스트리의 기본 URL (기본값: ${BASE_URL})`, {
|
|
233
|
+
default: BASE_URL,
|
|
234
|
+
})
|
|
235
|
+
.example("seed-design docs")
|
|
236
|
+
.example("seed-design docs action-button")
|
|
237
|
+
.example("seed-design docs react")
|
|
238
|
+
.example("seed-design docs react/components")
|
|
239
|
+
.example("seed-design docs react/components/action-button")
|
|
240
|
+
.action(async (query, opts) => {
|
|
241
|
+
const startTime = Date.now();
|
|
242
|
+
const verbose = isVerboseMode(opts);
|
|
243
|
+
p.intro("seed-design docs");
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const parsed = docsOptionsSchema.safeParse({ query, ...opts });
|
|
247
|
+
if (!parsed.success) {
|
|
248
|
+
throw parsed.error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const { data: options } = parsed;
|
|
252
|
+
const baseUrl = options.baseUrl ?? BASE_URL;
|
|
253
|
+
|
|
254
|
+
const { start, stop } = p.spinner();
|
|
255
|
+
start("문서 목록을 가져오고 있어요...");
|
|
256
|
+
|
|
257
|
+
const docsIndex = await (async () => {
|
|
258
|
+
try {
|
|
259
|
+
const index = await fetchDocsIndex({ baseUrl });
|
|
260
|
+
stop("문서 목록을 가져왔어요.");
|
|
261
|
+
return index;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
stop("문서 목록을 가져오지 못했어요.");
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
const { categories } = docsIndex;
|
|
269
|
+
let selectedItem: DocsItem;
|
|
270
|
+
|
|
271
|
+
if (options.query) {
|
|
272
|
+
const segments = parseQueryPath(options.query);
|
|
273
|
+
|
|
274
|
+
// Try to resolve as path: category / section / item
|
|
275
|
+
const matchedCategory = categories.find((c) => c.id === segments[0]);
|
|
276
|
+
|
|
277
|
+
if (matchedCategory && segments.length >= 2) {
|
|
278
|
+
const matchedSection = matchedCategory.sections.find((s) => s.id === segments[1]);
|
|
279
|
+
|
|
280
|
+
if (matchedSection && segments.length >= 3) {
|
|
281
|
+
// Full path: category/section/item
|
|
282
|
+
const matchedItem = matchedSection.items.find((i) => i.id === segments[2]);
|
|
283
|
+
if (matchedItem) {
|
|
284
|
+
selectedItem = matchedItem;
|
|
285
|
+
} else {
|
|
286
|
+
// Item not found in section — search within the section
|
|
287
|
+
const q = segments[2].toLowerCase();
|
|
288
|
+
const matched = matchedSection.items.filter(
|
|
289
|
+
(i) => i.id.toLowerCase().includes(q) || i.title.toLowerCase().includes(q),
|
|
290
|
+
);
|
|
291
|
+
if (matched.length === 0) {
|
|
292
|
+
const similarItems = findSimilar(
|
|
293
|
+
segments[2],
|
|
294
|
+
matchedSection.items.map((i) => i.id),
|
|
295
|
+
);
|
|
296
|
+
const suggestion =
|
|
297
|
+
similarItems.length > 0
|
|
298
|
+
? `\n\n💡 이것을 의미했나요?\n${similarItems
|
|
299
|
+
.slice(0, 3)
|
|
300
|
+
.map((s) => ` - ${matchedCategory.id}/${matchedSection.id}/${s}`)
|
|
301
|
+
.join("\n")}`
|
|
302
|
+
: "";
|
|
303
|
+
throw new CliError({
|
|
304
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.${suggestion}`,
|
|
305
|
+
hint: `\`seed-design docs ${matchedCategory.id}/${matchedSection.id}\`로 목록을 확인해보세요.`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (matched.length === 1) {
|
|
309
|
+
selectedItem = matched[0];
|
|
310
|
+
} else {
|
|
311
|
+
selectedItem = await selectItem(matched);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else if (matchedSection) {
|
|
315
|
+
// category/section — select item within section
|
|
316
|
+
selectedItem = await selectItem(matchedSection.items);
|
|
317
|
+
} else {
|
|
318
|
+
// category/??? — search within category
|
|
319
|
+
const q = segments[1].toLowerCase();
|
|
320
|
+
const matched = matchedCategory.sections.flatMap((s) =>
|
|
321
|
+
s.items
|
|
322
|
+
.filter(
|
|
323
|
+
(i) => i.id.toLowerCase().includes(q) || i.title.toLowerCase().includes(q),
|
|
324
|
+
)
|
|
325
|
+
.map((item) => ({ item, sectionLabel: s.label })),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (matched.length === 0) {
|
|
329
|
+
const sectionIds = matchedCategory.sections.map((s) => s.id);
|
|
330
|
+
const similarSections = findSimilar(segments[1], sectionIds);
|
|
331
|
+
const allItemIds = matchedCategory.sections.flatMap((s) =>
|
|
332
|
+
s.items.map((i) => ({ path: `${matchedCategory.id}/${s.id}/${i.id}`, id: i.id })),
|
|
333
|
+
);
|
|
334
|
+
const similarItems = findSimilar(
|
|
335
|
+
segments[1],
|
|
336
|
+
allItemIds.map((x) => x.id),
|
|
337
|
+
);
|
|
338
|
+
const suggestions: string[] = [
|
|
339
|
+
...similarSections.slice(0, 2).map((s) => `${matchedCategory.id}/${s}`),
|
|
340
|
+
...similarItems
|
|
341
|
+
.slice(0, 2)
|
|
342
|
+
.map((id) => allItemIds.find((x) => x.id === id)?.path)
|
|
343
|
+
.filter((p): p is string => p != null),
|
|
344
|
+
];
|
|
345
|
+
const suggestion =
|
|
346
|
+
suggestions.length > 0
|
|
347
|
+
? `\n\n💡 이것을 의미했나요?\n${suggestions.map((s) => ` - ${s}`).join("\n")}`
|
|
348
|
+
: "";
|
|
349
|
+
throw new CliError({
|
|
350
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.${suggestion}`,
|
|
351
|
+
hint: `\`seed-design docs ${matchedCategory.id}\`로 목록을 확인해보세요.`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (matched.length === 1) {
|
|
355
|
+
selectedItem = matched[0].item;
|
|
356
|
+
} else {
|
|
357
|
+
const selected = await p.select({
|
|
358
|
+
message: `${highlight(segments[1])}에 해당하는 항목을 선택해주세요`,
|
|
359
|
+
options: matched.map(({ item, sectionLabel }) => ({
|
|
360
|
+
label: `[${sectionLabel}] ${item.title}`,
|
|
361
|
+
value: item,
|
|
362
|
+
hint: item.description,
|
|
363
|
+
})),
|
|
364
|
+
});
|
|
365
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
366
|
+
selectedItem = selected;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} else if (matchedCategory) {
|
|
370
|
+
// Single segment matching a category — drill into it
|
|
371
|
+
if (matchedCategory.sections.length === 1) {
|
|
372
|
+
selectedItem = await selectItem(matchedCategory.sections[0].items);
|
|
373
|
+
} else {
|
|
374
|
+
const section = await selectSection(matchedCategory.sections);
|
|
375
|
+
selectedItem = await selectItem(section.items);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
// No category match — global search
|
|
379
|
+
const matched = searchAllItems(categories, options.query);
|
|
380
|
+
|
|
381
|
+
if (matched.length === 0) {
|
|
382
|
+
const suggestion = buildSuggestionHint(segments, categories);
|
|
383
|
+
throw new CliError({
|
|
384
|
+
message: `${highlight(options.query)}: 문서를 찾을 수 없어요.${suggestion ?? ""}`,
|
|
385
|
+
hint: "`seed-design docs`로 전체 목록을 확인해보세요.",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (matched.length === 1) {
|
|
389
|
+
selectedItem = matched[0].item;
|
|
390
|
+
} else {
|
|
391
|
+
const selected = await p.select({
|
|
392
|
+
message: `${highlight(options.query)}에 해당하는 항목을 선택해주세요`,
|
|
393
|
+
options: matched.map(({ item, categoryLabel, sectionLabel }) => ({
|
|
394
|
+
label: `[${categoryLabel} > ${sectionLabel}] ${item.title}`,
|
|
395
|
+
value: item,
|
|
396
|
+
hint: item.description,
|
|
397
|
+
})),
|
|
398
|
+
});
|
|
399
|
+
if (p.isCancel(selected)) throw new CliCancelError();
|
|
400
|
+
selectedItem = selected;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
// Full interactive flow: category → section → item
|
|
405
|
+
const category = await selectCategory(categories);
|
|
406
|
+
|
|
407
|
+
let section: DocsSection;
|
|
408
|
+
if (category.sections.length === 1) {
|
|
409
|
+
section = category.sections[0];
|
|
410
|
+
} else {
|
|
411
|
+
section = await selectSection(category.sections);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
selectedItem = await selectItem(section.items);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
printDocsResult(selectedItem, baseUrl);
|
|
418
|
+
p.outro("완료했어요.");
|
|
419
|
+
|
|
420
|
+
const duration = Date.now() - startTime;
|
|
421
|
+
try {
|
|
422
|
+
await analytics.track(process.cwd(), {
|
|
423
|
+
event: "docs",
|
|
424
|
+
properties: {
|
|
425
|
+
query: options.query ?? null,
|
|
426
|
+
item_id: selectedItem.id,
|
|
427
|
+
has_snippet: !!(selectedItem.snippets && selectedItem.snippets.length > 0),
|
|
428
|
+
duration_ms: duration,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
} catch (telemetryError) {
|
|
432
|
+
if (verbose) {
|
|
433
|
+
console.error("[Telemetry] docs tracking failed:", telemetryError);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (isCliCancelError(error)) {
|
|
438
|
+
p.outro(highlight(error.message));
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
handleCliError(error, {
|
|
443
|
+
defaultMessage: "문서 조회에 실패했어요.",
|
|
444
|
+
defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
|
|
445
|
+
verbose,
|
|
446
|
+
});
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
};
|