@shift-css/cli
Advanced tools
+801
-325
| #!/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 |
+6
-6
| { | ||
| "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
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
151001
57.52%1383
51.31%9
350%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
Updated