Skip to content

Commit 7999f2a

Browse files
committed
Update exec, add create support, and remove more than one binary
1 parent 187403e commit 7999f2a

5 files changed

Lines changed: 492 additions & 77 deletions

File tree

app/pages/[...package].vue

Lines changed: 141 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
import type { NpmVersionDist, PackumentVersion, ReadmeResponse } from '#shared/types'
33
import type { JsrPackageInfo } from '#shared/types/jsr'
44
import { 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'
613
import { onKeyStroke } from '@vueuse/core'
714
import { joinURL } from 'ufo'
815
import { 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)
294316
function 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>

app/utils/install-command.ts

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,54 @@
11
import type { JsrPackageInfo } from '#shared/types/jsr'
22

33
export const packageManagers = [
4-
{ id: 'npm', label: 'npm', action: 'install', execute: 'npx' },
5-
{ id: 'pnpm', label: 'pnpm', action: 'add', execute: 'pnpm dlx' },
6-
{ id: 'yarn', label: 'yarn', action: 'add', execute: 'yarn dlx' },
7-
{ id: 'bun', label: 'bun', action: 'add', execute: 'bunx' },
8-
{ id: 'deno', label: 'deno', action: 'add', execute: 'deno run' },
9-
{ id: 'vlt', label: 'vlt', action: 'install', execute: 'vlt x' },
4+
{
5+
id: 'npm',
6+
label: 'npm',
7+
action: 'install',
8+
executeLocal: 'npx',
9+
executeRemote: 'npx',
10+
create: 'npm create',
11+
},
12+
{
13+
id: 'pnpm',
14+
label: 'pnpm',
15+
action: 'add',
16+
executeLocal: 'pnpm exec',
17+
executeRemote: 'pnpm dlx',
18+
create: 'pnpm create',
19+
},
20+
{
21+
id: 'yarn',
22+
label: 'yarn',
23+
action: 'add',
24+
executeLocal: 'yarn',
25+
executeRemote: 'yarn dlx',
26+
create: 'yarn create',
27+
},
28+
{
29+
id: 'bun',
30+
label: 'bun',
31+
action: 'add',
32+
executeLocal: 'bunx',
33+
executeRemote: 'bunx',
34+
create: 'bun create',
35+
},
36+
{
37+
id: 'deno',
38+
label: 'deno',
39+
action: 'add',
40+
executeLocal: 'deno run',
41+
executeRemote: 'deno run',
42+
create: 'deno run',
43+
},
44+
{
45+
id: 'vlt',
46+
label: 'vlt',
47+
action: 'install',
48+
executeLocal: 'vlt x',
49+
executeRemote: 'vlt x',
50+
create: 'vlt x',
51+
},
1052
] as const
1153

1254
export type PackageManagerId = (typeof packageManagers)[number]['id']
@@ -60,12 +102,56 @@ export function getInstallCommandParts(options: InstallCommandOptions): string[]
60102
return [pm.label, pm.action, `${spec}${version}`]
61103
}
62104

63-
export function getExecuteCommand(options: InstallCommandOptions): string {
105+
export interface ExecuteCommandOptions extends InstallCommandOptions {
106+
/** Whether this is a binary-only package (download & run vs local run) */
107+
isBinaryOnly?: boolean
108+
/** Whether this is a create-* package (uses shorthand create command) */
109+
isCreatePackage?: boolean
110+
}
111+
112+
export function getExecuteCommand(options: ExecuteCommandOptions): string {
64113
return getExecuteCommandParts(options).join(' ')
65114
}
66115

67-
export function getExecuteCommandParts(options: InstallCommandOptions): string[] {
116+
export function getExecuteCommandParts(options: ExecuteCommandOptions): string[] {
68117
const pm = packageManagers.find(p => p.id === options.packageManager)
69118
if (!pm) return []
70-
return [pm.execute, getPackageSpecifier(options)]
119+
120+
// For create-* packages, use the shorthand create command
121+
if (options.isCreatePackage) {
122+
const createName = extractCreateName(options.packageName)
123+
if (createName) {
124+
return [...pm.create.split(' '), createName]
125+
}
126+
}
127+
128+
// Choose remote or local execute based on package type
129+
const executeCmd = options.isBinaryOnly ? pm.executeRemote : pm.executeLocal
130+
return [...executeCmd.split(' '), getPackageSpecifier(options)]
131+
}
132+
133+
/**
134+
* Extract the short name from a create-* package.
135+
* e.g., "create-vite" -> "vite", "@vue/create-app" -> "vue"
136+
*/
137+
function extractCreateName(packageName: string): string | null {
138+
// Handle scoped packages: @scope/create-foo -> foo
139+
if (packageName.startsWith('@')) {
140+
const parts = packageName.split('/')
141+
const baseName = parts[1]
142+
if (baseName?.startsWith('create-')) {
143+
return baseName.slice('create-'.length)
144+
}
145+
// Handle @vue/create-app style (scope as the name)
146+
if (baseName?.startsWith('create')) {
147+
return parts[0]!.slice(1) // Remove @ from scope
148+
}
149+
}
150+
151+
// Handle unscoped: create-foo -> foo
152+
if (packageName.startsWith('create-')) {
153+
return packageName.slice('create-'.length)
154+
}
155+
156+
return null
71157
}

0 commit comments

Comments
 (0)