@seed-design/cli 1.2.0 → 1.2.2

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.
@@ -11,6 +11,18 @@ import { BASE_URL } from "../constants";
11
11
  import { highlight } from "../utils/color";
12
12
  import { installDependencies } from "../utils/install";
13
13
  import { analytics } from "../utils/analytics";
14
+ import {
15
+ analyzeRegistryItemCompatibility,
16
+ getProjectSeedPackageVersionSpecs,
17
+ logCompatibilityReport,
18
+ } from "../utils/compatibility";
19
+ import {
20
+ CliCancelError,
21
+ CliError,
22
+ handleCliError,
23
+ isCliCancelError,
24
+ isVerboseMode,
25
+ } from "../utils/error";
14
26
 
15
27
  const addOptionsSchema = z.object({
16
28
  itemIds: z.array(z.string()).optional(),
@@ -20,7 +32,7 @@ const addOptionsSchema = z.object({
20
32
  all: z.boolean(),
21
33
  cwd: z.string(),
22
34
  baseUrl: z.string().optional(),
23
- overwrite: z.boolean().optional(),
35
+ onDiff: z.enum(["overwrite", "backup"]).optional(),
24
36
  });
25
37
 
26
38
  export const addCommand = (cli: CAC) => {
@@ -37,157 +49,173 @@ export const addCommand = (cli: CAC) => {
37
49
  "the base url of the registry. defaults to the current directory.",
38
50
  { default: BASE_URL },
39
51
  )
40
- .option("--overwrite", "Overwrite existing files without confirmation", {
41
- default: false,
42
- })
52
+ .option("--on-diff <mode>", "Action when file differs: overwrite or backup")
43
53
  .example("seed-design add ui:action-button")
44
54
  .example("seed-design add ui:alert-dialog")
45
55
  .action(async (itemIds, opts) => {
46
56
  const startTime = Date.now();
57
+ const verbose = isVerboseMode(opts);
47
58
  p.intro("seed-design add");
48
59
 
49
- const {
50
- success,
51
- data: { all, ...options },
52
- error,
53
- } = addOptionsSchema.safeParse({ itemIds, ...opts });
60
+ try {
61
+ const parsed = addOptionsSchema.safeParse({ itemIds, ...opts });
62
+ if (!parsed.success) {
63
+ throw parsed.error;
64
+ }
54
65
 
55
- if (!success) {
56
- p.log.error(`잘못된 옵션이에요: ${error?.message}`);
66
+ const {
67
+ data: { all, ...options },
68
+ } = parsed;
57
69
 
58
- process.exit(1);
59
- }
70
+ if (all) {
71
+ throw new CliError({
72
+ message:
73
+ "`--all` 옵션은 더 이상 지원되지 않아요. 대신 `seed-design add-all` 명령어를 사용해주세요.",
74
+ });
75
+ }
60
76
 
61
- if (all) {
62
- p.log.error(
63
- "`--all` 옵션은 이상 지원되지 않아요. 대신 `seed-design add-all` 명령어를 사용해주세요.",
64
- );
77
+ const cwd = options.cwd;
78
+ const baseUrl = options.baseUrl;
79
+ const config = await getConfig(cwd);
80
+ const rootPath = path.resolve(cwd, config.path);
65
81
 
66
- process.exit(1);
67
- }
82
+ const { start, stop } = p.spinner();
83
+ start("Registry를 가져오고 있어요...");
68
84
 
69
- const cwd = options.cwd;
70
- const baseUrl = options.baseUrl;
71
- const config = await getConfig(cwd);
72
- const rootPath = path.resolve(cwd, config.path);
73
-
74
- const { start, stop } = p.spinner();
75
-
76
- start("Registry를 가져오고 있어요...");
77
-
78
- const publicRegistries = await Promise.all(
79
- (await fetchAvailableRegistries({ baseUrl })).map(async ({ id }) =>
80
- fetchRegistry({ baseUrl, registryId: id }),
81
- ),
82
- );
83
-
84
- stop("Registry를 가져왔어요.");
85
-
86
- const selectedItemKeys: string[] = await (async () => {
87
- if (options.itemIds.length > 0) return options.itemIds;
88
-
89
- const selected = await p.multiselect({
90
- message: "추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)",
91
- options: publicRegistries
92
- .filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
93
- .flatMap(({ id: registryId, items }) =>
94
- items
95
- .filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
96
- .sort((a, b) => a.id.localeCompare(b.id))
97
- .map(({ id, description, deprecated }) => ({
98
- label: `${deprecated ? "(deprecated) " : ""}${highlight(registryId)}:${id}`,
99
- value: `${registryId}:${id}`,
100
- hint: description,
101
-
102
- // used for sorting
103
- deprecated,
104
- registryItemCount: items.length,
105
- })),
106
- )
107
- .sort((a, b) => {
108
- if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
109
-
110
- return b.registryItemCount - a.registryItemCount;
111
- }),
112
- });
85
+ const publicRegistries = await (async () => {
86
+ try {
87
+ const registries = await Promise.all(
88
+ (await fetchAvailableRegistries({ baseUrl })).map(async ({ id }) =>
89
+ fetchRegistry({ baseUrl, registryId: id }),
90
+ ),
91
+ );
92
+ stop("Registry를 가져왔어요.");
113
93
 
114
- if (p.isCancel(selected)) {
115
- p.log.error("취소되었어요.");
116
- process.exit(0);
117
- }
94
+ return registries;
95
+ } catch (error) {
96
+ stop("Registry를 가져오지 못했어요.");
97
+ throw error;
98
+ }
99
+ })();
118
100
 
119
- return selected;
120
- })();
101
+ const selectedItemKeys: string[] = await (async () => {
102
+ if (options.itemIds?.length) {
103
+ return options.itemIds;
104
+ }
121
105
 
122
- if (!selectedItemKeys?.length) {
123
- p.log.error("항목을 찾을 없어요.");
106
+ const selected = await p.multiselect({
107
+ message: "추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)",
108
+ options: publicRegistries
109
+ .filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
110
+ .flatMap(({ id: registryId, items }) =>
111
+ items
112
+ .filter(({ hideFromCLICatalog }) => !hideFromCLICatalog)
113
+ .sort((a, b) => a.id.localeCompare(b.id))
114
+ .map(({ id, description, deprecated }) => ({
115
+ label: `${deprecated ? "(deprecated) " : ""}${highlight(registryId)}:${id}`,
116
+ value: `${registryId}:${id}`,
117
+ hint: description,
118
+
119
+ // used for sorting
120
+ deprecated,
121
+ registryItemCount: items.length,
122
+ })),
123
+ )
124
+ .sort((a, b) => {
125
+ if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1;
126
+
127
+ return b.registryItemCount - a.registryItemCount;
128
+ }),
129
+ });
124
130
 
125
- process.exit(0);
126
- }
131
+ if (p.isCancel(selected)) {
132
+ throw new CliCancelError();
133
+ }
127
134
 
128
- p.log.message(`선택된 항목: ${highlight(selectedItemKeys.join(", "))}`);
135
+ return selected;
136
+ })();
129
137
 
130
- const filteredItemKeys: string[] = [];
138
+ if (!selectedItemKeys?.length) {
139
+ throw new CliCancelError("추가할 항목이 선택되지 않았어요.");
140
+ }
131
141
 
132
- for (const itemKey of selectedItemKeys) {
133
- const [registryId, ...rest] = itemKey.split(":");
134
- const itemId = rest.join(":");
142
+ p.log.message(`선택된 항목: ${highlight(selectedItemKeys.join(", "))}`);
135
143
 
136
- if (!registryId || !itemId) {
137
- p.log.error(
138
- `${highlight(itemKey)}: 항목 이름이 잘못되었어요. ${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
139
- );
144
+ const filteredItemKeys: string[] = [];
140
145
 
141
- process.exit(1);
142
- }
146
+ for (const itemKey of selectedItemKeys) {
147
+ const [registryId, ...rest] = itemKey.split(":");
148
+ const itemId = rest.join(":");
143
149
 
144
- const foundItem = publicRegistries
145
- .find((r) => r.id === registryId)
146
- ?.items.find((i) => i.id === itemId);
150
+ if (!registryId || !itemId) {
151
+ throw new CliError({
152
+ message: `${highlight(itemKey)}: 항목 이름이 잘못되었어요.`,
153
+ hint: `${highlight("ui:action-button")}과 같은 형식으로 입력해보세요.`,
154
+ });
155
+ }
147
156
 
148
- if (!foundItem) {
149
- p.log.error(`${highlight(itemKey)}: 항목을 찾을 없어요.`);
157
+ const foundItem = publicRegistries
158
+ .find((r) => r.id === registryId)
159
+ ?.items.find((i) => i.id === itemId);
150
160
 
151
- process.exit(1);
152
- }
161
+ if (!foundItem) {
162
+ throw new CliError({
163
+ message: `${highlight(itemKey)}: 항목을 찾을 수 없어요.`,
164
+ });
165
+ }
153
166
 
154
- if (foundItem.deprecated) {
155
- const confirm = await p.confirm({
156
- message: `${highlight(foundItem.id)}: deprecated 되었어요. 추가할까요?`,
157
- initialValue: false,
158
- });
167
+ if (foundItem.deprecated) {
168
+ const confirm = await p.confirm({
169
+ message: `${highlight(foundItem.id)}: deprecated 되었어요. 추가할까요?`,
170
+ initialValue: false,
171
+ });
159
172
 
160
- if (confirm === false || p.isCancel(confirm)) {
161
- p.log.info(`${highlight(foundItem.id)}: 추가하지 않을게요.`);
173
+ if (p.isCancel(confirm)) {
174
+ throw new CliCancelError();
175
+ }
162
176
 
163
- continue;
177
+ if (confirm === false) {
178
+ p.log.info(`${highlight(foundItem.id)}: 추가하지 않을게요.`);
179
+ continue;
180
+ }
164
181
  }
182
+
183
+ filteredItemKeys.push(itemKey);
165
184
  }
166
185
 
167
- filteredItemKeys.push(itemKey);
168
- }
186
+ const { registryItemsToAdd, npmDependenciesToAdd } = resolveDependencies({
187
+ selectedItemKeys: filteredItemKeys,
188
+ publicRegistries,
189
+ });
190
+
191
+ const compatibilityReport = analyzeRegistryItemCompatibility({
192
+ publicRegistries,
193
+ itemKeys: registryItemsToAdd.flatMap(({ registryId, items }) =>
194
+ items.map((item) => `${registryId}:${item.id}`),
195
+ ),
196
+ projectPackageVersions: getProjectSeedPackageVersionSpecs(options.cwd),
197
+ });
169
198
 
170
- const { registryItemsToAdd, npmDependenciesToAdd } = resolveDependencies({
171
- selectedItemKeys: filteredItemKeys,
172
- publicRegistries,
173
- });
199
+ logCompatibilityReport({
200
+ report: compatibilityReport,
201
+ title: "현재 프로젝트 버전과 호환되지 않을 수 있는 스니펫이 있어요.",
202
+ });
174
203
 
175
- p.log.info(
176
- `추가할 항목: ${highlight(registryItemsToAdd.map((r) => r.items.map((i) => `${r.registryId}:${i.id}`).join(", ")).join(", ") || "없음")}
204
+ p.log.info(
205
+ `추가할 항목: ${highlight(registryItemsToAdd.map((r) => r.items.map((i) => `${r.registryId}:${i.id}`).join(", ")).join(", ") || "없음")}
177
206
 
178
207
  설치할 의존성: ${highlight(Array.from(npmDependenciesToAdd).join(", ") || "없음")}`,
179
- );
208
+ );
180
209
 
181
- await writeRegistryItemSnippets({
182
- registryItemsToAdd,
183
- rootPath,
184
- cwd,
185
- baseUrl,
186
- config,
187
- overwrite: options.overwrite,
188
- });
210
+ await writeRegistryItemSnippets({
211
+ registryItemsToAdd,
212
+ rootPath,
213
+ cwd,
214
+ baseUrl,
215
+ config,
216
+ onDiff: options.onDiff,
217
+ });
189
218
 
190
- try {
191
219
  const { installed, filtered } = await installDependencies({
192
220
  cwd,
193
221
  deps: Array.from(npmDependenciesToAdd),
@@ -207,31 +235,46 @@ export const addCommand = (cli: CAC) => {
207
235
  }
208
236
  }
209
237
  p.outro("완료했어요.");
238
+
239
+ // add 성공 이벤트 추적
240
+ const duration = Date.now() - startTime;
241
+ const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
242
+ const hasDeprecated = selectedItemKeys.some((itemKey) => {
243
+ const [registryId, ...rest] = itemKey.split(":");
244
+ const itemId = rest.join(":");
245
+ return publicRegistries
246
+ .find((r) => r.id === registryId)
247
+ ?.items.find((i) => i.id === itemId)?.deprecated;
248
+ });
249
+
250
+ try {
251
+ await analytics.track(options.cwd, {
252
+ event: "add",
253
+ properties: {
254
+ items_count: filteredItemKeys.length,
255
+ registries: Array.from(uniqueRegistries),
256
+ has_deprecated: hasDeprecated,
257
+ dependencies_count: npmDependenciesToAdd.size,
258
+ duration_ms: duration,
259
+ },
260
+ });
261
+ } catch (telemetryError) {
262
+ if (verbose) {
263
+ console.error("[Telemetry] add tracking failed:", telemetryError);
264
+ }
265
+ }
210
266
  } catch (error) {
211
- p.log.error(`추가에 실패했어요. ${error}`);
212
- p.outro(highlight("작업이 취소됐어요."));
267
+ if (isCliCancelError(error)) {
268
+ p.outro(highlight(error.message));
269
+ process.exit(0);
270
+ }
271
+
272
+ handleCliError(error, {
273
+ defaultMessage: "추가에 실패했어요.",
274
+ defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
275
+ verbose,
276
+ });
213
277
  process.exit(1);
214
278
  }
215
-
216
- // add 성공 이벤트 추적
217
- const duration = Date.now() - startTime;
218
- const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
219
- const hasDeprecated = selectedItemKeys.some((itemKey) => {
220
- const [registryId, ...rest] = itemKey.split(":");
221
- const itemId = rest.join(":");
222
- return publicRegistries.find((r) => r.id === registryId)?.items.find((i) => i.id === itemId)
223
- ?.deprecated;
224
- });
225
-
226
- await analytics.track(options.cwd, {
227
- event: "add",
228
- properties: {
229
- items_count: filteredItemKeys.length,
230
- registries: Array.from(uniqueRegistries),
231
- has_deprecated: hasDeprecated,
232
- dependencies_count: npmDependenciesToAdd.size,
233
- duration_ms: duration,
234
- },
235
- });
236
279
  });
237
280
  };
@@ -0,0 +1,281 @@
1
+ import { fetchAvailableRegistries, fetchRegistry } from "@/src/utils/fetch";
2
+ import { getRawConfig } from "@/src/utils/get-config";
3
+ import * as p from "@clack/prompts";
4
+ import path from "path";
5
+ import { z } from "zod";
6
+
7
+ import type { CAC } from "cac";
8
+ import { BASE_URL } from "../constants";
9
+ import { analytics } from "../utils/analytics";
10
+ import { highlight } from "../utils/color";
11
+ import {
12
+ analyzeRegistryItemCompatibility,
13
+ findInstalledSnippetItemKeys,
14
+ getProjectSeedPackageVersionSpecs,
15
+ logCompatibilityReport,
16
+ } from "../utils/compatibility";
17
+ import { CliError, handleCliError, isCliCancelError, isVerboseMode } from "../utils/error";
18
+
19
+ const compatOptionsSchema = z.object({
20
+ itemIds: z.array(z.string()).optional(),
21
+ component: z.union([z.string(), z.array(z.string())]).optional(),
22
+ all: z.boolean(),
23
+ registry: z.string().optional(),
24
+ cwd: z.string(),
25
+ baseUrl: z.string().optional(),
26
+ });
27
+
28
+ function parseTargetInputs({
29
+ itemIds,
30
+ component,
31
+ }: {
32
+ itemIds?: string[];
33
+ component?: string | string[];
34
+ }) {
35
+ const normalizeInput = (value: string) => value.trim().replace(/\s+/g, "-");
36
+ const itemInputs = (itemIds ?? []).map(normalizeInput).filter(Boolean);
37
+ const componentInputs = (Array.isArray(component) ? component : [component])
38
+ .filter((value): value is string => !!value)
39
+ .flatMap((value) => value.split(","))
40
+ .map(normalizeInput)
41
+ .filter(Boolean);
42
+
43
+ return Array.from(new Set([...itemInputs, ...componentInputs]));
44
+ }
45
+
46
+ function resolveExplicitItemKeys({
47
+ publicRegistries,
48
+ targetInputs,
49
+ defaultRegistry,
50
+ }: {
51
+ publicRegistries: Array<{ id?: string; items?: Array<{ id?: string }> }>;
52
+ targetInputs: string[];
53
+ defaultRegistry?: string;
54
+ }) {
55
+ const allItemKeys = publicRegistries
56
+ .filter((registry): registry is { id: string; items: Array<{ id: string }> } => {
57
+ return typeof registry.id === "string" && Array.isArray(registry.items);
58
+ })
59
+ .flatMap((registry) =>
60
+ registry.items
61
+ .filter((item): item is { id: string } => typeof item.id === "string")
62
+ .map((item) => `${registry.id}:${item.id}`),
63
+ );
64
+ const result = new Set<string>();
65
+
66
+ for (const input of targetInputs) {
67
+ const itemKey = input.includes(":")
68
+ ? input
69
+ : defaultRegistry
70
+ ? `${defaultRegistry}:${input}`
71
+ : (() => {
72
+ const matchedItemKeys = allItemKeys.filter((itemKey) => itemKey.endsWith(`:${input}`));
73
+ if (!matchedItemKeys.length) {
74
+ throw new CliError({
75
+ message: `${highlight(input)}: 항목을 찾을 수 없어요.`,
76
+ hint: `${highlight("ui:action-button")}처럼 registry를 포함해서 입력해보세요.`,
77
+ });
78
+ }
79
+
80
+ if (matchedItemKeys.length > 1) {
81
+ throw new CliError({
82
+ message: `${highlight(input)}: 같은 이름의 항목이 여러 레지스트리에 있어요.`,
83
+ details: matchedItemKeys.map((itemKey) => `- ${itemKey}`),
84
+ hint: `${highlight("ui:action-button")}처럼 registry를 포함해서 입력해보세요.`,
85
+ });
86
+ }
87
+
88
+ return matchedItemKeys[0];
89
+ })();
90
+
91
+ if (!allItemKeys.includes(itemKey)) {
92
+ throw new CliError({
93
+ message: `${highlight(itemKey)}: 항목을 찾을 수 없어요.`,
94
+ });
95
+ }
96
+
97
+ result.add(itemKey);
98
+ }
99
+
100
+ return Array.from(result);
101
+ }
102
+
103
+ export const compatCommand = (cli: CAC) => {
104
+ cli
105
+ .command("compat [...item-ids]", "check snippet compatibility")
106
+ .option("-c, --component <component>", "검사할 컴포넌트. 여러 번 또는 쉼표로 지정 가능")
107
+ .option("-a, --all", "모든 registry 항목을 검사", {
108
+ default: false,
109
+ })
110
+ .option("-r, --registry <registryId>", "컴포넌트 shorthand 입력 시 기본 registry")
111
+ .option("--cwd <cwd>", "the working directory. defaults to the current directory.", {
112
+ default: process.cwd(),
113
+ })
114
+ .option(
115
+ "-u, --baseUrl <baseUrl>",
116
+ "the base url of the registry. defaults to the current directory.",
117
+ { default: BASE_URL },
118
+ )
119
+ .example("seed-design compat")
120
+ .example("seed-design compat -c action-button")
121
+ .example("seed-design compat ui:action-button ui:alert-dialog")
122
+ .example("seed-design compat --all")
123
+ .action(async (itemIds, opts) => {
124
+ const startTime = Date.now();
125
+ const verbose = isVerboseMode(opts);
126
+ p.intro("seed-design compat");
127
+
128
+ try {
129
+ const parsed = compatOptionsSchema.safeParse({ itemIds, ...opts });
130
+ if (!parsed.success) {
131
+ throw parsed.error;
132
+ }
133
+
134
+ const { data: options } = parsed;
135
+ const { start, stop } = p.spinner();
136
+
137
+ start("Registry를 가져오고 있어요...");
138
+ const publicRegistries = await (async () => {
139
+ try {
140
+ const registries = await Promise.all(
141
+ (await fetchAvailableRegistries({ baseUrl: options.baseUrl })).map(async ({ id }) =>
142
+ fetchRegistry({ baseUrl: options.baseUrl, registryId: id }),
143
+ ),
144
+ );
145
+ stop("Registry를 가져왔어요.");
146
+ return registries;
147
+ } catch (error) {
148
+ stop("Registry를 가져오지 못했어요.");
149
+ throw error;
150
+ }
151
+ })();
152
+
153
+ const targetInputs = parseTargetInputs({
154
+ itemIds: options.itemIds,
155
+ component: options.component,
156
+ });
157
+
158
+ const targetItemKeys = (() => {
159
+ if (options.all) {
160
+ return publicRegistries.flatMap((registry) =>
161
+ registry.items.map((item) => `${registry.id}:${item.id}`),
162
+ );
163
+ }
164
+
165
+ if (targetInputs.length > 0) {
166
+ return resolveExplicitItemKeys({
167
+ publicRegistries,
168
+ targetInputs,
169
+ defaultRegistry: options.registry,
170
+ });
171
+ }
172
+
173
+ const rawConfigPromise = getRawConfig(options.cwd);
174
+ return rawConfigPromise;
175
+ })();
176
+
177
+ const resolvedTargetItemKeys = Array.isArray(targetItemKeys)
178
+ ? targetItemKeys
179
+ : await (async () => {
180
+ const rawConfig = await targetItemKeys;
181
+ if (!rawConfig) {
182
+ throw new CliError({
183
+ message: "seed-design.json 파일이 없어 설치된 스니펫 경로를 알 수 없어요.",
184
+ hint: "`seed-design init`으로 설정을 만든 뒤 실행하거나, `--all`/`-c`로 검사 대상을 직접 지정해주세요.",
185
+ });
186
+ }
187
+
188
+ const rootPath = path.resolve(options.cwd, rawConfig.path);
189
+ const installedItemKeys = findInstalledSnippetItemKeys({
190
+ publicRegistries,
191
+ rootPath,
192
+ });
193
+
194
+ if (!installedItemKeys.length) {
195
+ p.log.info(
196
+ `${highlight(path.relative(options.cwd, rootPath) || rawConfig.path)}에서 설치된 스니펫을 찾지 못했어요.`,
197
+ );
198
+ return [];
199
+ }
200
+
201
+ return installedItemKeys;
202
+ })();
203
+
204
+ if (!resolvedTargetItemKeys.length) {
205
+ p.outro("검사할 스니펫이 없어요.");
206
+ process.exit(0);
207
+ }
208
+
209
+ const projectPackageVersions = getProjectSeedPackageVersionSpecs(options.cwd);
210
+ const compatibilityReport = analyzeRegistryItemCompatibility({
211
+ publicRegistries,
212
+ itemKeys: resolvedTargetItemKeys,
213
+ projectPackageVersions,
214
+ });
215
+
216
+ p.log.info(`검사 대상: ${highlight(compatibilityReport.checkedItemKeys.join(", "))}`);
217
+
218
+ if (!compatibilityReport.issues.length) {
219
+ p.outro("모든 스니펫이 현재 @seed-design/react, @seed-design/css와 호환돼요.");
220
+
221
+ try {
222
+ await analytics.track(options.cwd, {
223
+ event: "compat",
224
+ properties: {
225
+ checked_items_count: compatibilityReport.checkedItemKeys.length,
226
+ incompatible_items_count: 0,
227
+ duration_ms: Date.now() - startTime,
228
+ },
229
+ });
230
+ } catch (telemetryError) {
231
+ if (verbose) {
232
+ console.error("[Telemetry] compat tracking failed:", telemetryError);
233
+ }
234
+ }
235
+
236
+ process.exit(0);
237
+ }
238
+
239
+ logCompatibilityReport({
240
+ report: compatibilityReport,
241
+ title: "현재 프로젝트 버전과 호환되지 않는 스니펫을 찾았어요.",
242
+ });
243
+ p.log.info(
244
+ "필요한 버전으로 @seed-design/react 또는 @seed-design/css를 맞춘 뒤 다시 실행해보세요.",
245
+ );
246
+ p.outro("호환성 이슈가 있어요.");
247
+
248
+ try {
249
+ await analytics.track(options.cwd, {
250
+ event: "compat",
251
+ properties: {
252
+ checked_items_count: compatibilityReport.checkedItemKeys.length,
253
+ incompatible_items_count: new Set(
254
+ compatibilityReport.issues.map((issue) => issue.itemKey),
255
+ ).size,
256
+ issue_count: compatibilityReport.issues.length,
257
+ duration_ms: Date.now() - startTime,
258
+ },
259
+ });
260
+ } catch (telemetryError) {
261
+ if (verbose) {
262
+ console.error("[Telemetry] compat tracking failed:", telemetryError);
263
+ }
264
+ }
265
+
266
+ process.exit(1);
267
+ } catch (error) {
268
+ if (isCliCancelError(error)) {
269
+ p.outro(highlight(error.message));
270
+ process.exit(0);
271
+ }
272
+
273
+ handleCliError(error, {
274
+ defaultMessage: "호환성 검사에 실패했어요.",
275
+ defaultHint: "`--verbose` 옵션으로 상세 오류를 확인해보세요.",
276
+ verbose,
277
+ });
278
+ process.exit(1);
279
+ }
280
+ });
281
+ };