diff --git a/apps/web/app/(main)/agents/[slug]/page.tsx b/apps/web/app/(main)/agents/[slug]/page.tsx
index c150a5a6..5fdc0dc0 100644
--- a/apps/web/app/(main)/agents/[slug]/page.tsx
+++ b/apps/web/app/(main)/agents/[slug]/page.tsx
@@ -15,6 +15,7 @@ import { FavoriteButton } from '@/components/favorite-button'
import { InstallCommand } from '@/components/install-command'
import { JsonLd } from '@/components/json-ld'
import { MobileInstallBar } from '@/components/mobile-install-bar'
+import { StickyInstallCta } from '@/components/sticky-install-cta'
import {
compareRelatedAgents,
countFilesByKind,
@@ -209,10 +210,6 @@ async function AgentDetailContent({ agent }: { agent: AgentWithAuthor }) {
/>
-
-
- {agent.description}
-
{agent.authorUsername ? (
+
+
+
+
+ {agent.description}
+
+
- Install
+
+ Other package managers
+
- Run this command in your eve app to add the agent.
+ Prefer pnpm, yarn, or bun? Copy the matching install command.
+
diff --git a/apps/web/components/agent-file-viewer.tsx b/apps/web/components/agent-file-viewer.tsx
index 96840b62..c479b793 100644
--- a/apps/web/components/agent-file-viewer.tsx
+++ b/apps/web/components/agent-file-viewer.tsx
@@ -278,7 +278,9 @@ export function AgentFileViewer({ files }: { files: AgentRegistryFile[] }) {
-
Files
+
+ Files this command writes
+
{files.length}
diff --git a/apps/web/components/sticky-install-cta.tsx b/apps/web/components/sticky-install-cta.tsx
new file mode 100644
index 00000000..db1bde48
--- /dev/null
+++ b/apps/web/components/sticky-install-cta.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { Button } from '@evex/ui/button'
+import { cn } from '@evex/ui/lib/utils'
+import { Check, Copy } from 'lucide-react'
+import posthog from 'posthog-js'
+import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
+import { buildInstallCommand } from '@/lib/package-managers'
+
+/**
+ * Above-the-fold install affordance under the agent title. Sticky on sm+ only —
+ * on small viewports it stays in document flow so MobileInstallBar remains the
+ * sole sticky install control. Always shows the canonical
+ * `npx shadcn@latest add @evex/
` form; package-manager tabs live on the
+ * secondary install card below, not here.
+ */
+export function StickyInstallCta({
+ slug,
+ agentAuthor,
+ viewerIsAuthor,
+ className,
+}: {
+ slug: string
+ agentAuthor: string | null
+ viewerIsAuthor: boolean | null
+ className?: string
+}) {
+ const command = buildInstallCommand(slug)
+ const { copied, copy } = useCopyToClipboard()
+
+ const handleCopy = async () => {
+ const didCopy = await copy(command, {
+ successMessage: 'Copied install command',
+ })
+ if (didCopy) {
+ posthog.capture('agent_install_command_copied', {
+ agent_author: agentAuthor,
+ agent_slug: slug,
+ package_manager: 'npm',
+ surface: 'sticky_install_cta',
+ viewer_is_author: viewerIsAuthor,
+ })
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/web/lib/package-managers.ts b/apps/web/lib/package-managers.ts
index 42278186..b1f4b685 100644
--- a/apps/web/lib/package-managers.ts
+++ b/apps/web/lib/package-managers.ts
@@ -21,10 +21,19 @@ export function parsePackageManager(
return match ? match.id : DEFAULT_PACKAGE_MANAGER
}
+// Canonical public install command. Always the npx form — product surfaces
+// that must not rotate package managers (hero, sticky CTA, SEO) use this.
+export function buildInstallCommand(slug: string): string {
+ return `npx shadcn@latest add @evex/${slug}`
+}
+
export function buildInstallCommandForManager(
manager: PackageManagerId,
slug: string,
): string {
+ if (manager === 'npm') {
+ return buildInstallCommand(slug)
+ }
const match = PACKAGE_MANAGERS.find((entry) => entry.id === manager)
const runner = match?.runner ?? 'npx'
return `${runner} shadcn@latest add @evex/${slug}`
diff --git a/apps/web/tests/package-managers.test.ts b/apps/web/tests/package-managers.test.ts
new file mode 100644
index 00000000..6a099593
--- /dev/null
+++ b/apps/web/tests/package-managers.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest'
+import {
+ buildInstallCommand,
+ buildInstallCommandForManager,
+} from '@/lib/package-managers'
+
+describe('buildInstallCommand', () => {
+ it('always returns the canonical npx form', () => {
+ expect(buildInstallCommand('code-reviewer')).toBe(
+ 'npx shadcn@latest add @evex/code-reviewer',
+ )
+ expect(buildInstallCommand('brand-visual-asset-generator')).toBe(
+ 'npx shadcn@latest add @evex/brand-visual-asset-generator',
+ )
+ })
+
+ it('matches the server-side helper in site-url', async () => {
+ // Keep the client-safe and server-only copies in lockstep without a
+ // barrel re-export (ultracite bans those).
+ const { buildInstallCommand: serverBuildInstallCommand } = await import(
+ '@/lib/site-url'
+ )
+ expect(buildInstallCommand('code-reviewer')).toBe(
+ serverBuildInstallCommand('code-reviewer'),
+ )
+ })
+})
+
+describe('buildInstallCommandForManager', () => {
+ it('matches the canonical command for npm', () => {
+ expect(buildInstallCommandForManager('npm', 'openui')).toBe(
+ buildInstallCommand('openui'),
+ )
+ })
+
+ it('uses the selected runner for other managers', () => {
+ expect(buildInstallCommandForManager('pnpm', 'openui')).toBe(
+ 'pnpm dlx shadcn@latest add @evex/openui',
+ )
+ expect(buildInstallCommandForManager('yarn', 'openui')).toBe(
+ 'yarn dlx shadcn@latest add @evex/openui',
+ )
+ expect(buildInstallCommandForManager('bun', 'openui')).toBe(
+ 'bunx --bun shadcn@latest add @evex/openui',
+ )
+ })
+})