<# .SYNOPSIS Downloads and installs the latest DSH Plugin Installer release. .DESCRIPTION Installs the latest stable release into a DSH Profile through DSH's own plugin command. The `web` Profile is started after a successful installation; the `desktop` Profile is installed through the DeepSeek Harness Desktop application's own bundled CLI, because DSH refuses to manage that reserved Profile from an npm-installed CLI. .EXAMPLE .\Install-DshPluginInstaller.ps1 .EXAMPLE .\Install-DshPluginInstaller.ps1 -Profile work -NoStart .EXAMPLE .\Install-DshPluginInstaller.ps1 -Profile desktop #> [CmdletBinding()] param( [ValidatePattern('^[A-Za-z0-9_-]+$')] [string] $Profile = 'web', [switch] $NoStart, # Explicit dsh launcher to use instead of auto-detection. [string] $DshCommand ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $repository = 'Toukaiteio/dsh-plugin-installer' $headers = @{ Accept = 'application/vnd.github+json' 'User-Agent' = 'dsh-plugin-installer-bootstrap' } # Optional, only to lift the anonymous GitHub API rate limit. if ($null -ne $env:GITHUB_TOKEN -and $env:GITHUB_TOKEN.Trim().Length -gt 0) { $headers.Authorization = "Bearer $($env:GITHUB_TOKEN.Trim())" } <# The launcher DeepSeek Harness Desktop ships in its own installation. The desktop application reserves the `desktop` Profile and its CLI is the only one allowed to manage that Profile, so the installer must find it rather than rely on whatever `dsh` happens to be first on PATH. #> function Find-DesktopDsh { $candidates = [System.Collections.Generic.List[string]]::new() $uninstallRoots = @( 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' ) foreach ($root in $uninstallRoots) { if (-not (Test-Path -LiteralPath $root)) { continue } foreach ($entry in Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue) { $properties = Get-ItemProperty -LiteralPath $entry.PSPath -ErrorAction SilentlyContinue if ($null -eq $properties) { continue } if ($null -eq $properties.PSObject.Properties['DisplayName']) { continue } if ($properties.DisplayName -notmatch '^DeepSeek Harness') { continue } if ($null -ne $properties.PSObject.Properties['InstallLocation'] -and $properties.InstallLocation) { $candidates.Add([string]$properties.InstallLocation) } } } if ($null -ne $env:LOCALAPPDATA) { $candidates.Add((Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness')) } if ($null -ne $env:ProgramFiles) { $candidates.Add((Join-Path $env:ProgramFiles 'DeepSeek Harness')) } if ($null -ne ${env:ProgramFiles(x86)}) { $candidates.Add((Join-Path ${env:ProgramFiles(x86)} 'DeepSeek Harness')) } foreach ($directory in $candidates) { $launcher = Join-Path $directory 'resources\runtime\cli\bin\dsh.cmd' if (Test-Path -LiteralPath $launcher -PathType Leaf) { return $launcher } } return $null } function Resolve-DshLauncher { param([string] $RequestedProfile, [string] $Override) if ($Override) { if (-not (Test-Path -LiteralPath $Override -PathType Leaf)) { throw "The dsh launcher '$Override' does not exist." } return (Resolve-Path -LiteralPath $Override).Path } $onPath = Get-Command dsh -CommandType Application, ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1 $pathDsh = if ($null -ne $onPath) { $onPath.Source } else { $null } $desktopDsh = Find-DesktopDsh # The desktop Profile is reserved: an npm-installed CLI refuses it outright, # while the bundled desktop CLI is allowed to manage it. if ($RequestedProfile -eq 'desktop') { if ($null -ne $desktopDsh) { return $desktopDsh } if ($null -ne $pathDsh) { Write-Warning 'The DeepSeek Harness Desktop installation was not found; falling back to the dsh on PATH. If it refuses the desktop Profile, install DeepSeek Harness Desktop first.' return $pathDsh } throw 'Neither the DeepSeek Harness Desktop installation nor a dsh command on PATH was found. Install DeepSeek Harness first, then run this script again.' } if ($null -ne $pathDsh) { return $pathDsh } if ($null -ne $desktopDsh) { return $desktopDsh } throw 'The DSH CLI was not found on PATH. Install DeepSeek Harness first, then run this script again.' } $dshLauncher = Resolve-DshLauncher -RequestedProfile $Profile -Override $DshCommand Write-Host 'Finding the latest DSH Plugin Installer release...' try { $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$repository/releases/latest" -Headers $headers } catch { $hint = if ($headers.ContainsKey('Authorization')) { 'GitHub rejected the release request.' } else { 'GitHub rejected the release request. If this is an anonymous rate limit, set the GITHUB_TOKEN environment variable and run the script again.' } throw "$hint $($_.Exception.Message)" } $asset = @($release.assets) | Where-Object { $_.name -match '^dsh-plugin-installer-.+\.tgz$' } | Select-Object -First 1 if ($null -eq $asset) { throw "Release $($release.tag_name) does not contain a DSH Plugin Installer package archive." } $dshHome = if ($null -ne $env:DSH_HOME -and $env:DSH_HOME.Trim().Length -gt 0) { [System.IO.Path]::GetFullPath($env:DSH_HOME) } else { Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)) '.dsh' } $archiveDirectory = Join-Path (Join-Path $dshHome 'plugin-archives') 'dsh-plugin-installer' $archivePath = Join-Path $archiveDirectory $asset.name $downloadPath = Join-Path $archiveDirectory ".$($asset.name).$([guid]::NewGuid().ToString('N')).download" try { New-Item -ItemType Directory -Path $archiveDirectory -Force | Out-Null Write-Host "Downloading $($release.tag_name)..." Invoke-WebRequest -Uri $asset.browser_download_url -Headers $headers -OutFile $downloadPath if ($asset.PSObject.Properties.Name -contains 'digest' -and $asset.digest -match '^sha256:(?[0-9a-fA-F]{64})$') { $expectedHash = $Matches.hash.ToLowerInvariant() $actualHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualHash -ne $expectedHash) { throw 'The downloaded package checksum does not match the GitHub Release checksum.' } } # `dsh plugin add` preserves a file: dependency. Keep this archive under # DSH_HOME instead of a temporary directory so later plugin operations can # still resolve the installed package. Move-Item -LiteralPath $downloadPath -Destination $archivePath -Force Write-Host "Installing into the '$Profile' DSH Profile..." & $dshLauncher plugin --profile $Profile add $archivePath if ($LASTEXITCODE -ne 0) { throw "DSH plugin installation failed with exit code $LASTEXITCODE." } Write-Host "DSH Plugin Installer $($release.tag_name) was installed successfully." if ($NoStart) { Write-Host "Installation is complete. Start this Profile with: dsh --profile $Profile" } elseif ($Profile -eq 'desktop') { # The desktop application composes its Profile at startup, so the new # bundle layer only applies after a full restart of the application. Write-Host 'Fully quit and reopen DeepSeek Harness Desktop to load the marketplace.' } elseif ($Profile -eq 'web') { Write-Host 'Starting DSH Web...' & $dshLauncher web if ($LASTEXITCODE -ne 0) { throw "DSH Web exited with code $LASTEXITCODE." } } else { Write-Host "Installation is complete. Start this Profile with: dsh --profile $Profile" } } finally { if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath -Force } }