Files
chainbase-labs__agentkey/scripts/install.ps1
T
zzAllenn 8e93c67a33 fix(installer): avoid PromptScript failure for DSH (#97)
## Summary

- install the AgentKey Skill through the Skills CLI universal target for
DSH-only installs
- avoid passing either dsh or the unsupported global PromptScript target
while preserving explicit mixed-agent targets
- update Bash, PowerShell, English/Chinese setup guidance, retry
instructions, and release-asset source docs
- add Bash mixed-target coverage and a Windows PowerShell invocation
regression

## Root cause

skills@1.5.23 -g -y adds PromptScript to its universal target set even
though PromptScript has no global install directory. The canonical
AgentKey Skill still installs, but the command prints Failed to install
1. Explicitly targeting universal writes the same
~/.agents/skills/agentkey copy without selecting PromptScript.

## Validation

- bats tests/ — 42/42
- bash -n scripts/install.sh
- bash -n scripts/uninstall.sh
- bash -n scripts/build-release-assets.sh
- scripts/build-release-assets.sh with an isolated output directory
- workflow YAML parse
- git diff --check
- Windows behavior regression added to the windows-latest Scripts tests
job

## Deployment follow-up

Merging/releasing this PR does not automatically update
agentkey.app/install.sh. After release, publish the current
Bash/PowerShell install and uninstall scripts to the website origin and
purge the CDN; the live endpoints were serving stale pre-DSH content
during verification.

Co-authored-by: Allen <0xfatdog@gmail.com>
2026-08-25 00:55:26 +08:00

442 lines
21 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#Requires -Version 5.1
<#
.SYNOPSIS
AgentKey installer for Windows
.DESCRIPTION
Usage:
irm https://agentkey.app/install.ps1 | iex
& ([scriptblock]::Create((irm https://agentkey.app/install.ps1))) -Yes
& ([scriptblock]::Create((irm https://agentkey.app/install.ps1))) -Only "claude-code,cursor"
& ([scriptblock]::Create((irm https://agentkey.app/install.ps1))) -NoTelemetry
Behavior mirrors install.sh: checks Node >= 18 (installs via winget/scoop/choco),
auto-detects which AI agents are installed and runs `npx skills add` for them,
then `npx @agentkey/cli --auth-login` for device auth. The auth step always
tries to open a local browser AND prints the URL so headless / SSH users can
copy it elsewhere. MCP config is written automatically for Claude Code /
Claude Desktop / Cursor.
#>
[CmdletBinding()]
param(
[switch]$Yes,
[switch]$Interactive,
[string]$Only,
[switch]$AllAgents,
[switch]$ListAgents,
[switch]$SkipSkill,
[switch]$SkipMcp,
[switch]$NoTelemetry,
[switch]$Help
)
$ErrorActionPreference = 'Stop'
$SkillRepo = 'chainbase-labs/agentkey'
$CliPackage = '@agentkey/cli'
$NodeMinMajor = 18
# ── Agent markers (mirror of install.sh) ──────────────────────────────────
# Subset of vercel-labs/skills' 45 supported agent IDs that have reliable
# Windows-side markers. Sync source:
# https://github.com/vercel-labs/skills (Supported Agents table).
#
# IMPORTANT: ids here MUST match the `--only` ids accepted by both
# `npx skills add -a` and `npx -y @agentkey/cli --auth-login --only`.
# `claude-desktop` and `dsh` are exceptions. Neither is passed to
# `skills add -a`; DSH reads the global skill installed for `universal`.
$DshHome = if ([string]::IsNullOrWhiteSpace($env:DSH_HOME)) { Join-Path ([Environment]::GetFolderPath('UserProfile')) '.dsh' } else { $env:DSH_HOME }
if ($DshHome -eq '~') {
$DshHome = [Environment]::GetFolderPath('UserProfile')
} elseif ($DshHome -match '^~[\\/]') {
$DshHome = Join-Path ([Environment]::GetFolderPath('UserProfile')) $DshHome.Substring(2)
}
$DshHome = [System.IO.Path]::GetFullPath($DshHome)
$AgentMarkers = @(
@{ Id = 'claude-code'; Markers = @("path:$env:USERPROFILE\.claude.json", 'cmd:claude') }
@{ Id = 'claude-desktop'; Markers = @("path:$env:LOCALAPPDATA\AnthropicClaude", "path:$env:APPDATA\Claude\claude_desktop_config.json", "path:$env:APPDATA\Claude") }
@{ Id = 'cursor'; Markers = @("path:$env:USERPROFILE\.cursor", 'cmd:cursor', "path:$env:LOCALAPPDATA\Programs\cursor") }
@{ Id = 'codex'; Markers = @("path:$env:USERPROFILE\.codex", 'cmd:codex') }
@{ Id = 'dsh'; Markers = @("path:$DshHome", 'cmd:dsh') }
@{ Id = 'gemini-cli'; Markers = @("path:$env:USERPROFILE\.gemini", 'cmd:gemini') }
@{ Id = 'opencode'; Markers = @("path:$env:APPDATA\opencode", "path:$env:USERPROFILE\.opencode", 'cmd:opencode') }
@{ Id = 'openclaw'; Markers = @("path:$env:USERPROFILE\.openclaw", 'cmd:openclaw') }
@{ Id = 'qwen-code'; Markers = @("path:$env:USERPROFILE\.qwen", 'cmd:qwen') }
@{ Id = 'iflow-cli'; Markers = @("path:$env:USERPROFILE\.iflow", 'cmd:iflow') }
@{ Id = 'windsurf'; Markers = @("path:$env:USERPROFILE\.codeium\windsurf", "path:$env:USERPROFILE\.windsurf", 'cmd:windsurf') }
@{ Id = 'warp'; Markers = @("path:$env:USERPROFILE\.warp") }
@{ Id = 'amp'; Markers = @("path:$env:APPDATA\amp", 'cmd:amp') }
@{ Id = 'crush'; Markers = @("path:$env:APPDATA\crush", 'cmd:crush') }
@{ Id = 'goose'; Markers = @("path:$env:APPDATA\goose", 'cmd:goose') }
@{ Id = 'droid'; Markers = @('cmd:droid') }
@{ Id = 'kode'; Markers = @('cmd:kode') }
@{ Id = 'kilo'; Markers = @('cmd:kilo') }
@{ Id = 'kimi-cli'; Markers = @("path:$env:USERPROFILE\.kimi", 'cmd:kimi') }
@{ Id = 'kiro-cli'; Markers = @("path:$env:USERPROFILE\.kiro", 'cmd:kiro') }
)
# Agent ids excluded from per-agent `skills add -a`: Claude Desktop has no
# skill path, while DSH consumes the global `universal` copy.
$SkillsAgentExclusions = @('claude-desktop', 'dsh')
# Agent ids whose MCP registration the installer can drive automatically.
# Mirror of MCP_AUTO_AGENTS in install.sh and AGENT_REGISTRY in
# AgentKey-Server/cli/src/lib/mcp-clients.ts. Keep these three in sync.
$McpAutoAgents = @(
'claude-code', 'claude-desktop', 'cursor', 'codex', 'gemini-cli',
'opencode', 'qwen-code', 'iflow-cli', 'kimi-cli', 'kiro-cli',
'windsurf', 'warp', 'amp', 'crush', 'droid', 'openclaw', 'dsh'
)
# ── UI helpers ────────────────────────────────────────────────────────────
function Write-Banner {
Write-Host ''
Write-Host ' █████ ██████ ███████ ███ ██ ████████ ██ ██ ███████ ██ ██' -ForegroundColor Cyan
Write-Host ' ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ' -ForegroundColor Cyan
Write-Host ' ███████ ██ ███ █████ ██ ██ ██ ██ █████ █████ ████ ' -ForegroundColor Cyan
Write-Host ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ' -ForegroundColor Cyan
Write-Host ' ██ ██ ██████ ███████ ██ ████ ██ ██ ██ ███████ ██ ' -ForegroundColor Cyan
Write-Host ''
Write-Host ' One command. Full internet access for your AI agent.' -ForegroundColor White
Write-Host ' https://agentkey.app' -ForegroundColor DarkGray
Write-Host ''
}
function Write-Step ($text) { Write-Host ''; Write-Host " $text" -ForegroundColor White }
function Write-Info ($text) { Write-Host " $text" -ForegroundColor Gray }
function Write-Ok ($text) { Write-Host "$text" -ForegroundColor Green }
function Write-Warn2($text) { Write-Host " ! $text" -ForegroundColor Yellow }
function Write-Err ($text) { Write-Host "$text" -ForegroundColor Red }
function Write-Muted($text) { Write-Host " $text" -ForegroundColor DarkGray }
function Die ($text) { Write-Err $text; exit 1 }
# ── Helpers: agent detection ──────────────────────────────────────────────
function Test-AgentMarker {
param([string]$Marker)
if ($Marker.StartsWith('cmd:')) {
return [bool](Get-Command $Marker.Substring(4) -ErrorAction SilentlyContinue)
}
if ($Marker.StartsWith('path:')) {
return Test-Path -LiteralPath $Marker.Substring(5)
}
return $false
}
function Get-DetectedAgents {
$hits = New-Object System.Collections.Generic.List[string]
foreach ($entry in $AgentMarkers) {
foreach ($m in $entry.Markers) {
if (Test-AgentMarker $m) { $hits.Add($entry.Id) | Out-Null; break }
}
}
return @($hits | Sort-Object -Unique)
}
# ── Help ──────────────────────────────────────────────────────────────────
if ($Help) {
@'
AgentKey installer for Windows
Usage:
irm https://agentkey.app/install.ps1 | iex
& ([scriptblock]::Create((irm https://agentkey.app/install.ps1))) -Yes
Parameters:
-Yes Non-interactive: install skill to every detected agent, no prompts
-Interactive Force interactive mode (fails if console input is redirected)
-Only <a,b,c> Only install skill for these agents (e.g. "claude-code,cursor")
-AllAgents Skip auto-detection; let 'skills' CLI install for every detected agent
-ListAgents Print the agents we'd auto-select on this machine and exit
-SkipSkill Skip the skill install step (only run MCP auth)
-SkipMcp Skip the MCP auth step (only install the skill)
-NoTelemetry Disable anonymous usage telemetry (writes
%USERPROFILE%\.config\agentkey\telemetry-disabled so
the skill stays opted-out across runs)
-Help Show this help
Behavior:
The installer auto-detects which AI agents are on this machine and
pre-selects them for skill installation. The auth step always tries
to open a browser and also prints the URL — so SSH / WinRM / Docker
/ OpenClaw users can copy the URL to any device with a browser.
'@
exit 0
}
if ($ListAgents) {
$detected = Get-DetectedAgents
if ($detected.Count -gt 0) { $detected -join "`n" | Write-Output }
else { Write-Host 'no agents detected on this host' -ForegroundColor Yellow }
exit 0
}
Write-Banner
# ── 1. Preflight ──────────────────────────────────────────────────────────
Write-Step '1. Preflight'
# Platform guard
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) {
Die 'This script targets Windows. On macOS/Linux use install.sh instead.'
}
Write-Ok 'Platform: windows'
# Resolve interactive mode. PowerShell's `iex` runs in the current session, so
# Read-Host works natively even under `irm | iex`. The only thing we need to
# guard is truly redirected input (scheduled tasks, CI with redirected stdin).
$InputRedirected = $false
try { $InputRedirected = [Console]::IsInputRedirected } catch { $InputRedirected = $false }
$Mode = $null
if ($Yes) { $Mode = 'noninteractive' }
elseif ($Interactive) {
if ($InputRedirected) { Die '-Interactive requested but console input is redirected.' }
$Mode = 'interactive'
}
elseif ($InputRedirected) {
$Mode = 'noninteractive'
Write-Warn2 'No interactive console detected — falling back to -Yes'
}
else {
$Mode = 'interactive'
}
Write-Ok "Mode: $Mode"
# Resolve telemetry intent: -NoTelemetry overrides everything; existing
# %USERPROFILE%\.config\agentkey\telemetry-disabled file means already-opted-out.
$TelemetryOptOutFile = Join-Path $env:USERPROFILE '.config\agentkey\telemetry-disabled'
if ($NoTelemetry) {
New-Item -ItemType Directory -Path (Split-Path $TelemetryOptOutFile) -Force | Out-Null
New-Item -ItemType File -Path $TelemetryOptOutFile -Force | Out-Null
Write-Ok 'Telemetry: disabled (-NoTelemetry)'
} elseif (Test-Path -LiteralPath $TelemetryOptOutFile) {
Write-Ok "Telemetry: disabled ($TelemetryOptOutFile exists)"
} else {
Write-Info 'Telemetry: anonymous usage stats enabled (re-run with -NoTelemetry to opt out)'
}
# Node check
function Get-NodeMajor {
try {
$v = (& node --version) 2>$null
if ($v -match '^v(\d+)\.') { return [int]$Matches[1] }
} catch {}
return 0
}
function Install-Node {
Write-Info "Installing Node.js LTS ..."
if (Get-Command winget -ErrorAction SilentlyContinue) {
winget install -e --id OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements | Out-Null
} elseif (Get-Command scoop -ErrorAction SilentlyContinue) {
scoop install nodejs-lts | Out-Null
} elseif (Get-Command choco -ErrorAction SilentlyContinue) {
choco install nodejs-lts -y | Out-Null
} else {
Die 'No package manager found (winget/scoop/choco). Install Node.js LTS manually: https://nodejs.org/'
}
# Refresh PATH so this session sees the newly installed node
$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
[System.Environment]::GetEnvironmentVariable('Path', 'User')
Write-Ok 'Node.js installed'
}
$nodeMajor = Get-NodeMajor
if ($nodeMajor -ge $NodeMinMajor) {
Write-Ok "Node.js: v$nodeMajor.x"
} else {
if ($nodeMajor -gt 0) { Write-Warn2 "Node.js v$nodeMajor found but v$NodeMinMajor+ is required" }
if ($Mode -eq 'interactive') {
Write-Host ''
Write-Host " Node.js v$NodeMinMajor+ is required but not found." -ForegroundColor White
$reply = Read-Host ' Install it now? [Y/n]'
if ($reply -match '^(n|no)$') { Die 'Node.js required. Aborting.' }
}
Install-Node
}
if (-not (Get-Command npx -ErrorAction SilentlyContinue)) {
Die 'npx not found after Node install — please reopen your terminal or reinstall Node.js.'
}
# Resolve target agent list — shared between the skill step and the MCP step.
# $AllTargets — every detected agent, including local exceptions
# $SkillTargets — $AllTargets minus ids excluded from `skills add -a`
# $McpTargets — $AllTargets filtered to ids the MCP CLI knows how to write
$AllTargets = @()
if ($Only) {
$AllTargets = @($Only -split ',' | Where-Object { $_ -ne '' })
Write-Info "Targeting agents from -Only: $($AllTargets -join ', ')"
} elseif ($AllAgents) {
Write-Info "Installing for every agent the 'skills' CLI detects (-AllAgents)"
} else {
$AllTargets = @(Get-DetectedAgents)
if ($AllTargets.Count -gt 0) {
Write-Ok "Detected agents on this host: $($AllTargets -join ', ')"
Write-Muted '(override with -Only <ids>, or use -AllAgents)'
} else {
Write-Info "No agents auto-detected — letting 'skills' CLI scan."
}
}
$SkillTargets = @($AllTargets | Where-Object { $_ -notin $SkillsAgentExclusions })
$McpTargets = @($AllTargets | Where-Object { $_ -in $McpAutoAgents })
$DshSelected = $AllTargets -contains 'dsh'
$DshConfigured = $false
# ── 2. Install the AgentKey skill ─────────────────────────────────────────
if ($SkipSkill) {
Write-Step '2. Install the AgentKey skill'
Write-Muted 'Skipped (-SkipSkill)'
} elseif ($AllTargets.Count -gt 0 -and $SkillTargets.Count -eq 0 -and -not $DshSelected) {
# DSH never enters this branch: `-Only dsh` must still run the global
# `skills add -g -a universal` path, without passing dsh to `-a`.
Write-Step '2. Install the AgentKey skill'
Write-Muted "Skipped — selected targets ($($AllTargets -join ',')) are MCP-only (no skill install path)."
} else {
Write-Step '2. Install the AgentKey skill'
$skillsArgs = @('-y', 'skills', 'add', $SkillRepo, '-g')
if ($SkillTargets.Count -gt 0) {
$skillsArgs += '-a'
$skillsArgs += $SkillTargets
} elseif ($DshSelected) {
$skillsArgs += @('-a', 'universal', '-s', 'agentkey')
}
# Always pass -y in noninteractive mode AND when we already resolved
# an explicit target list — there's nothing left to ask the user.
if ($Mode -eq 'noninteractive' -or $AllTargets.Count -gt 0) {
$skillsArgs += '-y'
}
& npx @skillsArgs
if ($LASTEXITCODE -ne 0) { Die "Failed to install skill via 'skills' CLI" }
# The skills CLI sometimes prints "Installation failed" and still
# exits 0 (e.g. network error during git clone). Verify the skill
# actually landed on disk before declaring success. Paths must mirror
# the `path:` markers in $AgentMarkers: most agents live under
# %USERPROFILE%\.<agent>, but amp / crush / goose / opencode live
# under %APPDATA%\<agent>.
$userHome = [Environment]::GetFolderPath('UserProfile')
$candidatePaths = @(
(Join-Path $userHome '.agents\skills\agentkey'),
(Join-Path $userHome '.claude\skills\agentkey'),
(Join-Path $userHome '.cursor\skills\agentkey'),
(Join-Path $userHome '.codex\skills\agentkey'),
(Join-Path $userHome '.gemini\skills\agentkey'),
(Join-Path $userHome '.opencode\skills\agentkey'),
(Join-Path $userHome '.openclaw\skills\agentkey'),
(Join-Path $userHome '.qwen\skills\agentkey'),
(Join-Path $userHome '.iflow\skills\agentkey'),
(Join-Path $userHome '.windsurf\skills\agentkey'),
(Join-Path $userHome '.warp\skills\agentkey'),
(Join-Path $userHome '.kimi\skills\agentkey'),
(Join-Path $userHome '.kiro\skills\agentkey'),
# APPDATA-rooted agents (parity with install.sh's $HOME/.config/<agent>)
(Join-Path $env:APPDATA 'amp\skills\agentkey'),
(Join-Path $env:APPDATA 'crush\skills\agentkey'),
(Join-Path $env:APPDATA 'goose\skills\agentkey'),
(Join-Path $env:APPDATA 'opencode\skills\agentkey')
)
$agentkeyFound = $false
foreach ($abs in $candidatePaths) {
if (Test-Path (Join-Path $abs 'SKILL.md')) {
$agentkeyFound = $true
break
}
}
if (-not $agentkeyFound) {
Die "Skill install reported success but no agentkey SKILL.md was created — likely a network or git clone failure. Retry: npx -y skills add $SkillRepo -g -a universal -s agentkey -y"
}
Write-Ok 'Skill installed'
}
# ── 3. MCP authentication ────────────────────────────────────────────────
# Always run auth-login. The CLI itself decides whether the existing token
# can be reused or a fresh device-code flow is needed — the installer no
# longer second-guesses by sniffing config files (which produced false
# positives across the stdio → HTTP schema change).
if ($SkipMcp) {
Write-Step '3. Register the MCP server'
Write-Muted 'Skipped (-SkipMcp)'
} elseif ($AllTargets.Count -gt 0 -and $McpTargets.Count -eq 0) {
# User selected ONLY MCP-incompatible agents (goose / kode / kilo via
# -Only). Running auth-login without --only would silently register MCP
# in every detected agent, overriding the user's explicit scope. Skip
# rather than over-register. See PR #41 B1.
Write-Step '3. Register the MCP server'
Write-Muted "Skipped — selected agents ($($AllTargets -join ',')) need manual MCP setup (see SKILL.md Fallback section)."
} else {
# Pin MCP registration to the same agent list the skill step targeted.
# When McpTargets is empty (auto-detect found nothing), let
# `@agentkey/cli` do its own detection — same fallback we use for skill
# install. Older CLI versions silently ignore --only, so this is
# forward-compatible.
$authArgs = @('--auth-login')
if ($McpTargets.Count -gt 0) {
$authArgs += '--only'
$authArgs += ($McpTargets -join ',')
}
Write-Step '3. Register the MCP server'
Write-Info 'Opening your browser for AgentKey device authentication ...'
if ($McpTargets.Count -gt 0) {
Write-Muted "Will register MCP in: $($McpTargets -join ', ')"
} else {
Write-Muted "If a browser doesn't open (SSH / WinRM / Docker / headless), the auth URL is also printed below — open it on any device to finish."
}
Write-Host ''
# Telemetry context for install_completed. Opt-out is honored at the
# SOURCE: when AGENTKEY_TELEMETRY=0, no other context env vars are
# exported — hostname-derived fingerprint, agent lists, and installer
# flags are never computed nor passed to the child `npx @agentkey/cli`
# process. The server treats AGENTKEY_TELEMETRY=0 as a hard skip.
if ($NoTelemetry -or (Test-Path -LiteralPath $TelemetryOptOutFile)) {
$env:AGENTKEY_TELEMETRY = '0'
} else {
$env:AGENTKEY_TELEMETRY = '1'
$_hn = [System.Net.Dns]::GetHostName()
$_user = $env:USERNAME
$_input = "$_hn|windows|$_user"
$_bytes = [System.Text.Encoding]::UTF8.GetBytes($_input)
$_sha = [System.Security.Cryptography.SHA256]::Create()
$_hash = ($_sha.ComputeHash($_bytes) | ForEach-Object { $_.ToString('x2') }) -join ''
$DeviceFingerprint = $_hash.Substring(0, 16)
$DetectedAgents = Get-DetectedAgents
$env:AGENTKEY_INSTALL_SOURCE = 'one_liner'
$env:AGENTKEY_DETECTED_AGENTS = ($DetectedAgents -join ',')
$env:AGENTKEY_SELECTED_AGENTS = ($AllTargets -join ',')
$env:AGENTKEY_INSTALLER_FLAGS = ($PSBoundParameters.Keys | ForEach-Object { "-$_" }) -join ','
$env:AGENTKEY_DEVICE_FINGERPRINT = $DeviceFingerprint
}
& npx -y $CliPackage @authArgs
if ($LASTEXITCODE -ne 0) {
Write-Err 'MCP auth failed.'
Write-Muted "Retry manually: npx -y $CliPackage $($authArgs -join ' ')"
exit 1
}
$DshConfigured = $McpTargets -contains 'dsh'
Write-Ok 'MCP server registered'
}
# ── 4. Summary ───────────────────────────────────────────────────────────
Write-Step '✨ Installation complete'
Write-Host ''
Write-Host ' Next steps' -ForegroundColor White
if ($DshConfigured) {
Write-Muted '1. Running DSH profiles hot-apply the home patch; start stopped DSH profiles and restart other clients.'
} else {
Write-Muted '1. Restart your agent (Claude Code / Cursor / etc.)'
}
Write-Muted '2. Ask it something that needs the internet:'
Write-Host ' "What has Musk been tweeting about lately?"' -ForegroundColor Cyan
Write-Host ''
Write-Host ' Docs https://agentkey.app/docs' -ForegroundColor White
Write-Host ' Uninstall irm https://agentkey.app/uninstall.ps1 | iex' -ForegroundColor White
Write-Host ''