diff --git a/COPY.md b/COPY.md
index 474a9e8f..d3474c3c 100644
--- a/COPY.md
+++ b/COPY.md
@@ -42,7 +42,8 @@ These exact strings. Do not paraphrase them per page.
| CLI line | Search, run, install, and keep Skills current. | guidance only: the `/cli` line under the H1. It is the CLI's own `--help` line, so change it in the CLI first |
| CLI intro | Give your Agent Skills that real maintainers write. A run reads the current source every time, and one command updates the Skills you install. Install the skilld Skill once, and your Agent searches and loads Skills on its own. | guidance only: the `/cli` intro under the line |
| CLI claims line | Open-source CLI · No telemetry · 19 Agent targets | guidance only: the `/cli` claims line. The page counts the `--agent` values of `skilld install --help` |
-| CLI install label | Install the CLI | guidance only: the label beside `npm install --global skilld` on `/cli` |
+| CLI install label | Install the CLI | guidance only: the label beside the CLI install chip on `/cli`. A switch above the chip picks `macOS / Linux`, `Windows`, or `npm`, and `macOS / Linux` is preselected |
+| CLI install consequence | One native binary. It upgrades itself from signed releases. · Needs Node.js. npm handles upgrades. | guidance only: the line under the CLI install chip on `/cli`. The first is for `macOS / Linux` and `Windows`, the second for `npm`. `crates/skilld-command/src/upgrade.rs` in the CLI decides both. If it changes, change these strings |
| Claim: open source | open-source CLI | guidance only: the CLI. Never call skilld.dev or its site open source |
| Claim: privacy | analytics without cookies or IPs | guidance only: analytics and privacy copy. Never the bare "privacy-friendly" |
| Claim: comparison | skills.sh alternative | guidance only: always a link to `/vs/skills-sh` |
diff --git a/app/components/CliInstallChip.vue b/app/components/CliInstallChip.vue
index 8e50a060..a018972d 100644
--- a/app/components/CliInstallChip.vue
+++ b/app/components/CliInstallChip.vue
@@ -1,12 +1,16 @@
-
+
+
- Installs the skilld CLI for your user account.
+
+
+
+
diff --git a/layers/marketing/app/pages/cli/index.vue b/layers/marketing/app/pages/cli/index.vue
index 6cbcaca9..1ca48b91 100644
--- a/layers/marketing/app/pages/cli/index.vue
+++ b/layers/marketing/app/pages/cli/index.vue
@@ -14,7 +14,6 @@
* the parent route of `/cli/authorize` and swallow the CLI sign-in page.
*/
import {
- cliNativeInstallCmd,
skillInstallCmd,
skillOutdatedCmd,
skillPageUrl,
@@ -62,7 +61,6 @@ const EXAMPLE = { owner: 'antfu', repo: 'skills', skill: 'vue' } as const
const agentPrompt = skillRunPrompt(skillPageUrl(EXAMPLE.owner, EXAMPLE.repo, EXAMPLE.skill))
const askPrompt = 'Find a skilld Skill for Vue and use it'
-const nativeInstall = cliNativeInstallCmd()
/** Each id is a stable anchor, so other pages can link one verb. */
const verbs = [
@@ -223,13 +221,7 @@ const codeClass = 'font-mono text-xs text-default'
Run the two commands at the top of this page. Then ask your Agent in your own words.
-
-
-
- No Node.js? On macOS and Linux, install one native binary:
-
- No runtime to install, and it starts in under a millisecond. The npm package selects the same native executable for your system.
+ No runtime to install, and it starts in under a millisecond. A curl or PowerShell install upgrades itself from signed releases. An npm install runs the same executable, and npm handles its upgrades.
diff --git a/layers/marketing/content/pages/docs/cli.md b/layers/marketing/content/pages/docs/cli.md
index 52ae213a..40b1a3c8 100644
--- a/layers/marketing/content/pages/docs/cli.md
+++ b/layers/marketing/content/pages/docs/cli.md
@@ -3,12 +3,12 @@ title: skilld CLI reference
description: Every skilld command with its help text. Generated from the CLI help for skilld 3.3.0.
label: Reference
author: Harlan Wilton
-command: npm install --global skilld
+command: curl -fsSL https://skilld.dev/install.sh | sh
publishedAt: 2026-09-01
updatedAt: 2026-10-02
---
-Generated from `skilld --help` for skilld `3.3.0`. Run `npx skilld ` or install the CLI once with `npm install --global skilld`.
+Generated from `skilld --help` for skilld `3.3.0`. Run `npx skilld `, or install the CLI once. On macOS and Linux, run `curl -fsSL https://skilld.dev/install.sh | sh`. On Windows, run `irm https://skilld.dev/install.ps1 | iex` in PowerShell. With Node.js, run `npm install --global skilld`.
The npm package selects a native executable for your system. It has no JavaScript engine or fallback.
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 1d37c69d..00bc5087 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -10,6 +10,7 @@ import { externalCheckin } from './shared/checkin-external'
import { iconifyCollections } from './shared/icon-collections'
import { SENTRY_DSN, sentryRelease, sentryReportingEnabled } from './shared/sentry'
import { SESSION_NAME } from './shared/server/session-access'
+import { CLI_INSTALL_SCRIPTS } from './shared/skill-commands'
/** Every `/agents/*` page file, so the sitemap reads the admission decision for a page nobody listed. */
function discoveredAgentRoutes(): string[] {
@@ -617,6 +618,9 @@ export default defineNuxtConfig({
// .md (aiReady.llmsTxt.markdownLinks), so the dump redirects there. 302 so
// this reverses the moment per-section splitting is worth building.
'/llms-full.txt': { redirect: { to: '/llms.txt', statusCode: 302 } } as any,
+ // The printed native install commands. `CLI_INSTALL_SCRIPTS` says why each is a 302.
+ '/install.sh': { redirect: { to: CLI_INSTALL_SCRIPTS['/install.sh'], statusCode: 302 } } as any,
+ '/install.ps1': { redirect: { to: CLI_INSTALL_SCRIPTS['/install.ps1'], statusCode: 302 } } as any,
// `/gh` has no index page: owner hubs live at `/gh/`. It 404'd while
// `/orgs` 301'd straight into it, so every legacy orgs-index link dead-ended
// on a redirect chain. `/community` is the browsable owner surface.
diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts
index 89c3c9d8..408807d2 100644
--- a/scripts/generate-cli-docs.ts
+++ b/scripts/generate-cli-docs.ts
@@ -14,6 +14,7 @@ import { execFile } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
import process from 'node:process'
import { promisify } from 'node:util'
+import { cliGlobalInstallCmd, cliNativeInstallCmd, cliWindowsInstallCmd } from '../shared/skill-commands'
const run = promisify(execFile)
const PACKAGE = 'skilld'
@@ -74,12 +75,12 @@ export function renderCliDocs(version: string, root: string, commands: readonly
`description: Every skilld command with its help text. Generated from the CLI help for skilld ${version}.`,
'label: Reference',
'author: Harlan Wilton',
- 'command: npm install --global skilld',
+ `command: ${cliNativeInstallCmd()}`,
'publishedAt: 2026-09-01',
`updatedAt: ${today}`,
'---',
'',
- `Generated from \`skilld --help\` for skilld \`${version}\`. Run \`npx skilld \` or install the CLI once with \`npm install --global skilld\`.`,
+ `Generated from \`skilld --help\` for skilld \`${version}\`. Run \`npx skilld \`, or install the CLI once. On macOS and Linux, run \`${cliNativeInstallCmd()}\`. On Windows, run \`${cliWindowsInstallCmd()}\` in PowerShell. With Node.js, run \`${cliGlobalInstallCmd()}\`.`,
'',
'The npm package selects a native executable for your system. It has no JavaScript engine or fallback.',
'',
diff --git a/scripts/lib/production-smoke.ts b/scripts/lib/production-smoke.ts
index 8fc1a850..95a6208b 100644
--- a/scripts/lib/production-smoke.ts
+++ b/scripts/lib/production-smoke.ts
@@ -1,4 +1,5 @@
import { CLUSTER_BY_SLUG } from '../../layers/registry/server/data/clusters'
+import { CLI_INSTALL_SCRIPTS } from '../../shared/skill-commands'
export interface SmokeExpectation {
path: string
@@ -112,6 +113,9 @@ export const PRODUCTION_SMOKE_EXPECTATIONS: SmokeExpectation[] = [
// The leaderboard is now the `all` range of the trending board. This checks
// the 301 that carries its ~4,100/mo repository cluster across.
{ path: '/skills/leaderboard', status: 301, location: '/skills/trending?range=all' },
+ // Every native install command fetches one of these, so a lost redirect
+ // breaks the install for each person who copies it.
+ ...Object.entries(CLI_INSTALL_SCRIPTS).map(([path, location]) => ({ path, status: 302, location })),
// The category surface is the reason the rework exists, so it is checked for
// rendered content, not just a 200.
//
diff --git a/shared/skill-commands.ts b/shared/skill-commands.ts
index ba2960fc..a980d5fc 100644
--- a/shared/skill-commands.ts
+++ b/shared/skill-commands.ts
@@ -88,17 +88,38 @@ export function skillRemoveCmd(name: string): string {
return `${CLI_PREFIX} remove ${name}`
}
+const CLI_RELEASE_DOWNLOAD = 'https://github.com/skilld-dev/skilld/releases/latest/download'
+
+/**
+ * The native install scripts, by the site path that serves them. Each path is a
+ * 302 to the latest release asset, so the printed command stays short and
+ * GitHub Releases stays the source. `latest` moves, so it is never a 301.
+ */
+export const CLI_INSTALL_SCRIPTS = {
+ '/install.sh': `${CLI_RELEASE_DOWNLOAD}/install.sh`,
+ '/install.ps1': `${CLI_RELEASE_DOWNLOAD}/install.ps1`,
+} as const
+
/**
* Installs the CLI itself. npm runs it, so it is no skilld subcommand and the
- * grammar gate does not check it. The package selects a native executable.
+ * grammar gate does not check it. The package selects a native executable, and
+ * npm owns its upgrades.
*/
export function cliGlobalInstallCmd(): string {
return 'npm install --global skilld'
}
-/** Installs the CLI as one native binary in `~/.skilld/bin`, with no Node.js. macOS and Linux. */
+/**
+ * Installs the CLI as one native binary in `~/.skilld/bin`, with no Node.js.
+ * macOS and Linux. This install upgrades itself from signed releases.
+ */
export function cliNativeInstallCmd(): string {
- return 'curl -fsSL https://github.com/skilld-dev/skilld/releases/latest/download/install.sh | sh'
+ return `curl -fsSL ${SITE_ORIGIN}/install.sh | sh`
+}
+
+/** The same native binary on Windows, from PowerShell, in `%LOCALAPPDATA%\skilld\bin`. */
+export function cliWindowsInstallCmd(): string {
+ return `irm ${SITE_ORIGIN}/install.ps1 | iex`
}
/**
diff --git a/test/a11y.spec.ts b/test/a11y.spec.ts
index 8ba32428..a89c2477 100644
--- a/test/a11y.spec.ts
+++ b/test/a11y.spec.ts
@@ -571,7 +571,7 @@ describe('accessibility: components', () => {
wrapper.unmount()
})
- it('cliInstallChip has no violations and names what its copy button copies', async () => {
+ it('cliInstallChip has no violations, leads with the native install, and switches platform', async () => {
const container = createIsolatedContainer()
const wrapper = await mountSuspended(
await loadComponent('CliInstallChip'),
@@ -579,8 +579,18 @@ describe('accessibility: components', () => {
)
const results = await runAxe(container)
expect(results.violations, formatViolations(results)).toHaveLength(0)
+ expect(container.querySelector('code')?.textContent).toBe('curl -fsSL https://skilld.dev/install.sh | sh')
+ expect(container.querySelector('button[aria-label]')?.getAttribute('aria-label')).toBe('Copy CLI install command')
+
+ const option = (label: string) => wrapper.findAll('button[aria-pressed]').find(button => button.text() === label)!
+ await option('Windows').trigger('click')
+ await nextTick()
+ expect(container.querySelector('code')?.textContent).toBe('irm https://skilld.dev/install.ps1 | iex')
+
+ await option('npm').trigger('click')
+ await nextTick()
expect(container.querySelector('code')?.textContent).toBe('npm install --global skilld')
- expect(container.querySelector('button')?.getAttribute('aria-label')).toBe('Copy CLI install command')
+ expect(container.textContent).toContain('Needs Node.js.')
wrapper.unmount()
})
@@ -642,6 +652,7 @@ describe('accessibility: component coverage', () => {
'StatsHBar', // Decorative chart, tested at page level
'StatsLeaderboard', // Tested at page level
'UiTooltip', // Wrapper around UTooltip, exercised by parent components
+ '_ChipSwitch', // The switch above RunChip and CliInstallChip, axe-scanned and clicked through both
]
/**