@seed-design/cli 1.0.0 → 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 +11 -5
- package/package.json +5 -3
- package/src/commands/add-all.ts +36 -15
- package/src/commands/add.ts +42 -14
- package/src/commands/init.ts +36 -1
- package/src/env.d.ts +13 -0
- package/src/tests/resolve-dependencies.test.ts +3 -3
- package/src/utils/analytics.ts +119 -0
- package/src/utils/get-config.ts +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,76 @@
|
|
|
1
1
|
# @seed-design/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SEED Design 컴포넌트를 프로젝트에 추가하기 위한 CLI 도구입니다.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
## 개발 환경 설정
|
|
6
|
+
|
|
7
|
+
### 의존성 설치
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 환경 변수 설정 (선택사항)
|
|
14
|
+
|
|
15
|
+
PostHog 텔레메트리를 사용하려면 `.env` 파일을 생성하세요:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# packages/cli/.env
|
|
19
|
+
POSTHOG_API_KEY=your-api-key
|
|
20
|
+
POSTHOG_HOST=https://us.i.posthog.com
|
|
21
|
+
|
|
22
|
+
# 텔레메트리 비활성화 (로컬 개발 시)
|
|
23
|
+
DISABLE_TELEMETRY=true
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**참고**: 환경 변수가 없어도 CLI는 정상적으로 동작합니다. 텔레메트리만 비활성화됩니다.
|
|
27
|
+
|
|
28
|
+
## 개발
|
|
29
|
+
|
|
30
|
+
### Dev 모드 실행
|
|
31
|
+
|
|
32
|
+
Watch 모드로 CLI를 실행합니다:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Dev 모드에서는:
|
|
39
|
+
- 코드 변경 시 자동으로 재빌드됩니다
|
|
40
|
+
- `NODE_ENV=dev`로 설정되어 텔레메트리 이벤트가 콘솔에만 출력됩니다
|
|
41
|
+
- PostHog API 호출이 실제로 발생하지 않습니다
|
|
42
|
+
|
|
43
|
+
### 로컬 테스트
|
|
44
|
+
|
|
45
|
+
1. `@seed-design/docs`에서 `bun dev` 실행 (snippet 서버)
|
|
46
|
+
2. `packages/cli`에서 `bun dev` 실행 (watch 모드)
|
|
47
|
+
3. `bun run ./bin/index.mjs` 실행하여 CLI 명령어 테스트:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bun run ./bin/index.mjs init
|
|
51
|
+
bun run ./bin/index.mjs add ui:action-button
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 빌드
|
|
55
|
+
|
|
56
|
+
프로덕션 빌드:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bun run build
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
빌드 결과물:
|
|
63
|
+
- `bin/index.mjs` - 번들링 및 minify된 CLI 실행 파일
|
|
64
|
+
- 환경 변수 (`POSTHOG_API_KEY`, `POSTHOG_HOST`)가 빌드 시 번들에 주입됩니다
|
|
65
|
+
|
|
66
|
+
## 테스트
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bun test
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 배포
|
|
73
|
+
|
|
74
|
+
이 패키지는 [Changesets](https://github.com/changesets/changesets)을 통해 자동으로 배포됩니다.
|
|
75
|
+
|
|
76
|
+
**참고**: 배포 시 GitHub Secrets에 `POSTHOG_API_KEY`와 `POSTHOG_HOST`가 설정되어 있어야 텔레메트리가 활성화됩니다.
|
package/bin/index.mjs
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import*as
|
|
3
|
-
${
|
|
4
|
-
`)}`),
|
|
2
|
+
import*as C from"@clack/prompts";import{cosmiconfig as xe}from"cosmiconfig";import{execa as be}from"execa";import{z as _}from"zod";import he from"picocolors";var a=e=>he.cyan(e);import{detect as we}from"@antfu/ni";async function z(e){let t=await we({programmatic:!0,cwd:e});return t==="yarn@berry"?"yarn":t==="pnpm@6"?"pnpm":t==="bun"?"bun":t==="deno"?"deno":t??"npm"}var te="seed-design",Ie=xe(te,{searchPlaces:[`${te}.json`]}),re=_.object({$schema:_.string().optional(),rsc:_.coerce.boolean().default(!1),tsx:_.coerce.boolean().default(!0),path:_.string(),telemetry:_.coerce.boolean().optional().default(!0)}).strict();async function D(e){let t=await $e(e);return t?re.parse(t):null}async function $e(e){try{let t=await Ie.search(e);return re.parse(t.config)}catch{C.log.error("\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uACBD\uB85C\uC5D0 `seed-design.json` \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC694."),await C.confirm({message:"seed-design.json \uD30C\uC77C\uC744 \uC0DD\uC131\uD558\uC2DC\uACA0\uC5B4\uC694?"})||(C.outro(a("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1));let i=await z(e);await be(i,["seed-design","init","--default"],{cwd:e}),C.log.message("seed-design.json \uD30C\uC77C\uC774 \uC0DD\uC131\uB410\uC5B4\uC694.")}}function L({selectedItemKeys:e,publicRegistries:t}){let i=[],c=new Set;function p(r,n){let l=i.find(o=>o.registryId===r);if(!l?.items.some(o=>o.id===n.id)){if(l?l.items.push(n):i.push({registryId:r,items:[n]}),n.dependencies?.length)for(let o of n.dependencies)c.add(o);if(n.innerDependencies?.length)for(let o of n.innerDependencies)for(let u of o.itemIds){let w=t.find(R=>R.id===o.registryId)?.items.find(R=>R.id===u);if(!w)throw new Error(`Cannot find dependency item: ${o.registryId}:${u}`);p(o.registryId,w)}}}for(let r of e){let[n,...l]=r.split(":"),o=l.join(":");if(!n||!o)throw new Error(`Invalid snippet format: "${r}"`);let u=t.find(w=>w.id===n)?.items.find(w=>w.id===o);if(!u)throw new Error(`Cannot find snippet: "${r}"`);p(n,u)}return{registryItemsToAdd:i,npmDependenciesToAdd:c}}import*as Q from"@clack/prompts";import{z as f}from"zod";var q=f.object({id:f.string(),description:f.string().optional(),deprecated:f.boolean().optional(),hideFromCLICatalog:f.boolean().optional(),dependencies:f.array(f.string()).optional(),innerDependencies:f.array(f.object({registryId:f.string(),itemIds:f.array(f.string())})).optional(),snippets:f.array(f.object({path:f.string(),content:f.string()}))}),W=f.object({id:f.string(),hideFromCLICatalog:f.boolean().optional(),items:f.array(q.omit({snippets:!0}).extend({snippets:f.array(f.object({path:f.string()}))}))}),se=f.array(f.object({id:f.string()}));async function K({baseUrl:e}){let t=await fetch(`${e}/__registry__/index.json`);if(!t.ok)throw new Error(`Failed to fetch registries: ${t.status} ${t.statusText}`);let i=await t.json(),{success:c,data:p,error:r}=se.safeParse(i);if(!c)throw new Error(`Failed to parse registries: ${r?.message}`);return p}async function N({baseUrl:e,registryId:t}){let i=await fetch(`${e}/__registry__/${t}/index.json`);if(!i.ok)throw new Error(`Failed to fetch ${t} registry: ${i.status} ${i.statusText}`);let c=await i.json(),{success:p,data:r,error:n}=W.safeParse(c);if(!p)throw new Error(`Failed to parse ${t} registry: ${n?.message}`);return r}async function Pe({baseUrl:e,registryId:t,registryItemId:i}){let c=await fetch(`${e}/__registry__/${t}/${i}.json`);if(!c.ok)throw new Error(`Failed to fetch ${i}: ${c.status} ${c.statusText}`);let p=await c.json(),{success:r,data:n,error:l}=q.safeParse(p);if(!r)throw new Error(`Failed to parse ${i}: ${l?.message}`);return n}async function ie({baseUrl:e,registryId:t,registryItemIds:i}){return await Promise.all(i.map(async c=>{try{return await Pe({baseUrl:e,registryId:t,registryItemId:c})}catch(p){let r=await fetch(`${e}/__registry__/${t}/index.json`);if(!r.ok)throw new Error(`${t} \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC5B4\uC694: ${r.status} ${r.statusText}`);let n=await r.json(),{success:l,data:o}=W.safeParse(n);throw l?(Q.log.error(`${c} \uC2A4\uB2C8\uD3AB\uC774 ${t} \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC5D0 \uC5C6\uC5B4\uC694.`),Q.log.info(`${t} \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC5D0 \uC874\uC7AC\uD558\uB294 \uC2A4\uB2C8\uD3AB:
|
|
3
|
+
${o.items.map(u=>u.id).join(`
|
|
4
|
+
`)}`),p):new Error(`Failed to parse registry index for ${t}`)}}))}import{promises as Ce}from"fs";import{tmpdir as Ae}from"os";import ae from"path";import{transformFromAstSync as Se}from"@babel/core";import Re from"@babel/plugin-transform-typescript";import*as B from"recast";import{parse as Te}from"@babel/parser";var je={sourceType:"module",allowImportExportEverywhere:!0,allowReturnOutsideFunction:!0,startLine:1,tokens:!0,plugins:["asyncGenerators","bigInt","classPrivateMethods","classPrivateProperties","classProperties","classStaticBlock","decimal","decorators-legacy","doExpressions","dynamicImport","exportDefaultFrom","exportNamespaceFrom","functionBind","functionSent","importAssertions","importMeta","nullishCoalescingOperator","numericSeparator","objectRestSpread","optionalCatchBinding","optionalChaining",["pipelineOperator",{proposal:"minimal"}],["recordAndTuple",{syntaxType:"hash"}],"throwExpressions","topLevelAwait","v8intrinsic","typescript","jsx"]},oe=async({sourceFile:e,config:t})=>{let i=e.getFullText();if(t.tsx)return i;let c=B.parse(i,{parser:{parse:r=>Te(r,je)}}),p=Se(c,i,{cloneInputAst:!1,code:!1,ast:!0,plugins:[Re],configFile:!1});if(!p||!p.ast)throw new Error("Failed to transform JSX");return B.print(p.ast).code};import{SyntaxKind as Ee}from"ts-morph";var ne=async({sourceFile:e,config:t})=>{if(t.rsc)return e;let i=e.getFirstChildByKind(Ee.ExpressionStatement);if(!i)return e;let c=i.getExpression();if(!c)return e;let p=c.getText().trim();if(p!=='"use client"'&&p!=="'use client'")return e;let r=i.getText(),n=i.getFullText();if(r.trim()===n.trim())return e;let o=n.replace(r,"").replace(/^\s*\n/,"").replace(/\n\s*$/,"");return i.replaceWithText(o),e};import{Project as ve,ScriptKind as _e}from"ts-morph";var De=[ne],Oe=new ve({compilerOptions:{}});async function ke(e){let t=await Ce.mkdtemp(ae.join(Ae(),"seed-design-"));return ae.join(t,e)}async function ce(e){let t=await ke(e.filename),i=Oe.createSourceFile(t,e.raw,{scriptKind:_e.TSX});for(let c of De)c({sourceFile:i,...e});return await oe({sourceFile:i,...e})}import*as pe from"@clack/prompts";import Z from"fs-extra";import J from"path";async function H({registryItemsToAdd:e,rootPath:t,cwd:i,baseUrl:c,config:p}){let r=[];for(let{registryId:n,items:l}of e){let o=J.join(t,n);Z.ensureDirSync(o);let u=await ie({baseUrl:c,registryId:n,registryItemIds:l.map(w=>w.id)});for(let{id:w,snippets:R}of u){let E=await Promise.all(R.map(async x=>{let $=await ce({filename:x.path,config:p,raw:x.content}),I=J.join(o,x.path);return p.tsx||(I=I.replace(/\.tsx$/,".jsx"),I=I.replace(/\.ts$/,".js")),{filePath:I,content:$,relativePath:J.relative(i,I),name:`${n}:${w}`}}));await Promise.all(E.map(async({filePath:x,content:$})=>{await Z.ensureDir(J.dirname(x)),await Z.writeFile(x,$)}));let b=E.map(({name:x,relativePath:$})=>({name:x,path:$}));r.push(...b),pe.log.success(`${a(`${n}:${w}`)} \uAD00\uB828 \uC2A4\uB2C8\uD3AB \uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC: ${a(b.map(x=>x.path).join(", "))}`)}}}import*as d from"@clack/prompts";import Ge from"path";import{z as k}from"zod";var V="https://seed-design.io";import*as le from"@clack/prompts";import{execa as Le}from"execa";import Fe from"findup-sync";import Me from"fs-extra";var Ue="package.json";function ze(){let e=Fe(Ue);if(!e)throw new Error("No package.json file found in the project.");return e}function G(){let e=ze();return Me.readJSONSync(e)}async function Y({cwd:e,deps:t,dev:i=!1}){let{start:c,stop:p}=le.spinner(),r=await z(e),l={...G().dependencies},o=new Set(t.filter(b=>!l[b])),u=new Set(t.filter(b=>l[b]));if(!o.size)return{installed:new Set,filtered:o};c("\uC758\uC874\uC131 \uC124\uCE58\uC911...");let E=[r==="npm"?"install":"add",i?"-D":null,...o].filter(Boolean);try{await Le(r,E,{cwd:e})}catch(b){console.error(`\uC758\uC874\uC131 \uC124\uCE58 \uC2E4\uD328: ${b}`),process.exit(1)}return p("\uC758\uC874\uC131 \uC124\uCE58\uAC00 \uC644\uB8CC\uB410\uC5B4\uC694."),{installed:o,filtered:u}}import{randomUUID as Ke}from"node:crypto";import*as me from"@clack/prompts";var Ne="seed_cli";async function Be(e){if(process.env.DISABLE_TELEMETRY==="true"||process.env.SEED_DISABLE_TELEMETRY==="true")return!1;try{if((await D(e))?.telemetry===!1)return!1}catch{}return!0}function Je(){return Ke()}var He=Je(),de=!1;async function Ve(e,{event:t,properties:i={}}){if(!await Be(e))return;let p=`${Ne}.${t}`;de||(me.log.info("\u{1F4CA} \uC0AC\uC6A9 \uB370\uC774\uD130 \uC218\uC9D1 \uC911 (\uBE44\uD65C\uC131\uD654: seed-design.json \uB610\uB294 DISABLE_TELEMETRY \uD658\uACBD \uBCC0\uC218)"),de=!0);try{let r="https://us.i.posthog.com/capture",n={"Content-Type":"application/json"},l={api_key:"phc_seod8HhifElOP1R92KmvsQybrtUmkOTgZBsq0mfCelR",event:p,distinct_id:He,properties:{...i,$process_person_profile:!1},timestamp:new Date().toISOString()},o=new AbortController,u=setTimeout(()=>o.abort(),5e3);try{await fetch(r,{method:"POST",headers:n,body:JSON.stringify(l),signal:o.signal})}finally{clearTimeout(u)}}catch{}}var O={track:Ve};var Ye=k.object({itemIds:k.array(k.string()).optional(),all:k.boolean(),cwd:k.string(),baseUrl:k.string().optional()}),fe=e=>{e.command("add [...item-ids]","add items").option("-a, --all","[Deprecated] Add all items",{default:!1}).option("-c, --cwd <cwd>","the working directory. defaults to the current directory.",{default:process.cwd()}).option("-u, --baseUrl <baseUrl>","the base url of the registry. defaults to the current directory.",{default:V}).example("seed-design add ui:action-button").example("seed-design add ui:alert-dialog").action(async(t,i)=>{let c=Date.now();d.intro("seed-design add");let{success:p,data:{all:r,...n},error:l}=Ye.safeParse({itemIds:t,...i});p||(d.log.error(`\uC798\uBABB\uB41C \uC635\uC158\uC774\uC5D0\uC694: ${l?.message}`),process.exit(1)),r&&(d.log.error("`--all` \uC635\uC158\uC740 \uB354 \uC774\uC0C1 \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uC544\uC694. \uB300\uC2E0 `seed-design add-all` \uBA85\uB839\uC5B4\uB97C \uC0AC\uC6A9\uD574\uC8FC\uC138\uC694."),process.exit(1));let o=n.cwd,u=n.baseUrl,w=await D(o),R=Ge.resolve(o,w.path),{start:E,stop:b}=d.spinner();E("Registry\uB97C \uAC00\uC838\uC624\uACE0 \uC788\uC5B4\uC694...");let x=await Promise.all((await K({baseUrl:u})).map(async({id:s})=>N({baseUrl:u,registryId:s})));b("Registry\uB97C \uAC00\uC838\uC654\uC5B4\uC694.");let $=await(async()=>{if(n.itemIds.length>0)return n.itemIds;let s=await d.multiselect({message:"\uCD94\uAC00\uD560 \uD56D\uBAA9\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694 (\uC2A4\uD398\uC774\uC2A4 \uBC14\uB85C \uC5EC\uB7EC \uAC1C \uC120\uD0DD \uAC00\uB2A5)",options:x.filter(({hideFromCLICatalog:m})=>!m).flatMap(({id:m,items:T})=>T.filter(({hideFromCLICatalog:S})=>!S).sort((S,P)=>S.id.localeCompare(P.id)).map(({id:S,description:P,deprecated:j})=>({label:`${j?"(deprecated) ":""}${a(m)}:${S}`,value:`${m}:${S}`,hint:P,deprecated:j,registryItemCount:T.length}))).sort((m,T)=>m.deprecated!==T.deprecated?m.deprecated?1:-1:T.registryItemCount-m.registryItemCount)});return d.isCancel(s)&&(d.log.error("\uCDE8\uC18C\uB418\uC5C8\uC5B4\uC694."),process.exit(0)),s})();$?.length||(d.log.error("\uD56D\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4\uC694."),process.exit(0)),d.log.message(`\uC120\uD0DD\uB41C \uD56D\uBAA9: ${a($.join(", "))}`);let I=[];for(let s of $){let[m,...T]=s.split(":"),S=T.join(":");(!m||!S)&&(d.log.error(`${a(s)}: \uD56D\uBAA9 \uC774\uB984\uC774 \uC798\uBABB\uB418\uC5C8\uC5B4\uC694. ${a("ui:action-button")}\uACFC \uAC19\uC740 \uD615\uC2DD\uC73C\uB85C \uC785\uB825\uD574\uBCF4\uC138\uC694.`),process.exit(1));let P=x.find(j=>j.id===m)?.items.find(j=>j.id===S);if(P||(d.log.error(`${a(s)}: \uD56D\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4\uC694.`),process.exit(1)),P.deprecated){let j=await d.confirm({message:`${a(P.id)}: deprecated \uB418\uC5C8\uC5B4\uC694. \uCD94\uAC00\uD560\uAE4C\uC694?`,initialValue:!1});if(j===!1||d.isCancel(j)){d.log.info(`${a(P.id)}: \uCD94\uAC00\uD558\uC9C0 \uC54A\uC744\uAC8C\uC694.`);continue}}I.push(s)}let{registryItemsToAdd:v,npmDependenciesToAdd:M}=L({selectedItemKeys:I,publicRegistries:x});d.log.info(`\uCD94\uAC00\uD560 \uD56D\uBAA9: ${a(v.map(s=>s.items.map(m=>`${s.registryId}:${m.id}`).join(", ")).join(", ")||"\uC5C6\uC74C")}
|
|
5
5
|
|
|
6
|
-
\uC124\uCE58\uD560 \uC758\uC874\uC131: ${a(Array.from(
|
|
7
|
-
`,"utf-8");let
|
|
6
|
+
\uC124\uCE58\uD560 \uC758\uC874\uC131: ${a(Array.from(M).join(", ")||"\uC5C6\uC74C")}`),await H({registryItemsToAdd:v,rootPath:R,cwd:o,baseUrl:u,config:w});try{let{installed:s,filtered:m}=await Y({cwd:o,deps:Array.from(M)});s.size===0&&d.log.message("\uBAA8\uB4E0 \uC758\uC874\uC131\uC774 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC5B4\uC694."),s.size&&(d.log.message(`\uC758\uC874\uC131 \uC124\uCE58 \uC644\uB8CC: ${a(Array.from(s).join(", "))}`),m.size&&d.log.message(`\uC124\uCE58\uD558\uC9C0 \uC54A\uC740 \uC758\uC874\uC131 (\uC774\uBBF8 \uC124\uCE58\uB428): ${a(Array.from(m).join(", "))}`)),d.outro("\uC644\uB8CC\uD588\uC5B4\uC694.")}catch(s){d.log.error(`\uCD94\uAC00\uC5D0 \uC2E4\uD328\uD588\uC5B4\uC694. ${s}`),d.outro(a("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let U=Date.now()-c,X=new Set(v.map(s=>s.registryId)),h=$.some(s=>{let[m,...T]=s.split(":"),S=T.join(":");return x.find(P=>P.id===m)?.items.find(P=>P.id===S)?.deprecated});await O.track(n.cwd,{event:"add",properties:{items_count:I.length,registries:Array.from(X),has_deprecated:h,dependencies_count:M.size,duration_ms:U}})})};import*as g from"@clack/prompts";import Xe from"path";import{z as A}from"zod";var qe=A.object({registryIds:A.array(A.string()).optional(),all:A.boolean(),includeDeprecated:A.boolean().optional(),cwd:A.string(),baseUrl:A.string().optional()}),ge=e=>{e.command("add-all [...registry-ids]","add all items from registries").option("-a, --all","Add all items from all registries",{default:!1}).option("--include-deprecated","Include deprecated items when used with `--all`",{default:!1}).option("-c, --cwd <cwd>","the working directory. defaults to the current directory.",{default:process.cwd()}).option("-u, --baseUrl <baseUrl>","the base url of the registry. defaults to the current directory.",{default:V}).example("seed-design add-all ui --include-deprecated").example("seed-design add-all ui lib breeze").action(async(t,i)=>{let c=Date.now();g.intro("seed-design add-all");let{success:p,data:r,error:n}=qe.safeParse({registryIds:t,...i});p||(g.log.error(`\uC798\uBABB\uB41C \uC635\uC158\uC774\uC5D0\uC694: ${n?.message}`),process.exit(1));let l=r.cwd,o=r.baseUrl,u=await D(l),w=Xe.resolve(l,u.path),{start:R,stop:E}=g.spinner();R("Registry\uB97C \uAC00\uC838\uC624\uACE0 \uC788\uC5B4\uC694...");let b=await Promise.all((await K({baseUrl:o})).map(async({id:h})=>N({baseUrl:o,registryId:h})));E("Registry\uB97C \uAC00\uC838\uC654\uC5B4\uC694.");let x=await(async()=>{if(r.all){let s=b.map(m=>m.id);return g.log.message(`\uBAA8\uB4E0 \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC758 \uBAA8\uB4E0 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4: ${a(s.join(", "))}`),s}if(r.registryIds?.length){let s=b.map(m=>m.id);for(let m of r.registryIds)s.includes(m)||(g.log.error(`\uB808\uC9C0\uC2A4\uD2B8\uB9AC '${m}'\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4\uC694.`),g.log.info(`\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB808\uC9C0\uC2A4\uD2B8\uB9AC: ${s.join(", ")}`),process.exit(1));return g.log.message(`\uC120\uD0DD\uB41C \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC758 \uBAA8\uB4E0 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4: ${a(r.registryIds.join(", "))}`),r.registryIds}let h=await g.multiselect({message:"\uCD94\uAC00\uD560 \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uB97C \uC120\uD0DD\uD574\uC8FC\uC138\uC694 (\uC2A4\uD398\uC774\uC2A4 \uBC14\uB85C \uC5EC\uB7EC \uAC1C \uC120\uD0DD \uAC00\uB2A5)",options:b.filter(({hideFromCLICatalog:s})=>!s).sort((s,m)=>m.items.length-s.items.length).map(s=>({label:s.id,value:s.id,hint:`${s.items.length}\uAC1C \uD56D\uBAA9 (${s.items[0].id} \uB4F1)`}))});return g.isCancel(h)&&(g.log.error("\uCDE8\uC18C\uB418\uC5C8\uC5B4\uC694."),process.exit(0)),g.log.message(`\uC120\uD0DD\uB41C \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC758 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4: ${a(h.join(", "))}`),h})(),$=b.filter(h=>x.includes(h.id)),I=$.flatMap(h=>h.items.filter(s=>s.deprecated?r.includeDeprecated:!0).map(s=>`${h.id}:${s.id}`)),v=$.flatMap(h=>h.items.filter(s=>s.deprecated).map(()=>1)).length;!r.includeDeprecated&&v>0&&g.log.info(`${v}\uAC1C\uC758 deprecated \uD56D\uBAA9\uC740 \uC81C\uC678\uB418\uC5C8\uC5B4\uC694. --include-deprecated \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uBA74 \uCD94\uAC00\uD560 \uC218 \uC788\uC5B4\uC694.`),I.length||(g.log.error("\uCD94\uAC00\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC5B4\uC694."),process.exit(0)),g.log.message(`\uCD1D ${a(I.length.toString())}\uAC1C\uC758 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4.`);let{registryItemsToAdd:M,npmDependenciesToAdd:U}=L({selectedItemKeys:I,publicRegistries:b});await H({registryItemsToAdd:M,rootPath:w,cwd:l,baseUrl:o,config:u});try{let{installed:h,filtered:s}=await Y({cwd:l,deps:Array.from(U)});h.size===0&&g.log.message("\uBAA8\uB4E0 \uC758\uC874\uC131\uC774 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC5B4\uC694."),h.size&&(g.log.message(`\uC758\uC874\uC131 \uC124\uCE58 \uC644\uB8CC: ${a(Array.from(h).join(", "))}`),s.size&&g.log.message(`\uC124\uCE58\uD558\uC9C0 \uC54A\uC740 \uC758\uC874\uC131 (\uC774\uBBF8 \uC124\uCE58\uB428): ${a(Array.from(s).join(", "))}`)),g.outro("\uC644\uB8CC\uD588\uC5B4\uC694.")}catch(h){g.log.error(`\uCD94\uAC00\uC5D0 \uC2E4\uD328\uD588\uC5B4\uC694. ${h}`),g.outro(a("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let X=Date.now()-c;await O.track(r.cwd,{event:"add-all",properties:{registries:x,items_count:I.length,include_deprecated:r.includeDeprecated||!1,dependencies_count:U.size,duration_ms:X}})})};import*as y from"@clack/prompts";import We from"fs-extra";import ue from"path";import{z as ee}from"zod";import Qe from"dedent";var Ze=ee.object({cwd:ee.string(),yes:ee.boolean().optional()}),ye=e=>{e.command("init","seed-design.json \uD30C\uC77C \uC0DD\uC131").option("-c, --cwd <cwd>","\uC791\uC5C5 \uB514\uB809\uD1A0\uB9AC. \uAE30\uBCF8\uAC12\uC740 \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC.",{default:process.cwd()}).option("-y, --yes","\uBAA8\uB4E0 \uC9C8\uBB38\uC5D0 \uB300\uD574 \uAE30\uBCF8\uAC12\uC73C\uB85C \uB2F5\uBCC0\uD569\uB2C8\uB2E4.").action(async t=>{let i=Date.now();y.intro("seed-design.json \uD30C\uC77C \uC0DD\uC131");let c=Ze.parse(t),p=c.yes,r={rsc:!1,tsx:!0,path:"./seed-design",telemetry:!0};p||(r={...await y.group({tsx:()=>y.confirm({message:`${a("TypeScript")}\uB97C \uC0AC\uC6A9\uC911\uC774\uC2E0\uAC00\uC694?`,initialValue:!0}),rsc:()=>y.confirm({message:`${a("React Server Components")}\uB97C \uC0AC\uC6A9\uC911\uC774\uC2E0\uAC00\uC694?`,initialValue:!1}),path:()=>y.text({message:`${a("seed-design \uD3F4\uB354")} \uACBD\uB85C\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694. (\uAE30\uBCF8\uAC12\uC740 \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8\uC5D0 \uC0DD\uC131\uB429\uB2C8\uB2E4.)`,initialValue:"./seed-design",defaultValue:"./seed-design",placeholder:"./seed-design"}),telemetry:()=>y.confirm({message:`\uAC1C\uC120\uC744 \uC704\uD574 ${a("\uC775\uBA85 \uC0AC\uC6A9 \uB370\uC774\uD130")}\uB97C \uC218\uC9D1\uD560\uAE4C\uC694?`,initialValue:!0})},{onCancel:()=>{y.cancel("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694."),process.exit(0)}})});try{let{start:l,stop:o}=y.spinner();l("seed-design.json \uD30C\uC77C \uC0DD\uC131\uC911...");let u=ue.resolve(c.cwd,"seed-design.json");await We.writeFile(u,`${JSON.stringify(r,null,2)}
|
|
7
|
+
`,"utf-8");let w=ue.relative(process.cwd(),u);o(`seed-design.json \uD30C\uC77C\uC774 ${a(w)}\uC5D0 \uC0DD\uC131\uB410\uC5B4\uC694.`),y.log.info(a("seed-design add {component} \uBA85\uB839\uC5B4\uB85C \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uCD94\uAC00\uD574\uBCF4\uC138\uC694!")),y.log.info(a("seed-design add \uBA85\uB839\uC5B4\uB85C \uCD94\uAC00\uD560 \uC218 \uC788\uB294 \uBAA8\uB4E0 \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uD655\uC778\uD574\uBCF4\uC138\uC694.")),y.note(Qe(`SEED Design CLI\uB294 \uAC1C\uC120\uC744 \uC704\uD574 \uC775\uBA85 \uC0AC\uC6A9 \uB370\uC774\uD130\uB97C \uC218\uC9D1\uD574\uC694.
|
|
8
|
+
|
|
9
|
+
\uBE44\uD65C\uC131\uD654\uD558\uB824\uBA74:
|
|
10
|
+
\u2022 seed-design.json\uC5D0\uC11C ${a('"telemetry": false')}\uB85C \uC124\uC815
|
|
11
|
+
\u2022 ${a("DISABLE_TELEMETRY=true")} \uD658\uACBD \uBCC0\uC218 \uC124\uC815
|
|
12
|
+
|
|
13
|
+
\uC790\uC138\uD55C \uB0B4\uC6A9: https://seed-design.com/react/getting-started/cli/configuration#telemetry`),"Telemetry \uC548\uB0B4"),y.outro("\uC791\uC5C5\uC774 \uC644\uB8CC\uB410\uC5B4\uC694.")}catch(l){y.log.error(`seed-design.json \uD30C\uC77C \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC5B4\uC694. ${l}`),y.outro(a("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let n=Date.now()-i;await O.track(c.cwd,{event:"init",properties:{tsx:r.tsx,rsc:r.rsc,telemetry:r.telemetry,yes_option:p,duration_ms:n}})})};import{cac as et}from"cac";var tt="seed-design",F=et(tt);async function rt(){let e=G();fe(F),ge(F),ye(F),F.version(e.version||"1.0.0","-v, --version"),F.help(),F.parse()}rt();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"lint:publish": "bun publint"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@antfu/ni": "^
|
|
28
|
+
"@antfu/ni": "^27.0.0",
|
|
29
29
|
"@babel/core": "^7.26.10",
|
|
30
30
|
"@babel/parser": "^7.27.0",
|
|
31
31
|
"@babel/plugin-transform-typescript": "^7.27.0",
|
|
32
32
|
"@clack/prompts": "^0.11.0",
|
|
33
33
|
"cac": "^6.7.14",
|
|
34
34
|
"cosmiconfig": "^9.0.0",
|
|
35
|
+
"dedent": "^1.7.0",
|
|
35
36
|
"execa": "^9.5.2",
|
|
36
37
|
"findup-sync": "^5.0.0",
|
|
37
38
|
"fs-extra": "^11.3.0",
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/babel__core": "^7.20.5",
|
|
45
46
|
"@types/fs-extra": "^11.0.4",
|
|
46
|
-
"
|
|
47
|
+
"dotenv": "^17.2.3",
|
|
48
|
+
"esbuild": "^0.27.0",
|
|
47
49
|
"type-fest": "^5.0.0",
|
|
48
50
|
"typescript": "^5.9.2"
|
|
49
51
|
},
|
package/src/commands/add-all.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { fetchAvailableRegistries, fetchRegistry } from "@/src/utils/fetch";
|
|
1
2
|
import { getConfig } from "@/src/utils/get-config";
|
|
2
3
|
import { resolveDependencies } from "@/src/utils/resolve-dependencies";
|
|
3
|
-
import { fetchAvailableRegistries, fetchRegistry } from "@/src/utils/fetch";
|
|
4
4
|
import { writeRegistryItemSnippets } from "@/src/utils/write";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
import path from "path";
|
|
@@ -8,6 +8,7 @@ import { z } from "zod";
|
|
|
8
8
|
|
|
9
9
|
import type { CAC } from "cac";
|
|
10
10
|
import { BASE_URL } from "../constants";
|
|
11
|
+
import { analytics } from "../utils/analytics";
|
|
11
12
|
import { highlight } from "../utils/color";
|
|
12
13
|
import { installDependencies } from "../utils/install";
|
|
13
14
|
|
|
@@ -39,6 +40,7 @@ export const addAllCommand = (cli: CAC) => {
|
|
|
39
40
|
.example("seed-design add-all ui --include-deprecated")
|
|
40
41
|
.example("seed-design add-all ui lib breeze")
|
|
41
42
|
.action(async (registryIds, opts) => {
|
|
43
|
+
const startTime = Date.now();
|
|
42
44
|
p.intro("seed-design add-all");
|
|
43
45
|
|
|
44
46
|
const {
|
|
@@ -156,25 +158,44 @@ export const addAllCommand = (cli: CAC) => {
|
|
|
156
158
|
|
|
157
159
|
await writeRegistryItemSnippets({ registryItemsToAdd, rootPath, cwd, baseUrl, config });
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
try {
|
|
162
|
+
const { installed, filtered } = await installDependencies({
|
|
163
|
+
cwd,
|
|
164
|
+
deps: Array.from(npmDependenciesToAdd),
|
|
165
|
+
});
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
if (installed.size === 0) {
|
|
168
|
+
p.log.message("모든 의존성이 이미 설치되어 있어요.");
|
|
169
|
+
}
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
if (installed.size) {
|
|
172
|
+
p.log.message(`의존성 설치 완료: ${highlight(Array.from(installed).join(", "))}`);
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
if (filtered.size) {
|
|
175
|
+
p.log.message(
|
|
176
|
+
`설치하지 않은 의존성 (이미 설치됨): ${highlight(Array.from(filtered).join(", "))}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
175
179
|
}
|
|
180
|
+
|
|
181
|
+
p.outro("완료했어요.");
|
|
182
|
+
} catch (error) {
|
|
183
|
+
p.log.error(`추가에 실패했어요. ${error}`);
|
|
184
|
+
p.outro(highlight("작업이 취소됐어요."));
|
|
185
|
+
process.exit(1);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
// add-all 성공 이벤트 추적
|
|
189
|
+
const duration = Date.now() - startTime;
|
|
190
|
+
await analytics.track(options.cwd, {
|
|
191
|
+
event: "add-all",
|
|
192
|
+
properties: {
|
|
193
|
+
registries: selectedRegistryIds,
|
|
194
|
+
items_count: itemKeys.length,
|
|
195
|
+
include_deprecated: options.includeDeprecated || false,
|
|
196
|
+
dependencies_count: npmDependenciesToAdd.size,
|
|
197
|
+
duration_ms: duration,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
179
200
|
});
|
|
180
201
|
};
|
package/src/commands/add.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { CAC } from "cac";
|
|
|
10
10
|
import { BASE_URL } from "../constants";
|
|
11
11
|
import { highlight } from "../utils/color";
|
|
12
12
|
import { installDependencies } from "../utils/install";
|
|
13
|
+
import { analytics } from "../utils/analytics";
|
|
13
14
|
|
|
14
15
|
const addOptionsSchema = z.object({
|
|
15
16
|
itemIds: z.array(z.string()).optional(),
|
|
@@ -38,6 +39,7 @@ export const addCommand = (cli: CAC) => {
|
|
|
38
39
|
.example("seed-design add ui:action-button")
|
|
39
40
|
.example("seed-design add ui:alert-dialog")
|
|
40
41
|
.action(async (itemIds, opts) => {
|
|
42
|
+
const startTime = Date.now();
|
|
41
43
|
p.intro("seed-design add");
|
|
42
44
|
|
|
43
45
|
const {
|
|
@@ -180,25 +182,51 @@ export const addCommand = (cli: CAC) => {
|
|
|
180
182
|
config,
|
|
181
183
|
});
|
|
182
184
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
try {
|
|
186
|
+
const { installed, filtered } = await installDependencies({
|
|
187
|
+
cwd,
|
|
188
|
+
deps: Array.from(npmDependenciesToAdd),
|
|
189
|
+
});
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
if (installed.size === 0) {
|
|
192
|
+
p.log.message("모든 의존성이 이미 설치되어 있어요.");
|
|
193
|
+
}
|
|
191
194
|
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
if (installed.size) {
|
|
196
|
+
p.log.message(`의존성 설치 완료: ${highlight(Array.from(installed).join(", "))}`);
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
if (filtered.size) {
|
|
199
|
+
p.log.message(
|
|
200
|
+
`설치하지 않은 의존성 (이미 설치됨): ${highlight(Array.from(filtered).join(", "))}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
199
203
|
}
|
|
204
|
+
p.outro("완료했어요.");
|
|
205
|
+
} catch (error) {
|
|
206
|
+
p.log.error(`추가에 실패했어요. ${error}`);
|
|
207
|
+
p.outro(highlight("작업이 취소됐어요."));
|
|
208
|
+
process.exit(1);
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
|
|
211
|
+
// add 성공 이벤트 추적
|
|
212
|
+
const duration = Date.now() - startTime;
|
|
213
|
+
const uniqueRegistries = new Set(registryItemsToAdd.map((r) => r.registryId));
|
|
214
|
+
const hasDeprecated = selectedItemKeys.some((itemKey) => {
|
|
215
|
+
const [registryId, ...rest] = itemKey.split(":");
|
|
216
|
+
const itemId = rest.join(":");
|
|
217
|
+
return publicRegistries.find((r) => r.id === registryId)?.items.find((i) => i.id === itemId)
|
|
218
|
+
?.deprecated;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await analytics.track(options.cwd, {
|
|
222
|
+
event: "add",
|
|
223
|
+
properties: {
|
|
224
|
+
items_count: filteredItemKeys.length,
|
|
225
|
+
registries: Array.from(uniqueRegistries),
|
|
226
|
+
has_deprecated: hasDeprecated,
|
|
227
|
+
dependencies_count: npmDependenciesToAdd.size,
|
|
228
|
+
duration_ms: duration,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
203
231
|
});
|
|
204
232
|
};
|
package/src/commands/init.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { highlight } from "../utils/color";
|
|
5
4
|
import { z } from "zod";
|
|
5
|
+
import { analytics } from "../utils/analytics";
|
|
6
|
+
import { highlight } from "../utils/color";
|
|
6
7
|
|
|
7
8
|
import type { Config } from "@/src/utils/get-config";
|
|
8
9
|
|
|
9
10
|
import type { CAC } from "cac";
|
|
11
|
+
import dedent from "dedent";
|
|
10
12
|
|
|
11
13
|
const initOptionsSchema = z.object({
|
|
12
14
|
cwd: z.string(),
|
|
@@ -21,6 +23,7 @@ export const initCommand = (cli: CAC) => {
|
|
|
21
23
|
})
|
|
22
24
|
.option("-y, --yes", "모든 질문에 대해 기본값으로 답변합니다.")
|
|
23
25
|
.action(async (opts) => {
|
|
26
|
+
const startTime = Date.now();
|
|
24
27
|
p.intro("seed-design.json 파일 생성");
|
|
25
28
|
|
|
26
29
|
const options = initOptionsSchema.parse(opts);
|
|
@@ -29,6 +32,7 @@ export const initCommand = (cli: CAC) => {
|
|
|
29
32
|
rsc: false,
|
|
30
33
|
tsx: true,
|
|
31
34
|
path: "./seed-design",
|
|
35
|
+
telemetry: true,
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
if (!isYesOption) {
|
|
@@ -51,6 +55,11 @@ export const initCommand = (cli: CAC) => {
|
|
|
51
55
|
defaultValue: "./seed-design",
|
|
52
56
|
placeholder: "./seed-design",
|
|
53
57
|
}),
|
|
58
|
+
telemetry: () =>
|
|
59
|
+
p.confirm({
|
|
60
|
+
message: `개선을 위해 ${highlight("익명 사용 데이터")}를 수집할까요?`,
|
|
61
|
+
initialValue: true,
|
|
62
|
+
}),
|
|
54
63
|
},
|
|
55
64
|
{
|
|
56
65
|
onCancel: () => {
|
|
@@ -72,15 +81,41 @@ export const initCommand = (cli: CAC) => {
|
|
|
72
81
|
await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
73
82
|
const relativePath = path.relative(process.cwd(), targetPath);
|
|
74
83
|
stop(`seed-design.json 파일이 ${highlight(relativePath)}에 생성됐어요.`);
|
|
84
|
+
|
|
75
85
|
p.log.info(highlight("seed-design add {component} 명령어로 컴포넌트를 추가해보세요!"));
|
|
76
86
|
p.log.info(
|
|
77
87
|
highlight("seed-design add 명령어로 추가할 수 있는 모든 컴포넌트를 확인해보세요."),
|
|
78
88
|
);
|
|
89
|
+
|
|
90
|
+
p.note(
|
|
91
|
+
dedent(`SEED Design CLI는 개선을 위해 익명 사용 데이터를 수집해요.
|
|
92
|
+
|
|
93
|
+
비활성화하려면:
|
|
94
|
+
• seed-design.json에서 ${highlight('"telemetry": false')}로 설정
|
|
95
|
+
• ${highlight("DISABLE_TELEMETRY=true")} 환경 변수 설정
|
|
96
|
+
|
|
97
|
+
자세한 내용: https://seed-design.com/react/getting-started/cli/configuration#telemetry`),
|
|
98
|
+
"Telemetry 안내",
|
|
99
|
+
);
|
|
100
|
+
|
|
79
101
|
p.outro("작업이 완료됐어요.");
|
|
80
102
|
} catch (error) {
|
|
81
103
|
p.log.error(`seed-design.json 파일 생성에 실패했어요. ${error}`);
|
|
82
104
|
p.outro(highlight("작업이 취소됐어요."));
|
|
83
105
|
process.exit(1);
|
|
84
106
|
}
|
|
107
|
+
|
|
108
|
+
// init 성공 이벤트 추적
|
|
109
|
+
const duration = Date.now() - startTime;
|
|
110
|
+
await analytics.track(options.cwd, {
|
|
111
|
+
event: "init",
|
|
112
|
+
properties: {
|
|
113
|
+
tsx: config.tsx,
|
|
114
|
+
rsc: config.rsc,
|
|
115
|
+
telemetry: config.telemetry,
|
|
116
|
+
yes_option: isYesOption,
|
|
117
|
+
duration_ms: duration,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
85
120
|
});
|
|
86
121
|
};
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
namespace NodeJS {
|
|
3
|
+
interface ProcessEnv {
|
|
4
|
+
NODE_ENV: "dev" | "prod";
|
|
5
|
+
POSTHOG_API_KEY?: string;
|
|
6
|
+
POSTHOG_HOST?: string;
|
|
7
|
+
DISABLE_TELEMETRY?: "true" | "false";
|
|
8
|
+
SEED_DISABLE_TELEMETRY?: "true" | "false";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {};
|
|
@@ -29,7 +29,7 @@ describe("resolveDependencies", () => {
|
|
|
29
29
|
{
|
|
30
30
|
id: "button",
|
|
31
31
|
description: "Button component",
|
|
32
|
-
|
|
32
|
+
snippets: [{ path: "button.tsx" }],
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
35
|
});
|
|
@@ -105,7 +105,7 @@ describe("resolveDependencies", () => {
|
|
|
105
105
|
{
|
|
106
106
|
id: "dialog",
|
|
107
107
|
description: "Dialog component",
|
|
108
|
-
|
|
108
|
+
snippets: [{ path: "dialog.tsx" }],
|
|
109
109
|
innerDependencies: [
|
|
110
110
|
{
|
|
111
111
|
registryId: "breeze",
|
|
@@ -121,7 +121,7 @@ describe("resolveDependencies", () => {
|
|
|
121
121
|
{
|
|
122
122
|
id: "animate-number",
|
|
123
123
|
description: "Animate number utility",
|
|
124
|
-
|
|
124
|
+
snippets: [{ path: "animate-number.ts" }],
|
|
125
125
|
dependencies: ["framer-motion"],
|
|
126
126
|
},
|
|
127
127
|
],
|
|
@@ -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
|
+
};
|