UNPKG

starwind

Version:

Add beautifully designed components to your Astro applications

1,533 lines (1,499 loc) 66.6 kB
#!/usr/bin/env node // src/index.ts import { Command } from "commander"; // package.json var package_default = { name: "starwind", version: "1.13.0", description: "Add beautifully designed components to your Astro applications", license: "MIT", author: { name: "webreaper", url: "https://x.com/BowTiedWebReapr" }, repository: { type: "git", url: "https://github.com/starwind-ui/starwind-ui.git", directory: "packages/cli" }, keywords: [ "starwind", "starwind ui", "starwind cli", "astro", "astro component", "astro ui", "astro ui library", "tailwind", "components", "add component" ], type: "module", bin: { starwind: "./dist/index.js" }, main: "./dist/index.js", types: "./dist/index.d.ts", files: [ "dist" ], scripts: { build: "tsup", dev: "tsup --watch", "cli:link": "pnpm link --global", "cli:unlink": "pnpm rm --global starwind", "cli:yalc:link": "yalc publish && yalc link @starwind-ui/core", "cli:yalc:unlink": "yalc remove @starwind-ui/core && yalc remove starwind", test: "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage", typecheck: "tsc --noEmit", "format:check": 'prettier --check "**/*.{ts,tsx,md,json}"', "format:write": 'prettier --write "**/*.{ts,tsx,md,json}" --cache', "publish:beta": "pnpm publish --tag beta --access public", "publish:next": "pnpm publish --tag next --access public", "publish:release": "pnpm publish --access public" }, dependencies: { "@clack/prompts": "^0.11.0", "@starwind-ui/core": "1.13.0", chalk: "^5.6.2", commander: "^14.0.2", execa: "^9.6.0", "fs-extra": "^11.3.2", semver: "^7.7.3", zod: "^3.25.74" }, devDependencies: { "@types/fs-extra": "^11.0.4", "@types/node": "^24.10.1", "@types/prompts": "^2.4.9", "@types/semver": "^7.7.1", tsup: "^8.5.1" }, engines: { node: "^20.6.0 || >=22.0.0" } }; // src/commands/add.ts import * as p6 from "@clack/prompts"; import { execa as execa2 } from "execa"; // src/utils/constants.ts var MIN_ASTRO_VERSION = "5.0.0"; var PATHS = { STARWIND_CORE: "@starwind-ui/core", STARWIND_CORE_COMPONENTS: "src/components", STARWIND_REMOTE_COMPONENT_REGISTRY: "https://starwind.dev/registry.json", STARWIND_PRO_REGISTRY: "https://pro.starwind.dev/r/{name}", LOCAL_CSS_FILE: "src/styles/starwind.css", LOCAL_CONFIG_FILE: "starwind.config.json", LOCAL_STYLES_DIR: "src/styles", LOCAL_COMPONENTS_DIR: "src/components" }; var ASTRO_PACKAGES = { core: "astro@latest" }; var OTHER_PACKAGES = { tailwindCore: "tailwindcss@^4", tailwindVite: "@tailwindcss/vite@^4", tailwindForms: "@tailwindcss/forms@^0.5", tailwindAnimate: "tw-animate-css@^1", tailwindVariants: "tailwind-variants@^3", tailwindMerge: "tailwind-merge@^3", tablerIcons: "@tabler/icons@^3" }; function getOtherPackages() { return Object.values(OTHER_PACKAGES); } // src/utils/fs.ts import fs from "fs-extra"; async function ensureDirectory(dir) { await fs.ensureDir(dir); } async function readJsonFile(filePath) { return fs.readJson(filePath); } async function writeJsonFile(filePath, data) { await fs.writeJson(filePath, data, { spaces: 2 }); } async function fileExists(filePath) { return fs.pathExists(filePath); } async function writeCssFile(filePath, content) { await fs.writeFile(filePath, content, "utf-8"); } // src/utils/config.ts var defaultConfig = { $schema: "https://starwind.dev/config-schema.json", tailwind: { css: "src/styles/starwind.css", baseColor: "neutral", cssVariables: true }, // aliases: { // components: "@/components", // }, componentDir: "src/components/starwind", components: [] }; async function getConfig() { try { if (await fileExists(PATHS.LOCAL_CONFIG_FILE)) { const config = await readJsonFile(PATHS.LOCAL_CONFIG_FILE); return { ...defaultConfig, ...config, components: Array.isArray(config.components) ? config.components : [] }; } } catch (error) { console.error("Error reading config:", error); } return defaultConfig; } async function updateConfig(updates, options = { appendComponents: true }) { const currentConfig = await getConfig(); const currentComponents = Array.isArray(currentConfig.components) ? currentConfig.components : []; let finalComponents = currentComponents; if (updates.components) { if (options.appendComponents) { const componentMap = /* @__PURE__ */ new Map(); for (const comp of currentComponents) { componentMap.set(comp.name, comp); } for (const comp of updates.components) { componentMap.set(comp.name, comp); } finalComponents = Array.from(componentMap.values()); } else { finalComponents = updates.components; } } const newConfig = { ...currentConfig, tailwind: { ...currentConfig.tailwind, ...updates.tailwind || {} }, componentDir: updates.componentDir ? updates.componentDir : currentConfig.componentDir, components: finalComponents }; try { await writeJsonFile(PATHS.LOCAL_CONFIG_FILE, newConfig); } catch (error) { throw new Error( `Failed to update config: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // src/utils/highlighter.ts import chalk from "chalk"; var highlighter = { error: chalk.red, warn: chalk.yellow, info: chalk.cyan, infoBright: chalk.cyanBright, success: chalk.greenBright, underline: chalk.underline, title: chalk.bgBlue }; // src/utils/install.ts import * as p3 from "@clack/prompts"; // src/utils/component.ts import * as path from "path"; import { fileURLToPath } from "url"; import * as p from "@clack/prompts"; import fs2 from "fs-extra"; import semver from "semver"; // src/utils/registry.ts import { registry as localRegistry } from "@starwind-ui/core"; import { z } from "zod"; var REGISTRY_CONFIG = { // Set to 'remote' to fetch from remote server or 'local' to use the imported registry SOURCE: "local" }; var componentSchema = z.object({ name: z.string(), version: z.string(), dependencies: z.array(z.string()).default([]), type: z.enum(["component"]) }); var registryRootSchema = z.object({ $schema: z.string().optional(), components: z.array(componentSchema) }); var registryCache = /* @__PURE__ */ new Map(); async function getRegistry(forceRefresh = false) { const cacheKey = REGISTRY_CONFIG.SOURCE === "remote" ? PATHS.STARWIND_REMOTE_COMPONENT_REGISTRY : "local-registry"; if (!forceRefresh && registryCache.has(cacheKey)) { return registryCache.get(cacheKey); } const registryPromise = REGISTRY_CONFIG.SOURCE === "remote" ? fetchRemoteRegistry() : Promise.resolve(getLocalRegistry()); registryCache.set(cacheKey, registryPromise); return registryPromise; } async function fetchRemoteRegistry() { try { const response = await fetch(PATHS.STARWIND_REMOTE_COMPONENT_REGISTRY); if (!response.ok) { throw new Error(`Failed to fetch registry: ${response.status} ${response.statusText}`); } const data = await response.json(); const parsedRegistry = registryRootSchema.parse(data); return parsedRegistry.components; } catch (error) { console.error("Failed to load remote registry:", error); throw error; } } function getLocalRegistry() { try { const components = localRegistry.map((comp) => componentSchema.parse(comp)); return components; } catch (error) { console.error("Failed to validate local registry:", error); throw error; } } async function getComponent(name, forceRefresh = false) { const registry = await getRegistry(forceRefresh); return registry.find((component) => component.name === name); } async function getAllComponents(forceRefresh = false) { return getRegistry(forceRefresh); } // src/utils/component.ts async function copyComponent(name, overwrite = false) { const config = await getConfig(); const currentComponents = Array.isArray(config.components) ? config.components : []; if (!overwrite && currentComponents.some((component) => component.name === name)) { const existingComponent = currentComponents.find((c) => c.name === name); return { status: "skipped", name, version: existingComponent?.version ?? "unknown" }; } const componentDir = path.join(config.componentDir, "starwind", name); try { await fs2.ensureDir(componentDir); const pkgUrl = import.meta.resolve?.(PATHS.STARWIND_CORE); if (!pkgUrl) { throw new Error(`Could not resolve ${PATHS.STARWIND_CORE} package, is it installed?`); } const coreDir = path.dirname(fileURLToPath(pkgUrl)); const sourceDir = path.join(coreDir, PATHS.STARWIND_CORE_COMPONENTS, name); const files = await fs2.readdir(sourceDir); for (const file of files) { const sourcePath = path.join(sourceDir, file); const destPath = path.join(componentDir, file); await fs2.copy(sourcePath, destPath, { overwrite: true }); } const registry = await getRegistry(); const componentInfo = registry.find((c) => c.name === name); if (!componentInfo) { throw new Error(`Component ${name} not found in registry`); } return { status: "installed", name, version: componentInfo.version }; } catch (error) { return { status: "failed", name, error: error instanceof Error ? error.message : "Unknown error" }; } } async function removeComponent(name, componentDir) { try { const componentPath = path.join(componentDir, "starwind", name); if (await fs2.pathExists(componentPath)) { await fs2.remove(componentPath); return { name, status: "removed" }; } else { return { name, status: "failed", error: "Component directory not found" }; } } catch (error) { return { name, status: "failed", error: error instanceof Error ? error.message : "Unknown error" }; } } async function updateComponent(name, currentVersion, skipConfirm) { try { const registryComponent = await getComponent(name); if (!registryComponent) { return { name, status: "failed", error: "Component not found in registry" }; } if (!semver.gt(registryComponent.version, currentVersion)) { return { name, status: "skipped", oldVersion: currentVersion, newVersion: registryComponent.version }; } let confirmUpdate = true; if (!skipConfirm) { const confirmedResult = await p.confirm({ message: `Update component ${highlighter.info( name )} from ${highlighter.warn(`v${currentVersion}`)} to ${highlighter.success( `v${registryComponent.version}` )}? This will override the existing implementation.` }); if (p.isCancel(confirmedResult)) { p.cancel("Update cancelled."); return { name, status: "skipped", oldVersion: currentVersion, newVersion: registryComponent.version // Still useful to return the target version }; } confirmUpdate = confirmedResult; } if (!confirmUpdate) { p.log.info(`Skipping update for ${highlighter.info(name)}`); return { name, status: "skipped", oldVersion: currentVersion, newVersion: registryComponent.version }; } const result = await copyComponent(name, true); if (result.status === "installed") { return { name, status: "updated", oldVersion: currentVersion, newVersion: result.version }; } else { return { name, status: "failed", error: result.status === "failed" ? result.error : "Failed to update component" }; } } catch (error) { return { name, status: "failed", error: error instanceof Error ? error.message : "Unknown error" }; } } // src/utils/dependency-resolver.ts import semver2 from "semver"; async function filterUninstalledDependencies(dependencies) { try { const pkg = await readJsonFile("package.json"); const installedDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} }; const dependenciesToInstall = []; for (const dep of dependencies) { let packageName; let requiredVersion; if (dep.startsWith("@")) { const lastAtIndex = dep.lastIndexOf("@"); if (lastAtIndex > 0) { packageName = dep.substring(0, lastAtIndex); requiredVersion = dep.substring(lastAtIndex + 1); } else { packageName = dep; requiredVersion = "*"; } } else { const atIndex = dep.indexOf("@"); if (atIndex > 0) { packageName = dep.substring(0, atIndex); requiredVersion = dep.substring(atIndex + 1); } else { packageName = dep; requiredVersion = "*"; } } const installedVersion = installedDeps[packageName]; if (!installedVersion) { dependenciesToInstall.push(dep); } else if (requiredVersion && requiredVersion !== "*") { const cleanInstalledVersion = semver2.clean(installedVersion) || installedVersion.replace(/^[\^~>=<= ]+/, ""); try { if (!semver2.satisfies(cleanInstalledVersion, requiredVersion)) { dependenciesToInstall.push(dep); } } catch (error) { dependenciesToInstall.push(dep); } } } return dependenciesToInstall; } catch (error) { return dependencies; } } function parseStarwindDependency(dependency) { const starwindPattern = /^@starwind-ui\/core\/([^@]+)@(.+)$/; const match = dependency.match(starwindPattern); if (!match) { return null; } const [, name, version] = match; return { name, version, originalSpec: dependency }; } function isStarwindDependency(dependency) { return parseStarwindDependency(dependency) !== null; } async function getInstalledComponentVersion(componentName) { try { const config = await getConfig(); const installedComponent = config.components.find((comp) => comp.name === componentName); return installedComponent?.version; } catch { return void 0; } } async function resolveStarwindDependency(dependency) { const parsed = parseStarwindDependency(dependency); if (!parsed) { return { component: dependency, requiredVersion: "", needsInstall: false, needsUpdate: false, isStarwindComponent: false }; } const currentVersion = await getInstalledComponentVersion(parsed.name); const registryComponent = await getComponent(parsed.name); if (!registryComponent) { throw new Error(`Starwind component "${parsed.name}" not found in registry`); } let needsInstall = false; let needsUpdate = false; if (!currentVersion) { needsInstall = true; } else { if (!semver2.satisfies(currentVersion, parsed.version)) { if (semver2.satisfies(registryComponent.version, parsed.version)) { needsUpdate = true; } else { throw new Error( `No version of "${parsed.name}" satisfies requirement "${parsed.version}". Latest available: ${registryComponent.version}, currently installed: ${currentVersion}` ); } } } return { component: parsed.name, currentVersion, requiredVersion: parsed.version, needsInstall, needsUpdate, isStarwindComponent: true }; } async function resolveAllStarwindDependencies(componentNames, resolved = /* @__PURE__ */ new Set()) { const resolutions = []; for (const componentName of componentNames) { if (resolved.has(componentName)) { continue; } resolved.add(componentName); const component = await getComponent(componentName); if (!component) { throw new Error(`Component "${componentName}" not found in registry`); } for (const dependency of component.dependencies) { const resolution = await resolveStarwindDependency(dependency); if (resolution && resolution.isStarwindComponent) { if (resolution.needsInstall || resolution.needsUpdate) { resolutions.push(resolution); } const nestedResolutions = await resolveAllStarwindDependencies( [resolution.component], resolved ); resolutions.push(...nestedResolutions); } } } const uniqueResolutions = /* @__PURE__ */ new Map(); for (const resolution of resolutions) { const existing = uniqueResolutions.get(resolution.component); if (!existing) { uniqueResolutions.set(resolution.component, resolution); } else { if (resolution.needsInstall && !existing.needsInstall) { uniqueResolutions.set(resolution.component, resolution); } } } return Array.from(uniqueResolutions.values()); } function separateDependencies(dependencies) { const starwindDependencies = []; const npmDependencies = []; for (const dependency of dependencies) { if (isStarwindDependency(dependency)) { starwindDependencies.push(dependency); } else { npmDependencies.push(dependency); } } return { starwindDependencies, npmDependencies }; } // src/utils/package-manager.ts import * as p2 from "@clack/prompts"; import { execa } from "execa"; async function requestPackageManager() { const pm = await p2.select({ message: "Select your preferred package manager", options: [ { value: "pnpm", label: "pnpm", hint: "Default" }, { value: "npm", label: "npm" }, { value: "yarn", label: "yarn" }, { value: "bun", label: "bun" } ] }); if (p2.isCancel(pm)) { p2.log.warn("No package manager selected, defaulting to npm"); return "npm"; } return pm; } function getCurrentPackageManager() { const userAgent = process.env.npm_config_user_agent; if (userAgent) { if (userAgent.includes("pnpm")) { return "pnpm"; } else if (userAgent.includes("yarn")) { return "yarn"; } else if (userAgent.includes("npm")) { return "npm"; } else if (userAgent.includes("bun")) { return "bun"; } } return null; } async function getDefaultPackageManager() { const current = getCurrentPackageManager(); if (current) { return current; } if (await fileExists("yarn.lock")) { return "yarn"; } else if (await fileExists("pnpm-lock.yaml")) { return "pnpm"; } else { return "npm"; } } async function getShadcnCommand() { const pm = await getDefaultPackageManager(); switch (pm) { case "pnpm": return ["pnpm", ["dlx", "shadcn@3"]]; case "yarn": return ["yarn", ["dlx", "shadcn@3"]]; case "bun": return ["bunx", ["shadcn@3"]]; case "npm": default: return ["npx", ["shadcn@3"]]; } } async function installDependencies(packages, pm, dev = false, force = false) { const args = [ pm === "npm" ? "install" : "add", ...packages, dev ? pm === "npm" || pm === "pnpm" ? "-D" : "--dev" : "", force ? "--force" : "" ].filter(Boolean); await execa(pm, args); } // src/utils/prompts.ts import { confirm as confirm2, multiselect } from "@clack/prompts"; async function selectComponents() { const components = await getAllComponents(); const selected = await multiselect({ message: "Select components to add ('a' for all, space to select, enter to confirm)", options: components.map((component) => ({ label: component.name, value: component.name })), required: false }); if (typeof selected === "symbol") { return []; } return selected; } async function confirmStarwindDependencies(componentNames) { try { const resolutions = await resolveAllStarwindDependencies(componentNames); if (resolutions.length === 0) { return true; } const toInstall = resolutions.filter((r) => r.needsInstall); const toUpdate = resolutions.filter((r) => r.needsUpdate); if (toUpdate.length === 0) { return true; } let message = "This component has Starwind component dependencies that need updating:\n\n"; if (toUpdate.length > 0) { message += `${highlighter.warn("Components to update:")} `; for (const dep of toUpdate) { message += ` \u2022 ${dep.component} (${dep.currentVersion} \u2192 latest, requires ${dep.requiredVersion}) `; } message += "\n"; } if (toInstall.length > 0) { message += `${highlighter.info("Components to install (automatic):")} `; for (const dep of toInstall) { message += ` \u2022 ${dep.component} (requires ${dep.requiredVersion}) `; } message += "\n"; } message += "Proceed with updates?"; const confirmed = await confirm2({ message }); if (typeof confirmed === "symbol") { return false; } return confirmed; } catch (error) { console.error("Error resolving Starwind dependencies:", error); const confirmed = await confirm2({ message: `Error resolving dependencies: ${error instanceof Error ? error.message : "Unknown error"}. Continue anyway?` }); if (typeof confirmed === "symbol") { return false; } return confirmed; } } async function getStarwindDependencyResolutions(componentNames) { return resolveAllStarwindDependencies(componentNames); } async function confirmInstall(component) { if (component.dependencies.length === 0) return true; const { starwindDependencies, npmDependencies } = separateDependencies(component.dependencies); if (npmDependencies.length > 0) { const dependenciesToInstall = await filterUninstalledDependencies(npmDependencies); if (dependenciesToInstall.length > 0) { const confirmed = await confirm2({ message: `The ${component.name} component requires the following npm dependencies: ${dependenciesToInstall.join(", ")}. Install them?` }); if (typeof confirmed === "symbol" || !confirmed) { return false; } } } if (starwindDependencies.length > 0) { const confirmed = await confirmStarwindDependencies([component.name]); if (!confirmed) { return false; } } return true; } // src/utils/install.ts async function installComponent(name) { const component = await getComponent(name); if (!component) { return { status: "failed", name, error: "Component not found in registry" }; } let dependencyResults = []; if (component.dependencies.length > 0) { const confirmed = await confirmInstall(component); if (!confirmed) { return { status: "failed", name, error: "Installation cancelled by user" }; } const { starwindDependencies, npmDependencies } = separateDependencies(component.dependencies); if (npmDependencies.length > 0) { try { const dependenciesToInstall = await filterUninstalledDependencies(npmDependencies); if (dependenciesToInstall.length > 0) { const pm = await requestPackageManager(); const installTasks = [ { title: `Installing ${dependenciesToInstall.length === 1 ? "dependency" : "dependencies"}`, task: async () => { await installDependencies(dependenciesToInstall, pm); return `${highlighter.info("Dependencies installed successfully")}`; } } ]; await p3.tasks(installTasks); } else { p3.log.info( `${highlighter.info("All npm dependencies are already installed with valid versions")}` ); } } catch (error) { return { status: "failed", name, error: `Failed to install npm dependencies: ${error instanceof Error ? error.message : String(error)}` }; } } if (starwindDependencies.length > 0) { let resolutions = []; try { resolutions = await getStarwindDependencyResolutions([name]); } catch (error) { console.warn( "Proceeding without Starwind dependency installs due to resolution error:", error ); resolutions = []; } if (resolutions.length > 0) { const installResults = await installStarwindDependencies(resolutions); dependencyResults = installResults; const failedDeps = installResults.filter((r) => r.status === "failed"); if (failedDeps.length > 0) { return { status: "failed", name, error: `Failed to install Starwind dependencies: ${failedDeps.map((r) => r.name).join(", ")}`, dependencyResults }; } } } } const result = await copyComponent(name); if (dependencyResults.length > 0) { return { ...result, dependencyResults }; } return result; } async function installStarwindDependencies(resolutions) { const results = []; const componentsToInstall = []; const componentsToUpdate = []; for (const resolution of resolutions) { if (resolution.needsInstall) { const result = await copyComponent(resolution.component, true); results.push(result); if (result.status === "installed" && result.version) { componentsToInstall.push({ name: result.name, version: result.version }); } } else if (resolution.needsUpdate) { const result = await copyComponent(resolution.component, true); results.push(result); if (result.status === "installed" && result.version) { componentsToUpdate.push({ name: result.name, version: result.version }); } } } if (componentsToInstall.length > 0) { try { await updateConfig({ components: componentsToInstall }, { appendComponents: true }); } catch (error) { console.error("Failed to update config after installing new dependencies:", error); } } if (componentsToUpdate.length > 0) { try { await updateExistingComponents(componentsToUpdate); } catch (error) { console.error("Failed to update config after updating dependencies:", error); } } return results; } async function updateExistingComponents(componentsToUpdate) { const config = await getConfig(); const updatedComponents = [...config.components]; for (const componentUpdate of componentsToUpdate) { const existingIndex = updatedComponents.findIndex((comp) => comp.name === componentUpdate.name); if (existingIndex >= 0) { updatedComponents[existingIndex] = { name: componentUpdate.name, version: componentUpdate.version }; } else { updatedComponents.push(componentUpdate); } } await updateConfig({ components: updatedComponents }, { appendComponents: false }); } // src/utils/shadcn-config.ts var COMPONENTS_JSON_PATH = "components.json"; function createDefaultShadcnConfig(cssFilePath, baseColor = "neutral") { return { $schema: "https://ui.shadcn.com/schema.json", registries: { "@starwind-pro": { url: PATHS.STARWIND_PRO_REGISTRY, headers: { Authorization: "Bearer ${STARWIND_LICENSE_KEY}" } } }, aliases: { components: "@/components", utils: "@/lib/utils" }, tailwind: { config: "", css: cssFilePath, baseColor, cssVariables: true }, style: "default", rsc: true }; } async function componentsJsonExists() { return fileExists(COMPONENTS_JSON_PATH); } async function readComponentsJson() { try { return await readJsonFile(COMPONENTS_JSON_PATH); } catch (error) { throw new Error(`Failed to read components.json: ${error}`); } } async function writeComponentsJson(config) { try { await writeJsonFile(COMPONENTS_JSON_PATH, config); } catch (error) { throw new Error(`Failed to write components.json: ${error}`); } } function addStarwindProRegistry(config) { const updatedConfig = { ...config }; if (!updatedConfig.registries) { updatedConfig.registries = {}; } updatedConfig.registries["@starwind-pro"] = { url: PATHS.STARWIND_PRO_REGISTRY, headers: { Authorization: "Bearer ${STARWIND_LICENSE_KEY}" } }; return updatedConfig; } async function setupShadcnProConfig(cssFilePath, baseColor = "neutral") { const exists = await componentsJsonExists(); if (!exists) { const config = createDefaultShadcnConfig(cssFilePath, baseColor); await writeComponentsJson(config); } else { const existingConfig = await readComponentsJson(); const updatedConfig = addStarwindProRegistry(existingConfig); await writeComponentsJson(updatedConfig); } } async function hasStarwindProRegistry() { if (!await componentsJsonExists()) { return false; } try { const config = await readComponentsJson(); const starwindProRegistry = config.registries?.["@starwind-pro"]; if (!starwindProRegistry?.url) { return false; } const url = starwindProRegistry.url; const isAuthorized = url.startsWith("http://localhost") || url.startsWith("https://pro.starwind.dev"); return isAuthorized; } catch { return false; } } // src/utils/sleep.ts var sleep = async (ms) => { await new Promise((resolve) => setTimeout(resolve, ms)); }; // src/utils/validate.ts async function isValidComponent(component, availableComponents) { const components = availableComponents || await getAllComponents(); return components.some((c) => c.name === component); } // src/commands/init.ts import path2 from "path"; import * as p5 from "@clack/prompts"; import semver4 from "semver"; // src/templates/starwind.css.ts var tailwindConfig = `@import "tailwindcss"; @import "tw-animate-css"; @plugin "@tailwindcss/forms"; @custom-variant dark (&:where(.dark, .dark *)); @theme { --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; @keyframes accordion-down { from { height: 0; } to { height: var(--starwind-accordion-content-height); } } @keyframes accordion-up { from { height: var(--starwind-accordion-content-height); } to { height: 0; } } } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-primary-accent: var(--primary-accent); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-secondary-accent: var(--secondary-accent); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-info: var(--info); --color-info-foreground: var(--info-foreground); --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); --color-error: var(--error); --color-error-foreground: var(--error-foreground); --color-border: var(--border); --color-input: var(--input); --color-outline: var(--outline); --radius-xs: calc(var(--radius) - 0.375rem); --radius-sm: calc(var(--radius) - 0.25rem); --radius-md: calc(var(--radius) - 0.125rem); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 0.25rem); --radius-2xl: calc(var(--radius) + 0.5rem); --radius-3xl: calc(var(--radius) + 1rem); } :root { --background: var(--color-white); --foreground: var(--color-neutral-950); --card: var(--color-white); --card-foreground: var(--color-neutral-950); --popover: var(--color-white); --popover-foreground: var(--color-neutral-950); --primary: var(--color-blue-700); --primary-foreground: var(--color-neutral-50); --primary-accent: var(--color-blue-700); --secondary: var(--color-fuchsia-700); --secondary-foreground: var(--color-neutral-50); --secondary-accent: var(--color-fuchsia-700); --muted: var(--color-neutral-100); --muted-foreground: var(--color-neutral-600); --accent: var(--color-neutral-100); --accent-foreground: var(--color-neutral-900); --info: var(--color-sky-300); --info-foreground: var(--color-sky-950); --success: var(--color-green-300); --success-foreground: var(--color-green-950); --warning: var(--color-amber-300); --warning-foreground: var(--color-amber-950); --error: var(--color-red-700); --error-foreground: var(--color-neutral-50); --border: var(--color-neutral-200); --input: var(--color-neutral-200); --outline: var(--color-neutral-400); --radius: 0.625rem; } .dark { --background: var(--color-neutral-950); --foreground: var(--color-neutral-50); --card: var(--color-neutral-900); --card-foreground: var(--color-neutral-50); --popover: var(--color-neutral-800); --popover-foreground: var(--color-neutral-50); --primary: var(--color-blue-700); --primary-foreground: var(--color-neutral-50); --primary-accent: var(--color-blue-400); --secondary: var(--color-fuchsia-700); --secondary-foreground: var(--color-neutral-50); --secondary-accent: var(--color-fuchsia-400); --muted: var(--color-neutral-800); --muted-foreground: var(--color-neutral-400); --accent: var(--color-neutral-700); --accent-foreground: var(--color-neutral-100); --info: var(--color-sky-300); --info-foreground: var(--color-sky-950); --success: var(--color-green-300); --success-foreground: var(--color-green-950); --warning: var(--color-amber-300); --warning-foreground: var(--color-amber-950); --error: var(--color-red-800); --error-foreground: var(--color-neutral-50); --border: --alpha(var(--color-neutral-50) / 10%); --input: --alpha(var(--color-neutral-50) / 15%); --outline: var(--color-neutral-500); } @layer base { * { @apply border-border outline-outline/50; } body { @apply bg-background text-foreground scheme-light dark:scheme-dark; } button { @apply cursor-pointer; } } `; // src/utils/astro-config.ts import * as p4 from "@clack/prompts"; import fs3 from "fs-extra"; import semver3 from "semver"; var CONFIG_EXTENSIONS = ["ts", "js", "mjs", "cjs"]; async function findAstroConfig() { for (const ext of CONFIG_EXTENSIONS) { const configPath = `astro.config.${ext}`; if (await fileExists(configPath)) { return configPath; } } return null; } async function getAstroVersion() { try { const pkg = await readJsonFile("package.json"); if (pkg.dependencies?.astro) { const astroVersion = pkg.dependencies.astro.replace(/^\^|~/, ""); return astroVersion; } p4.log.error( highlighter.error( "Astro seems not installed in your project, please check your package.json" ) ); return null; } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; p4.log.error(highlighter.error(`Failed to check Astro version: ${errorMessage}`)); return null; } } async function setupAstroConfig() { try { let configPath = await findAstroConfig(); let content = ""; if (configPath) { content = await fs3.readFile(configPath, "utf-8"); } else { configPath = "astro.config.ts"; content = `import { defineConfig } from "astro/config"; export default defineConfig({}); `; } if (!content.includes('import tailwindcss from "@tailwindcss/vite"')) { content = `import tailwindcss from "@tailwindcss/vite"; ${content}`; } const configStart = content.indexOf("defineConfig(") + "defineConfig(".length; const configEnd = content.lastIndexOf(");"); let config = content.slice(configStart, configEnd); config = config.trim().replace(/^{|}$/g, "").trim(); const astroVersion = await getAstroVersion(); if (astroVersion && semver3.lt(astroVersion, "5.7.0")) { if (!config.includes("experimental")) { const needsComma = config.length > 0 && !config.trimEnd().endsWith(","); config += (needsComma ? "," : "") + ` experimental: { svg: true, },`; } else if (!config.includes("svg: true") && !config.includes("svg: {")) { const expEnd = config.indexOf("experimental:") + "experimental:".length; const blockStart = config.indexOf("{", expEnd) + 1; config = config.slice(0, blockStart) + ` svg: true,` + config.slice(blockStart); } } if (!config.includes("vite:")) { const needsComma = config.length > 0 && !config.trimEnd().endsWith(","); config += (needsComma ? "," : "") + ` vite: { plugins: [tailwindcss()], },`; } else if (!config.includes("plugins: [")) { const viteEnd = config.indexOf("vite:") + "vite:".length; const blockStart = config.indexOf("{", viteEnd) + 1; config = config.slice(0, blockStart) + ` plugins: [tailwindcss()],` + config.slice(blockStart); } else if (!config.includes("tailwindcss()")) { const pluginsStart = config.indexOf("plugins:") + "plugins:".length; const arrayStart = config.indexOf("[", pluginsStart) + 1; config = config.slice(0, arrayStart) + `tailwindcss(), ` + config.slice(arrayStart); } const newContent = `${content.slice(0, configStart)}{ ${config} }${content.slice(configEnd)}`; await fs3.writeFile(configPath, newContent, "utf-8"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; p4.log.error(highlighter.error(`Failed to setup Astro config: ${errorMessage}`)); return false; } } // src/commands/init.ts async function init(withinAdd = false, options) { if (!withinAdd) { p5.intro(highlighter.title(" Welcome to the Starwind CLI ")); } try { if (!await fileExists("package.json")) { throw new Error( "No package.json found. Please run this command in the root of your project." ); } const pkg = await readJsonFile("package.json"); const installTasks = []; const configTasks = []; let configChoices; if (options?.defaults) { configChoices = { installLocation: PATHS.LOCAL_COMPONENTS_DIR, cssFile: PATHS.LOCAL_CSS_FILE, twBaseColor: "neutral" }; if (!withinAdd) { p5.log.info("Using default configuration values"); } } else { configChoices = await p5.group( { // ask where to install components installLocation: () => p5.text({ message: "What is your components directory?", placeholder: PATHS.LOCAL_COMPONENTS_DIR, initialValue: PATHS.LOCAL_COMPONENTS_DIR, validate(value) { if (value.length === 0) return `Value is required!`; if (path2.isAbsolute(value)) return `Please use a relative path`; if (value.includes("..")) return `Path traversal is not allowed`; const invalidChars = /[<>:"|?*]/; if (invalidChars.test(value)) return `Path contains invalid characters`; const systemDirs = ["windows", "program files", "system32"]; if (systemDirs.some((dir) => value.toLowerCase().startsWith(dir))) { return `Cannot install in system directories`; } } }), // ask where to add the css file cssFile: () => p5.text({ message: `Where would you like to add the Tailwind ${highlighter.info(".css")} file?`, placeholder: PATHS.LOCAL_CSS_FILE, initialValue: PATHS.LOCAL_CSS_FILE, validate(value) { if (value.length === 0) return `Value is required!`; if (!value.endsWith(".css")) return `File must end with .css extension`; if (path2.isAbsolute(value)) return `Please use a relative path`; if (value.includes("..")) return `Path traversal is not allowed`; const invalidChars = /[<>:"|?*]/; if (invalidChars.test(value)) return `Path contains invalid characters`; const systemDirs = ["windows", "program files", "system32"]; if (systemDirs.some((dir) => value.toLowerCase().startsWith(dir))) { return `Cannot use system directories`; } const basename = path2.basename(value, ".css"); if (!basename || basename.trim().length === 0) { return `Invalid filename`; } } }), twBaseColor: () => p5.select({ message: "What Tailwind base color would you like to use?", initialValue: "neutral", options: [ { label: "Neutral (default)", value: "neutral" }, { label: "Stone", value: "stone" }, { label: "Zinc", value: "zinc" }, { label: "Gray", value: "gray" }, { label: "Slate", value: "slate" } ] }) }, { // On Cancel callback that wraps the group // So if the user cancels one of the prompts in the group this function will be called onCancel: () => { p5.cancel("Operation cancelled."); process.exit(0); } } ); } const cssFileDir = path2.dirname(configChoices.cssFile); const componentInstallDir = path2.join(configChoices.installLocation, "starwind"); configTasks.push({ title: "Creating project structure", task: async () => { await ensureDirectory(componentInstallDir); await ensureDirectory(cssFileDir); await sleep(250); return "Created project structure"; } }); configTasks.push({ title: "Setup Astro config file", task: async () => { const success = await setupAstroConfig(); if (!success) { throw new Error("Failed to setup Astro config"); } await sleep(250); return "Astro config setup completed"; } }); const cssFileExists = await fileExists(configChoices.cssFile); let updatedTailwindConfig = tailwindConfig; if (configChoices.twBaseColor !== "neutral") { updatedTailwindConfig = updatedTailwindConfig.replace( /--color-neutral-/g, `--color-${configChoices.twBaseColor}-` ); } if (cssFileExists) { const shouldOverride = options?.defaults ? true : await p5.confirm({ message: `${highlighter.info(configChoices.cssFile)} already exists. Do you want to override it?` }); if (p5.isCancel(shouldOverride)) { p5.cancel("Operation cancelled"); return process.exit(0); } if (!shouldOverride) { p5.log.info("Skipping Tailwind CSS configuration"); } else { configTasks.push({ title: "Creating Tailwind CSS configuration", task: async () => { await writeCssFile(configChoices.cssFile, updatedTailwindConfig); await sleep(250); return "Created Tailwind configuration"; } }); } } else { configTasks.push({ title: "Creating Tailwind CSS configuration", task: async () => { await writeCssFile(configChoices.cssFile, updatedTailwindConfig); await sleep(250); return "Created Tailwind configuration"; } }); } configTasks.push({ title: "Updating project configuration", task: async () => { await updateConfig({ tailwind: { css: configChoices.cssFile, baseColor: configChoices.twBaseColor, cssVariables: true }, // aliases: { // components: "@/components", // }, componentDir: configChoices.installLocation, components: [] }); await sleep(250); return "Updated project starwind configuration"; } }); if (options?.pro) { const alreadyHasPro = await hasStarwindProRegistry(); if (!alreadyHasPro) { if (!withinAdd) { p5.log.info(highlighter.info("Setting up Starwind Pro configuration...")); } configTasks.push({ title: "Setting up Starwind Pro registry", task: async () => { await setupShadcnProConfig(configChoices.cssFile, configChoices.twBaseColor); await sleep(250); return "Configured Starwind Pro registry in components.json"; } }); } else { if (!withinAdd) { p5.log.info(highlighter.info("Starwind Pro registry already configured")); } } } const pm = options?.defaults ? await getDefaultPackageManager() : await requestPackageManager(); if (pkg.dependencies?.astro) { const astroVersion = pkg.dependencies.astro.replace(/^\^|~/, ""); if (!semver4.gte(astroVersion, MIN_ASTRO_VERSION)) { const shouldUpgrade = options?.defaults ? true : await p5.confirm({ message: `Starwind requires Astro v${MIN_ASTRO_VERSION} or higher. Would you like to upgrade from v${astroVersion}?`, initialValue: true }); if (p5.isCancel(shouldUpgrade)) { p5.cancel("Operation cancelled"); return process.exit(0); } if (!shouldUpgrade) { p5.cancel("Astro v5 or higher is required to use Starwind"); return process.exit(1); } installTasks.push({ title: "Upgrading Astro", task: async () => { await installDependencies([ASTRO_PACKAGES.core], pm); return "Upgraded Astro successfully"; } }); } } else { const shouldInstall2 = options?.defaults ? true : await p5.confirm({ message: `Starwind requires Astro v${MIN_ASTRO_VERSION} or higher. Would you like to install it?`, initialValue: true }); if (p5.isCancel(shouldInstall2)) { p5.cancel("Operation cancelled"); return process.exit(0); } if (!shouldInstall2) { p5.cancel("Astro is required to use Starwind"); return process.exit(1); } installTasks.push({ title: `Installing ${ASTRO_PACKAGES.core}`, task: async () => { await installDependencies([ASTRO_PACKAGES.core], pm); return `Installed ${highlighter.info(ASTRO_PACKAGES.core)} successfully`; } }); } const otherPackages = getOtherPackages(); const shouldInstall = options?.defaults ? true : await p5.confirm({ message: `Install ${highlighter.info(otherPackages.join(", "))} using ${highlighter.info(pm)}?` }); if (p5.isCancel(shouldInstall)) { p5.cancel("Operation cancelled"); return process.exit(0); } if (shouldInstall) { installTasks.push({ title: `Installing packages`, task: async () => { await installDependencies(getOtherPackages(), pm, false, false); return `${highlighter.info("Packages installed successfully")}`; } }); } else { p5.log.warn( highlighter.warn(`Skipped installation of packages. Make sure to install them manually`) ); } if (installTasks.length > 0) { await p5.tasks(installTasks); } if (configTasks.length > 0) { await p5.tasks(configTasks); } await sleep(250); let nextStepsMessage = `Make sure your layout imports the ${highlighter.infoBright(configChoices.cssFile)} file`; if (options?.pro) { nextStepsMessage += ` Starwind Pro is now configured! You can install pro components using: ${highlighter.info("npx starwind@latest add @starwind-pro/component-name")} Make sure to set your ${highlighter.infoBright("STARWIND_LICENSE_KEY")} environment variable.`; } p5.note(nextStepsMessage, "Next steps"); if (!withinAdd) { sleep(1e3); const outroMessage = options?.pro ? "Enjoy using Starwind UI with Pro components! \u{1F680}\u2728" : "Enjoy using Starwind UI \u{1F680}"; p5.outro(outroMessage); } } catch (error) { p5.log.error(error instanceof Error ? error.message : "Failed to add components"); p5.cancel("Operation cancelled"); process.exit(1); } } // src/commands/add.ts async function add(components, options) { try { p6.intro(highlighter.title(" Welcome to the Starwind CLI ")); const configExists = await fileExists(PATHS.LOCAL_CONFIG_FILE); if (!configExists) { const shouldInit = await p6.confirm({ message: `Starwind configuration not found. Would you like to run ${highlighter.info("starwind init")} now?`, initialValue: true }); if (p6.isCancel(shouldInit)) { p6.cancel("Operation cancelled"); process.exit(0); } if (shouldInit) { await init(true); } else { p6.log.error( `Please initialize starwind with ${highlighter.info("starwind init")} before adding components` ); process.exit(1); } } let componentsToInstall = []; const registryComponents = []; let registryResults = null; if (options?.all) { const availableComponents = await getAllComponents(); componentsToInstall = availableComponents.map((c) => c.name); p6.log.info(`Adding all ${componentsToInstall.length} available components...`); } else if (components && components.length > 0) { const regularComponents = []; for (const component of components) { if (component.startsWith("@")) { registryComponents.push(component); } else { regular