22import type { NpmVersionDist , PackumentVersion , ReadmeResponse } from ' #shared/types'
33import type { JsrPackageInfo } from ' #shared/types/jsr'
44import { assertValidPackageName } from ' #shared/utils/npm'
5- import { getExecutableInfo , getRunCommandParts , getRunCommand } from ' ~/utils/run-command'
5+ import {
6+ getExecutableInfo ,
7+ getRunCommandParts ,
8+ getRunCommand ,
9+ isBinaryOnlyPackage ,
10+ isCreatePackage ,
11+ } from ' ~/utils/run-command'
12+ import { getExecuteCommandParts , getExecuteCommand } from ' ~/utils/install-command'
613import { onKeyStroke } from ' @vueuse/core'
714import { joinURL } from ' ufo'
815import { areUrlsEquivalent } from ' #shared/utils/url'
@@ -287,18 +294,67 @@ const executableInfo = computed(() => {
287294 return getExecutableInfo (pkg .value .name , displayVersion .value .bin )
288295})
289296
290- // Run command expanded state (for packages with multiple bin commands)
291- const runExpanded = ref (false )
297+ // Detect if package is binary-only (show only execute commands, no install)
298+ const isBinaryOnly = computed (() => {
299+ if (! displayVersion .value || ! pkg .value ) return false
300+ return isBinaryOnlyPackage ({
301+ name: pkg .value .name ,
302+ bin: displayVersion .value .bin ,
303+ main: displayVersion .value .main ,
304+ module: displayVersion .value .module ,
305+ exports: displayVersion .value .exports ,
306+ })
307+ })
308+
309+ // Detect if package uses create-* naming convention
310+ const isCreatePkg = computed (() => {
311+ if (! pkg .value ) return false
312+ return isCreatePackage (pkg .value .name )
313+ })
292314
293- // Run command parts for a specific command
315+ // Run command parts for a specific command (local execute after install)
294316function getRunParts(command ? : string ) {
295317 if (! pkg .value ) return []
296318 return getRunCommandParts ({
297319 packageName: pkg .value .name ,
298320 packageManager: selectedPM .value ,
299321 jsrInfo: jsrInfo .value ,
300322 command ,
323+ isBinaryOnly: false , // Local execute
324+ })
325+ }
326+
327+ // Execute command parts for binary-only packages (remote execute)
328+ const executeCommandParts = computed (() => {
329+ if (! pkg .value ) return []
330+ return getExecuteCommandParts ({
331+ packageName: pkg .value .name ,
332+ packageManager: selectedPM .value ,
333+ jsrInfo: jsrInfo .value ,
334+ isBinaryOnly: true ,
335+ isCreatePackage: isCreatePkg .value ,
301336 })
337+ })
338+
339+ // Full execute command string for copying
340+ const executeCommand = computed (() => {
341+ if (! pkg .value ) return ' '
342+ return getExecuteCommand ({
343+ packageName: pkg .value .name ,
344+ packageManager: selectedPM .value ,
345+ jsrInfo: jsrInfo .value ,
346+ isBinaryOnly: true ,
347+ isCreatePackage: isCreatePkg .value ,
348+ })
349+ })
350+
351+ // Copy execute command (for binary-only packages)
352+ const executeCopied = ref (false )
353+ async function copyExecuteCommand() {
354+ if (! executeCommand .value ) return
355+ await navigator .clipboard .writeText (executeCommand .value )
356+ executeCopied .value = true
357+ setTimeout (() => (executeCopied .value = false ), 2000 )
302358}
303359
304360// Primary run command parts
@@ -754,8 +810,85 @@ defineOgImageComponent('Package', {
754810 :version =" displayVersion.version"
755811 />
756812
757- <!-- Install command with package manager selector -->
758- <section aria-labelledby =" install-heading" class =" mb-8" >
813+ <!-- Binary-only packages: Show only execute command (no install) -->
814+ <section v-if =" isBinaryOnly" aria-labelledby =" run-heading" class =" mb-8" >
815+ <div class =" flex flex-wrap items-center justify-between mb-3" >
816+ <h2 id =" run-heading" class =" text-xs text-fg-subtle uppercase tracking-wider" >Run</h2 >
817+ <!-- Package manager tabs -->
818+ <div
819+ class =" flex items-center gap-1 p-0.5 bg-bg-subtle border border-border rounded-md"
820+ role =" tablist"
821+ aria-label =" Package manager"
822+ >
823+ <ClientOnly >
824+ <button
825+ v-for =" pm in packageManagers"
826+ :key =" pm.id"
827+ role =" tab"
828+ :aria-selected =" selectedPM === pm.id"
829+ class =" px-2 py-1 font-mono text-xs rounded transition-all duration-150"
830+ :class ="
831+ selectedPM === pm.id
832+ ? 'bg-bg-elevated text-fg'
833+ : 'text-fg-subtle hover:text-fg-muted'
834+ "
835+ @click =" selectedPM = pm.id"
836+ >
837+ {{ pm.label }}
838+ </button >
839+ <template #fallback >
840+ <span
841+ v-for =" pm in packageManagers"
842+ :key =" pm.id"
843+ class =" px-2 py-1 font-mono text-xs rounded"
844+ :class =" pm.id === 'npm' ? 'bg-bg-elevated text-fg' : 'text-fg-subtle'"
845+ >
846+ {{ pm.label }}
847+ </span >
848+ </template >
849+ </ClientOnly >
850+ </div >
851+ </div >
852+ <div class =" relative group" >
853+ <!-- Terminal-style execute command -->
854+ <div class =" bg-[#0d0d0d] border border-border rounded-lg overflow-hidden" >
855+ <div class =" flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3" >
856+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
857+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
858+ <span class =" w-2.5 h-2.5 rounded-full bg-[#333]" />
859+ </div >
860+ <div class =" px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4 space-y-1" >
861+ <!-- Execute command -->
862+ <div class =" flex items-center gap-2 group/executecmd" >
863+ <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
864+ <code class =" font-mono text-sm"
865+ ><ClientOnly
866+ ><span
867+ v-for =" (part, i) in executeCommandParts"
868+ :key =" i"
869+ :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
870+ >{{ i > 0 ? ' ' : '' }}{{ part }}</span
871+ ><template #fallback
872+ ><span class =" text-fg" >npx</span
873+ ><span class =" text-fg-muted" > {{ pkg.name }}</span ></template
874+ ></ClientOnly
875+ ></code
876+ >
877+ <button
878+ type =" button"
879+ 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/executecmd: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"
880+ @click.stop =" copyExecuteCommand"
881+ >
882+ {{ executeCopied ? 'copied!' : 'copy' }}
883+ </button >
884+ </div >
885+ </div >
886+ </div >
887+ </div >
888+ </section >
889+
890+ <!-- Regular packages: Install command with optional run command -->
891+ <section v-else aria-labelledby =" install-heading" class =" mb-8" >
759892 <div class =" flex flex-wrap items-center justify-between mb-3" >
760893 <h2 id =" install-heading" class =" text-xs text-fg-subtle uppercase tracking-wider" >
761894 Install
@@ -829,13 +962,11 @@ defineOgImageComponent('Package', {
829962 </button >
830963 </div >
831964
832- <!-- Run commands (only if package has executables) -->
965+ <!-- Run command (only if package has executables) -->
833966 <template v-if =" executableInfo ?.hasExecutable " >
834967 <!-- Comment line -->
835968 <div class =" flex items-center gap-2 pt-1" >
836- <span class =" text-fg-subtle/50 font-mono text-sm select-none"
837- ># Run {{ executableInfo.commands.length > 1 ? 'commands' : 'command' }}</span
838- >
969+ <span class =" text-fg-subtle/50 font-mono text-sm select-none" ># Run locally</span >
839970 </div >
840971
841972 <!-- Primary run command -->
@@ -848,14 +979,6 @@ defineOgImageComponent('Package', {
848979 :key =" i"
849980 :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
850981 >{{ i > 0 ? ' ' : '' }}{{ part }}</span
851- ><button
852- v-if =" !runExpanded && executableInfo.commands.length > 1"
853- type =" button"
854- class =" text-fg-muted hover:underline cursor-pointer ml-1"
855- :aria-expanded =" runExpanded"
856- @click =" runExpanded = true"
857- >
858- (+{{ executableInfo.commands.length - 1 }} more)</button
859982 ><template #fallback
860983 ><span class =" text-fg" >npx</span >{{ ' '
861984 }}<span class =" text-fg-muted" >{{
@@ -876,44 +999,6 @@ defineOgImageComponent('Package', {
876999 }}
8771000 </button >
8781001 </div >
879-
880- <!-- Additional commands (shown when expanded) -->
881- <ClientOnly >
882- <template v-if =" runExpanded && executableInfo .commands .length > 1 " >
883- <div
884- v-for =" cmd in executableInfo.commands.filter(
885- c => c !== executableInfo.primaryCommand,
886- )"
887- :key =" cmd"
888- class =" flex items-center gap-2 group/runcmd"
889- >
890- <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
891- <code class =" font-mono text-sm"
892- ><span
893- v-for =" (part, i) in getRunParts(cmd)"
894- :key =" i"
895- :class =" i === 0 ? 'text-fg' : 'text-fg-muted'"
896- >{{ i > 0 ? ' ' : '' }}{{ part }}</span
897- ></code
898- >
899- <button
900- type =" button"
901- 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/runcmd: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"
902- @click.stop =" copyRunCommand(cmd)"
903- >
904- {{ runCopied && runCopiedCommand === cmd ? 'copied!' : 'copy' }}
905- </button >
906- </div >
907- <!-- Collapse button -->
908- <button
909- type =" button"
910- class =" text-fg-muted hover:underline cursor-pointer font-mono text-sm pl-5"
911- @click =" runExpanded = false"
912- >
913- (show less)
914- </button >
915- </template >
916- </ClientOnly >
9171002 </template >
9181003 </div >
9191004 </div >
0 commit comments