@@ -2,13 +2,44 @@ import { tool } from "ai";
22import fs from "node:fs/promises" ;
33import path from "node:path" ;
44import { z } from "zod" ;
5+ import { typeTableGenerator } from "../../components/type-table/generator" ;
6+ import { getReactTypeTableOutput } from "../../components/type-table/get-react-type-table" ;
57import { findRelatedLinks as searchRelatedLinks } from "./sitemap-links" ;
68
79const SITEMAP_URL = "https://seed-design.io/sitemap.xml" ;
810
9- async function findExamplesDir ( ) : Promise < string | null > {
11+ async function findDocsRoot ( ) : Promise < string | null > {
1012 const cwd = process . cwd ( ) ;
11- const candidates = [ path . join ( cwd , "examples" ) , path . join ( cwd , "docs" , "examples" ) ] ;
13+ const candidates = [ path . join ( cwd , "docs" ) , cwd ] ;
14+
15+ for ( const candidate of candidates ) {
16+ try {
17+ const [ hasRegistry , hasComponents ] = await Promise . all ( [
18+ fs
19+ . stat ( path . join ( candidate , "registry" ) )
20+ . then ( ( stat ) => stat . isDirectory ( ) )
21+ . catch ( ( ) => false ) ,
22+ fs
23+ . stat ( path . join ( candidate , "components" ) )
24+ . then ( ( stat ) => stat . isDirectory ( ) )
25+ . catch ( ( ) => false ) ,
26+ ] ) ;
27+
28+ if ( hasRegistry && hasComponents ) {
29+ return candidate ;
30+ }
31+ } catch {
32+ // ignore and try next candidate
33+ }
34+ }
35+
36+ return null ;
37+ }
38+
39+ async function findExamplesDir ( ) : Promise < string | null > {
40+ const docsRoot = await findDocsRoot ( ) ;
41+ if ( ! docsRoot ) return null ;
42+ const candidates = [ path . join ( docsRoot , "examples" ) ] ;
1243
1344 for ( const candidate of candidates ) {
1445 try {
@@ -40,6 +71,123 @@ async function loadExampleCode(name: string): Promise<string | null> {
4071 }
4172}
4273
74+ function toPascalCase ( kebabCase : string ) : string {
75+ return kebabCase
76+ . split ( "-" )
77+ . filter ( Boolean )
78+ . map ( ( segment ) => segment . charAt ( 0 ) . toUpperCase ( ) + segment . slice ( 1 ) )
79+ . join ( "" ) ;
80+ }
81+
82+ function resolveDocsRelativePath ( docsRoot : string , docsRelativePath : string ) : string | null {
83+ const normalizedPath = path . normalize ( docsRelativePath ) ;
84+ const withoutDotPrefix = normalizedPath . startsWith ( "./" )
85+ ? normalizedPath . slice ( 2 )
86+ : normalizedPath ;
87+
88+ if ( withoutDotPrefix . startsWith ( ".." ) ) return null ;
89+
90+ const resolvedPath = path . resolve ( docsRoot , withoutDotPrefix ) ;
91+ const rootPath = path . resolve ( docsRoot ) ;
92+ if ( ! resolvedPath . startsWith ( `${ rootPath } ${ path . sep } ` ) ) return null ;
93+
94+ return resolvedPath ;
95+ }
96+
97+ async function loadReactTypeTable ( input : {
98+ component ?: string ;
99+ path ?: string ;
100+ name ?: string ;
101+ } ) : Promise < {
102+ shown : boolean ;
103+ typeName : string ;
104+ sourcePath : string ;
105+ rows : Array < {
106+ name : string ;
107+ type : string ;
108+ required : boolean ;
109+ description : string ;
110+ defaultValue : string | null ;
111+ } > ;
112+ error ?: string ;
113+ } > {
114+ const docsRoot = await findDocsRoot ( ) ;
115+ if ( ! docsRoot ) {
116+ return {
117+ shown : false ,
118+ typeName : input . name ?? "" ,
119+ sourcePath : input . path ?? "" ,
120+ rows : [ ] ,
121+ error : "docs 루트를 찾지 못했어요." ,
122+ } ;
123+ }
124+
125+ const componentName = input . component ?. trim ( ) ;
126+ const sourcePath =
127+ input . path ?. trim ( ) ||
128+ ( componentName ? `./registry/ui/${ componentName } .tsx` : "./registry/ui/action-button.tsx" ) ;
129+ const typeName =
130+ input . name ?. trim ( ) || ( componentName ? `${ toPascalCase ( componentName ) } Props` : "" ) ;
131+
132+ if ( ! typeName ) {
133+ return {
134+ shown : false ,
135+ typeName : "" ,
136+ sourcePath,
137+ rows : [ ] ,
138+ error : "타입 이름(name)이 비어 있습니다." ,
139+ } ;
140+ }
141+
142+ const resolvedPath = resolveDocsRelativePath ( docsRoot , sourcePath ) ;
143+ if ( ! resolvedPath ) {
144+ return {
145+ shown : false ,
146+ typeName,
147+ sourcePath,
148+ rows : [ ] ,
149+ error : "유효하지 않은 타입 경로입니다." ,
150+ } ;
151+ }
152+
153+ try {
154+ const output = await getReactTypeTableOutput ( {
155+ generator : typeTableGenerator ,
156+ path : resolvedPath ,
157+ name : typeName ,
158+ } ) ;
159+
160+ const table = output . find ( ( item ) => item . name === typeName ) ?? output [ 0 ] ;
161+ const rows =
162+ table ?. entries . map ( ( entry ) => ( {
163+ name : entry . name ,
164+ type : entry . type ,
165+ required : entry . required ,
166+ description : entry . description ,
167+ defaultValue :
168+ entry . tags . find ( ( tag ) => tag . name === "default" || tag . name === "defaultValue" ) ?. text ??
169+ null ,
170+ } ) ) ?? [ ] ;
171+
172+ return {
173+ shown : rows . length > 0 ,
174+ typeName : table ?. name ?? typeName ,
175+ sourcePath,
176+ rows,
177+ ...( rows . length === 0 ? { error : "타입 테이블 항목을 찾지 못했어요." } : { } ) ,
178+ } ;
179+ } catch ( error ) {
180+ const message = error instanceof Error ? error . message : "타입 테이블 로딩에 실패했습니다." ;
181+ return {
182+ shown : false ,
183+ typeName,
184+ sourcePath,
185+ rows : [ ] ,
186+ error : message ,
187+ } ;
188+ }
189+ }
190+
43191/**
44192 * 채팅 UI 렌더링용 도구.
45193 * 서버에서도 execute를 제공해 tool result가 누락되지 않도록 한다.
@@ -101,6 +249,46 @@ export const clientTools = {
101249 } ) ,
102250 } ) ,
103251
252+ showReactTypeTable : tool ( {
253+ description :
254+ "Show React props type table. Use when users ask for props/types of a React component. Prefer component input like 'action-button'." ,
255+ inputSchema : z
256+ . object ( {
257+ component : z
258+ . string ( )
259+ . min ( 1 )
260+ . max ( 64 )
261+ . regex (
262+ / ^ [ a - z 0 - 9 ] + (?: - [ a - z 0 - 9 ] + ) * $ / ,
263+ "Expected kebab-case component name (lowercase letters, numbers, hyphen)" ,
264+ )
265+ . optional ( )
266+ . describe ( "React component name in kebab-case, e.g., action-button" ) ,
267+ path : z
268+ . string ( )
269+ . min ( 1 )
270+ . max ( 200 )
271+ . optional ( )
272+ . describe ( "Path to source file from docs root, e.g., ./registry/ui/action-button.tsx" ) ,
273+ name : z
274+ . string ( )
275+ . min ( 1 )
276+ . max ( 120 )
277+ . optional ( )
278+ . describe ( "Type name to extract, e.g., ActionButtonProps" ) ,
279+ } )
280+ . refine ( ( value ) => Boolean ( value . component || value . path ) , {
281+ message : "Either component or path is required" ,
282+ } ) ,
283+ execute : async ( { component, path : sourcePath , name } ) => {
284+ return await loadReactTypeTable ( {
285+ component,
286+ path : sourcePath ,
287+ name,
288+ } ) ;
289+ } ,
290+ } ) ,
291+
104292 findRelatedLinks : tool ( {
105293 description :
106294 "Find related documentation URLs from the SEED Design sitemap. Use this before final response and attach related links when available. Prefer a mix of docs and react links when relevant." ,
0 commit comments