glasscn
uicode-block-commandsource: ok · hit
Code Block Command
npx shadcn add "https://glasscn-components.vercel.app/r/code-block-command.json"free
Visual style
terminal-mono
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumiconslucide-react
animationmotion
Registry dependencies
@glasscn/utils@glasscn/button@glasscn/badge
Files5 · source cached 10/9/2026, 6:44:56 PM
components/ui/code-block-command.tsx
"use client";
import * as React from "react";
import { WandSparklesIcon } from "lucide-react";
import { CopyButton } from "@/components/custom/copy-button";
import { type Badge } from "@/components/ui/badge";
import { PackageManagerIcon } from "@/components/ui/icons/package-manager-icons";
import { cn } from "@/lib/utils";
import type { PackageManager } from "@/lib/package-manager";
export type { PackageManager };
export { convertNpmCommand } from "@/lib/package-manager";
// ─── Context ─────────────────────────────────────────────────────────────────
type PackageManagerContextValue = {
packageManager: PackageManager;
setPackageManager: (pm: PackageManager) => void;
};
const PackageManagerContext = React.createContext<PackageManagerContextValue>({
packageManager: "npm",
setPackageManager: () => { },
});
export function PackageManagerProvider({
children,
defaultPackageManager = "npm",
}: {
children: React.ReactNode;
defaultPackageManager?: PackageManager;
}) {
const [packageManager, setPackageManagerState] =
React.useState<PackageManager>(() => {
if (typeof window !== "undefined") {
return (
(localStorage.getItem("packageManager") as PackageManager) ??
defaultPackageManager
);
}
return defaultPackageManager;
});
const setPackageManager = React.useCallback((pm: PackageManager) => {
setPackageManagerState(pm);
localStorage.setItem("packageManager", pm);
}, []);
return (
<PackageManagerContext.Provider value={{ packageManager, setPackageManager }}>
{children}
</PackageManagerContext.Provider>
);
}
export function usePackageManager() {
return React.useContext(PackageManagerContext);
}
type ActiveTab = PackageManager | "prompt";
export type CodeBlockCommandProps = {
prompt?: string;
npm?: string;
pnpm?: string;
yarn?: string;
bun?: string;
onCopySuccess?: (text: string) => void;
onCopyError?: (error: Error) => void;
className?: string;
badgeComponent?: React.ComponentType<React.ComponentProps<typeof Badge>>;
};
const PACKAGE_MANAGERS: PackageManager[] = ["pnpm", "yarn", "npm", "bun"];
export function CodeBlockCommand({
prompt,
npm,
pnpm,
yarn,
bun,
onCopySuccess,
onCopyError,
className,
badgeComponent: BadgeComponent,
}: CodeBlockCommandProps) {
const { packageManager, setPackageManager } = usePackageManager();
const commands: Record<PackageManager, string | undefined> = { npm, pnpm, yarn, bun };
const availablePMs = PACKAGE_MANAGERS.filter((pm) => commands[pm] !== undefined);
// Local tab state — "prompt" tab doesn't affect the shared PM context
const [activeTab, setActiveTab] = React.useState<ActiveTab>(() => {
if (availablePMs.includes(packageManager)) return packageManager;
return availablePMs[0] ?? "npm";
});
// Keep active tab in sync when the shared PM context changes
React.useEffect(() => {
if (availablePMs.includes(packageManager)) {
setActiveTab(packageManager);
}
}, [packageManager]); // eslint-disable-line react-hooks/exhaustive-deps
function handleTabClick(tab: ActiveTab) {
setActiveTab(tab);
if (tab !== "prompt") {
setPackageManager(tab);
}
}
const activeContent =
activeTab === "prompt" ? (prompt ?? "") : (commands[activeTab] ?? "");
return (
<div
data-slot="code-block-command"
className={cn("relative overflow-hidden rounded-xl border bg-muted", className)}
>
<div data-slot="code-block-command-header" className="flex items-center border-b px-4">
{BadgeComponent ? (
<BadgeComponent
data-slot="code-block-command-active-icon"
variant="outline"
className="mr-3 size-6 shrink-0 rounded-full p-0"
>
{activeTab === "prompt" ? (
<WandSparklesIcon aria-hidden className="size-3.5" />
) : (
<PackageManagerIcon packageManager={activeTab} className="size-3.5" />
)}
</BadgeComponent>
) : activeTab === "prompt" ? (
<WandSparklesIcon
aria-hidden
data-slot="code-block-command-active-icon"
className="mr-3 size-5 shrink-0 text-muted-foreground"
/>
) : (
<PackageManagerIcon
packageManager={activeTab}
data-slot="code-block-command-active-icon"
className="mr-3 size-5 shrink-0 text-muted-foreground"
/>
)}
{prompt && (
<TabButton active={activeTab === "prompt"} onClick={() => handleTabClick("prompt")}>
prompt
</TabButton>
)}
{availablePMs.map((pm) => (
<TabButton key={pm} active={activeTab === pm} onClick={() => handleTabClick(pm)}>
{pm}
</TabButton>
))}
</div>
<div data-slot="code-block-command-content" className="px-4 py-3 pr-12">
<pre className="overflow-x-auto overscroll-x-contain">
<code
data-slot="code-block-command-code"
className="font-mono text-sm leading-none text-foreground/80"
>
{activeTab !== "prompt" && <span className="select-none">$ </span>}
{activeContent}
</code>
</pre>
</div>
<CopyButton
data-slot="code-block-command-copy"
size="icon-sm"
className="absolute top-2 right-2"
text={activeContent}
onCopySuccess={onCopySuccess}
onCopyError={onCopyError}
/>
</div>
);
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function TabButton({
active,
onClick,
children,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
data-slot="code-block-command-tab"
data-active={active}
className={cn(
"h-10 border-b-2 border-transparent px-2 font-mono text-sm transition-colors",
active
? "border-foreground text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
{children}
</button>
);
}
components/ui/icons/package-manager-icons.tsx
import type { SVGProps } from "react";
import type { PackageManager } from "@/lib/package-manager";
import { cn } from "@/lib/utils";
// Brand colors from Simple Icons (simpleicons.org)
// npm → #CB3837 pnpm → #F69220
// yarn → #2C8EBB bun → #FBF0DF (cream — dark bg brand; inverted for light mode)
function NpmIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="#cb3837"
d="M2 38.5h124v43.71H64v7.29H36.44v-7.29H2zm6.89 36.43h13.78V53.07h6.89v21.86h6.89V45.79H8.89zm34.44-29.14v36.42h13.78v-7.28h13.78V45.79zm13.78 7.29H64v14.56h-6.89zm20.67-7.29v29.14h13.78V53.07h6.89v21.86h6.89V53.07h6.89v21.86h6.89V45.79z"
/>
</svg>
);
}
function PnpmIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg role="img" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M0 0v7.5h7.5V0zm8.25 0v7.5h7.498V0zm8.25 0v7.5H24V0zM8.25 8.25v7.5h7.498v-7.5zm8.25 0v7.5H24v-7.5zM0 16.5V24h7.5v-7.5zm8.25 0V24h7.498v-7.5zm8.25 0V24H24v-7.5z" />
</svg>
);
}
function YarnIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
<g fill="#2c8ebb">
<path d="M99.24 80.71C94.9 80.76 91.1 83 87.89 85c-6 3.71-9 3.47-9 3.47l-.1-.17c-.41-.67 1.92-6.68-.69-13.84-2.82-7.83-7.3-9.72-6.94-10.32 1.53-2.59 5.36-6.7 6.89-14.36.91-4.64.67-12.28-1.39-16.28-.38-.74-3.78 1.24-3.78 1.24s-3.18-7.09-4.07-7.66c-2.87-1.84-6 7.61-6 7.61a14 14 0 00-11.71 4.5 9.64 9.64 0 01-3.85 2.27c-.41.14-.91.12-2.15 3.47-1.9 5.07 3.24 10.81 3.24 10.81s-6.13 4.33-8.4 9.72a24.78 24.78 0 00-1.75 11.68s-4.36 3.78-4.64 7.68a12.87 12.87 0 001.77 7.83 1.94 1.94 0 002.63.91s-2.9 3.38-.19 4.81c2.47 1.29 6.63 2 8.83-.19 1.6-1.6 1.92-5.17 2.51-6.63.14-.34.62.57 1.08 1a10 10 0 001.36 1s-3.9 1.68-2.3 5.51c.53 1.27 2.42 2.08 5.51 2.06 1.15 0 13.76-.72 17.12-1.53a4.33 4.33 0 002.61-1.46 63 63 0 0015.49-7c4.74-3.09 6.68-3.93 10.51-4.84 3.16-.75 2.95-5.65-1.24-5.58z" />
<path d="M64 2a62 62 0 1062 62A62 62 0 0064 2zm37.3 87.83c-3.35.81-4.91 1.44-9.41 4.36a67 67 0 01-15.56 7.18 8.71 8.71 0 01-3.64 1.77c-3.81.93-16.88 1.63-17.91 1.63h-.24c-4 0-6.27-1.24-7.49-2.54-3.4 1.7-7.8 1-11-.69a5.55 5.55 0 01-3-3.9 6 6 0 010-2.06 6.66 6.66 0 01-.79-1A16.38 16.38 0 0130 84.52c.29-3.73 2.87-7.06 4.55-8.83A28.56 28.56 0 0136.61 64a26.82 26.82 0 016.82-9c-1.65-2.78-3.33-7.06-1.7-11.42 1.17-3.11 2.13-4.84 4.24-5.58a6.84 6.84 0 002.51-1.34A17.65 17.65 0 0160.34 31c.19-.48.41-1 .65-1.46 1.6-3.4 3.3-5.31 5.29-6a4.88 4.88 0 014.4.5c.65.43 1.48 1 3.9 6a4.69 4.69 0 012.85-.1 3.81 3.81 0 012.39 1.94c2.47 4.74 2.8 13.19 1.72 18.62a33.8 33.8 0 01-5.84 13.31 25.73 25.73 0 015.77 9.43 25.42 25.42 0 011.41 10.41A28.7 28.7 0 0186 81.91c3.06-1.89 7.68-4.74 13.19-4.81a6.62 6.62 0 017 5.7 6.35 6.35 0 01-4.89 7.03z" />
</g>
</svg>
);
}
function BunIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg role="img" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M113.744 41.999a18.558 18.558 0 0 0-.8-.772c-.272-.246-.528-.524-.8-.771s-.528-.525-.8-.771c-.272-.247-.528-.525-.8-.772s-.528-.524-.8-.771-.528-.525-.8-.772-.528-.524-.8-.771c7.936 7.52 12.483 17.752 12.656 28.481 0 25.565-26.912 46.363-60 46.363-18.528 0-35.104-6.526-46.128-16.756l.8.772.8.771.8.772.8.771.8.772.8.771.8.771c11.008 10.662 27.952 17.527 46.928 17.527 33.088 0 60-20.797 60-46.285 0-10.893-4.864-21.215-13.456-29.33z" />
<path
fill="#fbf0df"
d="M116.8 65.08c0 23.467-25.072 42.49-56 42.49s-56-19.023-56-42.49c0-14.55 9.6-27.401 24.352-35.023C43.904 22.435 53.088 14.628 60.8 14.628S75.104 21 92.448 30.058C107.2 37.677 116.8 50.53 116.8 65.08Z"
/>
<path
fill="#f6dece"
d="M116.8 65.08a32.314 32.314 0 0 0-1.28-8.918c-4.368 51.377-69.36 53.846-94.912 38.48 11.486 8.584 25.66 13.144 40.192 12.928 30.88 0 56-19.054 56-42.49z"
/>
<path
fill="#fffefc"
d="M39.248 27.234c7.152-4.135 16.656-11.896 26-11.911a15.372 15.372 0 0 0-4.448-.695c-3.872 0-8 1.93-13.2 4.83-1.808 1.018-3.68 2.144-5.664 3.317-3.728 2.222-8 4.736-12.8 7.251C13.904 37.972 4.8 51.071 4.8 65.08v1.836c9.696-33.033 27.312-35.547 34.448-39.682z"
/>
<path
fill="#ccbea7"
d="M56.192 18.532A24.553 24.553 0 0 1 53.867 29.1a25.407 25.407 0 0 1-6.683 8.671c-.448.386-.096 1.127.48.91 5.392-2.02 12.672-8.068 9.6-20.272-.128-.695-1.072-.51-1.072.123zm3.632 0a24.474 24.474 0 0 1 3.646 10.12c.445 3.587.08 7.224-1.07 10.662-.192.54.496 1.003.88.556 3.504-4.32 6.56-12.899-2.592-22.156-.464-.4-1.184.216-.864.756zm4.416-.262a25.702 25.702 0 0 1 7.521 7.925A24.71 24.71 0 0 1 75.2 36.414c-.016.13.02.26.101.365a.543.543 0 0 0 .718.117.509.509 0 0 0 .221-.313c1.472-5.384.64-14.564-11.472-19.332-.64-.246-1.056.587-.528.957zM34.704 34.315a27.418 27.418 0 0 0 9.91-5.222 26.262 26.262 0 0 0 6.842-8.663c.288-.556 1.2-.34 1.056.277-2.768 12.343-12.032 14.92-17.792 14.58-.608.016-.592-.802-.016-.972z"
/>
<path d="M60.8 111.443c-33.088 0-60-20.798-60-46.363 0-15.429 9.888-29.823 26.448-38.448 4.8-2.469 8.912-4.953 12.576-7.128 2.016-1.203 3.92-2.33 5.76-3.379C51.2 12.916 56 10.771 60.8 10.771c4.8 0 8.992 1.852 14.24 4.845 1.6.88 3.2 1.836 4.912 2.885 3.984 2.376 8.48 5.06 14.4 8.131 16.56 8.625 26.448 23.004 26.448 38.448 0 25.565-26.912 46.363-60 46.363zm0-96.814c-3.872 0-8 1.928-13.2 4.829-1.808 1.018-3.68 2.144-5.664 3.317-3.728 2.222-8 4.736-12.8 7.251C13.904 37.972 4.8 51.071 4.8 65.08c0 23.436 25.12 42.506 56 42.506s56-19.07 56-42.506c0-14.01-9.104-27.108-24.352-35.023-6.048-3.086-10.768-5.986-14.592-8.27-1.744-1.033-3.344-1.99-4.8-2.838-4.848-2.778-8.384-4.32-12.256-4.32z" />
<path
fill="#b71422"
d="M72.08 76.343c-.719 2.839-2.355 5.383-4.672 7.267a11.07 11.07 0 0 1-6.4 2.9 11.13 11.13 0 0 1-6.608-2.9c-2.293-1.892-3.906-4.436-4.608-7.267a1.073 1.073 0 0 1 .05-.5 1.11 1.11 0 0 1 .272-.428 1.19 1.19 0 0 1 .958-.322h19.744a1.185 1.185 0 0 1 .947.33 1.073 1.073 0 0 1 .317.92z"
/>
<path
fill="#ff6164"
d="M54.4 83.733a11.24 11.24 0 0 0 6.592 2.932 11.239 11.239 0 0 0 6.576-2.932 16.652 16.652 0 0 0 1.6-1.65 10.904 10.904 0 0 0-3.538-2.564 11.26 11.26 0 0 0-4.302-1 10.121 10.121 0 0 0-4.549 1.192 9.71 9.71 0 0 0-3.451 3.097c.368.323.688.632 1.072.925z"
/>
<path d="M54.656 82.514a8.518 8.518 0 0 1 2.97-2.347 8.836 8.836 0 0 1 3.734-.862 9.78 9.78 0 0 1 6.4 2.608c.368-.386.72-.787 1.056-1.188-2.035-1.87-4.726-2.933-7.536-2.978a10.487 10.487 0 0 0-4.335.975 10.125 10.125 0 0 0-3.489 2.666c.378.396.779.772 1.2 1.126z" />
<path d="M60.944 87.436a12.078 12.078 0 0 1-7.12-3.086c-2.477-2.02-4.22-4.75-4.976-7.791-.054-.27-.045-.55.027-.817a1.83 1.83 0 0 1 .389-.726 2.25 2.25 0 0 1 .81-.595 2.32 2.32 0 0 1 .998-.192h19.744c.343-.007.683.06.996.196a2.3 2.3 0 0 1 .812.591c.182.212.313.46.382.728.07.267.076.545.018.815-.756 3.042-2.5 5.771-4.976 7.791a12.078 12.078 0 0 1-7.104 3.086zm-9.872-11.417c-.256 0-.32.108-.336.139.676 2.638 2.206 4.999 4.368 6.742a10.122 10.122 0 0 0 5.84 2.7 10.207 10.207 0 0 0 5.84-2.67c2.155-1.745 3.679-4.106 4.352-6.741a.333.333 0 0 0-.14-.113.348.348 0 0 0-.18-.026z" />
<path
fill="#febbd0"
d="M85.152 77.3c5.17 0 9.36-2.377 9.36-5.308s-4.19-5.307-9.36-5.307c-5.17 0-9.36 2.376-9.36 5.307 0 2.931 4.19 5.307 9.36 5.307zm-48.432 0c5.17 0 9.36-2.377 9.36-5.308s-4.19-5.307-9.36-5.307c-5.17 0-9.36 2.376-9.36 5.307 0 2.931 4.19 5.307 9.36 5.307z"
/>
<path d="M41.12 69.863a9.052 9.052 0 0 0 4.902-1.425 8.578 8.578 0 0 0 3.254-3.812 8.22 8.22 0 0 0 .508-4.913 8.41 8.41 0 0 0-2.408-4.357 8.92 8.92 0 0 0-4.514-2.33 9.12 9.12 0 0 0-5.096.48 8.755 8.755 0 0 0-3.96 3.131 8.287 8.287 0 0 0-1.486 4.725c0 2.252.927 4.412 2.577 6.005 1.65 1.594 3.888 2.492 6.223 2.496zm39.632 0a9.054 9.054 0 0 0 4.915-1.403 8.582 8.582 0 0 0 3.275-3.802 8.22 8.22 0 0 0 .528-4.917 8.408 8.408 0 0 0-2.398-4.368 8.92 8.92 0 0 0-4.512-2.344 9.12 9.12 0 0 0-5.103.473 8.756 8.756 0 0 0-3.967 3.13 8.287 8.287 0 0 0-1.49 4.73c-.004 2.245.914 4.4 2.555 5.994 1.64 1.593 3.869 2.495 6.197 2.507z" />
<path
fill="#fff"
d="M38.4 61.902a3.4 3.4 0 0 0 1.844-.531c.547-.35.974-.847 1.227-1.43a3.088 3.088 0 0 0 .195-1.847 3.16 3.16 0 0 0-.902-1.639 3.351 3.351 0 0 0-1.696-.878 3.426 3.426 0 0 0-1.916.179 3.29 3.29 0 0 0-1.489 1.176 3.113 3.113 0 0 0-.559 1.776c0 .844.347 1.654.964 2.253a3.374 3.374 0 0 0 2.332.94zm39.632 0a3.4 3.4 0 0 0 1.844-.531c.547-.35.974-.847 1.227-1.43a3.088 3.088 0 0 0 .195-1.847 3.16 3.16 0 0 0-.902-1.639 3.351 3.351 0 0 0-1.696-.878 3.426 3.426 0 0 0-1.916.179 3.29 3.29 0 0 0-1.489 1.176 3.113 3.113 0 0 0-.559 1.776c0 .84.342 1.644.953 2.242.61.598 1.44.94 2.311.952z"
/>
</svg>
);
}
const PM_STYLES: Record<PackageManager, { style?: React.CSSProperties; className?: string }> = {
npm: {},
pnpm: { style: { color: "#F69220" } },
yarn: {},
bun: {},
};
export function PackageManagerIcon({
packageManager,
className,
}: {
packageManager: PackageManager;
className?: string;
}) {
const { style, className: colorClass } = PM_STYLES[packageManager];
const sharedProps = { "aria-hidden": true as const, style, className: cn(colorClass, className) };
switch (packageManager) {
case "npm":
return <NpmIcon {...sharedProps} />;
case "pnpm":
return <PnpmIcon {...sharedProps} />;
case "yarn":
return <YarnIcon {...sharedProps} />;
case "bun":
return <BunIcon {...sharedProps} />;
}
}
components/custom/copy-button.tsx
"use client";
import { CheckIcon, CircleXIcon, CopyIcon } from "lucide-react";
import type { HTMLMotionProps, Variants } from "motion/react";
import { AnimatePresence, motion } from "motion/react";
import type { ComponentProps } from "react";
import { Button } from "@/components/ui/button";
import type { CopyState } from "@/hooks/use-copy-to-clipboard";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
export const motionIconVariants: Variants = {
initial: { opacity: 0, scale: 0.8, filter: "blur(2px)" },
animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
exit: { opacity: 0, scale: 0.8 },
};
export const motionIconProps: HTMLMotionProps<"span"> = {
variants: motionIconVariants,
initial: "initial",
animate: "animate",
exit: "exit",
transition: { duration: 0.25, ease: "easeOut" },
};
export type CopyStateIconProps = {
state: CopyState;
idleIcon?: React.ReactNode;
doneIcon?: React.ReactNode;
errorIcon?: React.ReactNode;
};
export function CopyStateIcon({ state, idleIcon, doneIcon, errorIcon }: CopyStateIconProps) {
return (
<AnimatePresence mode="wait" initial={false}>
{state === "idle" ? (
<motion.span key="idle" {...motionIconProps}>
{idleIcon ?? <CopyIcon />}
</motion.span>
) : state === "done" ? (
<motion.span key="done" {...motionIconProps}>
{doneIcon ?? <CheckIcon strokeWidth={3} color="green" />}
</motion.span>
) : state === "error" ? (
<motion.span key="error" {...motionIconProps}>
{errorIcon ?? <CircleXIcon />}
</motion.span>
) : null}
</AnimatePresence>
);
}
export type CopyButtonProps = ComponentProps<typeof Button> & {
text: string | (() => string);
onCopySuccess?: (text: string) => void;
onCopyError?: (error: Error) => void;
} & Pick<CopyStateIconProps, "idleIcon" | "doneIcon" | "errorIcon">;
export function CopyButton({
size = "icon",
variant = "ghost",
children,
text,
idleIcon,
doneIcon,
errorIcon,
onClick,
onCopySuccess,
onCopyError,
...props
}: CopyButtonProps) {
const { state, copy } = useCopyToClipboard({ onCopySuccess, onCopyError });
return (
<Button
size={size}
variant={variant}
onClick={(e) => {
void copy(text);
onClick?.(e);
}}
aria-label="Copy"
{...props}
>
<CopyStateIcon state={state} idleIcon={idleIcon} doneIcon={doneIcon} errorIcon={errorIcon} />
{children}
</Button>
);
}
hooks/use-copy-to-clipboard.ts
import { useState } from "react";
export type CopyState = "idle" | "done" | "error";
type UseCopyToClipboardOptions = {
onCopySuccess?: (text: string) => void;
onCopyError?: (error: Error) => void;
timeout?: number;
};
export function useCopyToClipboard({ onCopySuccess, onCopyError, timeout = 2000 }: UseCopyToClipboardOptions = {}) {
const [state, setState] = useState<CopyState>("idle");
async function copy(text: string | (() => string)) {
const value = typeof text === "function" ? text() : text;
try {
await navigator.clipboard.writeText(value);
setState("done");
onCopySuccess?.(value);
} catch (err) {
setState("error");
onCopyError?.(err instanceof Error ? err : new Error(String(err)));
} finally {
setTimeout(() => setState("idle"), timeout);
}
}
return { state, copy };
}
lib/package-manager.ts
export type PackageManager = "pnpm" | "yarn" | "npm" | "bun";
export function convertNpmCommand(command: string): Record<PackageManager, string> {
const pnpm = command
.replace(/^npm install/, "pnpm add")
.replace(/^npx create-(\S+)/, "pnpm create $1")
.replace(/^npx/, "pnpm dlx")
.replace(/^npm run/, "pnpm");
const yarn = command
.replace(/^npm install/, "yarn add")
.replace(/^npx create-(\S+)/, "yarn create $1")
.replace(/^npx/, "yarn dlx")
.replace(/^npm run/, "yarn");
const bun = command
.replace(/^npm install/, "bun add")
.replace(/^npx create-(\S+)/, "bunx --bun create-$1")
.replace(/^npx/, "bunx --bun")
.replace(/^npm run/, "bun");
return { npm: command, pnpm, yarn, bun };
}