starwind
Version:
Add beautifully designed components to your Astro applications
1,533 lines (1,499 loc) • 66.6 kB
JavaScript
#!/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