@seed-design/cli 0.0.3 → 1.1.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.md +73 -4
- package/bin/index.mjs +12 -2
- package/package.json +8 -6
- package/src/commands/add-all.ts +201 -0
- package/src/commands/add.ts +149 -149
- package/src/commands/init.ts +42 -8
- package/src/env.d.ts +13 -0
- package/src/index.ts +2 -0
- package/src/schema.ts +38 -64
- package/src/tests/resolve-dependencies.test.ts +424 -0
- package/src/utils/analytics.ts +119 -0
- package/src/utils/fetch.ts +122 -0
- package/src/utils/get-config.ts +15 -48
- package/src/utils/install.ts +7 -12
- package/src/utils/resolve-dependencies.ts +77 -0
- package/src/utils/transformers/index.ts +2 -1
- package/src/utils/transformers/transform-jsx.ts +0 -1
- package/src/utils/transformers/transform-rsc.ts +21 -4
- package/src/utils/write.ts +75 -0
- package/src/test/add-relative-registries.test.ts +0 -182
- package/src/utils/add-relative-registries.ts +0 -43
- package/src/utils/get-metadata.ts +0 -75
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveDependencies } from "../utils/resolve-dependencies";
|
|
3
|
+
import type { PublicRegistry } from "@/src/schema";
|
|
4
|
+
|
|
5
|
+
describe("resolveDependencies", () => {
|
|
6
|
+
it("should resolve a simple item without dependencies", () => {
|
|
7
|
+
const publicRegistries: PublicRegistry[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "ui",
|
|
10
|
+
items: [
|
|
11
|
+
{
|
|
12
|
+
id: "button",
|
|
13
|
+
description: "Button component",
|
|
14
|
+
snippets: [{ path: "button.tsx" }],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const result = resolveDependencies({
|
|
21
|
+
selectedItemKeys: ["ui:button"],
|
|
22
|
+
publicRegistries,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(result.registryItemsToAdd).toHaveLength(1);
|
|
26
|
+
expect(result.registryItemsToAdd[0]).toEqual({
|
|
27
|
+
registryId: "ui",
|
|
28
|
+
items: [
|
|
29
|
+
{
|
|
30
|
+
id: "button",
|
|
31
|
+
description: "Button component",
|
|
32
|
+
snippets: [{ path: "button.tsx" }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
expect(result.npmDependenciesToAdd.size).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should resolve npm dependencies", () => {
|
|
40
|
+
const publicRegistries: PublicRegistry[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "ui",
|
|
43
|
+
items: [
|
|
44
|
+
{
|
|
45
|
+
id: "tabs",
|
|
46
|
+
description: "Tabs component",
|
|
47
|
+
snippets: [{ path: "tabs.tsx" }],
|
|
48
|
+
dependencies: ["@seed-design/react-tabs", "clsx"],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const result = resolveDependencies({
|
|
55
|
+
selectedItemKeys: ["ui:tabs"],
|
|
56
|
+
publicRegistries,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.npmDependenciesToAdd.size).toBe(2);
|
|
60
|
+
expect(result.npmDependenciesToAdd.has("@seed-design/react-tabs")).toBe(true);
|
|
61
|
+
expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should resolve inner dependencies recursively", () => {
|
|
65
|
+
const publicRegistries: PublicRegistry[] = [
|
|
66
|
+
{
|
|
67
|
+
id: "ui",
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
id: "dialog",
|
|
71
|
+
description: "Dialog component",
|
|
72
|
+
snippets: [{ path: "dialog.tsx" }],
|
|
73
|
+
innerDependencies: [
|
|
74
|
+
{
|
|
75
|
+
registryId: "breeze",
|
|
76
|
+
itemIds: ["animate-number"],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "breeze",
|
|
84
|
+
items: [
|
|
85
|
+
{
|
|
86
|
+
id: "animate-number",
|
|
87
|
+
description: "Animate number utility",
|
|
88
|
+
snippets: [{ path: "animate-number.ts" }],
|
|
89
|
+
dependencies: ["framer-motion"],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const result = resolveDependencies({
|
|
96
|
+
selectedItemKeys: ["ui:dialog"],
|
|
97
|
+
publicRegistries,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.registryItemsToAdd).toHaveLength(2);
|
|
101
|
+
expect(result.registryItemsToAdd).toEqual([
|
|
102
|
+
{
|
|
103
|
+
registryId: "ui",
|
|
104
|
+
items: [
|
|
105
|
+
{
|
|
106
|
+
id: "dialog",
|
|
107
|
+
description: "Dialog component",
|
|
108
|
+
snippets: [{ path: "dialog.tsx" }],
|
|
109
|
+
innerDependencies: [
|
|
110
|
+
{
|
|
111
|
+
registryId: "breeze",
|
|
112
|
+
itemIds: ["animate-number"],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
registryId: "breeze",
|
|
120
|
+
items: [
|
|
121
|
+
{
|
|
122
|
+
id: "animate-number",
|
|
123
|
+
description: "Animate number utility",
|
|
124
|
+
snippets: [{ path: "animate-number.ts" }],
|
|
125
|
+
dependencies: ["framer-motion"],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
expect(result.npmDependenciesToAdd.size).toBe(1);
|
|
131
|
+
expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle multiple selected items", () => {
|
|
135
|
+
const publicRegistries: PublicRegistry[] = [
|
|
136
|
+
{
|
|
137
|
+
id: "ui",
|
|
138
|
+
items: [
|
|
139
|
+
{
|
|
140
|
+
id: "button",
|
|
141
|
+
description: "Button component",
|
|
142
|
+
snippets: [{ path: "button.tsx" }],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "chip",
|
|
146
|
+
description: "Chip component",
|
|
147
|
+
snippets: [{ path: "chip.tsx" }],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const result = resolveDependencies({
|
|
154
|
+
selectedItemKeys: ["ui:button", "ui:chip"],
|
|
155
|
+
publicRegistries,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.registryItemsToAdd).toHaveLength(1);
|
|
159
|
+
expect(result.registryItemsToAdd[0].registryId).toBe("ui");
|
|
160
|
+
expect(result.registryItemsToAdd[0].items).toHaveLength(2);
|
|
161
|
+
expect(result.registryItemsToAdd[0].items.map((i) => i.id)).toEqual(["button", "chip"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should prevent duplicate items", () => {
|
|
165
|
+
const publicRegistries: PublicRegistry[] = [
|
|
166
|
+
{
|
|
167
|
+
id: "ui",
|
|
168
|
+
items: [
|
|
169
|
+
{
|
|
170
|
+
id: "dialog",
|
|
171
|
+
description: "Dialog component",
|
|
172
|
+
snippets: [{ path: "dialog.tsx" }],
|
|
173
|
+
innerDependencies: [
|
|
174
|
+
{
|
|
175
|
+
registryId: "ui",
|
|
176
|
+
itemIds: ["button"],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "button",
|
|
182
|
+
description: "Button component",
|
|
183
|
+
snippets: [{ path: "button.tsx" }],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Select both dialog and button, but button is already a dependency of dialog
|
|
190
|
+
const result = resolveDependencies({
|
|
191
|
+
selectedItemKeys: ["ui:dialog", "ui:button"],
|
|
192
|
+
publicRegistries,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result.registryItemsToAdd).toHaveLength(1);
|
|
196
|
+
expect(result.registryItemsToAdd[0].items).toHaveLength(2);
|
|
197
|
+
// Button should appear only once
|
|
198
|
+
const buttonCount = result.registryItemsToAdd[0].items.filter((i) => i.id === "button").length;
|
|
199
|
+
expect(buttonCount).toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should handle nested inner dependencies", () => {
|
|
203
|
+
const publicRegistries: PublicRegistry[] = [
|
|
204
|
+
{
|
|
205
|
+
id: "ui",
|
|
206
|
+
items: [
|
|
207
|
+
{
|
|
208
|
+
id: "complex",
|
|
209
|
+
description: "Complex component",
|
|
210
|
+
snippets: [{ path: "complex.tsx" }],
|
|
211
|
+
innerDependencies: [
|
|
212
|
+
{
|
|
213
|
+
registryId: "ui",
|
|
214
|
+
itemIds: ["dialog"],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "dialog",
|
|
220
|
+
description: "Dialog component",
|
|
221
|
+
snippets: [{ path: "dialog.tsx" }],
|
|
222
|
+
innerDependencies: [
|
|
223
|
+
{
|
|
224
|
+
registryId: "breeze",
|
|
225
|
+
itemIds: ["animate"],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "breeze",
|
|
233
|
+
items: [
|
|
234
|
+
{
|
|
235
|
+
id: "animate",
|
|
236
|
+
description: "Animate utility",
|
|
237
|
+
snippets: [{ path: "animate.ts" }],
|
|
238
|
+
innerDependencies: [
|
|
239
|
+
{
|
|
240
|
+
registryId: "lib",
|
|
241
|
+
itemIds: ["utils"],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "lib",
|
|
249
|
+
items: [
|
|
250
|
+
{
|
|
251
|
+
id: "utils",
|
|
252
|
+
description: "Utility functions",
|
|
253
|
+
snippets: [{ path: "utils.ts" }],
|
|
254
|
+
dependencies: ["lodash"],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const result = resolveDependencies({
|
|
261
|
+
selectedItemKeys: ["ui:complex"],
|
|
262
|
+
publicRegistries,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(result.registryItemsToAdd).toHaveLength(3);
|
|
266
|
+
expect(result.registryItemsToAdd.map((r) => r.registryId)).toEqual(["ui", "breeze", "lib"]);
|
|
267
|
+
expect(result.npmDependenciesToAdd.size).toBe(1);
|
|
268
|
+
expect(result.npmDependenciesToAdd.has("lodash")).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should throw error for invalid snippet format", () => {
|
|
272
|
+
const publicRegistries: PublicRegistry[] = [];
|
|
273
|
+
|
|
274
|
+
expect(() =>
|
|
275
|
+
resolveDependencies({
|
|
276
|
+
selectedItemKeys: ["invalid-format"],
|
|
277
|
+
publicRegistries,
|
|
278
|
+
}),
|
|
279
|
+
).toThrowError('Invalid snippet format: "invalid-format"');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should throw error for non-existent snippet", () => {
|
|
283
|
+
const publicRegistries: PublicRegistry[] = [
|
|
284
|
+
{
|
|
285
|
+
id: "ui",
|
|
286
|
+
items: [],
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
expect(() =>
|
|
291
|
+
resolveDependencies({
|
|
292
|
+
selectedItemKeys: ["ui:non-existent"],
|
|
293
|
+
publicRegistries,
|
|
294
|
+
}),
|
|
295
|
+
).toThrowError('Cannot find snippet: "ui:non-existent"');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should throw error for missing inner dependency", () => {
|
|
299
|
+
const publicRegistries: PublicRegistry[] = [
|
|
300
|
+
{
|
|
301
|
+
id: "ui",
|
|
302
|
+
items: [
|
|
303
|
+
{
|
|
304
|
+
id: "broken",
|
|
305
|
+
description: "Broken component",
|
|
306
|
+
snippets: [{ path: "broken.tsx" }],
|
|
307
|
+
innerDependencies: [
|
|
308
|
+
{
|
|
309
|
+
registryId: "breeze",
|
|
310
|
+
itemIds: ["missing"],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "breeze",
|
|
318
|
+
items: [],
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
expect(() =>
|
|
323
|
+
resolveDependencies({
|
|
324
|
+
selectedItemKeys: ["ui:broken"],
|
|
325
|
+
publicRegistries,
|
|
326
|
+
}),
|
|
327
|
+
).toThrowError("Cannot find dependency item: breeze:missing");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should handle multiple registries with multiple items", () => {
|
|
331
|
+
const publicRegistries: PublicRegistry[] = [
|
|
332
|
+
{
|
|
333
|
+
id: "ui",
|
|
334
|
+
items: [
|
|
335
|
+
{
|
|
336
|
+
id: "button",
|
|
337
|
+
description: "Button component",
|
|
338
|
+
snippets: [{ path: "button.tsx" }],
|
|
339
|
+
dependencies: ["clsx"],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "chip",
|
|
343
|
+
description: "Chip component",
|
|
344
|
+
snippets: [{ path: "chip.tsx" }],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: "breeze",
|
|
350
|
+
items: [
|
|
351
|
+
{
|
|
352
|
+
id: "animate",
|
|
353
|
+
description: "Animate utility",
|
|
354
|
+
snippets: [{ path: "animate.ts" }],
|
|
355
|
+
dependencies: ["framer-motion"],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const result = resolveDependencies({
|
|
362
|
+
selectedItemKeys: ["ui:button", "breeze:animate", "ui:chip"],
|
|
363
|
+
publicRegistries,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(result.registryItemsToAdd).toHaveLength(2);
|
|
367
|
+
|
|
368
|
+
const uiRegistry = result.registryItemsToAdd.find((r) => r.registryId === "ui");
|
|
369
|
+
expect(uiRegistry?.items).toHaveLength(2);
|
|
370
|
+
expect(uiRegistry?.items.map((i) => i.id)).toEqual(["button", "chip"]);
|
|
371
|
+
|
|
372
|
+
const breezeRegistry = result.registryItemsToAdd.find((r) => r.registryId === "breeze");
|
|
373
|
+
expect(breezeRegistry?.items).toHaveLength(1);
|
|
374
|
+
expect(breezeRegistry?.items[0].id).toBe("animate");
|
|
375
|
+
|
|
376
|
+
expect(result.npmDependenciesToAdd.size).toBe(2);
|
|
377
|
+
expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
|
|
378
|
+
expect(result.npmDependenciesToAdd.has("framer-motion")).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("should collect all npm dependencies from nested dependencies", () => {
|
|
382
|
+
const publicRegistries: PublicRegistry[] = [
|
|
383
|
+
{
|
|
384
|
+
id: "ui",
|
|
385
|
+
items: [
|
|
386
|
+
{
|
|
387
|
+
id: "rich",
|
|
388
|
+
description: "Rich component",
|
|
389
|
+
snippets: [{ path: "rich.tsx" }],
|
|
390
|
+
dependencies: ["react-hook-form"],
|
|
391
|
+
innerDependencies: [
|
|
392
|
+
{
|
|
393
|
+
registryId: "ui",
|
|
394
|
+
itemIds: ["field", "label"],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "field",
|
|
400
|
+
description: "Field component",
|
|
401
|
+
snippets: [{ path: "field.tsx" }],
|
|
402
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "label",
|
|
406
|
+
description: "Label component",
|
|
407
|
+
snippets: [{ path: "label.tsx" }],
|
|
408
|
+
dependencies: ["clsx"],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const result = resolveDependencies({
|
|
415
|
+
selectedItemKeys: ["ui:rich"],
|
|
416
|
+
publicRegistries,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(result.npmDependenciesToAdd.size).toBe(3);
|
|
420
|
+
expect(result.npmDependenciesToAdd.has("react-hook-form")).toBe(true);
|
|
421
|
+
expect(result.npmDependenciesToAdd.has("clsx")).toBe(true);
|
|
422
|
+
expect(result.npmDependenciesToAdd.has("tailwind-merge")).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { getConfig } from "./get-config";
|
|
4
|
+
|
|
5
|
+
const EVENT_PREFIX = "seed_cli";
|
|
6
|
+
|
|
7
|
+
interface TrackOptions {
|
|
8
|
+
event: string;
|
|
9
|
+
properties?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 텔레메트리 활성화 여부를 확인합니다.
|
|
14
|
+
* 우선순위:
|
|
15
|
+
* 1. 환경 변수 DISABLE_TELEMETRY
|
|
16
|
+
* 2. 환경 변수 SEED_DISABLE_TELEMETRY
|
|
17
|
+
* 3. seed-design.json의 telemetry 설정
|
|
18
|
+
* 4. 기본값 true (Opt-out)
|
|
19
|
+
*/
|
|
20
|
+
async function isTelemetryEnabled(cwd: string): Promise<boolean> {
|
|
21
|
+
// 1. 환경 변수 체크
|
|
22
|
+
if (process.env.DISABLE_TELEMETRY === "true") return false;
|
|
23
|
+
if (process.env.SEED_DISABLE_TELEMETRY === "true") return false;
|
|
24
|
+
|
|
25
|
+
// 2. seed-design.json 체크
|
|
26
|
+
try {
|
|
27
|
+
const config = await getConfig(cwd);
|
|
28
|
+
if (config?.telemetry === false) return false;
|
|
29
|
+
} catch {
|
|
30
|
+
// 설정 파일이 없거나 읽기 실패 시 기본값 사용
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. 기본값
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 익명 세션 ID를 생성합니다.
|
|
39
|
+
* 각 CLI 실행마다 새로운 UUID가 생성됩니다.
|
|
40
|
+
*/
|
|
41
|
+
function generateSessionId(): string {
|
|
42
|
+
return randomUUID();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 세션당 한 번만 생성
|
|
46
|
+
const sessionId = generateSessionId();
|
|
47
|
+
|
|
48
|
+
// 세션당 한 번만 메시지 표시
|
|
49
|
+
let hasShownMessage = false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* PostHog에 이벤트를 전송합니다.
|
|
53
|
+
*/
|
|
54
|
+
async function track(cwd: string, { event, properties = {} }: TrackOptions): Promise<void> {
|
|
55
|
+
const enabled = await isTelemetryEnabled(cwd);
|
|
56
|
+
|
|
57
|
+
if (!enabled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fullEvent = `${EVENT_PREFIX}.${event}`;
|
|
62
|
+
|
|
63
|
+
// Dev 모드: 콘솔에만 출력
|
|
64
|
+
if (process.env.NODE_ENV === "dev") {
|
|
65
|
+
console.log(`📊 [Telemetry] ${fullEvent}`, properties);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 사용자에게 텔레메트리 수집 중임을 알림 (세션당 한 번만)
|
|
70
|
+
if (!hasShownMessage) {
|
|
71
|
+
p.log.info(
|
|
72
|
+
"📊 사용 데이터 수집 중 (비활성화: seed-design.json 또는 DISABLE_TELEMETRY 환경 변수)",
|
|
73
|
+
);
|
|
74
|
+
hasShownMessage = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// PostHog API 호출 (fire-and-forget)
|
|
78
|
+
try {
|
|
79
|
+
if (!process.env.POSTHOG_HOST || !process.env.POSTHOG_API_KEY) {
|
|
80
|
+
console.warn("[Analytics] Missing POSTHOG_HOST or POSTHOG_API_KEY");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const url = `${process.env.POSTHOG_HOST}/capture`;
|
|
85
|
+
const headers = {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const payload = {
|
|
90
|
+
api_key: process.env.POSTHOG_API_KEY,
|
|
91
|
+
event: fullEvent,
|
|
92
|
+
distinct_id: sessionId,
|
|
93
|
+
properties: {
|
|
94
|
+
...properties,
|
|
95
|
+
$process_person_profile: false,
|
|
96
|
+
},
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
// 5초 타임아웃 설정
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
102
|
+
try {
|
|
103
|
+
await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(payload),
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
});
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// 에러 발생 시 조용히 무시 (CLI 블로킹 방지)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const analytics = {
|
|
118
|
+
track,
|
|
119
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { PublicRegistry } from "@/src/schema";
|
|
2
|
+
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import {
|
|
5
|
+
type PublicRegistryItem,
|
|
6
|
+
publicRegistrySchema,
|
|
7
|
+
publicRegistryItemSchema,
|
|
8
|
+
type PublicAvailableRegistries,
|
|
9
|
+
publicAvailableRegistriesSchema,
|
|
10
|
+
} from "@/src/schema";
|
|
11
|
+
|
|
12
|
+
export async function fetchAvailableRegistries({
|
|
13
|
+
baseUrl,
|
|
14
|
+
}: {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
}): Promise<PublicAvailableRegistries> {
|
|
17
|
+
// TODO: make this file public
|
|
18
|
+
const response = await fetch(`${baseUrl}/__registry__/index.json`);
|
|
19
|
+
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw new Error(`Failed to fetch registries: ${response.status} ${response.statusText}`);
|
|
22
|
+
|
|
23
|
+
const registries = await response.json();
|
|
24
|
+
const {
|
|
25
|
+
success,
|
|
26
|
+
data: parsedRegistries,
|
|
27
|
+
error,
|
|
28
|
+
} = publicAvailableRegistriesSchema.safeParse(registries);
|
|
29
|
+
|
|
30
|
+
if (!success) throw new Error(`Failed to parse registries: ${error?.message}`);
|
|
31
|
+
|
|
32
|
+
return parsedRegistries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function fetchRegistry({
|
|
36
|
+
baseUrl,
|
|
37
|
+
registryId,
|
|
38
|
+
}: {
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
registryId: PublicRegistry["id"];
|
|
41
|
+
}): Promise<PublicRegistry> {
|
|
42
|
+
const response = await fetch(`${baseUrl}/__registry__/${registryId}/index.json`);
|
|
43
|
+
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Failed to fetch ${registryId} registry: ${response.status} ${response.statusText}`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const index = await response.json();
|
|
50
|
+
const { success, data: parsedIndex, error } = publicRegistrySchema.safeParse(index);
|
|
51
|
+
|
|
52
|
+
if (!success) throw new Error(`Failed to parse ${registryId} registry: ${error?.message}`);
|
|
53
|
+
|
|
54
|
+
return parsedIndex;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchRegistryItem({
|
|
58
|
+
baseUrl,
|
|
59
|
+
registryId,
|
|
60
|
+
registryItemId,
|
|
61
|
+
}: {
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
registryId: PublicRegistry["id"];
|
|
64
|
+
registryItemId: PublicRegistryItem["id"];
|
|
65
|
+
}): Promise<PublicRegistryItem> {
|
|
66
|
+
const response = await fetch(`${baseUrl}/__registry__/${registryId}/${registryItemId}.json`);
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`Failed to fetch ${registryItemId}: ${response.status} ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const item = await response.json();
|
|
73
|
+
const { success, data: parsedItem, error } = publicRegistryItemSchema.safeParse(item);
|
|
74
|
+
|
|
75
|
+
if (!success) {
|
|
76
|
+
throw new Error(`Failed to parse ${registryItemId}: ${error?.message}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parsedItem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function fetchRegistryItems({
|
|
83
|
+
baseUrl,
|
|
84
|
+
registryId,
|
|
85
|
+
registryItemIds,
|
|
86
|
+
}: {
|
|
87
|
+
baseUrl: string;
|
|
88
|
+
registryId: PublicRegistry["id"];
|
|
89
|
+
registryItemIds: PublicRegistryItem["id"][];
|
|
90
|
+
}): Promise<PublicRegistryItem[]> {
|
|
91
|
+
return await Promise.all(
|
|
92
|
+
registryItemIds.map(async (itemId) => {
|
|
93
|
+
try {
|
|
94
|
+
return await fetchRegistryItem({ baseUrl, registryId, registryItemId: itemId });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// show available registry items in the registry
|
|
97
|
+
const response = await fetch(`${baseUrl}/__registry__/${registryId}/index.json`);
|
|
98
|
+
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${registryId} 레지스트리를 가져오지 못했어요: ${response.status} ${response.statusText}`,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const index = await response.json();
|
|
105
|
+
const { success, data: parsedIndex } = publicRegistrySchema.safeParse(index);
|
|
106
|
+
|
|
107
|
+
// fatal, should not happen
|
|
108
|
+
if (!success) throw new Error(`Failed to parse registry index for ${registryId}`);
|
|
109
|
+
|
|
110
|
+
p.log.error(`${itemId} 스니펫이 ${registryId} 레지스트리에 없어요.`);
|
|
111
|
+
p.log.info(
|
|
112
|
+
`${registryId} 레지스트리에 존재하는 스니펫:\n${parsedIndex.items
|
|
113
|
+
.map((component) => component.id)
|
|
114
|
+
.join("\n")}`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// so fetchRegistryItems also can throw
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
}
|