@@ -3,6 +3,7 @@ import { joinURL } from 'ufo'
33import type { PackumentVersion , NpmVersionDist , ReadmeResponse } from ' #shared/types'
44import type { JsrPackageInfo } from ' #shared/types/jsr'
55import { assertValidPackageName } from ' #shared/utils/npm'
6+ import { getExecutableInfo , getRunCommandParts , getRunCommand } from ' ~/utils/run-command'
67
78definePageMeta ({
89 name: ' package' ,
@@ -240,6 +241,58 @@ async function copyInstallCommand() {
240241 setTimeout (() => (copied .value = false ), 2000 )
241242}
242243
244+ // Executable detection for run command
245+ const executableInfo = computed (() => {
246+ if (! displayVersion .value || ! pkg .value ) return null
247+ return getExecutableInfo (pkg .value .name , displayVersion .value .bin )
248+ })
249+
250+ // Run command expanded state (for packages with multiple bin commands)
251+ const runExpanded = ref (false )
252+
253+ // Run command parts for a specific command
254+ function getRunParts(command ? : string ) {
255+ if (! pkg .value ) return []
256+ return getRunCommandParts ({
257+ packageName: pkg .value .name ,
258+ packageManager: selectedPM .value ,
259+ jsrInfo: jsrInfo .value ,
260+ command ,
261+ })
262+ }
263+
264+ // Primary run command parts
265+ const runCommandParts = computed (() => {
266+ if (! executableInfo .value ?.hasExecutable ) return []
267+ return getRunParts (executableInfo .value .primaryCommand )
268+ })
269+
270+ // Full run command string for copying
271+ function getFullRunCommand(command ? : string ) {
272+ if (! pkg .value ) return ' '
273+ return getRunCommand ({
274+ packageName: pkg .value .name ,
275+ packageManager: selectedPM .value ,
276+ jsrInfo: jsrInfo .value ,
277+ command ,
278+ })
279+ }
280+
281+ // Copy run command
282+ const runCopied = ref (false )
283+ const runCopiedCommand = ref <string | null >(null )
284+ async function copyRunCommand(command ? : string ) {
285+ const cmd = getFullRunCommand (command )
286+ if (! cmd ) return
287+ await navigator .clipboard .writeText (cmd )
288+ runCopied .value = true
289+ runCopiedCommand .value = command || null
290+ setTimeout (() => {
291+ runCopied .value = false
292+ runCopiedCommand .value = null
293+ }, 2000 )
294+ }
295+
243296// Expandable description
244297const descriptionExpanded = ref (false )
245298const descriptionRef = ref <HTMLDivElement >()
@@ -681,6 +734,101 @@ defineOgImageComponent('Package', {
681734 </div >
682735 </section >
683736
737+ <!-- Run command section - only shown for packages with executables -->
738+ <section v-if =" executableInfo?.hasExecutable" aria-labelledby =" run-heading" class =" mb-8" >
739+ <h2 id =" run-heading" class =" text-xs text-fg-subtle uppercase tracking-wider mb-3" >Run</h2 >
740+ <div class =" relative group" >
741+ <div class =" bg-[#0d0d0d] border border-border rounded-lg overflow-hidden" >
742+ <!-- Terminal chrome with interactive green button for multiple commands -->
743+ <div class =" flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3" >
744+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
745+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
746+ <ClientOnly >
747+ <button
748+ v-if =" executableInfo.commands.length > 1"
749+ type =" button"
750+ class =" w-2.5 h-2.5 rounded-full transition-colors cursor-pointer"
751+ :class =" runExpanded ? 'bg-green-400' : 'bg-green-500 hover:bg-green-400'"
752+ :title ="
753+ runExpanded
754+ ? 'Show less'
755+ : `Show all ${executableInfo.commands.length} commands`
756+ "
757+ :aria-expanded =" runExpanded"
758+ @click =" runExpanded = !runExpanded"
759+ />
760+ <span v-else class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
761+ <template #fallback >
762+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
763+ </template >
764+ </ClientOnly >
765+ </div >
766+
767+ <!-- Primary command (always shown) -->
768+ <div
769+ class =" flex items-center gap-2 px-3 pt-2 sm:px-4 sm:pt-3"
770+ :class =" runExpanded ? 'pb-2' : 'pb-3 sm:pb-4'"
771+ >
772+ <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
773+ <code class =" font-mono text-sm flex-1"
774+ ><ClientOnly
775+ ><span
776+ v-for =" (part, i) in runCommandParts"
777+ :key =" i"
778+ :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
779+ >{{ i > 0 ? ' ' : '' }}{{ part }}</span
780+ ><template #fallback
781+ ><span class =" text-fg" >npx</span >{{ ' '
782+ }}<span class =" text-fg-muted" >{{
783+ executableInfo?.primaryCommand
784+ }}</span ></template
785+ ></ClientOnly
786+ ></code
787+ >
788+ </div >
789+
790+ <!-- Additional commands (shown when expanded) -->
791+ <ClientOnly >
792+ <template v-if =" runExpanded && executableInfo .commands .length > 1 " >
793+ <div
794+ v-for =" cmd in executableInfo.commands.slice(1)"
795+ :key =" cmd"
796+ class =" flex items-center gap-2 px-3 py-2 sm:px-4 border-t border-border/50 group/cmd"
797+ >
798+ <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
799+ <code class =" font-mono text-sm flex-1"
800+ ><span
801+ v-for =" (part, i) in getRunParts(cmd)"
802+ :key =" i"
803+ :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
804+ >{{ i > 0 ? ' ' : '' }}{{ part }}</span
805+ ></code
806+ >
807+ <button
808+ type =" button"
809+ 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"
810+ @click =" copyRunCommand(cmd)"
811+ >
812+ {{ runCopied && runCopiedCommand === cmd ? 'copied!' : 'copy' }}
813+ </button >
814+ </div >
815+ </template >
816+ </ClientOnly >
817+ </div >
818+ <button
819+ type =" button"
820+ 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"
821+ @click =" copyRunCommand(executableInfo?.primaryCommand)"
822+ >
823+ {{
824+ runCopied && runCopiedCommand === (executableInfo?.primaryCommand || null)
825+ ? 'copied!'
826+ : 'copy'
827+ }}
828+ </button >
829+ </div >
830+ </section >
831+
684832 <!-- Two column layout for sidebar content -->
685833 <div class =" grid lg:grid-cols-3 gap-8" >
686834 <!-- Main content (README) -->
0 commit comments