@@ -5,6 +5,7 @@ import { assertValidPackageName } from '#shared/utils/npm'
55import { onKeyStroke } from ' @vueuse/core'
66import { joinURL } from ' ufo'
77import { areUrlsEquivalent } from ' #shared/utils/url'
8+ import { getExecutableInfo , getRunCommandParts , getRunCommand } from ' ~/utils/run-command'
89
910definePageMeta ({
1011 name: ' package' ,
@@ -232,6 +233,58 @@ const {
232233 copyInstallCommand,
233234} = useInstallCommand (packageName , requestedVersion , jsrInfo , typesPackageName )
234235
236+ // Executable detection for run command
237+ const executableInfo = computed (() => {
238+ if (! displayVersion .value || ! pkg .value ) return null
239+ return getExecutableInfo (pkg .value .name , displayVersion .value .bin )
240+ })
241+
242+ // Run command expanded state (for packages with multiple bin commands)
243+ const runExpanded = ref (false )
244+
245+ // Run command parts for a specific command
246+ function getRunParts(command ? : string ) {
247+ if (! pkg .value ) return []
248+ return getRunCommandParts ({
249+ packageName: pkg .value .name ,
250+ packageManager: selectedPM .value ,
251+ jsrInfo: jsrInfo .value ,
252+ command ,
253+ })
254+ }
255+
256+ // Primary run command parts
257+ const runCommandParts = computed (() => {
258+ if (! executableInfo .value ?.hasExecutable ) return []
259+ return getRunParts (executableInfo .value .primaryCommand )
260+ })
261+
262+ // Full run command string for copying
263+ function getFullRunCommand(command ? : string ) {
264+ if (! pkg .value ) return ' '
265+ return getRunCommand ({
266+ packageName: pkg .value .name ,
267+ packageManager: selectedPM .value ,
268+ jsrInfo: jsrInfo .value ,
269+ command ,
270+ })
271+ }
272+
273+ // Copy run command
274+ const runCopied = ref (false )
275+ const runCopiedCommand = ref <string | null >(null )
276+ async function copyRunCommand(command ? : string ) {
277+ const cmd = getFullRunCommand (command )
278+ if (! cmd ) return
279+ await navigator .clipboard .writeText (cmd )
280+ runCopied .value = true
281+ runCopiedCommand .value = command || null
282+ setTimeout (() => {
283+ runCopied .value = false
284+ runCopiedCommand .value = null
285+ }, 2000 )
286+ }
287+
235288// Expandable description
236289const descriptionExpanded = ref (false )
237290const descriptionRef = useTemplateRef (' descriptionRef' )
@@ -763,6 +816,101 @@ defineOgImageComponent('Package', {
763816 </div >
764817 </section >
765818
819+ <!-- Run command section - only shown for packages with executables -->
820+ <section v-if =" executableInfo?.hasExecutable" aria-labelledby =" run-heading" class =" mb-8" >
821+ <h2 id =" run-heading" class =" text-xs text-fg-subtle uppercase tracking-wider mb-3" >Run</h2 >
822+ <div class =" relative group" >
823+ <div class =" bg-[#0d0d0d] border border-border rounded-lg overflow-hidden" >
824+ <!-- Terminal chrome with interactive green button for multiple commands -->
825+ <div class =" flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3" >
826+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
827+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
828+ <ClientOnly >
829+ <button
830+ v-if =" executableInfo.commands.length > 1"
831+ type =" button"
832+ class =" w-2.5 h-2.5 rounded-full transition-colors cursor-pointer"
833+ :class =" runExpanded ? 'bg-green-400' : 'bg-green-500 hover:bg-green-400'"
834+ :title ="
835+ runExpanded
836+ ? 'Show less'
837+ : `Show all ${executableInfo.commands.length} commands`
838+ "
839+ :aria-expanded =" runExpanded"
840+ @click =" runExpanded = !runExpanded"
841+ />
842+ <span v-else class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
843+ <template #fallback >
844+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
845+ </template >
846+ </ClientOnly >
847+ </div >
848+
849+ <!-- Primary command (always shown) -->
850+ <div
851+ class =" flex items-center gap-2 px-3 pt-2 sm:px-4 sm:pt-3"
852+ :class =" runExpanded ? 'pb-2' : 'pb-3 sm:pb-4'"
853+ >
854+ <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
855+ <code class =" font-mono text-sm flex-1"
856+ ><ClientOnly
857+ ><span
858+ v-for =" (part, i) in runCommandParts"
859+ :key =" i"
860+ :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
861+ >{{ i > 0 ? ' ' : '' }}{{ part }}</span
862+ ><template #fallback
863+ ><span class =" text-fg" >npx</span >{{ ' '
864+ }}<span class =" text-fg-muted" >{{
865+ executableInfo?.primaryCommand
866+ }}</span ></template
867+ ></ClientOnly
868+ ></code
869+ >
870+ </div >
871+
872+ <!-- Additional commands (shown when expanded) -->
873+ <ClientOnly >
874+ <template v-if =" runExpanded && executableInfo .commands .length > 1 " >
875+ <div
876+ v-for =" cmd in executableInfo.commands.slice(1)"
877+ :key =" cmd"
878+ class =" flex items-center gap-2 px-3 py-2 sm:px-4 border-t border-border/50 group/cmd"
879+ >
880+ <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
881+ <code class =" font-mono text-sm flex-1"
882+ ><span
883+ v-for =" (part, i) in getRunParts(cmd)"
884+ :key =" i"
885+ :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
886+ >{{ i > 0 ? ' ' : '' }}{{ part }}</span
887+ ></code
888+ >
889+ <button
890+ type =" button"
891+ class =" px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/cmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
892+ @click =" copyRunCommand(cmd)"
893+ >
894+ {{ runCopied && runCopiedCommand === cmd ? 'copied!' : 'copy' }}
895+ </button >
896+ </div >
897+ </template >
898+ </ClientOnly >
899+ </div >
900+ <button
901+ type =" button"
902+ class =" absolute top-3 right-3 px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
903+ @click =" copyRunCommand(executableInfo?.primaryCommand)"
904+ >
905+ {{
906+ runCopied && runCopiedCommand === (executableInfo?.primaryCommand || null)
907+ ? 'copied!'
908+ : 'copy'
909+ }}
910+ </button >
911+ </div >
912+ </section >
913+
766914 <!-- Two column layout for sidebar content -->
767915 <div class =" grid lg:grid-cols-3 gap-8" >
768916 <!-- Main content (README) -->
0 commit comments