@seed-design/cli 1.0.0 → 1.2.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 +13 -5
- package/package.json +7 -3
- package/src/commands/add-all.ts +47 -15
- package/src/commands/add.ts +47 -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/src/utils/write.ts +80 -14
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,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import*as
|
|
3
|
-
${n.items.map(
|
|
4
|
-
`)}`),
|
|
2
|
+
import*as k from"@clack/prompts";import{cosmiconfig as $e}from"cosmiconfig";import{execa as Ie}from"execa";import{z}from"zod";import be from"picocolors";var i=e=>be.cyan(e);import{detect as xe}from"@antfu/ni";async function B(e){let t=await xe({programmatic:!0,cwd:e});return t==="yarn@berry"?"yarn":t==="pnpm@6"?"pnpm":t==="bun"?"bun":t==="deno"?"deno":t??"npm"}var se="seed-design",Pe=$e(se,{searchPlaces:[`${se}.json`]}),oe=z.object({$schema:z.string().optional(),rsc:z.coerce.boolean().default(!1),tsx:z.coerce.boolean().default(!0),path:z.string(),telemetry:z.coerce.boolean().optional().default(!0)}).strict();async function M(e){let t=await Se(e);return t?oe.parse(t):null}async function Se(e){try{let t=await Pe.search(e);return oe.parse(t.config)}catch{k.log.error("\uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 \uACBD\uB85C\uC5D0 `seed-design.json` \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC694."),await k.confirm({message:"seed-design.json \uD30C\uC77C\uC744 \uC0DD\uC131\uD558\uC2DC\uACA0\uC5B4\uC694?"})||(k.outro(i("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1));let o=await B(e);await Ie(o,["seed-design","init","--default"],{cwd:e}),k.log.message("seed-design.json \uD30C\uC77C\uC774 \uC0DD\uC131\uB410\uC5B4\uC694.")}}function J({selectedItemKeys:e,publicRegistries:t}){let o=[],c=new Set;function l(r,a){let p=o.find(n=>n.registryId===r);if(!p?.items.some(n=>n.id===a.id)){if(p?p.items.push(a):o.push({registryId:r,items:[a]}),a.dependencies?.length)for(let n of a.dependencies)c.add(n);if(a.innerDependencies?.length)for(let n of a.innerDependencies)for(let u of n.itemIds){let $=t.find(S=>S.id===n.registryId)?.items.find(S=>S.id===u);if(!$)throw new Error(`Cannot find dependency item: ${n.registryId}:${u}`);l(n.registryId,$)}}}for(let r of e){let[a,...p]=r.split(":"),n=p.join(":");if(!a||!n)throw new Error(`Invalid snippet format: "${r}"`);let u=t.find($=>$.id===a)?.items.find($=>$.id===n);if(!u)throw new Error(`Cannot find snippet: "${r}"`);l(a,u)}return{registryItemsToAdd:o,npmDependenciesToAdd:c}}import*as ee 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()}))}),Z=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()}))}))}),ie=f.array(f.object({id:f.string()}));async function H({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 o=await t.json(),{success:c,data:l,error:r}=ie.safeParse(o);if(!c)throw new Error(`Failed to parse registries: ${r?.message}`);return l}async function V({baseUrl:e,registryId:t}){let o=await fetch(`${e}/__registry__/${t}/index.json`);if(!o.ok)throw new Error(`Failed to fetch ${t} registry: ${o.status} ${o.statusText}`);let c=await o.json(),{success:l,data:r,error:a}=Z.safeParse(c);if(!l)throw new Error(`Failed to parse ${t} registry: ${a?.message}`);return r}async function ve({baseUrl:e,registryId:t,registryItemId:o}){let c=await fetch(`${e}/__registry__/${t}/${o}.json`);if(!c.ok)throw new Error(`Failed to fetch ${o}: ${c.status} ${c.statusText}`);let l=await c.json(),{success:r,data:a,error:p}=Q.safeParse(l);if(!r)throw new Error(`Failed to parse ${o}: ${p?.message}`);return a}async function ne({baseUrl:e,registryId:t,registryItemIds:o}){return await Promise.all(o.map(async c=>{try{return await ve({baseUrl:e,registryId:t,registryItemId:c})}catch(l){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 a=await r.json(),{success:p,data:n}=Z.safeParse(a);throw p?(ee.log.error(`${c} \uC2A4\uB2C8\uD3AB\uC774 ${t} \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC5D0 \uC5C6\uC5B4\uC694.`),ee.log.info(`${t} \uB808\uC9C0\uC2A4\uD2B8\uB9AC\uC5D0 \uC874\uC7AC\uD558\uB294 \uC2A4\uB2C8\uD3AB:
|
|
3
|
+
${n.items.map(u=>u.id).join(`
|
|
4
|
+
`)}`),l):new Error(`Failed to parse registry index for ${t}`)}}))}import{promises as Ae}from"fs";import{tmpdir as _e}from"os";import pe from"path";import{transformFromAstSync as Re}from"@babel/core";import Te from"@babel/plugin-transform-typescript";import*as G from"recast";import{parse as je}from"@babel/parser";var Ce={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"]},ae=async({sourceFile:e,config:t})=>{let o=e.getFullText();if(t.tsx)return o;let c=G.parse(o,{parser:{parse:r=>je(r,Ce)}}),l=Re(c,o,{cloneInputAst:!1,code:!1,ast:!0,plugins:[Te],configFile:!1});if(!l||!l.ast)throw new Error("Failed to transform JSX");return G.print(l.ast).code};import{SyntaxKind as Ee}from"ts-morph";var ce=async({sourceFile:e,config:t})=>{if(t.rsc)return e;let o=e.getFirstChildByKind(Ee.ExpressionStatement);if(!o)return e;let c=o.getExpression();if(!c)return e;let l=c.getText().trim();if(l!=='"use client"'&&l!=="'use client'")return e;let r=o.getText(),a=o.getFullText();if(r.trim()===a.trim())return e;let n=a.replace(r,"").replace(/^\s*\n/,"").replace(/\n\s*$/,"");return o.replaceWithText(n),e};import{Project as De,ScriptKind as Oe}from"ts-morph";var ke=[ce],Fe=new De({compilerOptions:{}});async function ze(e){let t=await Ae.mkdtemp(pe.join(_e(),"seed-design-"));return pe.join(t,e)}async function le(e){let t=await ze(e.filename),o=Fe.createSourceFile(t,e.raw,{scriptKind:Oe.TSX});for(let c of ke)c({sourceFile:o,...e});return await ae({sourceFile:o,...e})}import*as E from"@clack/prompts";import U from"fs-extra";import A from"path";import{createPatch as Me}from"diff";import Ue from"@npmcli/disparity-colors";async function Y({registryItemsToAdd:e,rootPath:t,cwd:o,baseUrl:c,config:l,overwrite:r=!1}){let a=[];for(let{registryId:p,items:n}of e){let u=A.join(t,p);U.ensureDirSync(u);let $=await ne({baseUrl:c,registryId:p,registryItemIds:n.map(S=>S.id)});for(let{id:S,snippets:D}of $){let I=await Promise.all(D.map(async x=>{let y=await le({filename:x.path,config:l,raw:x.content}),b=A.join(u,x.path);return l.tsx||(b=b.replace(/\.tsx$/,".jsx"),b=b.replace(/\.ts$/,".js")),{filePath:b,content:y,relativePath:A.relative(o,b),name:`${p}:${S}`}})),T=[];for(let x of I){let{filePath:y,content:b,relativePath:j}=x;if(await U.ensureDir(A.dirname(y)),U.existsSync(y)){let O=await U.readFile(y,"utf-8");if(O===b){E.log.info(`${i(j)}: \uC774\uBBF8 \uCD5C\uC2E0 \uC0C1\uD0DC\uC608\uC694.`);continue}if(!r){let N=Me(j,O,b),w=Ue(N);E.log.message(`
|
|
5
|
+
${i(j)}: \uD604\uC7AC \uD30C\uC77C\uACFC \uBC1B\uC73C\uB824\uB294 \uD30C\uC77C\uC758 \uB0B4\uC6A9\uC774 \uB2EC\uB77C\uC694.
|
|
6
|
+
`),E.log.message(w);let s=A.basename(y),m=A.extname(y),C=A.basename(y,m),v=Date.now(),P=`legacy-${C}-${v}${m}`,R=await E.select({message:"\uD604\uC7AC \uD30C\uC77C\uC5D0 \uC2A4\uD0C0\uC77C \uBCC0\uACBD, \uB85C\uAE45 \uB4F1 \uCEE4\uC2A4\uD130\uB9C8\uC774\uC9D5\uC774 \uC801\uC6A9\uB418\uC5B4 \uC788\uB294 \uACBD\uC6B0 \uC2E0\uADDC \uD30C\uC77C\uC5D0 \uB3D9\uC77C\uD55C \uCEE4\uC2A4\uD130\uB9C8\uC774\uC9D5\uC744 \uC801\uC6A9\uD558\uB294 \uAC83\uC744 \uAC80\uD1A0\uD574\uBCF4\uC138\uC694.",options:[{value:"overwrite",label:`${s} \uB36E\uC5B4\uC4F0\uAE30`},{value:"backup",label:`\uAE30\uC874 \uD30C\uC77C \uB0B4\uC6A9\uC744 ${P}\uC73C\uB85C \uC62E\uAE30\uACE0 ${s} \uBC1B\uAE30`},{value:"skip",label:"\uC0C8 \uD30C\uC77C \uBC1B\uC9C0 \uC54A\uACE0 \uADF8\uB300\uB85C \uB450\uAE30"}]});if(E.isCancel(R)||R==="skip"){E.log.info(`${i(j)}: \uD30C\uC77C\uC744 \uBC1B\uC9C0 \uC54A\uACE0 \uAC74\uB108\uB6F0\uC5C8\uC5B4\uC694.`);continue}if(R==="backup"){let we=A.dirname(y),re=A.join(we,P);await U.rename(y,re),E.log.info(`${i(j)}: \uAE30\uC874 \uD30C\uC77C\uC744 ${i(A.relative(o,re))}\uB85C \uC62E\uACBC\uC5B4\uC694.`)}}}await U.writeFile(y,b),T.push(x)}if(T.length>0){let x=T.map(({name:y,relativePath:b})=>({name:y,path:b}));a.push(...x),E.log.success(`${i(`${p}:${S}`)} \uAD00\uB828 \uC2A4\uB2C8\uD3AB \uB2E4\uC6B4\uB85C\uB4DC \uC644\uB8CC: ${i(x.map(y=>y.path).join(", "))}`)}}}}import*as d from"@clack/prompts";import We from"path";import{z as F}from"zod";var X="https://seed-design.io";import*as me from"@clack/prompts";import{execa as Je}from"execa";import Le from"findup-sync";import Ke from"fs-extra";var Ne="package.json";function Be(){let e=Le(Ne);if(!e)throw new Error("No package.json file found in the project.");return e}function q(){let e=Be();return Ke.readJSONSync(e)}async function W({cwd:e,deps:t,dev:o=!1}){let{start:c,stop:l}=me.spinner(),r=await B(e),p={...q().dependencies},n=new Set(t.filter(I=>!p[I])),u=new Set(t.filter(I=>p[I]));if(!n.size)return{installed:new Set,filtered:n};c("\uC758\uC874\uC131 \uC124\uCE58\uC911...");let D=[r==="npm"?"install":"add",o?"-D":null,...n].filter(Boolean);try{await Je(r,D,{cwd:e})}catch(I){console.error(`\uC758\uC874\uC131 \uC124\uCE58 \uC2E4\uD328: ${I}`),process.exit(1)}return l("\uC758\uC874\uC131 \uC124\uCE58\uAC00 \uC644\uB8CC\uB410\uC5B4\uC694."),{installed:n,filtered:u}}import{randomUUID as He}from"node:crypto";import*as fe from"@clack/prompts";var Ve="seed_cli";async function Ge(e){if(process.env.DISABLE_TELEMETRY==="true"||process.env.SEED_DISABLE_TELEMETRY==="true")return!1;try{if((await M(e))?.telemetry===!1)return!1}catch{}return!0}function Ye(){return He()}var Xe=Ye(),de=!1;async function qe(e,{event:t,properties:o={}}){if(!await Ge(e))return;let l=`${Ve}.${t}`;de||(fe.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",a={"Content-Type":"application/json"},p={api_key:"phc_seod8HhifElOP1R92KmvsQybrtUmkOTgZBsq0mfCelR",event:l,distinct_id:Xe,properties:{...o,$process_person_profile:!1},timestamp:new Date().toISOString()},n=new AbortController,u=setTimeout(()=>n.abort(),5e3);try{await fetch(r,{method:"POST",headers:a,body:JSON.stringify(p),signal:n.signal})}finally{clearTimeout(u)}}catch{}}var L={track:qe};var Qe=F.object({itemIds:F.array(F.string()).optional(),all:F.boolean(),cwd:F.string(),baseUrl:F.string().optional(),overwrite:F.boolean().optional()}),ge=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:X}).option("--overwrite","Overwrite existing files without confirmation",{default:!1}).example("seed-design add ui:action-button").example("seed-design add ui:alert-dialog").action(async(t,o)=>{let c=Date.now();d.intro("seed-design add");let{success:l,data:{all:r,...a},error:p}=Qe.safeParse({itemIds:t,...o});l||(d.log.error(`\uC798\uBABB\uB41C \uC635\uC158\uC774\uC5D0\uC694: ${p?.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 n=a.cwd,u=a.baseUrl,$=await M(n),S=We.resolve(n,$.path),{start:D,stop:I}=d.spinner();D("Registry\uB97C \uAC00\uC838\uC624\uACE0 \uC788\uC5B4\uC694...");let T=await Promise.all((await H({baseUrl:u})).map(async({id:s})=>V({baseUrl:u,registryId:s})));I("Registry\uB97C \uAC00\uC838\uC654\uC5B4\uC694.");let x=await(async()=>{if(a.itemIds.length>0)return a.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:T.filter(({hideFromCLICatalog:m})=>!m).flatMap(({id:m,items:C})=>C.filter(({hideFromCLICatalog:v})=>!v).sort((v,P)=>v.id.localeCompare(P.id)).map(({id:v,description:P,deprecated:R})=>({label:`${R?"(deprecated) ":""}${i(m)}:${v}`,value:`${m}:${v}`,hint:P,deprecated:R,registryItemCount:C.length}))).sort((m,C)=>m.deprecated!==C.deprecated?m.deprecated?1:-1:C.registryItemCount-m.registryItemCount)});return d.isCancel(s)&&(d.log.error("\uCDE8\uC18C\uB418\uC5C8\uC5B4\uC694."),process.exit(0)),s})();x?.length||(d.log.error("\uD56D\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4\uC694."),process.exit(0)),d.log.message(`\uC120\uD0DD\uB41C \uD56D\uBAA9: ${i(x.join(", "))}`);let y=[];for(let s of x){let[m,...C]=s.split(":"),v=C.join(":");(!m||!v)&&(d.log.error(`${i(s)}: \uD56D\uBAA9 \uC774\uB984\uC774 \uC798\uBABB\uB418\uC5C8\uC5B4\uC694. ${i("ui:action-button")}\uACFC \uAC19\uC740 \uD615\uC2DD\uC73C\uB85C \uC785\uB825\uD574\uBCF4\uC138\uC694.`),process.exit(1));let P=T.find(R=>R.id===m)?.items.find(R=>R.id===v);if(P||(d.log.error(`${i(s)}: \uD56D\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC5B4\uC694.`),process.exit(1)),P.deprecated){let R=await d.confirm({message:`${i(P.id)}: deprecated \uB418\uC5C8\uC5B4\uC694. \uCD94\uAC00\uD560\uAE4C\uC694?`,initialValue:!1});if(R===!1||d.isCancel(R)){d.log.info(`${i(P.id)}: \uCD94\uAC00\uD558\uC9C0 \uC54A\uC744\uAC8C\uC694.`);continue}}y.push(s)}let{registryItemsToAdd:b,npmDependenciesToAdd:j}=J({selectedItemKeys:y,publicRegistries:T});d.log.info(`\uCD94\uAC00\uD560 \uD56D\uBAA9: ${i(b.map(s=>s.items.map(m=>`${s.registryId}:${m.id}`).join(", ")).join(", ")||"\uC5C6\uC74C")}
|
|
5
7
|
|
|
6
|
-
\uC124\uCE58\uD560 \uC758\uC874\uC131: ${
|
|
7
|
-
`,"utf-8");let
|
|
8
|
+
\uC124\uCE58\uD560 \uC758\uC874\uC131: ${i(Array.from(j).join(", ")||"\uC5C6\uC74C")}`),await Y({registryItemsToAdd:b,rootPath:S,cwd:n,baseUrl:u,config:$,overwrite:a.overwrite});try{let{installed:s,filtered:m}=await W({cwd:n,deps:Array.from(j)});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: ${i(Array.from(s).join(", "))}`),m.size&&d.log.message(`\uC124\uCE58\uD558\uC9C0 \uC54A\uC740 \uC758\uC874\uC131 (\uC774\uBBF8 \uC124\uCE58\uB428): ${i(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(i("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let O=Date.now()-c,N=new Set(b.map(s=>s.registryId)),w=x.some(s=>{let[m,...C]=s.split(":"),v=C.join(":");return T.find(P=>P.id===m)?.items.find(P=>P.id===v)?.deprecated});await L.track(a.cwd,{event:"add",properties:{items_count:y.length,registries:Array.from(N),has_deprecated:w,dependencies_count:j.size,duration_ms:O}})})};import*as g from"@clack/prompts";import Ze from"path";import{z as _}from"zod";var et=_.object({registryIds:_.array(_.string()).optional(),all:_.boolean(),includeDeprecated:_.boolean().optional(),cwd:_.string(),baseUrl:_.string().optional(),overwrite:_.boolean().optional()}),ue=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:X}).option("--overwrite","Overwrite existing files without confirmation",{default:!1}).example("seed-design add-all ui --include-deprecated").example("seed-design add-all ui lib breeze").action(async(t,o)=>{let c=Date.now();g.intro("seed-design add-all");let{success:l,data:r,error:a}=et.safeParse({registryIds:t,...o});l||(g.log.error(`\uC798\uBABB\uB41C \uC635\uC158\uC774\uC5D0\uC694: ${a?.message}`),process.exit(1));let p=r.cwd,n=r.baseUrl,u=await M(p),$=Ze.resolve(p,u.path),{start:S,stop:D}=g.spinner();S("Registry\uB97C \uAC00\uC838\uC624\uACE0 \uC788\uC5B4\uC694...");let I=await Promise.all((await H({baseUrl:n})).map(async({id:w})=>V({baseUrl:n,registryId:w})));D("Registry\uB97C \uAC00\uC838\uC654\uC5B4\uC694.");let T=await(async()=>{if(r.all){let s=I.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: ${i(s.join(", "))}`),s}if(r.registryIds?.length){let s=I.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: ${i(r.registryIds.join(", "))}`),r.registryIds}let w=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:I.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(w)&&(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: ${i(w.join(", "))}`),w})(),x=I.filter(w=>T.includes(w.id)),y=x.flatMap(w=>w.items.filter(s=>s.deprecated?r.includeDeprecated:!0).map(s=>`${w.id}:${s.id}`)),b=x.flatMap(w=>w.items.filter(s=>s.deprecated).map(()=>1)).length;!r.includeDeprecated&&b>0&&g.log.info(`${b}\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.`),y.length||(g.log.error("\uCD94\uAC00\uD560 \uD56D\uBAA9\uC774 \uC5C6\uC5B4\uC694."),process.exit(0)),g.log.message(`\uCD1D ${i(y.length.toString())}\uAC1C\uC758 \uD56D\uBAA9\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4.`);let{registryItemsToAdd:j,npmDependenciesToAdd:O}=J({selectedItemKeys:y,publicRegistries:I});await Y({registryItemsToAdd:j,rootPath:$,cwd:p,baseUrl:n,config:u,overwrite:r.overwrite});try{let{installed:w,filtered:s}=await W({cwd:p,deps:Array.from(O)});w.size===0&&g.log.message("\uBAA8\uB4E0 \uC758\uC874\uC131\uC774 \uC774\uBBF8 \uC124\uCE58\uB418\uC5B4 \uC788\uC5B4\uC694."),w.size&&(g.log.message(`\uC758\uC874\uC131 \uC124\uCE58 \uC644\uB8CC: ${i(Array.from(w).join(", "))}`),s.size&&g.log.message(`\uC124\uCE58\uD558\uC9C0 \uC54A\uC740 \uC758\uC874\uC131 (\uC774\uBBF8 \uC124\uCE58\uB428): ${i(Array.from(s).join(", "))}`)),g.outro("\uC644\uB8CC\uD588\uC5B4\uC694.")}catch(w){g.log.error(`\uCD94\uAC00\uC5D0 \uC2E4\uD328\uD588\uC5B4\uC694. ${w}`),g.outro(i("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let N=Date.now()-c;await L.track(r.cwd,{event:"add-all",properties:{registries:T,items_count:y.length,include_deprecated:r.includeDeprecated||!1,dependencies_count:O.size,duration_ms:N}})})};import*as h from"@clack/prompts";import tt from"fs-extra";import ye from"path";import{z as te}from"zod";import rt from"dedent";var st=te.object({cwd:te.string(),yes:te.boolean().optional()}),he=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 o=Date.now();h.intro("seed-design.json \uD30C\uC77C \uC0DD\uC131");let c=st.parse(t),l=c.yes,r={rsc:!1,tsx:!0,path:"./seed-design",telemetry:!0};l||(r={...await h.group({tsx:()=>h.confirm({message:`${i("TypeScript")}\uB97C \uC0AC\uC6A9\uC911\uC774\uC2E0\uAC00\uC694?`,initialValue:!0}),rsc:()=>h.confirm({message:`${i("React Server Components")}\uB97C \uC0AC\uC6A9\uC911\uC774\uC2E0\uAC00\uC694?`,initialValue:!1}),path:()=>h.text({message:`${i("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:()=>h.confirm({message:`\uAC1C\uC120\uC744 \uC704\uD574 ${i("\uC775\uBA85 \uC0AC\uC6A9 \uB370\uC774\uD130")}\uB97C \uC218\uC9D1\uD560\uAE4C\uC694?`,initialValue:!0})},{onCancel:()=>{h.cancel("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694."),process.exit(0)}})});try{let{start:p,stop:n}=h.spinner();p("seed-design.json \uD30C\uC77C \uC0DD\uC131\uC911...");let u=ye.resolve(c.cwd,"seed-design.json");await tt.writeFile(u,`${JSON.stringify(r,null,2)}
|
|
9
|
+
`,"utf-8");let $=ye.relative(process.cwd(),u);n(`seed-design.json \uD30C\uC77C\uC774 ${i($)}\uC5D0 \uC0DD\uC131\uB410\uC5B4\uC694.`),h.log.info(i("seed-design add {component} \uBA85\uB839\uC5B4\uB85C \uCEF4\uD3EC\uB10C\uD2B8\uB97C \uCD94\uAC00\uD574\uBCF4\uC138\uC694!")),h.log.info(i("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.")),h.note(rt(`SEED Design CLI\uB294 \uAC1C\uC120\uC744 \uC704\uD574 \uC775\uBA85 \uC0AC\uC6A9 \uB370\uC774\uD130\uB97C \uC218\uC9D1\uD574\uC694.
|
|
10
|
+
|
|
11
|
+
\uBE44\uD65C\uC131\uD654\uD558\uB824\uBA74:
|
|
12
|
+
\u2022 seed-design.json\uC5D0\uC11C ${i('"telemetry": false')}\uB85C \uC124\uC815
|
|
13
|
+
\u2022 ${i("DISABLE_TELEMETRY=true")} \uD658\uACBD \uBCC0\uC218 \uC124\uC815
|
|
14
|
+
|
|
15
|
+
\uC790\uC138\uD55C \uB0B4\uC6A9: https://seed-design.com/react/getting-started/cli/configuration#telemetry`),"Telemetry \uC548\uB0B4"),h.outro("\uC791\uC5C5\uC774 \uC644\uB8CC\uB410\uC5B4\uC694.")}catch(p){h.log.error(`seed-design.json \uD30C\uC77C \uC0DD\uC131\uC5D0 \uC2E4\uD328\uD588\uC5B4\uC694. ${p}`),h.outro(i("\uC791\uC5C5\uC774 \uCDE8\uC18C\uB410\uC5B4\uC694.")),process.exit(1)}let a=Date.now()-o;await L.track(c.cwd,{event:"init",properties:{tsx:r.tsx,rsc:r.rsc,telemetry:r.telemetry,yes_option:l,duration_ms:a}})})};import{cac as ot}from"cac";var it="seed-design",K=ot(it);async function nt(){let e=q();ge(K),ue(K),he(K),K.version(e.version||"1.0.0","-v, --version"),K.help(),K.parse()}nt();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-design/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,13 +25,16 @@
|
|
|
25
25
|
"lint:publish": "bun publint"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@antfu/ni": "^
|
|
28
|
+
"@antfu/ni": "^28.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
|
+
"@npmcli/disparity-colors": "^3.0.1",
|
|
33
34
|
"cac": "^6.7.14",
|
|
34
35
|
"cosmiconfig": "^9.0.0",
|
|
36
|
+
"dedent": "^1.7.0",
|
|
37
|
+
"diff": "^8.0.3",
|
|
35
38
|
"execa": "^9.5.2",
|
|
36
39
|
"findup-sync": "^5.0.0",
|
|
37
40
|
"fs-extra": "^11.3.0",
|
|
@@ -43,7 +46,8 @@
|
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@types/babel__core": "^7.20.5",
|
|
45
48
|
"@types/fs-extra": "^11.0.4",
|
|
46
|
-
"
|
|
49
|
+
"dotenv": "^17.2.3",
|
|
50
|
+
"esbuild": "^0.27.0",
|
|
47
51
|
"type-fest": "^5.0.0",
|
|
48
52
|
"typescript": "^5.9.2"
|
|
49
53
|
},
|
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
|
|
|
@@ -17,6 +18,7 @@ const addAllOptionsSchema = z.object({
|
|
|
17
18
|
includeDeprecated: z.boolean().optional(),
|
|
18
19
|
cwd: z.string(),
|
|
19
20
|
baseUrl: z.string().optional(),
|
|
21
|
+
overwrite: z.boolean().optional(),
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
export const addAllCommand = (cli: CAC) => {
|
|
@@ -36,9 +38,13 @@ export const addAllCommand = (cli: CAC) => {
|
|
|
36
38
|
"the base url of the registry. defaults to the current directory.",
|
|
37
39
|
{ default: BASE_URL },
|
|
38
40
|
)
|
|
41
|
+
.option("--overwrite", "Overwrite existing files without confirmation", {
|
|
42
|
+
default: false,
|
|
43
|
+
})
|
|
39
44
|
.example("seed-design add-all ui --include-deprecated")
|
|
40
45
|
.example("seed-design add-all ui lib breeze")
|
|
41
46
|
.action(async (registryIds, opts) => {
|
|
47
|
+
const startTime = Date.now();
|
|
42
48
|
p.intro("seed-design add-all");
|
|
43
49
|
|
|
44
50
|
const {
|
|
@@ -154,27 +160,53 @@ export const addAllCommand = (cli: CAC) => {
|
|
|
154
160
|
publicRegistries,
|
|
155
161
|
});
|
|
156
162
|
|
|
157
|
-
await writeRegistryItemSnippets({
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
await writeRegistryItemSnippets({
|
|
164
|
+
registryItemsToAdd,
|
|
165
|
+
rootPath,
|
|
160
166
|
cwd,
|
|
161
|
-
|
|
167
|
+
baseUrl,
|
|
168
|
+
config,
|
|
169
|
+
overwrite: options.overwrite,
|
|
162
170
|
});
|
|
163
171
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
try {
|
|
173
|
+
const { installed, filtered } = await installDependencies({
|
|
174
|
+
cwd,
|
|
175
|
+
deps: Array.from(npmDependenciesToAdd),
|
|
176
|
+
});
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
if (installed.size === 0) {
|
|
179
|
+
p.log.message("모든 의존성이 이미 설치되어 있어요.");
|
|
180
|
+
}
|
|
170
181
|
|
|
171
|
-
if (
|
|
172
|
-
p.log.message(
|
|
173
|
-
|
|
174
|
-
)
|
|
182
|
+
if (installed.size) {
|
|
183
|
+
p.log.message(`의존성 설치 완료: ${highlight(Array.from(installed).join(", "))}`);
|
|
184
|
+
|
|
185
|
+
if (filtered.size) {
|
|
186
|
+
p.log.message(
|
|
187
|
+
`설치하지 않은 의존성 (이미 설치됨): ${highlight(Array.from(filtered).join(", "))}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
175
190
|
}
|
|
191
|
+
|
|
192
|
+
p.outro("완료했어요.");
|
|
193
|
+
} catch (error) {
|
|
194
|
+
p.log.error(`추가에 실패했어요. ${error}`);
|
|
195
|
+
p.outro(highlight("작업이 취소됐어요."));
|
|
196
|
+
process.exit(1);
|
|
176
197
|
}
|
|
177
198
|
|
|
178
|
-
|
|
199
|
+
// add-all 성공 이벤트 추적
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
201
|
+
await analytics.track(options.cwd, {
|
|
202
|
+
event: "add-all",
|
|
203
|
+
properties: {
|
|
204
|
+
registries: selectedRegistryIds,
|
|
205
|
+
items_count: itemKeys.length,
|
|
206
|
+
include_deprecated: options.includeDeprecated || false,
|
|
207
|
+
dependencies_count: npmDependenciesToAdd.size,
|
|
208
|
+
duration_ms: duration,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
179
211
|
});
|
|
180
212
|
};
|
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(),
|
|
@@ -19,6 +20,7 @@ const addOptionsSchema = z.object({
|
|
|
19
20
|
all: z.boolean(),
|
|
20
21
|
cwd: z.string(),
|
|
21
22
|
baseUrl: z.string().optional(),
|
|
23
|
+
overwrite: z.boolean().optional(),
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
export const addCommand = (cli: CAC) => {
|
|
@@ -35,9 +37,13 @@ export const addCommand = (cli: CAC) => {
|
|
|
35
37
|
"the base url of the registry. defaults to the current directory.",
|
|
36
38
|
{ default: BASE_URL },
|
|
37
39
|
)
|
|
40
|
+
.option("--overwrite", "Overwrite existing files without confirmation", {
|
|
41
|
+
default: false,
|
|
42
|
+
})
|
|
38
43
|
.example("seed-design add ui:action-button")
|
|
39
44
|
.example("seed-design add ui:alert-dialog")
|
|
40
45
|
.action(async (itemIds, opts) => {
|
|
46
|
+
const startTime = Date.now();
|
|
41
47
|
p.intro("seed-design add");
|
|
42
48
|
|
|
43
49
|
const {
|
|
@@ -178,27 +184,54 @@ export const addCommand = (cli: CAC) => {
|
|
|
178
184
|
cwd,
|
|
179
185
|
baseUrl,
|
|
180
186
|
config,
|
|
187
|
+
overwrite: options.overwrite,
|
|
181
188
|
});
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
try {
|
|
191
|
+
const { installed, filtered } = await installDependencies({
|
|
192
|
+
cwd,
|
|
193
|
+
deps: Array.from(npmDependenciesToAdd),
|
|
194
|
+
});
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
if (installed.size === 0) {
|
|
197
|
+
p.log.message("모든 의존성이 이미 설치되어 있어요.");
|
|
198
|
+
}
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
if (installed.size) {
|
|
201
|
+
p.log.message(`의존성 설치 완료: ${highlight(Array.from(installed).join(", "))}`);
|
|
194
202
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
if (filtered.size) {
|
|
204
|
+
p.log.message(
|
|
205
|
+
`설치하지 않은 의존성 (이미 설치됨): ${highlight(Array.from(filtered).join(", "))}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
199
208
|
}
|
|
209
|
+
p.outro("완료했어요.");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
p.log.error(`추가에 실패했어요. ${error}`);
|
|
212
|
+
p.outro(highlight("작업이 취소됐어요."));
|
|
213
|
+
process.exit(1);
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
|
|
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
|
+
});
|
|
203
236
|
});
|
|
204
237
|
};
|
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
|
+
};
|
package/src/utils/get-config.ts
CHANGED
package/src/utils/write.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { transform } from "@/src/utils/transformers";
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import fs from "fs-extra";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { createPatch } from "diff";
|
|
7
|
+
import colorize from "@npmcli/disparity-colors";
|
|
6
8
|
import { highlight } from "./color";
|
|
7
9
|
import type { Config } from "@/src/utils/get-config";
|
|
8
10
|
import type { PublicRegistry } from "@/src/schema";
|
|
@@ -13,12 +15,14 @@ export async function writeRegistryItemSnippets({
|
|
|
13
15
|
cwd,
|
|
14
16
|
baseUrl,
|
|
15
17
|
config,
|
|
18
|
+
overwrite = false,
|
|
16
19
|
}: {
|
|
17
20
|
registryItemsToAdd: { registryId: string; items: PublicRegistry["items"] }[];
|
|
18
21
|
rootPath: string;
|
|
19
22
|
cwd: string;
|
|
20
23
|
baseUrl: string;
|
|
21
24
|
config: Config;
|
|
25
|
+
overwrite?: boolean;
|
|
22
26
|
}) {
|
|
23
27
|
const registryResult: { name: string; path: string }[] = [];
|
|
24
28
|
|
|
@@ -53,23 +57,85 @@ export async function writeRegistryItemSnippets({
|
|
|
53
57
|
}),
|
|
54
58
|
);
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
transformedSnippets.map(async ({ filePath, content }) => {
|
|
58
|
-
await fs.ensureDir(path.dirname(filePath));
|
|
59
|
-
await fs.writeFile(filePath, content);
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
60
|
+
const writtenSnippets: typeof transformedSnippets = [];
|
|
62
61
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
path: relativePath,
|
|
66
|
-
}));
|
|
62
|
+
for (const snippet of transformedSnippets) {
|
|
63
|
+
const { filePath, content, relativePath } = snippet;
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
// 파일 존재 여부 확인
|
|
68
|
+
if (fs.existsSync(filePath)) {
|
|
69
|
+
const existingContent = await fs.readFile(filePath, "utf-8");
|
|
70
|
+
|
|
71
|
+
// 내용이 동일하면 스킵
|
|
72
|
+
if (existingContent === content) {
|
|
73
|
+
p.log.info(`${highlight(relativePath)}: 이미 최신 상태예요.`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// diff가 있는 경우
|
|
78
|
+
if (!overwrite) {
|
|
79
|
+
// diff 생성 및 색상 적용
|
|
80
|
+
const patch = createPatch(relativePath, existingContent, content);
|
|
81
|
+
const coloredDiff = colorize(patch);
|
|
82
|
+
|
|
83
|
+
p.log.message(
|
|
84
|
+
`\n${highlight(relativePath)}: 현재 파일과 받으려는 파일의 내용이 달라요.\n`,
|
|
85
|
+
);
|
|
86
|
+
p.log.message(coloredDiff);
|
|
87
|
+
|
|
88
|
+
const filename = path.basename(filePath);
|
|
89
|
+
const ext = path.extname(filePath);
|
|
90
|
+
const base = path.basename(filePath, ext);
|
|
91
|
+
const timestamp = Date.now();
|
|
92
|
+
const legacyFilename = `legacy-${base}-${timestamp}${ext}`;
|
|
93
|
+
|
|
94
|
+
const action = await p.select({
|
|
95
|
+
message:
|
|
96
|
+
"현재 파일에 스타일 변경, 로깅 등 커스터마이징이 적용되어 있는 경우 신규 파일에 동일한 커스터마이징을 적용하는 것을 검토해보세요.",
|
|
97
|
+
options: [
|
|
98
|
+
{ value: "overwrite", label: `${filename} 덮어쓰기` },
|
|
99
|
+
{
|
|
100
|
+
value: "backup",
|
|
101
|
+
label: `기존 파일 내용을 ${legacyFilename}으로 옮기고 ${filename} 받기`,
|
|
102
|
+
},
|
|
103
|
+
{ value: "skip", label: "새 파일 받지 않고 그대로 두기" },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (p.isCancel(action) || action === "skip") {
|
|
108
|
+
p.log.info(`${highlight(relativePath)}: 파일을 받지 않고 건너뛰었어요.`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (action === "backup") {
|
|
113
|
+
const dir = path.dirname(filePath);
|
|
114
|
+
const legacyPath = path.join(dir, legacyFilename);
|
|
115
|
+
await fs.rename(filePath, legacyPath);
|
|
116
|
+
p.log.info(
|
|
117
|
+
`${highlight(relativePath)}: 기존 파일을 ${highlight(path.relative(cwd, legacyPath))}로 옮겼어요.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await fs.writeFile(filePath, content);
|
|
124
|
+
writtenSnippets.push(snippet);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (writtenSnippets.length > 0) {
|
|
128
|
+
const snippetResults = writtenSnippets.map(({ name, relativePath }) => ({
|
|
129
|
+
name,
|
|
130
|
+
path: relativePath,
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
registryResult.push(...snippetResults);
|
|
134
|
+
|
|
135
|
+
p.log.success(
|
|
136
|
+
`${highlight(`${registryId}:${id}`)} 관련 스니펫 다운로드 완료: ${highlight(snippetResults.map((r) => r.path).join(", "))}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
73
139
|
}
|
|
74
140
|
}
|
|
75
141
|
}
|