Skip to main content
Glama
reuvenaor

Shadcn Registry manager

by reuvenaor
create-project.ts7.24 kB
import os from "os" import path from "path" import { fetchRegistry } from "@/src/registry/api" import { getPackageManager } from "@/src/utils/get-package-manager" import { handleError } from "@/src/utils/handle-error" import { logger } from "@/src/utils/logger" import { spinner } from "@/src/utils/spinner" import { secureExeca } from "@/src/utils/secure-exec" import { getSafeWorkspaceCwd } from "@/src/utils/security" import fs from "fs-extra" import { z } from "zod" import { initOptionsSchema } from "@/src/schemas/init.schemas" import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol" import { ServerRequest, ServerNotification } from "@modelcontextprotocol/sdk/types" const MONOREPO_TEMPLATE_URL = "https://codeload.github.com/shadcn-ui/ui/tar.gz/main" export const TEMPLATES = { next: "next", "next-monorepo": "next-monorepo", } as const export async function createProject( options: z.infer<typeof initOptionsSchema> ) { const validatedOptions = initOptionsSchema.parse(options) return createProjectInternal(validatedOptions, { mcpSafe: false }) } // MCP-safe version that throws errors instead of exiting export async function createProjectMcp( options: z.infer<typeof initOptionsSchema>, extra?: RequestHandlerExtra<ServerRequest, ServerNotification> ) { const validatedOptions = initOptionsSchema.parse(options) return createProjectInternal(validatedOptions, { mcpSafe: true }, extra) } async function createProjectInternal( options: z.infer<typeof initOptionsSchema>, context: { mcpSafe: boolean }, extra?: RequestHandlerExtra<ServerRequest, ServerNotification> ) { options = { srcDir: false, ...options, } let template: keyof typeof TEMPLATES = options.template && TEMPLATES[options.template as keyof typeof TEMPLATES] ? (options.template as keyof typeof TEMPLATES) : "next" let projectName: string = template === TEMPLATES.next ? "my-app" : "my-monorepo" let nextVersion = "latest" const isRemoteComponent = options.components?.length === 1 && !!options.components[0].match(/\/chat\/b\//) if (options.components && isRemoteComponent) { try { const [result] = await fetchRegistry(options.components) const { meta } = z .object({ meta: z.object({ nextVersion: z.string(), }), }) .parse(result) nextVersion = meta.nextVersion // Force template to next for remote components. template = TEMPLATES.next } catch (error) { logger.break() handleError(error) } } if (!options.force) { template = (options?.template ?? template) as keyof typeof TEMPLATES projectName = (options?.name ?? projectName) as string } const packageManager = await getPackageManager(options.cwd, { withFallback: true, }) const projectPath = `${options.cwd}/${projectName}` // Validate and check if path is writable. try { const safeCwd = getSafeWorkspaceCwd(options.cwd) options.cwd = safeCwd await fs.access(safeCwd, fs.constants.W_OK) } catch (error) { logger.break() logger.error(`The path ${options.cwd} is not writable or secure.`) logger.error( `It is likely you do not have write permissions for this folder or the path ${options.cwd} does not exist.` ) logger.break() throw new Error(`The path ${options.cwd} is not writable or secure`) } if (fs.existsSync(path.resolve(options.cwd, projectName, "package.json"))) { logger.break() logger.error( `A project with the name ${projectName} already exists.` ) logger.error(`Please choose a different name and try again.`) logger.break() throw new Error(`A project with the name ${projectName} already exists`) } if (template === TEMPLATES.next) { await createNextProject(projectPath, { version: nextVersion, cwd: options.cwd, packageManager, srcDir: !!options.srcDir, }, context, extra) } if (template === TEMPLATES["next-monorepo"]) { await createMonorepoProject(projectPath, { packageManager, }, context, extra) } return { projectPath, projectName, template, } } async function createNextProject( projectPath: string, options: { version: string cwd: string packageManager: string srcDir: boolean }, context: { mcpSafe: boolean }, extra?: RequestHandlerExtra<ServerRequest, ServerNotification> ) { const createSpinner = spinner( `Creating a new Next.js project. This may take a few minutes.`, extra, "create-next-project" ).start() // Note: pnpm fails here. Fallback to npx with --use-PACKAGE-MANAGER. const args = [ "--tailwind", "--eslint", "--typescript", "--app", options.srcDir ? "--src-dir" : "--no-src-dir", "--no-import-alias", `--use-${options.packageManager}`, ] if ( options.version.startsWith("15") || options.version.startsWith("latest") || options.version.startsWith("canary") ) { args.push("--turbopack") } try { await secureExeca( "npx", [`create-next-app@${options.version}`, projectPath, "--silent", ...args], { cwd: options.cwd, } ) } catch (error) { logger.break() logger.error( `Something went wrong creating a new Next.js project. Please try again.` ) throw new Error(`Failed to create Next.js project: ${error}`) } createSpinner?.succeed('Creating a new Next.js project complete') } async function createMonorepoProject( projectPath: string, options: { packageManager: string }, context: { mcpSafe: boolean }, extra?: RequestHandlerExtra<ServerRequest, ServerNotification> ) { const createSpinner = spinner( `Creating a new Next.js monorepo. This may take a few minutes.`, extra, "create-monorepo-project" ).start() try { // Get the template. const templatePath = path.join(os.tmpdir(), `shadcn-template-${Date.now()}`) await fs.ensureDir(templatePath) const response = await fetch(MONOREPO_TEMPLATE_URL) if (!response.ok) { throw new Error(`Failed to download template: ${response.statusText}`) } // Write the tar file const tarPath = path.resolve(templatePath, "template.tar.gz") await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer())) await secureExeca("tar", [ "-xzf", tarPath, "-C", templatePath, "--strip-components=2", "ui-main/templates/monorepo-next", ]) const extractedPath = path.resolve(templatePath, "monorepo-next") await fs.move(extractedPath, projectPath) await fs.remove(templatePath) // Run install. if (options.packageManager === 'npm' || options.packageManager === 'pnpm') { await secureExeca(options.packageManager, ["install"], { cwd: projectPath, }) } else { throw new Error(`Package manager not supported for monorepo: ${options.packageManager}`) } createSpinner?.succeed('Creating a new Next.js monorepo complete') } catch (error) { createSpinner?.fail('Something went wrong creating a new Next.js monorepo') throw new Error(`Failed to create Next.js monorepo: ${error}`) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reuvenaor/shadcn-registry-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server