🚨 Active Supply Chain Attack:node-ipc Package Compromised.Learn More
Socket
Book a DemoSign in
Socket

@shift-css/cli

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@shift-css/cli - npm Package Compare versions

Comparing version
0.4.0
to
0.5.0
+801
-325
dist/index.js
#!/usr/bin/env node
// src/index.ts
import pc5 from "picocolors";
import pc6 from "picocolors";
// package.json
var package_default = {
name: "@shift-css/cli",
version: "0.4.0",
version: "0.5.0",
description: "CLI tool for Shift CSS framework setup and migration",

@@ -26,13 +26,13 @@ type: "module",

dependencies: {
"@clack/prompts": "^0.7.0",
"@clack/prompts": "^1.2.0",
picocolors: "^1.0.0"
},
devDependencies: {
"@types/bun": "^1.1.14",
"@types/node": "^22.10.5",
"@types/bun": "^1.3.12",
"@types/node": "^25.6.0",
tsx: "^4.19.2",
typescript: "^5.7.3"
typescript: "6.0.3"
},
engines: {
node: ">=20.0.0"
node: ">=22.0.0"
},

@@ -59,7 +59,683 @@ keywords: [

// src/commands/add.ts
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
import { dirname as dirname2, join as join2 } from "node:path";
import * as p2 from "@clack/prompts";
import pc2 from "picocolors";
// src/core/registry.ts
import { readFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
async function getCorePackagePath() {
if (process.env.SHIFT_CORE_PATH) {
return process.env.SHIFT_CORE_PATH;
}
const monorepoPath = resolve(process.cwd(), "..", "core");
try {
await readFile(join(monorepoPath, "package.json"), "utf-8");
return monorepoPath;
} catch {}
const packagesCorePath = join(process.cwd(), "packages", "core");
try {
await readFile(join(packagesCorePath, "package.json"), "utf-8");
return packagesCorePath;
} catch {}
try {
const corePackageJson = join(process.cwd(), "node_modules", "@shift-css", "core", "package.json");
await readFile(corePackageJson, "utf-8");
return dirname(corePackageJson);
} catch {}
const __dirname2 = dirname(fileURLToPath(import.meta.url));
const devPath = resolve(__dirname2, "..", "..", "..", "core");
try {
await readFile(join(devPath, "package.json"), "utf-8");
return devPath;
} catch {
throw new Error("Could not find @shift-css/core package. Make sure it is installed.");
}
}
async function loadRegistry() {
const corePath = await getCorePackagePath();
const registryPath = join(corePath, "registry", "registry.json");
try {
const content = await readFile(registryPath, "utf-8");
return JSON.parse(content);
} catch {
throw new Error("Could not load component registry. Make sure @shift-css/core is up to date.");
}
}
async function getAvailableComponents() {
const registry = await loadRegistry();
return Object.keys(registry.components).sort();
}
async function getComponent(name) {
const registry = await loadRegistry();
return registry.components[name] ?? null;
}
async function resolveDependencies(componentName, resolved = new Set) {
if (resolved.has(componentName)) {
return [];
}
const component = await getComponent(componentName);
if (!component) {
return [];
}
resolved.add(componentName);
const deps = [];
for (const dep of component.registryDependencies) {
if (!resolved.has(dep)) {
const nestedDeps = await resolveDependencies(dep, resolved);
deps.push(...nestedDeps, dep);
}
}
return deps;
}
async function getComponentsWithDependencies(componentNames) {
const allComponents = [];
const resolved = new Set;
for (const name of componentNames) {
const deps = await resolveDependencies(name, new Set(resolved));
for (const dep of deps) {
if (!resolved.has(dep)) {
allComponents.push(dep);
resolved.add(dep);
}
}
if (!resolved.has(name)) {
allComponents.push(name);
resolved.add(name);
}
}
return allComponents;
}
async function resolveComponent(name, framework) {
const registry = await loadRegistry();
const component = registry.components[name];
if (!component) {
return null;
}
const corePath = await getCorePackagePath();
const cssPath = join(corePath, "src", component.files.css);
let cssContent;
try {
cssContent = await readFile(cssPath, "utf-8");
} catch {
throw new Error(`Could not read CSS file for component "${name}": ${cssPath}`);
}
let templatePath;
let templateContent;
if (framework) {
const templates = registry.templates[name];
const templateRelPath = templates?.[framework];
if (templateRelPath) {
templatePath = join(corePath, templateRelPath);
try {
templateContent = await readFile(templatePath, "utf-8");
} catch {
templatePath = undefined;
}
}
}
return {
component,
cssPath,
cssContent,
templatePath,
templateContent
};
}
async function componentExists(name) {
const component = await getComponent(name);
return component !== null;
}
// src/core/transformer.ts
function getExistingLayerName(css) {
const match = css.match(/^@layer\s+([a-zA-Z_-][a-zA-Z0-9_-]*)\s*\{/);
return match?.[1] ?? null;
}
function extractLeadingComments(css) {
const match = css.match(/^((?:\/\*[\s\S]*?\*\/\s*)*)/);
if (match?.[1]) {
return {
comments: match[1],
content: css.slice(match[1].length)
};
}
return { comments: "", content: css };
}
function generateHeader(componentName) {
return `/**
* Shift CSS - ${componentName} Component
*
* This file was created from @shift-css/core using \`shift add ${componentName}\`.
* You can customize the CSS custom properties below.
*
* @see https://getshiftcss.com/components/${componentName}
*/
`;
}
function wrapInLayer(css, options = {}) {
const { layer = "components", addHeader = true, componentName = "component" } = options;
const { content } = extractLeadingComments(css);
const existingLayer = getExistingLayerName(content.trim());
if (existingLayer === layer) {
return addHeader ? generateHeader(componentName) + css : css;
}
if (existingLayer) {
const rewrapped = content.replace(new RegExp(`^@layer\\s+${existingLayer}`), `@layer ${layer}`);
return addHeader ? generateHeader(componentName) + rewrapped : rewrapped;
}
const trimmedContent = content.trim();
const indentedContent = trimmedContent.split(`
`).map((line) => line.trim() ? ` ${line}` : line).join(`
`);
const wrapped = `@layer ${layer} {
${indentedContent}
}
`;
return addHeader ? generateHeader(componentName) + wrapped : wrapped;
}
function extractImports(css) {
const importRegex = /@import\s+(?:url\([^)]+\)|"[^"]+"|'[^']+')[^;]*;/g;
const imports = [];
for (const match of css.matchAll(importRegex)) {
imports.push(match[0]);
}
const content = css.replace(importRegex, "").trim();
return { imports, content };
}
function transformForEjection(css, options = {}) {
const { layer = "components", componentName = "component" } = options;
const { imports, content } = extractImports(css);
const wrapped = wrapInLayer(content, {
layer,
addHeader: true,
componentName
});
if (imports.length > 0) {
return `${imports.join(`
`)}
${wrapped}`;
}
return wrapped;
}
// src/ui/prompts.ts
import * as p from "@clack/prompts";
import pc from "picocolors";
// src/core/color.ts
function hexToRgb(hex) {
const cleaned = hex.replace(/^#/, "");
const fullHex = cleaned.length === 3 ? cleaned.split("").map((c) => c + c).join("") : cleaned;
if (fullHex.length !== 6)
return null;
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
if (!result || result.length < 4)
return null;
const [, rHex, gHex, bHex] = result;
if (!rHex || !gHex || !bHex)
return null;
return {
r: Number.parseInt(rHex, 16),
g: Number.parseInt(gHex, 16),
b: Number.parseInt(bHex, 16)
};
}
function rgbToHue(r, g, b) {
const rNorm = r / 255;
const gNorm = g / 255;
const bNorm = b / 255;
const max = Math.max(rNorm, gNorm, bNorm);
const min = Math.min(rNorm, gNorm, bNorm);
const delta = max - min;
if (delta === 0)
return 250;
let hue;
if (max === rNorm) {
hue = (gNorm - bNorm) / delta % 6;
} else if (max === gNorm) {
hue = (bNorm - rNorm) / delta + 2;
} else {
hue = (rNorm - gNorm) / delta + 4;
}
hue = Math.round(hue * 60);
if (hue < 0)
hue += 360;
return hue;
}
function hexToHue(hex) {
const rgb = hexToRgb(hex);
if (!rgb)
return null;
return rgbToHue(rgb.r, rgb.g, rgb.b);
}
function isHexColor(value) {
return /^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/.test(value.trim());
}
function getColorName(hue) {
const h = (hue % 360 + 360) % 360;
if (h >= 0 && h < 15)
return "Red";
if (h >= 15 && h < 45)
return "Orange";
if (h >= 45 && h < 75)
return "Yellow";
if (h >= 75 && h < 105)
return "Lime";
if (h >= 105 && h < 135)
return "Green";
if (h >= 135 && h < 165)
return "Teal";
if (h >= 165 && h < 195)
return "Cyan";
if (h >= 195 && h < 225)
return "Sky";
if (h >= 225 && h < 255)
return "Blue";
if (h >= 255 && h < 285)
return "Indigo";
if (h >= 285 && h < 315)
return "Purple";
if (h >= 315 && h < 345)
return "Pink";
return "Red";
}
var PRESETS = [
{ name: "Plasma", hue: 260, description: "Electric Blue - High-tech default" },
{ name: "Laser", hue: 320, description: "Cyber-Pink - Neon futurism" },
{ name: "Acid", hue: 140, description: "Toxic Green - Engineering edge" },
{ name: "Void", hue: 0, description: "Monochrome - Industrial minimal" }
];
// src/ui/prompts.ts
function showIntro() {
console.clear();
p.intro(pc.bgMagenta(pc.white(" \uD83C\uDFA8 Shift CSS Init ")));
}
function showOutro(message) {
p.outro(message);
}
function handleCancel() {
p.cancel("Setup cancelled");
process.exit(0);
}
function createSpinner() {
return p.spinner();
}
function showNote(message, title) {
p.note(message, title);
}
async function askArchitectureMode(detectedFrameworks) {
const hasFrameworks = detectedFrameworks.length > 0;
const frameworkHint = hasFrameworks ? `Detected: ${detectedFrameworks.map((f) => f.type).join(", ")}` : undefined;
const mode = await p.select({
message: "What type of project is this?",
options: [
{
value: "greenfield",
label: "New project (Greenfield)",
hint: "Pure Shift CSS, no legacy frameworks"
},
{
value: "hybrid",
label: "Existing project (Hybrid)",
hint: frameworkHint ?? "Shift CSS alongside existing CSS"
}
],
initialValue: hasFrameworks ? "hybrid" : "greenfield"
});
if (p.isCancel(mode))
handleCancel();
return mode;
}
async function askPrimaryHue() {
const presetOptions = PRESETS.map((preset2) => ({
value: preset2.hue,
label: preset2.name,
hint: preset2.description
}));
const hue = await p.select({
message: "Choose your brand color:",
options: [
...presetOptions,
{ value: -1, label: "Custom", hint: "Enter hex code or hue value" }
]
});
if (p.isCancel(hue))
handleCancel();
if (hue === -1) {
p.log.info(pc.dim("Hue guide: 20=Red, 90=Yellow, 140=Green, 260=Blue, 320=Purple"));
const customValue = await p.text({
message: "Enter a hex code (#a855f7) or hue (0-360):",
placeholder: "#a855f7 or 260",
validate: (value) => {
if (!value)
return "Value is required";
const trimmed2 = value.trim();
if (isHexColor(trimmed2)) {
const parsedHue = hexToHue(trimmed2);
if (parsedHue === null)
return "Invalid hex color";
return;
}
const num = Number.parseInt(trimmed2, 10);
if (Number.isNaN(num))
return "Enter a hex code (#ff0000) or hue number (0-360)";
if (num < 0 || num > 360)
return "Hue must be between 0 and 360";
return;
}
});
if (p.isCancel(customValue))
handleCancel();
const trimmed = customValue.trim();
if (isHexColor(trimmed)) {
const parsedHue = hexToHue(trimmed);
if (parsedHue !== null) {
const colorName = getColorName(parsedHue);
p.log.success(`Converted ${pc.cyan(trimmed)} → ${pc.magenta(colorName)} (Hue: ${parsedHue})`);
return parsedHue;
}
}
return Number.parseInt(trimmed, 10);
}
const preset = PRESETS.find((p2) => p2.hue === hue);
if (preset) {
const colorName = getColorName(preset.hue);
p.log.info(`${pc.magenta(preset.name)} → ${colorName} (Hue: ${preset.hue})`);
}
return hue;
}
async function askStylesheetPath(defaultPath) {
const path = await p.text({
message: "Where should the stylesheet be created?",
placeholder: defaultPath,
defaultValue: defaultPath,
validate: (value) => {
if (!value)
return "Value is required";
if (!value)
return "Please enter a path";
if (!value.endsWith(".css"))
return "Path must end with .css";
return;
}
});
if (p.isCancel(path))
handleCancel();
return path;
}
async function confirmOverwrite(filePath) {
const confirmed = await p.confirm({
message: `${pc.yellow(filePath)} already exists. Overwrite it?`,
initialValue: false
});
if (p.isCancel(confirmed))
handleCancel();
return confirmed;
}
async function confirmInit(configPath, stylesheetPath, mode) {
const modeLabel = mode === "greenfield" ? "Greenfield" : "Hybrid";
showNote([
`${pc.bold("Config:")} ${pc.cyan(configPath)}`,
`${pc.bold("Stylesheet:")} ${pc.cyan(stylesheetPath)}`,
`${pc.bold("Mode:")} ${pc.cyan(modeLabel)}`
].join(`
`), "Files to create");
const confirmed = await p.confirm({
message: "Proceed with initialization?",
initialValue: true
});
if (p.isCancel(confirmed))
handleCancel();
return confirmed;
}
function logSuccess(message) {
p.log.success(message);
}
function logWarning(message) {
p.log.warn(message);
}
// src/commands/add.ts
var DEFAULT_ADD_CONFIG = {
stylesDir: "src/styles/components",
componentsDir: "src/components/ui",
layer: "components",
framework: undefined
};
async function loadAddConfig(rootDir) {
try {
const configPath = join2(rootDir, "shift.config.json");
const content = await readFile2(configPath, "utf-8");
const config = JSON.parse(content);
return {
stylesDir: config.add?.stylesDir ?? DEFAULT_ADD_CONFIG.stylesDir,
componentsDir: config.add?.componentsDir ?? DEFAULT_ADD_CONFIG.componentsDir,
layer: config.add?.layer ?? DEFAULT_ADD_CONFIG.layer,
framework: config.add?.framework ?? DEFAULT_ADD_CONFIG.framework
};
} catch {
return DEFAULT_ADD_CONFIG;
}
}
async function fileExists(path) {
try {
await readFile2(path, "utf-8");
return true;
} catch {
return false;
}
}
async function writeFileWithDir(path, content) {
await mkdir(dirname2(path), { recursive: true });
await writeFile(path, content, "utf-8");
}
async function selectComponents() {
const available = await getAvailableComponents();
const selected = await p2.multiselect({
message: "Which components would you like to add?",
options: available.map((name) => ({
value: name,
label: name
})),
required: true
});
if (p2.isCancel(selected))
handleCancel();
return selected;
}
async function confirmComponentsWithDeps(requested, withDeps) {
const deps = withDeps.filter((c) => !requested.includes(c));
if (deps.length === 0) {
return true;
}
p2.log.info(`${pc2.cyan(deps.join(", "))} ${deps.length === 1 ? "is" : "are"} required as ${deps.length === 1 ? "a dependency" : "dependencies"}.`);
const confirmed = await p2.confirm({
message: `Add ${deps.length === 1 ? "this dependency" : "these dependencies"} as well?`,
initialValue: true
});
if (p2.isCancel(confirmed))
handleCancel();
return confirmed;
}
async function confirmOverwrite2(files) {
showNote(files.map((f) => pc2.yellow(f)).join(`
`), "Files that will be overwritten");
const confirmed = await p2.confirm({
message: "These files already exist. Overwrite them?",
initialValue: false
});
if (p2.isCancel(confirmed))
handleCancel();
return confirmed;
}
function formatComponentName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
}
function showSuccessSummary(components, config, createdFiles) {
const cssFiles = createdFiles.filter((f) => f.endsWith(".css"));
const lines = [
"",
pc2.bold("Import in your styles:"),
...cssFiles.map((f) => pc2.cyan(`@import './${f.split("/").pop()}';`))
];
if (components.length > 0) {
const firstComponent = components[0];
const component = formatComponentName(firstComponent);
lines.push("");
lines.push(pc2.bold("Use in your template:"));
if (config.framework === "astro" || config.framework === "react" || config.framework === "vue") {
lines.push(pc2.cyan(`<${component} variant="primary">Click me</${component}>`));
} else {
if (firstComponent === "button") {
lines.push(pc2.cyan('<button s-btn="primary">Click me</button>'));
} else if (firstComponent === "card") {
lines.push(pc2.cyan("<article s-card>Card content</article>"));
} else if (firstComponent === "input") {
lines.push(pc2.cyan('<input s-input type="text" placeholder="Enter text">'));
} else if (firstComponent === "surface") {
lines.push(pc2.cyan('<section s-surface="raised">Content</section>'));
} else {
lines.push(pc2.cyan(`<element s-${firstComponent}>Content</element>`));
}
}
}
showNote(lines.join(`
`), `${components.length === 1 ? "Component" : "Components"} added successfully!`);
}
async function addCommand(componentNames, options = {}) {
const rootDir = process.cwd();
const config = await loadAddConfig(rootDir);
if (options.framework) {
config.framework = options.framework;
}
p2.intro(pc2.bgCyan(pc2.white(" \uD83D\uDCE6 Shift CSS Add ")));
let requestedComponents;
if (options.all) {
requestedComponents = await getAvailableComponents();
p2.log.info(`Adding all ${requestedComponents.length} components...`);
} else if (componentNames.length === 0) {
requestedComponents = await selectComponents();
} else {
requestedComponents = componentNames;
}
const spinner2 = createSpinner();
spinner2.start("Validating components...");
const invalidComponents = [];
for (const name of requestedComponents) {
if (!await componentExists(name)) {
invalidComponents.push(name);
}
}
if (invalidComponents.length > 0) {
spinner2.stop("Validation failed");
const available = await getAvailableComponents();
p2.log.error(`Unknown component${invalidComponents.length > 1 ? "s" : ""}: ${pc2.red(invalidComponents.join(", "))}`);
p2.log.info(`Available components: ${pc2.cyan(available.join(", "))}`);
showOutro(pc2.red("Add cancelled"));
return;
}
spinner2.stop("Components validated");
spinner2.start("Resolving dependencies...");
const componentsWithDeps = await getComponentsWithDependencies(requestedComponents);
spinner2.stop(`Resolved ${componentsWithDeps.length} component${componentsWithDeps.length === 1 ? "" : "s"}`);
if (componentsWithDeps.length > requestedComponents.length) {
const proceed = await confirmComponentsWithDeps(requestedComponents, componentsWithDeps);
if (!proceed) {
showOutro(pc2.yellow("Add cancelled"));
return;
}
}
const filesToCreate = [];
const existingFiles = [];
const templateExtension = config.framework ? getTemplateExtension(config.framework) : null;
if (config.framework && !templateExtension) {
p2.log.warn(pc2.yellow(`Framework "${config.framework}" is not supported. Only CSS files will be created.`));
}
for (const name of componentsWithDeps) {
const cssPath = join2(rootDir, config.stylesDir, `${name}.css`);
const templatePath = templateExtension ? join2(rootDir, config.componentsDir, `${formatComponentName(name)}.${templateExtension}`) : undefined;
filesToCreate.push({ component: name, cssPath, templatePath });
if (await fileExists(cssPath)) {
existingFiles.push(cssPath.replace(`${rootDir}/`, ""));
}
if (templatePath && await fileExists(templatePath)) {
existingFiles.push(templatePath.replace(`${rootDir}/`, ""));
}
}
if (existingFiles.length > 0 && !options.force) {
const proceed = await confirmOverwrite2(existingFiles);
if (!proceed) {
showOutro(pc2.yellow("Add cancelled"));
return;
}
}
spinner2.start("Creating files...");
const createdFiles = [];
const missingTemplates = [];
for (const { component, cssPath, templatePath } of filesToCreate) {
const resolved = await resolveComponent(component, config.framework);
if (!resolved)
continue;
const transformedCss = transformForEjection(resolved.cssContent, {
layer: config.layer,
componentName: component
});
await writeFileWithDir(cssPath, transformedCss);
createdFiles.push(cssPath.replace(`${rootDir}/`, ""));
if (templatePath && resolved.templateContent) {
await writeFileWithDir(templatePath, resolved.templateContent);
createdFiles.push(templatePath.replace(`${rootDir}/`, ""));
} else if (templatePath && !resolved.templateContent) {
missingTemplates.push(component);
}
}
spinner2.stop("Files created");
if (missingTemplates.length > 0 && config.framework) {
p2.log.warn(pc2.yellow(`${config.framework} templates not available for: ${missingTemplates.join(", ")}. Only CSS files were created.`));
}
for (const file of createdFiles) {
p2.log.success(`Created ${pc2.cyan(file)}`);
}
if (componentsWithDeps.length > 0) {
const firstComponent = await getComponent(componentsWithDeps[0]);
if (firstComponent?.customizationPoints.length) {
p2.log.info(`${pc2.dim("Customize via CSS variables:")} ${pc2.cyan(firstComponent.customizationPoints.join(", "))}`);
}
}
showSuccessSummary(componentsWithDeps, config, createdFiles);
showOutro(pc2.green("✨ Components added!"));
}
function getTemplateExtension(framework) {
switch (framework) {
case "astro":
return "astro";
case "react":
return "tsx";
case "vue":
return "vue";
default:
return null;
}
}
async function listComponents() {
const available = await getAvailableComponents();
p2.intro(pc2.bgCyan(pc2.white(" \uD83D\uDCE6 Shift CSS Components ")));
const lines = available.map((name) => ` ${pc2.cyan(name)}`);
showNote(lines.join(`
`), "Available components");
p2.outro(pc2.dim(`Run ${pc2.cyan("shift-css add <component>")} to add a component`));
}
// src/commands/init.ts
import pc3 from "picocolors";
import pc4 from "picocolors";
// src/core/detector.ts
import { readFile } from "node:fs/promises";
import { readFile as readFile3 } from "node:fs/promises";
var FRAMEWORK_SIGNATURES = [

@@ -195,3 +871,3 @@ {

async function readFileHead(filepath, maxBytes) {
const content = await readFile(filepath, "utf-8");
const content = await readFile3(filepath, "utf-8");
return content.slice(0, maxBytes);

@@ -201,4 +877,4 @@ }

// src/core/generator.ts
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
import { dirname as dirname3, join as join3 } from "node:path";

@@ -292,5 +968,5 @@ // src/types.ts

async function writeConfig(rootDir, config) {
const configPath = join(rootDir, "shift.config.json");
const configPath = join3(rootDir, "shift.config.json");
const content = JSON.stringify(config, null, "\t");
await writeFile(configPath, `${content}
await writeFile2(configPath, `${content}
`, "utf-8");

@@ -300,6 +976,6 @@ return configPath;

async function writeStylesheet(rootDir, relativePath, content) {
const fullPath = join(rootDir, relativePath);
const dir = dirname(fullPath);
await mkdir(dir, { recursive: true });
await writeFile(fullPath, content, "utf-8");
const fullPath = join3(rootDir, relativePath);
const dir = dirname3(fullPath);
await mkdir2(dir, { recursive: true });
await writeFile2(fullPath, content, "utf-8");
return fullPath;

@@ -309,3 +985,3 @@ }

try {
await readFile2(join(rootDir, "shift.config.json"), "utf-8");
await readFile4(join3(rootDir, "shift.config.json"), "utf-8");
return true;

@@ -318,3 +994,3 @@ } catch {

try {
await readFile2(join(rootDir, relativePath), "utf-8");
await readFile4(join3(rootDir, relativePath), "utf-8");
return true;

@@ -328,3 +1004,3 @@ } catch {

import { readdir, stat } from "node:fs/promises";
import { basename, join as join2, relative } from "node:path";
import { basename, join as join4, relative } from "node:path";
var IGNORED_DIRS = new Set([

@@ -347,3 +1023,3 @@ "node_modules",

for (const entry of entries) {
const fullPath = join2(dir, entry.name);
const fullPath = join4(dir, entry.name);
if (entry.isDirectory()) {

@@ -371,8 +1047,8 @@ if (!IGNORED_DIRS.has(entry.name)) {

// src/ui/display.ts
import pc from "picocolors";
import pc3 from "picocolors";
function showStylesheetPreview(content) {
console.log("");
console.log(pc.bold(" Stylesheet Preview:"));
console.log(pc3.bold(" Stylesheet Preview:"));
console.log("");
console.log(pc.dim(" ─".repeat(40)));
console.log(pc3.dim(" ─".repeat(40)));
console.log("");

@@ -385,7 +1061,7 @@ const lines = content.split(`

if (line.startsWith("/**") || line.startsWith(" *") || line.startsWith(" */")) {
console.log(` ${pc.dim(line)}`);
console.log(` ${pc3.dim(line)}`);
} else if (line.startsWith("@layer") || line.startsWith("@import")) {
console.log(` ${pc.cyan(line)}`);
console.log(` ${pc3.cyan(line)}`);
} else if (line.includes("/*") && line.includes("*/")) {
console.log(` ${pc.dim(line)}`);
console.log(` ${pc3.dim(line)}`);
} else {

@@ -396,6 +1072,6 @@ console.log(` ${line}`);

if (lines.length > maxLines) {
console.log(` ${pc.dim(`... ${lines.length - maxLines} more lines`)}`);
console.log(` ${pc3.dim(`... ${lines.length - maxLines} more lines`)}`);
}
console.log("");
console.log(pc.dim(" ─".repeat(40)));
console.log(pc3.dim(" ─".repeat(40)));
console.log("");

@@ -405,6 +1081,6 @@ }

console.log("");
console.log(pc.bold(" Next steps:"));
console.log(pc3.bold(" Next steps:"));
console.log("");
console.log(` 1. ${pc.cyan("npm install @shift-css/core")}`);
console.log(` 2. Import ${pc.cyan(result.stylesheetPath.split("/").pop())} in your app`);
console.log(` 1. ${pc3.cyan("npm install @shift-css/core")}`);
console.log(` 2. Import ${pc3.cyan(result.stylesheetPath.split("/").pop())} in your app`);
if (result.mode === "hybrid" && result.detectedFrameworks?.length) {

@@ -417,237 +1093,6 @@ console.log(` 3. Uncomment your framework imports in the @layer legacy block`);

console.log("");
console.log(pc.dim(` \uD83D\uDCDA Documentation: https://getshiftcss.com`));
console.log(pc3.dim(` \uD83D\uDCDA Documentation: https://getshiftcss.com`));
console.log("");
}
// src/ui/prompts.ts
import * as p from "@clack/prompts";
import pc2 from "picocolors";
// src/core/color.ts
function hexToRgb(hex) {
const cleaned = hex.replace(/^#/, "");
const fullHex = cleaned.length === 3 ? cleaned.split("").map((c) => c + c).join("") : cleaned;
if (fullHex.length !== 6)
return null;
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
if (!result)
return null;
return {
r: Number.parseInt(result[1], 16),
g: Number.parseInt(result[2], 16),
b: Number.parseInt(result[3], 16)
};
}
function rgbToHue(r, g, b) {
const rNorm = r / 255;
const gNorm = g / 255;
const bNorm = b / 255;
const max = Math.max(rNorm, gNorm, bNorm);
const min = Math.min(rNorm, gNorm, bNorm);
const delta = max - min;
if (delta === 0)
return 250;
let hue;
if (max === rNorm) {
hue = (gNorm - bNorm) / delta % 6;
} else if (max === gNorm) {
hue = (bNorm - rNorm) / delta + 2;
} else {
hue = (rNorm - gNorm) / delta + 4;
}
hue = Math.round(hue * 60);
if (hue < 0)
hue += 360;
return hue;
}
function hexToHue(hex) {
const rgb = hexToRgb(hex);
if (!rgb)
return null;
return rgbToHue(rgb.r, rgb.g, rgb.b);
}
function isHexColor(value) {
return /^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/.test(value.trim());
}
function getColorName(hue) {
const h = (hue % 360 + 360) % 360;
if (h >= 0 && h < 15)
return "Red";
if (h >= 15 && h < 45)
return "Orange";
if (h >= 45 && h < 75)
return "Yellow";
if (h >= 75 && h < 105)
return "Lime";
if (h >= 105 && h < 135)
return "Green";
if (h >= 135 && h < 165)
return "Teal";
if (h >= 165 && h < 195)
return "Cyan";
if (h >= 195 && h < 225)
return "Sky";
if (h >= 225 && h < 255)
return "Blue";
if (h >= 255 && h < 285)
return "Indigo";
if (h >= 285 && h < 315)
return "Purple";
if (h >= 315 && h < 345)
return "Pink";
return "Red";
}
var PRESETS = [
{ name: "Plasma", hue: 260, description: "Electric Blue - High-tech default" },
{ name: "Laser", hue: 320, description: "Cyber-Pink - Neon futurism" },
{ name: "Acid", hue: 140, description: "Toxic Green - Engineering edge" },
{ name: "Void", hue: 0, description: "Monochrome - Industrial minimal" }
];
// src/ui/prompts.ts
function showIntro() {
console.clear();
p.intro(pc2.bgMagenta(pc2.white(" \uD83C\uDFA8 Shift CSS Init ")));
}
function showOutro(message) {
p.outro(message);
}
function handleCancel() {
p.cancel("Setup cancelled");
process.exit(0);
}
function createSpinner() {
return p.spinner();
}
function showNote(message, title) {
p.note(message, title);
}
async function askArchitectureMode(detectedFrameworks) {
const hasFrameworks = detectedFrameworks.length > 0;
const frameworkHint = hasFrameworks ? `Detected: ${detectedFrameworks.map((f) => f.type).join(", ")}` : undefined;
const mode = await p.select({
message: "What type of project is this?",
options: [
{
value: "greenfield",
label: "New project (Greenfield)",
hint: "Pure Shift CSS, no legacy frameworks"
},
{
value: "hybrid",
label: "Existing project (Hybrid)",
hint: frameworkHint ?? "Shift CSS alongside existing CSS"
}
],
initialValue: hasFrameworks ? "hybrid" : "greenfield"
});
if (p.isCancel(mode))
handleCancel();
return mode;
}
async function askPrimaryHue() {
const presetOptions = PRESETS.map((preset2) => ({
value: preset2.hue,
label: preset2.name,
hint: preset2.description
}));
const hue = await p.select({
message: "Choose your brand color:",
options: [
...presetOptions,
{ value: -1, label: "Custom", hint: "Enter hex code or hue value" }
]
});
if (p.isCancel(hue))
handleCancel();
if (hue === -1) {
p.log.info(pc2.dim("Hue guide: 20=Red, 90=Yellow, 140=Green, 260=Blue, 320=Purple"));
const customValue = await p.text({
message: "Enter a hex code (#a855f7) or hue (0-360):",
placeholder: "#a855f7 or 260",
validate: (value) => {
const trimmed2 = value.trim();
if (isHexColor(trimmed2)) {
const parsedHue = hexToHue(trimmed2);
if (parsedHue === null)
return "Invalid hex color";
return;
}
const num = Number.parseInt(trimmed2, 10);
if (Number.isNaN(num))
return "Enter a hex code (#ff0000) or hue number (0-360)";
if (num < 0 || num > 360)
return "Hue must be between 0 and 360";
return;
}
});
if (p.isCancel(customValue))
handleCancel();
const trimmed = customValue.trim();
if (isHexColor(trimmed)) {
const parsedHue = hexToHue(trimmed);
if (parsedHue !== null) {
const colorName = getColorName(parsedHue);
p.log.success(`Converted ${pc2.cyan(trimmed)} → ${pc2.magenta(colorName)} (Hue: ${parsedHue})`);
return parsedHue;
}
}
return Number.parseInt(trimmed, 10);
}
const preset = PRESETS.find((p2) => p2.hue === hue);
if (preset) {
const colorName = getColorName(preset.hue);
p.log.info(`${pc2.magenta(preset.name)} → ${colorName} (Hue: ${preset.hue})`);
}
return hue;
}
async function askStylesheetPath(defaultPath) {
const path = await p.text({
message: "Where should the stylesheet be created?",
placeholder: defaultPath,
defaultValue: defaultPath,
validate: (value) => {
if (!value)
return "Please enter a path";
if (!value.endsWith(".css"))
return "Path must end with .css";
return;
}
});
if (p.isCancel(path))
handleCancel();
return path;
}
async function confirmOverwrite(filePath) {
const confirmed = await p.confirm({
message: `${pc2.yellow(filePath)} already exists. Overwrite it?`,
initialValue: false
});
if (p.isCancel(confirmed))
handleCancel();
return confirmed;
}
async function confirmInit(configPath, stylesheetPath, mode) {
const modeLabel = mode === "greenfield" ? "Greenfield" : "Hybrid";
showNote([
`${pc2.bold("Config:")} ${pc2.cyan(configPath)}`,
`${pc2.bold("Stylesheet:")} ${pc2.cyan(stylesheetPath)}`,
`${pc2.bold("Mode:")} ${pc2.cyan(modeLabel)}`
].join(`
`), "Files to create");
const confirmed = await p.confirm({
message: "Proceed with initialization?",
initialValue: true
});
if (p.isCancel(confirmed))
handleCancel();
return confirmed;
}
function logSuccess(message) {
p.log.success(message);
}
function logWarning(message) {
p.log.warn(message);
}
// src/commands/init.ts

@@ -662,3 +1107,3 @@ async function initCommand() {

if (!shouldOverwrite) {
showOutro(pc3.yellow("Initialization cancelled"));
showOutro(pc4.yellow("Initialization cancelled"));
return;

@@ -684,3 +1129,3 @@ }

if (!shouldOverwrite) {
showOutro(pc3.yellow("Initialization cancelled"));
showOutro(pc4.yellow("Initialization cancelled"));
return;

@@ -693,3 +1138,3 @@ }

if (!shouldProceed) {
showOutro(pc3.yellow("Initialization cancelled"));
showOutro(pc4.yellow("Initialization cancelled"));
return;

@@ -713,4 +1158,4 @@ }

spinner2.stop("Files created");
logSuccess(`Created ${pc3.cyan("shift.config.json")}`);
logSuccess(`Created ${pc3.cyan(stylesheetPath)}`);
logSuccess(`Created ${pc4.cyan("shift.config.json")}`);
logSuccess(`Created ${pc4.cyan(stylesheetPath)}`);
const result = {

@@ -723,3 +1168,3 @@ configPath,

showNextSteps(result);
showOutro(pc3.green("✨ Shift CSS initialized!"));
showOutro(pc4.green("✨ Shift CSS initialized!"));
}

@@ -729,6 +1174,6 @@

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join as join3 } from "node:path";
import pc4 from "picocolors";
import { join as join5 } from "node:path";
import pc5 from "picocolors";
function detectFramework(rootDir) {
const pkgPath = join3(rootDir, "package.json");
const pkgPath = join5(rootDir, "package.json");
if (!existsSync(pkgPath)) {

@@ -801,44 +1246,44 @@ return "unknown";

console.log();
console.log(pc4.bold("\uD83D\uDCDD Next Steps"));
console.log(pc5.bold("\uD83D\uDCDD Next Steps"));
console.log();
if (framework === "react") {
console.log(pc4.dim("Option 1: Add to tsconfig.json (recommended)"));
console.log(pc5.dim("Option 1: Add to tsconfig.json (recommended)"));
console.log();
console.log(pc4.cyan(" {"));
console.log(pc4.cyan(' "compilerOptions": {'));
console.log(pc4.cyan(' "types": ["@shift-css/core/types/react"]'));
console.log(pc4.cyan(" }"));
console.log(pc4.cyan(" }"));
console.log(pc5.cyan(" {"));
console.log(pc5.cyan(' "compilerOptions": {'));
console.log(pc5.cyan(' "types": ["@shift-css/core/types/react"]'));
console.log(pc5.cyan(" }"));
console.log(pc5.cyan(" }"));
console.log();
console.log(pc4.dim("Option 2: Add reference to any .d.ts file"));
console.log(pc5.dim("Option 2: Add reference to any .d.ts file"));
console.log();
console.log(pc4.cyan(' /// <reference types="@shift-css/core/types/react" />'));
console.log(pc5.cyan(' /// <reference types="@shift-css/core/types/react" />'));
} else if (framework === "vue") {
console.log(pc4.dim("Option 1: Add to tsconfig.json (recommended)"));
console.log(pc5.dim("Option 1: Add to tsconfig.json (recommended)"));
console.log();
console.log(pc4.cyan(" {"));
console.log(pc4.cyan(' "compilerOptions": {'));
console.log(pc4.cyan(' "types": ["@shift-css/core/types/vue"]'));
console.log(pc4.cyan(" }"));
console.log(pc4.cyan(" }"));
console.log(pc5.cyan(" {"));
console.log(pc5.cyan(' "compilerOptions": {'));
console.log(pc5.cyan(' "types": ["@shift-css/core/types/vue"]'));
console.log(pc5.cyan(" }"));
console.log(pc5.cyan(" }"));
console.log();
console.log(pc4.dim("Option 2: Add reference to any .d.ts file"));
console.log(pc5.dim("Option 2: Add reference to any .d.ts file"));
console.log();
console.log(pc4.cyan(' /// <reference types="@shift-css/core/types/vue" />'));
console.log(pc5.cyan(' /// <reference types="@shift-css/core/types/vue" />'));
} else if (framework === "svelte") {
console.log(pc4.dim("Svelte support is coming soon!"));
console.log(pc5.dim("Svelte support is coming soon!"));
console.log();
console.log(pc4.dim("For now, you can generate a local .d.ts file:"));
console.log(pc5.dim("For now, you can generate a local .d.ts file:"));
console.log();
console.log(pc4.cyan(" npx shift-css types --svelte -o shift.d.ts"));
console.log(pc5.cyan(" npx shift-css types --svelte -o shift.d.ts"));
} else {
console.log(pc4.dim("Add to your tsconfig.json based on your framework:"));
console.log(pc5.dim("Add to your tsconfig.json based on your framework:"));
console.log();
console.log(pc4.cyan(' React: "types": ["@shift-css/core/types/react"]'));
console.log(pc4.cyan(' Vue: "types": ["@shift-css/core/types/vue"]'));
console.log(pc4.cyan(" Svelte: Coming soon"));
console.log(pc5.cyan(' React: "types": ["@shift-css/core/types/react"]'));
console.log(pc5.cyan(' Vue: "types": ["@shift-css/core/types/vue"]'));
console.log(pc5.cyan(" Svelte: Coming soon"));
}
console.log();
console.log(pc4.dim("After setup, you'll get autocomplete for attributes like:"));
console.log(pc4.green(' s-btn="primary" s-grid="3" s-flex="center"'));
console.log(pc5.dim("After setup, you'll get autocomplete for attributes like:"));
console.log(pc5.green(' s-btn="primary" s-grid="3" s-flex="center"'));
console.log();

@@ -849,12 +1294,12 @@ }

console.log();
console.log(pc4.bold("\uD83C\uDFA8 Shift CSS Type Generator"));
console.log(pc5.bold("\uD83C\uDFA8 Shift CSS Type Generator"));
console.log();
const framework = options.framework || detectFramework(rootDir);
if (framework !== "unknown") {
console.log(`${pc4.green("✓")} Detected framework: ${pc4.cyan(framework)}`);
console.log(`${pc5.green("✓")} Detected framework: ${pc5.cyan(framework)}`);
} else {
console.log(`${pc4.yellow("!")} Could not detect framework automatically`);
console.log(`${pc5.yellow("!")} Could not detect framework automatically`);
}
if (options.output) {
const outputPath = join3(rootDir, options.output);
const outputPath = join5(rootDir, options.output);
let content;

@@ -873,7 +1318,7 @@ switch (framework) {

writeFileSync(outputPath, content, "utf-8");
console.log(`${pc4.green("✓")} Created ${pc4.cyan(options.output)}`);
console.log(`${pc5.green("✓")} Created ${pc5.cyan(options.output)}`);
} catch (error) {
console.error(`${pc4.red("✗")} Failed to write ${options.output}`);
console.error(`${pc5.red("✗")} Failed to write ${options.output}`);
if (error instanceof Error) {
console.error(pc4.dim(error.message));
console.error(pc5.dim(error.message));
}

@@ -890,23 +1335,31 @@ process.exit(1);

console.log(`
${pc5.bold("Shift CSS CLI")} ${pc5.dim(`v${VERSION}`)}
${pc6.bold("Shift CSS CLI")} ${pc6.dim(`v${VERSION}`)}
${pc5.dim("Usage:")}
${pc5.cyan("shift-css")} ${pc5.green("<command>")} ${pc5.dim("[options]")}
${pc6.dim("Usage:")}
${pc6.cyan("shift-css")} ${pc6.green("<command>")} ${pc6.dim("[options]")}
${pc5.dim("Commands:")}
${pc5.green("init")} Set up Shift CSS in your project
${pc6.dim("Commands:")}
${pc6.green("init")} Set up Shift CSS in your project
Detects existing frameworks and wraps them in @layer legacy
${pc5.green("types")} Generate TypeScript definitions for Shift CSS attributes
${pc6.green("add")} Add components to your project (shadcn-style ejection)
Copies component CSS wrapped in @layer for customization
${pc6.green("types")} Generate TypeScript definitions for Shift CSS attributes
Detects React/Vue/Svelte and shows setup instructions
${pc5.dim("Options:")}
${pc5.yellow("--help, -h")} Show this help message
${pc5.yellow("--version, -v")} Show version number
${pc6.dim("Options:")}
${pc6.yellow("--help, -h")} Show this help message
${pc6.yellow("--version, -v")} Show version number
${pc5.dim("Examples:")}
${pc5.cyan("npx shift-css init")}
${pc5.cyan("npx shift-css types")}
${pc5.cyan("npx shift-css types --react -o shift.d.ts")}
${pc6.dim("Add Options:")}
${pc6.yellow("--all")} Add all available components
${pc6.yellow("--force")} Overwrite existing files without prompting
${pc6.yellow("--framework")} Framework for templates (astro, react, vue)
${pc5.dim("Learn more:")} ${pc5.underline("https://getshiftcss.com")}
${pc6.dim("Examples:")}
${pc6.cyan("npx shift-css init")}
${pc6.cyan("npx shift-css add button card")}
${pc6.cyan("npx shift-css add --all")}
${pc6.cyan("npx shift-css types --react -o shift.d.ts")}
${pc6.dim("Learn more:")} ${pc6.underline("https://getshiftcss.com")}
`);

@@ -932,2 +1385,25 @@ }

}
if (command === "add") {
const addOptions = {};
const componentNames = [];
for (let i = 1;i < args.length; i++) {
const arg = args[i];
if (arg === "--all") {
addOptions.all = true;
} else if (arg === "--force" || arg === "-f") {
addOptions.force = true;
} else if (arg === "--framework" && args[i + 1]) {
addOptions.framework = args[++i];
} else if (arg.startsWith("--framework=")) {
addOptions.framework = arg.slice("--framework=".length);
} else if (arg === "--list" || arg === "-l") {
await listComponents();
return;
} else if (!arg.startsWith("-")) {
componentNames.push(arg);
}
}
await addCommand(componentNames, addOptions);
return;
}
if (command === "types") {

@@ -952,8 +1428,8 @@ const options = {};

}
console.error(pc5.red(`Unknown command: ${command}`));
console.log(pc5.dim(`Run ${pc5.cyan("shift-css --help")} for usage information.`));
console.error(pc6.red(`Unknown command: ${command}`));
console.log(pc6.dim(`Run ${pc6.cyan("shift-css --help")} for usage information.`));
process.exit(1);
}
main().catch((error) => {
console.error(pc5.red("Error:"), error.message);
console.error(pc6.red("Error:"), error.message);
if (process.env.DEBUG) {

@@ -965,2 +1441,2 @@ console.error(error.stack);

//# debugId=2629B6603A680EC964756E2164756E21
//# debugId=82AB64E14C18CC5864756E2164756E21
{
"name": "@shift-css/cli",
"version": "0.4.0",
"version": "0.5.0",
"description": "CLI tool for Shift CSS framework setup and migration",

@@ -21,13 +21,13 @@ "type": "module",

"dependencies": {
"@clack/prompts": "^0.7.0",
"@clack/prompts": "^1.2.0",
"picocolors": "^1.0.0"
},
"devDependencies": {
"@types/bun": "^1.1.14",
"@types/node": "^22.10.5",
"@types/bun": "^1.3.12",
"@types/node": "^25.6.0",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
"typescript": "6.0.3"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
},

@@ -34,0 +34,0 @@ "keywords": [

Sorry, the diff of this file is too big to display