Skip to content

Commit 5f96916

Browse files
committed
feat: refactor shadcn info command to output llm-friendly output
1 parent 4a96d95 commit 5f96916

2 files changed

Lines changed: 236 additions & 6 deletions

File tree

.changeset/smooth-feet-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": major
3+
---
4+
5+
refactor shadcn info command to output llm-friendly output
Lines changed: 231 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1+
import { existsSync } from "fs"
2+
import path from "path"
13
import { getConfig } from "@/src/utils/get-config"
2-
import { getProjectInfo } from "@/src/utils/get-project-info"
4+
import {
5+
formatMonorepoMessage,
6+
getMonorepoTargets,
7+
isMonorepoRoot,
8+
} from "@/src/utils/get-monorepo-info"
9+
import {
10+
getProjectComponents,
11+
getProjectInfo,
12+
type ProjectInfo,
13+
} from "@/src/utils/get-project-info"
314
import { handleError } from "@/src/utils/handle-error"
15+
import { highlighter } from "@/src/utils/highlighter"
416
import { logger } from "@/src/utils/logger"
517
import { Command } from "commander"
618

19+
const GITHUB_RAW_BASE =
20+
"https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/v4/registry/bases"
21+
722
export const info = new Command()
823
.name("info")
924
.description("get information about your project")
@@ -12,14 +27,224 @@ export const info = new Command()
1227
"the working directory. defaults to the current directory.",
1328
process.cwd()
1429
)
30+
.option("--json", "output as JSON.", false)
1531
.action(async (opts) => {
1632
try {
17-
logger.info("> project info")
18-
console.log(await getProjectInfo(opts.cwd))
19-
logger.break()
20-
logger.info("> components.json")
21-
console.log(await getConfig(opts.cwd))
33+
const cwd = path.resolve(opts.cwd)
34+
35+
// Check if we're in a monorepo root.
36+
if (
37+
!existsSync(path.resolve(cwd, "components.json")) &&
38+
(await isMonorepoRoot(cwd))
39+
) {
40+
const targets = await getMonorepoTargets(cwd)
41+
if (targets.length > 0) {
42+
if (opts.json) {
43+
console.log(
44+
JSON.stringify(
45+
{
46+
error: "monorepo_root",
47+
message:
48+
"You are running info from a monorepo root. Use the -c flag to specify a workspace.",
49+
targets: targets.map((t) => t.name),
50+
},
51+
null,
52+
2
53+
)
54+
)
55+
} else {
56+
formatMonorepoMessage("info", targets)
57+
}
58+
process.exit(1)
59+
}
60+
}
61+
62+
const projectInfo = await getProjectInfo(cwd)
63+
const config = await getConfig(cwd)
64+
const components = await getProjectComponents(cwd)
65+
const base = getBase(config?.style)
66+
const data = collectInfo(projectInfo, config, components, base)
67+
68+
if (opts.json) {
69+
console.log(JSON.stringify(data, null, 2))
70+
return
71+
}
72+
73+
printInfo(data)
2274
} catch (error) {
2375
handleError(error)
2476
}
2577
})
78+
79+
function getBase(style: string | undefined) {
80+
return style?.startsWith("base-") ? "base" : "radix"
81+
}
82+
83+
function getRegistries(
84+
registries: Record<string, string | { url: string }> | undefined
85+
) {
86+
if (!registries) {
87+
return {}
88+
}
89+
90+
const result: Record<string, string> = {}
91+
for (const [name, value] of Object.entries(registries)) {
92+
result[name] = typeof value === "string" ? value : value.url
93+
}
94+
return result
95+
}
96+
97+
function collectInfo(
98+
projectInfo: ProjectInfo | null,
99+
config: Awaited<ReturnType<typeof getConfig>>,
100+
components: string[],
101+
base: string
102+
) {
103+
return {
104+
project: projectInfo
105+
? {
106+
framework: projectInfo.framework.label,
107+
frameworkName: projectInfo.framework.name,
108+
frameworkVersion: projectInfo.frameworkVersion ?? null,
109+
srcDirectory: projectInfo.isSrcDir,
110+
rsc: projectInfo.isRSC,
111+
typescript: projectInfo.isTsx,
112+
tailwindVersion: projectInfo.tailwindVersion ?? null,
113+
tailwindConfig: projectInfo.tailwindConfigFile ?? null,
114+
tailwindCss: projectInfo.tailwindCssFile ?? null,
115+
importAlias: projectInfo.aliasPrefix ?? null,
116+
}
117+
: null,
118+
config: config
119+
? {
120+
style: config.style,
121+
base,
122+
rsc: config.rsc,
123+
typescript: config.tsx,
124+
iconLibrary: config.iconLibrary ?? null,
125+
rtl: config.rtl ?? false,
126+
menuColor: config.menuColor ?? null,
127+
menuAccent: config.menuAccent ?? null,
128+
aliases: {
129+
components: config.aliases.components,
130+
utils: config.aliases.utils,
131+
ui: config.aliases.ui ?? null,
132+
lib: config.aliases.lib ?? null,
133+
hooks: config.aliases.hooks ?? null,
134+
},
135+
resolvedPaths: {
136+
cwd: config.resolvedPaths.cwd,
137+
tailwindConfig: config.resolvedPaths.tailwindConfig || null,
138+
tailwindCss: config.resolvedPaths.tailwindCss || null,
139+
utils: config.resolvedPaths.utils,
140+
components: config.resolvedPaths.components,
141+
lib: config.resolvedPaths.lib,
142+
hooks: config.resolvedPaths.hooks,
143+
ui: config.resolvedPaths.ui,
144+
},
145+
registries: getRegistries(config.registries),
146+
}
147+
: null,
148+
components,
149+
links: {
150+
docs: "https://ui.shadcn.com/docs",
151+
components: `https://ui.shadcn.com/docs/components/${base}/[component].md`,
152+
ui: `${GITHUB_RAW_BASE}/${base}/ui/[component].tsx`,
153+
examples: `${GITHUB_RAW_BASE}/${base}/examples/[component]-example.tsx`,
154+
schema: "https://ui.shadcn.com/schema.json",
155+
},
156+
}
157+
}
158+
159+
function printInfo(data: ReturnType<typeof collectInfo>) {
160+
// Project.
161+
logger.log(highlighter.info("Project"))
162+
if (data.project) {
163+
printEntries({
164+
framework: `${data.project.framework} (${data.project.frameworkName})`,
165+
frameworkVersion: data.project.frameworkVersion ?? "-",
166+
srcDirectory: data.project.srcDirectory ? "Yes" : "No",
167+
rsc: data.project.rsc ? "Yes" : "No",
168+
typescript: data.project.typescript ? "Yes" : "No",
169+
tailwindVersion: data.project.tailwindVersion ?? "-",
170+
tailwindConfig: data.project.tailwindConfig ?? "-",
171+
tailwindCss: data.project.tailwindCss ?? "-",
172+
importAlias: data.project.importAlias ?? "-",
173+
})
174+
} else {
175+
logger.log(" No project info detected.")
176+
}
177+
178+
// Config.
179+
logger.break()
180+
logger.log(highlighter.info("Configuration"))
181+
if (data.config) {
182+
printEntries({
183+
style: data.config.style,
184+
base: data.config.base,
185+
rsc: data.config.rsc ? "Yes" : "No",
186+
typescript: data.config.typescript ? "Yes" : "No",
187+
iconLibrary: data.config.iconLibrary ?? "-",
188+
rtl: data.config.rtl ? "Yes" : "No",
189+
menuColor: data.config.menuColor ?? "-",
190+
menuAccent: data.config.menuAccent ?? "-",
191+
})
192+
193+
// Aliases.
194+
logger.break()
195+
logger.log(highlighter.info("Aliases"))
196+
printEntries({
197+
components: data.config.aliases.components,
198+
utils: data.config.aliases.utils,
199+
ui: data.config.aliases.ui ?? "-",
200+
lib: data.config.aliases.lib ?? "-",
201+
hooks: data.config.aliases.hooks ?? "-",
202+
})
203+
204+
// Resolved paths.
205+
logger.break()
206+
logger.log(highlighter.info("Resolved Paths"))
207+
printEntries({
208+
cwd: data.config.resolvedPaths.cwd,
209+
tailwindConfig: data.config.resolvedPaths.tailwindConfig ?? "-",
210+
tailwindCss: data.config.resolvedPaths.tailwindCss ?? "-",
211+
utils: data.config.resolvedPaths.utils,
212+
components: data.config.resolvedPaths.components,
213+
lib: data.config.resolvedPaths.lib,
214+
hooks: data.config.resolvedPaths.hooks,
215+
ui: data.config.resolvedPaths.ui,
216+
})
217+
218+
// Registries.
219+
if (Object.keys(data.config.registries).length > 0) {
220+
logger.break()
221+
logger.log(highlighter.info("Registries"))
222+
printEntries(data.config.registries)
223+
}
224+
} else {
225+
logger.log(" No components.json found.")
226+
}
227+
228+
// Installed components.
229+
logger.break()
230+
logger.log(highlighter.info("Installed Components"))
231+
if (data.components.length > 0) {
232+
logger.log(` ${data.components.join(", ")}`)
233+
} else {
234+
logger.log(" No components installed.")
235+
}
236+
237+
// Links.
238+
logger.break()
239+
logger.log(highlighter.info("Links"))
240+
printEntries(data.links)
241+
242+
logger.break()
243+
}
244+
245+
function printEntries(entries: Record<string, string>) {
246+
const maxKeyLength = Math.max(...Object.keys(entries).map((k) => k.length))
247+
for (const [key, value] of Object.entries(entries)) {
248+
logger.log(` ${key.padEnd(maxKeyLength + 2)}${value}`)
249+
}
250+
}

0 commit comments

Comments
 (0)