Display15
Form1
Playground3
CLI Playground
A component for displaying a CLI playground
Default
Default CLI playground
scaffold-cli
╔══════════════════════════════════════════╗
║ ║
║ Backend Scaffold CLI v1.0.0 ║
║ ║
║ Scaffold your backend API in seconds ║
║ ║
╚══════════════════════════════════════════╝
Type 'scaffold create' to get started
$
Click anywhere in the terminal to focus • Type
help for commandscli-playground.tsx
1"use client";23import { useState, useRef, useEffect, useCallback } from "react";4import { cn } from "@/lib/utils";56interface TerminalLine {7 type: "input" | "output" | "prompt" | "success" | "error" | "selection";8 content: string;9 isTyping?: boolean;10}1112interface SelectionOption {13 label: string;14 value: string;15 description?: string;16}1718type Step =19 | "awaiting-command"20 | "choose-mode"21 | "enter-name"22 | "choose-template"23 | "choose-framework"24 | "choose-database"25 | "choose-language"26 | "generating"27 | "complete";2829const TEMPLATES: SelectionOption[] = [30 {31 label: "REST API Starter",32 value: "rest-starter",33 description: "Basic REST API with CRUD operations",34 },35 {36 label: "GraphQL API",37 value: "graphql",38 description: "GraphQL server with schema and resolvers",39 },40 {41 label: "Microservices",42 value: "microservices",43 description: "Multi-service architecture template",44 },45 {46 label: "Real-time API",47 value: "realtime",48 description: "WebSocket-enabled real-time server",49 },50];5152const FRAMEWORKS: SelectionOption[] = [53 {54 label: "Express",55 value: "express",56 description: "Fast, unopinionated, minimalist",57 },58 {59 label: "Hono",60 value: "hono",61 description: "Ultrafast, lightweight, multi-runtime",62 },63 {64 label: "Fastify",65 value: "fastify",66 description: "Low overhead, high performance",67 },68 {69 label: "Koa",70 value: "koa",71 description: "Expressive middleware framework",72 },73];7475const DATABASES: SelectionOption[] = [76 {77 label: "PostgreSQL",78 value: "postgresql",79 description: "Powerful, open source relational database",80 },81 {82 label: "SQLite",83 value: "sqlite",84 description: "Lightweight, file-based database",85 },86 {87 label: "MySQL",88 value: "mysql",89 description: "Popular relational database",90 },91 {92 label: "MongoDB",93 value: "mongodb",94 description: "Flexible NoSQL document database",95 },96];9798const LANGUAGES: SelectionOption[] = [99 {100 label: "TypeScript",101 value: "typescript",102 description: "JavaScript with types",103 },104 { label: "JavaScript", value: "javascript", description: "ES6+ JavaScript" },105];106107/** Inner width between `║` characters (must match spacer / border row length). */108const CLI_BOX_INNER_WIDTH = 42;109110function centerCliBoxInner(text: string): string {111 const t = text.trim();112 if (t.length >= CLI_BOX_INNER_WIDTH) return t.slice(0, CLI_BOX_INNER_WIDTH);113 const pad = CLI_BOX_INNER_WIDTH - t.length;114 const left = Math.floor(pad / 2);115 return `${" ".repeat(left)}${t}${" ".repeat(pad - left)}`;116}117118function cliBoxRow(inner: string): string {119 return ` ║${inner}║`;120}121122/** Welcome banner (state initializer avoids Strict Mode double-run). */123const INITIAL_TERMINAL_LINES: TerminalLine[] = [124 { type: "output", content: "" },125 {126 type: "output",127 content: " ╔══════════════════════════════════════════╗",128 },129 {130 type: "output",131 content: cliBoxRow(centerCliBoxInner("")),132 },133 {134 type: "output",135 content: cliBoxRow(centerCliBoxInner("Backend Scaffold CLI v1.0.0")),136 },137 {138 type: "output",139 content: cliBoxRow(centerCliBoxInner("")),140 },141 {142 type: "output",143 content: cliBoxRow(centerCliBoxInner("Scaffold your backend API in seconds")),144 },145 {146 type: "output",147 content: cliBoxRow(centerCliBoxInner("")),148 },149 {150 type: "output",151 content: " ╚══════════════════════════════════════════╝",152 },153 { type: "output", content: "" },154 {155 type: "output",156 content: " Type 'scaffold create' to get started",157 },158 { type: "output", content: "" },159];160161export function CLIPlayground() {162 const [lines, setLines] = useState<TerminalLine[]>(() =>163 INITIAL_TERMINAL_LINES.map((line) => ({ ...line })),164 );165 const [currentInput, setCurrentInput] = useState("");166 const [step, setStep] = useState<Step>("awaiting-command");167 const [selectedIndex, setSelectedIndex] = useState(0);168 const [projectName, setProjectName] = useState("");169 const [config, setConfig] = useState({170 mode: "" as "template" | "custom",171 template: "",172 framework: "",173 database: "",174 language: "",175 });176177 const inputRef = useRef<HTMLInputElement>(null);178 const terminalRef = useRef<HTMLDivElement>(null);179180 const scrollToBottom = useCallback(() => {181 if (terminalRef.current) {182 terminalRef.current.scrollTop = terminalRef.current.scrollHeight;183 }184 }, []);185186 useEffect(() => {187 scrollToBottom();188 }, [lines, scrollToBottom]);189190 const addLine = (line: TerminalLine) => {191 setLines((prev) => [...prev, line]);192 };193194 const typeOutput = async (195 content: string,196 type: TerminalLine["type"] = "output",197 ) => {198 addLine({ type, content, isTyping: true });199 await new Promise((resolve) => setTimeout(resolve, 50));200 };201202 const getCurrentOptions = (): SelectionOption[] => {203 switch (step) {204 case "choose-mode":205 return [206 {207 label: "Use a template",208 value: "template",209 description: "Pre-configured project templates",210 },211 {212 label: "Custom configuration",213 value: "custom",214 description: "Choose each component individually",215 },216 ];217 case "choose-template":218 return TEMPLATES;219 case "choose-framework":220 return FRAMEWORKS;221 case "choose-database":222 return DATABASES;223 case "choose-language":224 return LANGUAGES;225 default:226 return [];227 }228 };229230 const handleSelection = async (option: SelectionOption) => {231 addLine({ type: "success", content: ` ✓ ${option.label}` });232 addLine({ type: "output", content: "" });233234 if (step === "choose-mode") {235 setConfig((prev) => ({236 ...prev,237 mode: option.value as "template" | "custom",238 }));239 await typeOutput(" What would you like to name your project?");240 addLine({ type: "output", content: "" });241 setStep("enter-name");242 setCurrentInput("");243 setTimeout(() => inputRef.current?.focus(), 100);244 } else if (step === "choose-template") {245 setConfig((prev) => ({ ...prev, template: option.value }));246 await generateProject();247 } else if (step === "choose-framework") {248 setConfig((prev) => ({ ...prev, framework: option.value }));249 await typeOutput(" Select your database:");250 addLine({ type: "output", content: "" });251 setStep("choose-database");252 setSelectedIndex(0);253 } else if (step === "choose-database") {254 setConfig((prev) => ({ ...prev, database: option.value }));255 await typeOutput(" Select your language:");256 addLine({ type: "output", content: "" });257 setStep("choose-language");258 setSelectedIndex(0);259 } else if (step === "choose-language") {260 setConfig((prev) => ({ ...prev, language: option.value }));261 await generateProject();262 }263 };264265 const generateProject = async () => {266 setStep("generating");267 addLine({ type: "output", content: "" });268 addLine({ type: "output", content: " Scaffolding your project..." });269 addLine({ type: "output", content: "" });270271 const steps = [272 "Creating project directory...",273 "Initializing package.json...",274 "Setting up project structure...",275 "Installing dependencies...",276 "Configuring database connection...",277 "Setting up routes and controllers...",278 "Adding middleware...",279 "Generating configuration files...",280 ];281282 for (const stepText of steps) {283 await new Promise((resolve) => setTimeout(resolve, 300));284 addLine({ type: "success", content: ` ✓ ${stepText}` });285 }286287 await new Promise((resolve) => setTimeout(resolve, 500));288 addLine({ type: "output", content: "" });289 addLine({290 type: "output",291 content: " ╔══════════════════════════════════════════╗",292 });293 addLine({294 type: "output",295 content: cliBoxRow(centerCliBoxInner("")),296 });297 addLine({298 type: "success",299 content: cliBoxRow(centerCliBoxInner("Project created successfully!")),300 });301 addLine({302 type: "output",303 content: cliBoxRow(centerCliBoxInner("")),304 });305 addLine({306 type: "output",307 content: " ╚══════════════════════════════════════════╝",308 });309 addLine({ type: "output", content: "" });310 addLine({ type: "output", content: ` cd ${projectName}` });311 addLine({ type: "output", content: " npm install" });312 addLine({ type: "output", content: " npm run dev" });313 addLine({ type: "output", content: "" });314 addLine({ type: "output", content: " Happy coding! 🎉" });315 addLine({ type: "output", content: "" });316 addLine({317 type: "output",318 content: " Type 'scaffold create' to create another project",319 });320 addLine({ type: "output", content: "" });321 setStep("awaiting-command");322 setSelectedIndex(0);323 setConfig({324 mode: "" as "template" | "custom",325 template: "",326 framework: "",327 database: "",328 language: "",329 });330 setProjectName("");331 };332333 const handleCommand = async (command: string) => {334 addLine({ type: "input", content: ` $ ${command}` });335 addLine({ type: "output", content: "" });336337 if (command.toLowerCase().trim() === "scaffold create") {338 await typeOutput(" How would you like to set up your project?");339 addLine({ type: "output", content: "" });340 setStep("choose-mode");341 setSelectedIndex(0);342 } else if (command.toLowerCase().trim() === "clear") {343 setLines(INITIAL_TERMINAL_LINES.map((line) => ({ ...line })));344 setStep("awaiting-command");345 } else if (command.toLowerCase().trim() === "help") {346 addLine({ type: "output", content: " Available commands:" });347 addLine({ type: "output", content: "" });348 addLine({349 type: "output",350 content: " scaffold create - Create a new backend project",351 });352 addLine({353 type: "output",354 content: " clear - Clear the terminal",355 });356 addLine({357 type: "output",358 content: " help - Show this help message",359 });360 addLine({ type: "output", content: "" });361 } else if (command.trim() === "") {362 // Do nothing for empty command363 } else {364 addLine({ type: "error", content: ` Command not found: ${command}` });365 addLine({366 type: "output",367 content: " Type 'help' for available commands",368 });369 addLine({ type: "output", content: "" });370 }371372 setCurrentInput("");373 };374375 const handleNameSubmit = async (name: string) => {376 if (name.trim() === "") {377 addLine({ type: "error", content: " Project name cannot be empty" });378 return;379 }380381 const projectNameSlug = name382 .toLowerCase()383 .replace(/\s+/g, "-")384 .replace(/[^a-z0-9-]/g, "");385 setProjectName(projectNameSlug);386 addLine({ type: "input", content: ` > ${projectNameSlug}` });387 addLine({388 type: "success",389 content: ` ✓ Project name: ${projectNameSlug}`,390 });391 addLine({ type: "output", content: "" });392393 if (config.mode === "template") {394 await typeOutput(" Select a template:");395 addLine({ type: "output", content: "" });396 setStep("choose-template");397 setSelectedIndex(0);398 } else {399 await typeOutput(" Select your framework:");400 addLine({ type: "output", content: "" });401 setStep("choose-framework");402 setSelectedIndex(0);403 }404 };405406 const handleKeyDown = (e: React.KeyboardEvent) => {407 const options = getCurrentOptions();408409 if (410 step === "choose-mode" ||411 step === "choose-template" ||412 step === "choose-framework" ||413 step === "choose-database" ||414 step === "choose-language"415 ) {416 if (e.key === "ArrowUp") {417 e.preventDefault();418 setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));419 } else if (e.key === "ArrowDown") {420 e.preventDefault();421 setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));422 } else if (e.key === "Enter") {423 e.preventDefault();424 handleSelection(options[selectedIndex]);425 }426 } else if (step === "enter-name") {427 if (e.key === "Enter") {428 e.preventDefault();429 handleNameSubmit(currentInput);430 setCurrentInput("");431 }432 } else if (step === "awaiting-command") {433 if (e.key === "Enter") {434 e.preventDefault();435 handleCommand(currentInput);436 }437 }438 };439440 const handleTerminalClick = () => {441 inputRef.current?.focus();442 };443444 const options = getCurrentOptions();445 const showOptions = [446 "choose-mode",447 "choose-template",448 "choose-framework",449 "choose-database",450 "choose-language",451 ].includes(step);452 const showInput = step === "enter-name" || step === "awaiting-command";453454 return (455 <div className="w-full mx-auto">456 <div className="rounded-lg overflow-hidden border border-border shadow-2xl">457 {/* Terminal Header */}458 <div className="bg-terminal-header px-4 py-3 flex items-center gap-2">459 <div className="flex gap-2">460 <div className="w-3 h-3 rounded-full bg-terminal-red" />461 <div className="w-3 h-3 rounded-full bg-terminal-yellow" />462 <div className="w-3 h-3 rounded-full bg-terminal-green" />463 </div>464 <div className="flex-1 text-center">465 <span className="text-muted-foreground text-sm font-mono">466 scaffold-cli467 </span>468 </div>469 <div className="w-14" />470 </div>471472 {/* Terminal Body */}473 <div474 ref={terminalRef}475 onClick={handleTerminalClick}476 className="bg-terminal-bg p-4 h-[500px] overflow-auto font-mono text-sm cursor-text [font-variant-ligatures:none]"477 >478 {/* Rendered Lines — pre not pre-wrap so box-drawing lines never wrap mid-row */}479 {lines.map((line, index) => (480 <div481 key={index}482 className={cn(483 "whitespace-pre leading-relaxed",484 line.type === "input" && "text-terminal-cyan",485 line.type === "output" && "text-foreground",486 line.type === "success" && "text-terminal-green",487 line.type === "error" && "text-terminal-red",488 line.type === "prompt" && "text-terminal-yellow",489 )}490 >491 {line.content}492 </div>493 ))}494495 {/* Selection Options */}496 {showOptions && (497 <div className="mt-1">498 {options.map((option, index) => (499 <div500 key={option.value}501 onClick={() => handleSelection(option)}502 className={cn(503 "py-1 px-2 cursor-pointer rounded transition-colors",504 index === selectedIndex505 ? "bg-primary/20 text-primary"506 : "text-muted-foreground hover:bg-muted/30",507 )}508 >509 <span className="inline-block w-6">510 {index === selectedIndex ? "❯" : " "}511 </span>512 <span513 className={cn(index === selectedIndex && "font-medium")}514 >515 {option.label}516 </span>517 {option.description && (518 <span className="text-muted-foreground ml-2 text-xs">519 — {option.description}520 </span>521 )}522 </div>523 ))}524 <div className="mt-2 text-muted-foreground text-xs">525 Use ↑↓ to navigate, Enter to select526 </div>527 </div>528 )}529530 {/* Input Line */}531 {showInput && (532 <div className="flex items-center text-terminal-cyan">533 <span className="mr-2">534 {step === "enter-name" ? " >" : " $"}535 </span>536 <input537 ref={inputRef}538 type="text"539 value={currentInput}540 onChange={(e) => setCurrentInput(e.target.value)}541 onKeyDown={handleKeyDown}542 className="flex-1 bg-transparent outline-none text-foreground caret-primary"543 autoFocus544 spellCheck={false}545 />546 <span className="w-2 h-5 bg-primary animate-pulse" />547 </div>548 )}549550 {/* Loading state */}551 {step === "generating" && (552 <div className="flex items-center gap-2 text-muted-foreground mt-2">553 <div className="flex gap-1">554 <span555 className="animate-bounce"556 style={{ animationDelay: "0ms" }}557 >558 ●559 </span>560 <span561 className="animate-bounce"562 style={{ animationDelay: "150ms" }}563 >564 ●565 </span>566 <span567 className="animate-bounce"568 style={{ animationDelay: "300ms" }}569 >570 ●571 </span>572 </div>573 </div>574 )}575 </div>576 </div>577578 {/* Footer hint */}579 <div className="mt-4 text-center text-muted-foreground text-sm">580 Click anywhere in the terminal to focus • Type{" "}581 <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">582 help583 </code>{" "}584 for commands585 </div>586 </div>587 );588}
Props
| Name | Type | Default | Description |
|---|