starwind
Version:
Add beautifully designed components to your Astro applications
689 lines (674 loc) • 22.8 kB
JavaScript
import {
PATHS,
fileExists,
getConfig,
highlighter,
init,
installDependencies,
requestPackageManager,
sleep,
updateConfig
} from "./chunk-HDAZQTOL.js";
// src/index.ts
import { Command } from "commander";
// src/commands/add.ts
import * as p2 from "@clack/prompts";
// src/utils/component.ts
import * as path from "path";
import { fileURLToPath } from "url";
import * as p from "@clack/prompts";
import fs 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
};
}
const componentDir = path.join(config.componentDir, "starwind", name);
try {
await fs.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 fs.readdir(sourceDir);
for (const file of files) {
const sourcePath = path.join(sourceDir, file);
const destPath = path.join(componentDir, file);
await fs.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 fs.pathExists(componentPath)) {
await fs.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.error || "Failed to update component"
};
}
} catch (error) {
return {
name,
status: "failed",
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
// 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",
options: components.map((component) => ({
label: component.name,
value: component.name
})),
required: false
});
if (typeof selected === "symbol") {
return [];
}
return selected;
}
async function confirmInstall(component) {
if (component.dependencies.length === 0) return true;
const confirmed = await confirm2({
message: `This component requires the following dependencies: ${component.dependencies.join(", ")}. Install them?`
});
if (typeof confirmed === "symbol") {
return false;
}
return confirmed;
}
// 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"
};
}
if (component.dependencies.length > 0) {
const confirmed = await confirmInstall(component);
if (!confirmed) {
return {
status: "failed",
name,
error: "Installation cancelled by user"
};
}
try {
const pm = await requestPackageManager();
await installDependencies(component.dependencies, pm);
} catch (error) {
return {
status: "failed",
name,
error: `Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`
};
}
}
return await copyComponent(name);
}
// src/utils/validate.ts
async function isValidComponent(component, availableComponents) {
const components = availableComponents || await getAllComponents();
return components.some((c) => c.name === component);
}
// src/commands/add.ts
var { init: init2 } = await import("./init-NBNT5V74.js");
async function add(components, options) {
try {
p2.intro(highlighter.title(" Welcome to the Starwind CLI "));
const configExists = await fileExists(PATHS.LOCAL_CONFIG_FILE);
if (!configExists) {
const shouldInit = await p2.confirm({
message: `Starwind configuration not found. Would you like to run ${highlighter.info("starwind init")} now?`,
initialValue: true
});
if (p2.isCancel(shouldInit)) {
p2.cancel("Operation cancelled");
process.exit(0);
}
if (shouldInit) {
await init2(true);
} else {
p2.log.error(
`Please initialize starwind with ${highlighter.info("starwind init")} before adding components`
);
process.exit(1);
}
}
let componentsToInstall = [];
if (options?.all) {
const availableComponents = await getAllComponents();
componentsToInstall = availableComponents.map((c) => c.name);
p2.log.info(`Adding all ${componentsToInstall.length} available components...`);
} else if (components && components.length > 0) {
const availableComponents = await getAllComponents();
const { valid, invalid } = await components.reduce(
async (accPromise, component) => {
const acc = await accPromise;
const isValid = await isValidComponent(component, availableComponents);
if (isValid) {
acc.valid.push(component);
} else {
acc.invalid.push(component);
}
return acc;
},
Promise.resolve({ valid: [], invalid: [] })
);
if (invalid.length > 0) {
p2.log.warn(
`${highlighter.warn("Invalid components found:")}
${invalid.map((name) => ` ${name}`).join("\n")}`
);
}
if (valid.length > 0) {
componentsToInstall = valid;
} else {
p2.log.warn(`${highlighter.warn("No valid components to install")}`);
p2.cancel("Operation cancelled");
return process.exit(0);
}
} else {
const selected = await selectComponents();
if (!selected) {
p2.cancel("No components selected");
return process.exit(0);
}
componentsToInstall = selected;
}
if (componentsToInstall.length === 0) {
p2.log.warn(`${highlighter.warn("No components selected")}`);
p2.cancel("Operation cancelled");
return process.exit(0);
}
const results = {
installed: [],
skipped: [],
failed: []
};
const installedComponents = [];
for (const comp of componentsToInstall) {
const result = await installComponent(comp);
switch (result.status) {
case "installed":
results.installed.push(result);
installedComponents.push({ name: result.name, version: result.version });
break;
case "skipped":
results.skipped.push(result);
break;
case "failed":
results.failed.push(result);
break;
}
}
if (installedComponents.length > 0) {
try {
await updateConfig({ components: installedComponents }, { appendComponents: true });
} catch (error) {
p2.log.error(
`Failed to update config: ${error instanceof Error ? error.message : "Unknown error"}`
);
process.exit(1);
}
}
p2.log.message(`
${highlighter.underline("Installation Summary")}`);
if (results.failed.length > 0) {
p2.log.error(
`${highlighter.error("Failed to install components:")}
${results.failed.map((r) => ` ${r.name} - ${r.error}`).join("\n")}`
);
}
if (results.skipped.length > 0) {
p2.log.warn(
`${highlighter.warn("Skipped components (already installed):")}
${results.skipped.map((r) => ` ${r.name} v${r.version}`).join("\n")}`
);
}
if (results.installed.length > 0) {
p2.log.success(
`${highlighter.success("Successfully installed components:")}
${results.installed.map((r) => ` ${r.name} v${r.version}`).join("\n")}`
);
}
await sleep(1e3);
p2.outro("Enjoy using Starwind UI \u{1F680}");
} catch (error) {
p2.log.error(error instanceof Error ? error.message : "Failed to add components");
p2.cancel("Operation cancelled");
process.exit(1);
}
}
// src/commands/remove.ts
import * as p3 from "@clack/prompts";
async function remove(components, options) {
try {
p3.intro(highlighter.title(" Welcome to the Starwind CLI "));
const configExists = await fileExists(PATHS.LOCAL_CONFIG_FILE);
if (!configExists) {
p3.log.error("No Starwind configuration found. Please run starwind init first.");
process.exit(1);
}
const config = await getConfig();
const installedComponents = config.components;
if (installedComponents.length === 0) {
p3.log.warn("No components are currently installed.");
process.exit(0);
}
let componentsToRemove = [];
if (options?.all) {
componentsToRemove = installedComponents.map((comp) => comp.name);
p3.log.info(`Removing all ${componentsToRemove.length} installed components...`);
} else if (components && components.length > 0) {
const invalid = components.filter(
(comp) => !installedComponents.some((ic) => ic.name === comp)
);
if (invalid.length > 0) {
p3.log.warn(
`${highlighter.warn("Components not found:")}
${invalid.map((name) => ` ${name}`).join("\n")}`
);
}
componentsToRemove = components.filter(
(comp) => installedComponents.some((ic) => ic.name === comp)
);
if (componentsToRemove.length === 0) {
p3.log.warn("No valid components to remove");
process.exit(0);
}
} else {
const choices = installedComponents.map((comp) => ({
value: comp.name,
label: comp.name
}));
const selected = await p3.multiselect({
message: "Select components to remove",
options: choices
});
if (p3.isCancel(selected)) {
p3.cancel("Operation cancelled");
process.exit(0);
}
componentsToRemove = selected;
}
if (componentsToRemove.length === 0) {
p3.log.warn("No components selected for removal");
process.exit(0);
}
const confirmed = await p3.confirm({
message: `Remove ${componentsToRemove.map((comp) => highlighter.info(comp)).join(", ")} ${componentsToRemove.length > 1 ? "components" : "component"}?`
});
if (!confirmed || p3.isCancel(confirmed)) {
p3.cancel("Operation cancelled");
process.exit(0);
}
const results = {
removed: [],
failed: []
};
for (const comp of componentsToRemove) {
const result = await removeComponent(comp, config.componentDir);
if (result.status === "removed") {
results.removed.push(result);
} else {
results.failed.push(result);
}
}
const updatedComponents = config.components.filter(
(comp) => !componentsToRemove.includes(comp.name)
);
await updateConfig(
{
...config,
components: updatedComponents
},
{ appendComponents: false }
);
p3.log.message(`
${highlighter.underline("Removal Summary")}`);
if (results.failed.length > 0) {
p3.log.error(
`${highlighter.error("Failed to remove components:")}
${results.failed.map((r) => ` ${r.name} - ${r.error}`).join("\n")}`
);
}
if (results.removed.length > 0) {
p3.log.success(
`${highlighter.success("Successfully removed components:")}
${results.removed.map((r) => ` ${r.name}`).join("\n")}`
);
}
await sleep(1e3);
if (results.removed.length > 0) {
p3.outro("Components removed successfully \u{1F5D1}\uFE0F");
} else {
p3.cancel("Errors occurred while removing components");
process.exit(1);
}
} catch (error) {
p3.log.error(error instanceof Error ? error.message : "Failed to remove components");
p3.cancel("Operation cancelled");
process.exit(1);
}
}
// src/commands/update.ts
import * as p4 from "@clack/prompts";
async function update(components, options) {
try {
p4.intro(highlighter.title(" Welcome to the Starwind CLI "));
const configExists = await fileExists(PATHS.LOCAL_CONFIG_FILE);
if (!configExists) {
p4.log.error("No Starwind configuration found. Please run starwind init first.");
process.exit(1);
}
const config = await getConfig();
const installedComponents = config.components;
if (installedComponents.length === 0) {
p4.log.warn("No components are currently installed.");
process.exit(0);
}
let componentsToUpdate = [];
if (options?.all) {
componentsToUpdate = installedComponents.map((comp) => comp.name);
p4.log.info(`Checking updates for all ${componentsToUpdate.length} installed components...`);
} else if (components && components.length > 0) {
const invalid = components.filter(
(comp) => !installedComponents.some((ic) => ic.name === comp)
);
if (invalid.length > 0) {
p4.log.warn(
`${highlighter.warn("Components not found in project:")}
${invalid.map((name) => ` ${name}`).join("\n")}`
);
}
componentsToUpdate = components.filter(
(comp) => installedComponents.some((ic) => ic.name === comp)
);
if (componentsToUpdate.length === 0) {
p4.log.warn("No valid components to update");
process.exit(0);
}
} else {
const choices = installedComponents.map((comp) => ({
value: comp.name,
label: comp.name
}));
const selected = await p4.multiselect({
message: "Select components to update",
options: choices
});
if (p4.isCancel(selected)) {
p4.cancel("Operation cancelled");
process.exit(0);
}
componentsToUpdate = selected;
}
if (componentsToUpdate.length === 0) {
p4.log.warn("No components selected for update");
process.exit(0);
}
const results = {
updated: [],
skipped: [],
failed: []
};
for (const comp of componentsToUpdate) {
const currentVersion = installedComponents.find((ic) => ic.name === comp)?.version;
if (!currentVersion) {
results.failed.push({
name: comp,
status: "failed",
error: "Could not determine current version"
});
continue;
}
const result = await updateComponent(comp, currentVersion, options?.yes);
switch (result.status) {
case "updated":
results.updated.push(result);
break;
case "skipped":
results.skipped.push(result);
break;
case "failed":
results.failed.push(result);
break;
}
}
if (results.updated.length > 0) {
try {
const updatedComponentNames = new Set(results.updated.map((r) => r.name));
const currentComponents = config.components.filter(
(comp) => !updatedComponentNames.has(comp.name)
);
const updatedComponents = [
...currentComponents,
...results.updated.map((r) => ({
name: r.name,
version: r.newVersion
}))
];
await updateConfig(
{
components: updatedComponents
},
{ appendComponents: false }
);
} catch (error) {
p4.log.error(
`Failed to update config: ${error instanceof Error ? error.message : "Unknown error"}`
);
process.exit(1);
}
}
p4.log.message(`
${highlighter.underline("Update Summary")}`);
if (results.failed.length > 0) {
p4.log.error(
`${highlighter.error("Failed to update components:")}
${results.failed.map((r) => ` ${r.name} - ${r.error}`).join("\n")}`
);
}
if (results.skipped.length > 0) {
p4.log.info(
`${highlighter.info("Components already up to date or skipped:")}
${results.skipped.map((r) => ` ${r.name} (${r.oldVersion})`).join("\n")}`
);
}
if (results.updated.length > 0) {
p4.log.success(
`${highlighter.success("Successfully updated components:")}
${results.updated.map((r) => ` ${r.name} (${r.oldVersion} \u2192 ${r.newVersion})`).join("\n")}`
);
}
await sleep(1e3);
if (results.updated.length > 0) {
p4.outro("Components updated successfully \u{1F680}");
} else if (results.skipped.length > 0 && results.failed.length === 0) {
p4.outro("Components already up to date or skipped \u2728");
} else {
p4.cancel("No components were updated");
process.exit(1);
}
} catch (error) {
p4.log.error(error instanceof Error ? error.message : "Failed to update components");
p4.cancel("Operation cancelled");
process.exit(1);
}
}
// src/index.ts
var program = new Command().name("starwind").description("Add beautifully designed components to your Astro applications").version("1.7.3");
program.command("init").description("Initialize your project with Starwind").option("-d, --defaults", "Use default values for all prompts").action((options) => init(false, { defaults: options.defaults }));
program.command("add").description("Add Starwind components to your project").argument("[components...]", "The components to add (space separated)").allowExcessArguments().option("-a, --all", "Add all available components").action(add);
program.command("update").description("Update Starwind components to their latest versions").argument("[components...]", "The components to update (space separated)").allowExcessArguments().option("-a, --all", "Update all installed components").option("-y, --yes", "Skip confirmation prompts").action(update);
program.command("remove").description("Remove Starwind components from your project").argument("[components...]", "The components to remove (space separated)").allowExcessArguments().option("-a, --all", "Remove all installed components").action(remove);
program.parse();
//# sourceMappingURL=index.js.map