From 245377b07a269ae2623e6513dffcf3ca1b5d955a Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:57:24 +0800 Subject: [PATCH] docs: add scripts and documents for automatic installation --- .gitignore | 4 + docs/getting-started.md | 50 ++ .../index.md} | 77 ++- .../current/getting-started.md | 48 ++ .../index.md} | 74 ++- sidebars.js | 2 +- static/scripts/picoclaw/install-picoclaw.ps1 | 516 ++++++++++++++++++ static/scripts/picoclaw/install-picoclaw.sh | 453 +++++++++++++++ 8 files changed, 1215 insertions(+), 9 deletions(-) rename docs/{installation.md => installation/index.md} (65%) rename i18n/zh-Hans/docusaurus-plugin-content-docs/current/{installation.md => installation/index.md} (63%) create mode 100644 static/scripts/picoclaw/install-picoclaw.ps1 create mode 100644 static/scripts/picoclaw/install-picoclaw.sh diff --git a/.gitignore b/.gitignore index 30f0747..ec570ec 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ build/ npm-debug.log* yarn-debug.log* yarn-error.log* + + +# Visual Studio Code workspace settings +.vscode/ diff --git a/docs/getting-started.md b/docs/getting-started.md index 9c72aa2..d7b7d6a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -3,6 +3,9 @@ id: getting-started title: Getting Started --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Getting Started Get PicoClaw running in 2 minutes. @@ -11,6 +14,53 @@ Get PicoClaw running in 2 minutes. Set your API Key in `~/.picoclaw/config.json`. Get API Keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Web search is **optional** — get a free [Tavily API](https://tavily.com) (1000 free queries/month) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month). ::: +## Step 0: Install PicoClaw + + + + +PowerShell script supports Windows and Unix-like systems. + +- On Linux/macOS/FreeBSD/NetBSD, install PowerShell Core (`pwsh`) first. +- Supports user mode and system mode. +- Run the following commands in a PowerShell terminal only. Do not paste them into bash/ash/zsh. +- For users in Chinese Mainland, you may append `-Source cdn` to use the CDN source. +- `user` mode installation is **not recommended** on lightweight Linux distros (e.g. OpenWrt) due to potential PATH issues. The better option is to run the installer in `system` mode with root privileges. + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode user +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode user +} else { + throw "PowerShell executable not found. Please install PowerShell Core (pwsh)." +} +``` + + + + +Bash installer supports Linux/macOS/FreeBSD/NetBSD. + +- Not supported on Windows. +- Requires GNU Bash 4+. +- `ash` / BusyBox shell are not supported. +- `user` mode installation is **not recommended** on lightweight Linux distros (e.g. OpenWrt) due to potential PATH issues. The better option is to run the installer in `system` mode with root privileges. + +```bash +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +# For users in Chinese Mainland, append: --source cdn +bash ./install-picoclaw.sh --mode user +``` + + + + +See [Installation](./installation) for full installation options. + ## Step 1: Initialize ```bash diff --git a/docs/installation.md b/docs/installation/index.md similarity index 65% rename from docs/installation.md rename to docs/installation/index.md index 844d6bd..54a3747 100644 --- a/docs/installation.md +++ b/docs/installation/index.md @@ -1,11 +1,80 @@ --- -id: installation +id: index title: Installation --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Installation -## Option 1: Precompiled Binary (Recommended) +## Option 1: Automated Installer (Recommended) + + + + +PowerShell installer works on Windows and Unix-like platforms. + +- On Linux/macOS/FreeBSD/NetBSD, install PowerShell Core (`pwsh`) first. +- Supports both user mode and system mode. +- Run the following commands in a PowerShell terminal only. Do not paste them into bash/ash/zsh. +- For users in Chinese Mainland, you may append `-Source cdn` to use the CDN source. +- `user` mode installation is **not recommended** on lightweight Linux distros (e.g. OpenWrt) due to potential PATH issues. The better option is to run the installer in `system` mode with root privileges. + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode user +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode user +} else { + throw "PowerShell executable not found. Please install PowerShell Core (pwsh)." +} +``` + +```powershell +# System install (requires Administrator/root privileges) +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode system +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode system +} else { + throw "PowerShell executable not found. Please install PowerShell Core (pwsh)." +} +``` + + + + +Bash installer supports Unix-like systems only. + +- Not supported on Windows. +- Requires GNU Bash 4+. +- `ash` / BusyBox shell are not supported. +- `user` mode installation is **not recommended** on lightweight Linux distros (e.g. OpenWrt) due to potential PATH issues. The better option is to run the installer in `system` mode with root privileges. + +```bash +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +# For users in Chinese Mainland, append: --source cdn +bash ./install-picoclaw.sh --mode user +``` + +```bash +# System install (requires root privileges) +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +# For users in Chinese Mainland, append: --source cdn +sudo bash ./install-picoclaw.sh --mode system +``` + + + + +## Option 2: Precompiled Binary Download the latest release from the [Releases page](https://github.com/sipeed/picoclaw/releases/latest). All releases are packaged as `.tar.gz` (Linux/macOS/FreeBSD) or `.zip` (Windows). @@ -31,7 +100,7 @@ tar -xzf picoclaw_Linux_arm64.tar.gz ./picoclaw onboard ``` -## Option 2: Build from Source +## Option 3: Build from Source Requires Go 1.21+. @@ -54,7 +123,7 @@ make install The binary is placed in `build/picoclaw-{platform}-{arch}`. -## Option 3: Docker Compose +## Option 4: Docker Compose Run PicoClaw without installing anything locally. diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/getting-started.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/getting-started.md index 7f19a4b..263c056 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/getting-started.md +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/getting-started.md @@ -3,6 +3,9 @@ id: getting-started title: 快速开始 --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # 快速开始 2 分钟内启动 PicoClaw。 @@ -11,6 +14,51 @@ title: 快速开始 在 `~/.picoclaw/config.json` 中设置您的 API Key。获取 API Key:[Volcengine(CodingPlan)](https://console.volcengine.com)(LLM)· [OpenRouter](https://openrouter.ai/keys)(LLM)· [智谱 AI](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys)(LLM)。网络搜索是**可选的** — 获取免费的 [Tavily API](https://tavily.com)(每月 1000 次免费查询)或 [Brave Search API](https://brave.com/search/api)(每月 2000 次免费查询) ::: +## 第零步:安装 PicoClaw + + + + +PowerShell 安装脚本支持 Windows 与类 Unix 平台。 + +- Linux/macOS/FreeBSD/NetBSD 需先安装 PowerShell Core(`pwsh`)。 +- 支持用户安装与系统安装两种模式。 +- 以下命令必须在 PowerShell 终端执行,不能直接粘贴到 bash/ash/zsh。 +- 在轻量 Linux 发行版(例如 OpenWrt)上,**不建议**使用 `user` 模式,可能会遇到 PATH 问题。更推荐使用 root 权限执行 `system` 模式安装。 + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode user -Source cdn +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode user -Source cdn +} else { + throw "未找到 PowerShell 可执行程序,请先安装 PowerShell Core(pwsh)。" +} +``` + + + + +Bash 安装脚本支持 Linux/macOS/FreeBSD/NetBSD。 + +- 暂不支持 Windows。 +- 需要 GNU Bash 4+。 +- 不支持 `ash` / BusyBox 等轻量 shell。 +- 在轻量 Linux 发行版(例如 OpenWrt)上,**不建议**使用 `user` 模式,可能会遇到 PATH 问题。更推荐使用 root 权限执行 `system` 模式安装。 + +```bash +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +bash ./install-picoclaw.sh --mode user --source cdn +``` + + + + +完整安装方式请查看 [安装页面](./installation)。 + ## 第一步:初始化 ```bash diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation/index.md similarity index 63% rename from i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.md rename to i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation/index.md index 9ed1030..0abc4e5 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation.md +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/installation/index.md @@ -1,11 +1,77 @@ --- -id: installation +id: index title: 安装 --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # 安装 -## 方式一:使用预编译二进制文件(推荐) +## 方式一:自动化安装(推荐) + + + + +PowerShell 安装脚本可用于 Windows 与类 Unix 平台。 + +- Linux/macOS/FreeBSD/NetBSD 需先安装 PowerShell Core(`pwsh`)。 +- 支持用户安装与系统安装两种模式。 +- 以下命令必须在 PowerShell 终端执行,不能直接粘贴到 bash/ash/zsh。 +- 在轻量 Linux 发行版(例如 OpenWrt)上,**不建议**使用 `user` 模式,可能会遇到 PATH 问题。更推荐使用 root 权限执行 `system` 模式安装。 + +```powershell +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode user -Source cdn +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode user -Source cdn +} else { + throw "未找到 PowerShell 可执行程序,请先安装 PowerShell Core(pwsh)。" +} +``` + +```powershell +# 系统安装(需要管理员或 root 权限) +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.ps1" -OutFile install-picoclaw.ps1 + +if (Get-Command pwsh -ErrorAction SilentlyContinue) { + pwsh -ExecutionPolicy Bypass -File ./install-picoclaw.ps1 -InstallMode system -Source cdn +} elseif ($env:OS -eq "Windows_NT" -and (Get-Command powershell.exe -ErrorAction SilentlyContinue)) { + powershell.exe -ExecutionPolicy Bypass -File .\install-picoclaw.ps1 -InstallMode system -Source cdn +} else { + throw "未找到 PowerShell 可执行程序,请先安装 PowerShell Core(pwsh)。" +} +``` + + + + +Bash 安装脚本仅支持类 Unix 系统。 + +- 暂不支持 Windows。 +- 需要 GNU Bash 4+。 +- 不支持 `ash` / BusyBox 等轻量 shell。 +- 在轻量 Linux 发行版(例如 OpenWrt)上,**不建议**使用 `user` 模式,可能会遇到 PATH 问题。更推荐使用 root 权限执行 `system` 模式安装。 + +```bash +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +bash ./install-picoclaw.sh --mode user --source cdn +``` + +```bash +# 系统安装(需要 root 权限) +curl -fsSL https://raw.githubusercontent.com/sipeed/picoclaw_docs/main/static/scripts/picoclaw/install-picoclaw.sh -o install-picoclaw.sh +chmod +x ./install-picoclaw.sh +sudo bash ./install-picoclaw.sh --mode system --source cdn +``` + + + + +## 方式二:使用预编译二进制文件 从 [Releases 页面](https://github.com/sipeed/picoclaw/releases/latest) 下载最新版本。所有版本打包为 `.tar.gz`(Linux/macOS/FreeBSD)或 `.zip`(Windows)。 @@ -31,7 +97,7 @@ tar -xzf picoclaw_Linux_arm64.tar.gz ./picoclaw onboard ``` -## 方式二:从源码构建 +## 方式三:从源码构建 需要 Go 1.21+。 @@ -52,7 +118,7 @@ make build-all make install ``` -## 方式三:Docker Compose +## 方式四:Docker Compose 无需本地安装,直接运行。 diff --git a/sidebars.js b/sidebars.js index 4bc5f10..e3326d7 100644 --- a/sidebars.js +++ b/sidebars.js @@ -16,7 +16,7 @@ const sidebars = { label: 'Installation', collapsible: true, collapsed: true, - link: { type: 'doc', id: 'installation' }, + link: { type: 'doc', id: 'installation/index' }, items: [ 'installation/licheervnano', 'installation/maixcam', diff --git a/static/scripts/picoclaw/install-picoclaw.ps1 b/static/scripts/picoclaw/install-picoclaw.ps1 new file mode 100644 index 0000000..c98f000 --- /dev/null +++ b/static/scripts/picoclaw/install-picoclaw.ps1 @@ -0,0 +1,516 @@ +[CmdletBinding()] +param( + [ValidateSet('system', 'user')] + [string]$InstallMode = 'user', + + [ValidateSet('github', 'cdn')] + [string]$Source = 'github', + + [string]$Arch +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$BaseUrls = @{ + github = 'https://github.com/sipeed/picoclaw/releases/latest/download/' + cdn = 'https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/' +} + +$Assets = @{ + windows = @{ + x86_64 = @{ file = 'picoclaw_Windows_x86_64.zip' } + arm64 = @{ file = 'picoclaw_Windows_arm64.zip' } + } + linux = @{ + x86_64 = @{ file = 'picoclaw_Linux_x86_64.tar.gz'; deb = 'picoclaw_x86_64.deb'; rpm = 'picoclaw_x86_64.rpm' } + arm64 = @{ file = 'picoclaw_Linux_arm64.tar.gz'; deb = 'picoclaw_aarch64.deb'; rpm = 'picoclaw_aarch64.rpm' } + armv7 = @{ file = 'picoclaw_Linux_armv7.tar.gz'; deb = 'picoclaw_armv7.deb'; rpm = 'picoclaw_armv7.rpm' } + armv6 = @{ file = 'picoclaw_Linux_armv6.tar.gz'; deb = 'picoclaw_armv6.deb'; rpm = 'picoclaw_armv6.rpm' } + riscv64 = @{ file = 'picoclaw_Linux_riscv64.tar.gz'; deb = 'picoclaw_riscv64.deb'; rpm = 'picoclaw_riscv64.rpm' } + mipsle = @{ file = 'picoclaw_Linux_mipsle.tar.gz'; deb = 'picoclaw_mipsle.deb'; rpm = 'picoclaw_mipsle.rpm' } + s390x = @{ file = 'picoclaw_Linux_s390x.tar.gz'; deb = 'picoclaw_s390x.deb'; rpm = 'picoclaw_s390x.rpm' } + } + macos = @{ + x86_64 = @{ file = 'picoclaw_Darwin_x86_64.tar.gz' } + arm64 = @{ file = 'picoclaw_Darwin_arm64.tar.gz' } + } + freebsd = @{ + x86_64 = @{ file = 'picoclaw_Freebsd_x86_64.tar.gz' } + arm64 = @{ file = 'picoclaw_Freebsd_arm64.tar.gz' } + armv7 = @{ file = 'picoclaw_Freebsd_armv7.tar.gz' } + } + netbsd = @{ + x86_64 = @{ file = 'picoclaw_Netbsd_x86_64.tar.gz' } + arm64 = @{ file = 'picoclaw_Netbsd_arm64.tar.gz' } + } +} + +$ExpectedBins = @('picoclaw', 'picoclaw-launcher', 'picoclaw-launcher-tui') + +function Get-CommandExists { + param([Parameter(Mandatory = $true)][string]$Name) + return $null -ne (Get-Command -Name $Name -ErrorAction SilentlyContinue) +} + +function Get-OsId { + if ($IsWindows) { + return 'windows' + } + if ($IsLinux) { + return 'linux' + } + if ($IsMacOS) { + return 'macos' + } + + $uname = (& uname -s).Trim().ToLowerInvariant() + switch ($uname) { + 'freebsd' { return 'freebsd' } + 'netbsd' { return 'netbsd' } + default { + throw "Unsupported operating system: $uname" + } + } +} + +function Resolve-Arch { + param([Parameter(Mandatory = $true)][string]$OsId) + + if (-not [string]::IsNullOrWhiteSpace($Arch)) { + return $Arch.ToLowerInvariant() + } + + $machine = if ($IsWindows) { + [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() + } + else { + (& uname -m).Trim().ToLowerInvariant() + } + + switch ($machine) { + 'x64' { return 'x86_64' } + 'amd64' { return 'x86_64' } + 'x86_64' { return 'x86_64' } + 'arm64' { return 'arm64' } + 'aarch64' { return 'arm64' } + 'armv7l' { return 'armv7' } + 'armv7' { return 'armv7' } + 'armv6l' { return 'armv6' } + 'armv6' { return 'armv6' } + 'riscv64' { return 'riscv64' } + 'mipsel' { return 'mipsle' } + 'mipsle' { return 'mipsle' } + 's390x' { return 's390x' } + default { + throw "Unsupported architecture '$machine' for OS '$OsId'. Use -Arch to override." + } + } +} + +function Assert-SystemInstallPrivileges { + param([Parameter(Mandatory = $true)][string]$OsId) + + if ($InstallMode -ne 'system') { + return + } + + if ($OsId -eq 'windows') { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if (-not $isAdmin) { + throw 'System install on Windows requires Administrator privileges.' + } + return + } + + $uid = (& id -u).Trim() + if ($uid -ne '0') { + throw 'System install requires root privileges (sudo/root).' + } +} + +function Get-UnixSystemLayout { + if (Test-Path -LiteralPath '/usr/local/share') { + return @{ + installRoot = '/usr/local/share/picoclaw' + linkDir = '/usr/local/bin' + mode = 'symlink' + } + } + + if (Test-Path -LiteralPath '/usr/share') { + return @{ + installRoot = '/usr/share/picoclaw' + linkDir = '/usr/bin' + mode = 'symlink' + } + } + + return @{ + installRoot = "/tmp/picoclaw-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" + linkDir = '/usr/bin' + mode = 'copy-executables' + } +} + +function Get-InstallLayout { + param([Parameter(Mandatory = $true)][string]$OsId) + + if ($InstallMode -eq 'user') { + if ($OsId -eq 'windows') { + $base = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + if ([string]::IsNullOrWhiteSpace($base) -or -not (Test-Path -LiteralPath $base)) { + throw 'Failed to resolve a valid LocalApplicationData path.' + } + return @{ + installRoot = (Join-Path $base 'picoclaw') + linkDir = $null + mode = 'user' + } + } + + return @{ + installRoot = (Join-Path $HOME '.local/share/picoclaw') + linkDir = (Join-Path $HOME '.local/bin') + mode = 'user' + } + } + + if ($OsId -eq 'windows') { + $programFiles = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles) + if ([string]::IsNullOrWhiteSpace($programFiles) -or -not (Test-Path -LiteralPath $programFiles)) { + throw 'ProgramFiles path from .NET API is invalid or missing. Installation terminated.' + } + return @{ + installRoot = (Join-Path $programFiles 'picoclaw') + linkDir = $null + mode = 'system' + } + } + + return Get-UnixSystemLayout +} + +function New-DirectoryIfMissing { + param([Parameter(Mandatory = $true)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -ItemType Directory -Path $Path -Force | Out-Null + } +} + +function Get-DownloadUrl { + param( + [Parameter(Mandatory = $true)][string]$FileName, + [Parameter(Mandatory = $true)][string]$SourceName + ) + return "$($BaseUrls[$SourceName])$FileName" +} + +function Invoke-ArtifactDownload { + param( + [Parameter(Mandatory = $true)][string]$Url, + [Parameter(Mandatory = $true)][string]$OutFile + ) + Write-Host "Downloading: $Url" + Invoke-WebRequest -Uri $Url -OutFile $OutFile +} + +function Expand-Artifact { + param( + [Parameter(Mandatory = $true)][string]$ArchivePath, + [Parameter(Mandatory = $true)][string]$Destination + ) + + New-DirectoryIfMissing -Path $Destination + + if ($ArchivePath.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)) { + Expand-Archive -LiteralPath $ArchivePath -DestinationPath $Destination -Force + return + } + + if ($ArchivePath.EndsWith('.tar.gz', [StringComparison]::OrdinalIgnoreCase)) { + & tar -xzf $ArchivePath -C $Destination + if ($LASTEXITCODE -ne 0) { + throw "Failed to extract tar archive: $ArchivePath" + } + return + } + + throw "Unsupported archive type: $ArchivePath" +} + +function Get-BinaryPath { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)][string]$Name + ) + + $direct = Join-Path $Root $Name + if (Test-Path -LiteralPath $direct -PathType Leaf) { + return $direct + } + + $directExe = Join-Path $Root ("$Name.exe") + if (Test-Path -LiteralPath $directExe -PathType Leaf) { + return $directExe + } + + return $null +} + +function Set-UnixUserPathExport { + param( + [Parameter(Mandatory = $true)][string]$HomeDir, + [Parameter(Mandatory = $true)][string]$BinDir + ) + + $line = 'export PATH="$HOME/.local/bin:$PATH"' + $rcFiles = @( + (Join-Path $HomeDir '.bashrc'), + (Join-Path $HomeDir '.zshrc'), + (Join-Path $HomeDir '.profile') + ) + + $updated = @() + foreach ($rcPath in $rcFiles) { + if (-not (Test-Path -LiteralPath $rcPath)) { + New-Item -ItemType File -Path $rcPath -Force | Out-Null + } + + $hasLine = Select-String -Path $rcPath -Pattern [regex]::Escape($line) -SimpleMatch -Quiet -ErrorAction SilentlyContinue + if (-not $hasLine) { + Add-Content -Path $rcPath -Value "`n$line" + $updated += $rcPath + } + } + + return $updated +} + +function Add-PathVariable { + param( + [Parameter(Mandatory = $true)][string]$PathToAdd, + [Parameter(Mandatory = $true)][ValidateSet('User', 'Machine')][string]$Scope + ) + + $current = [Environment]::GetEnvironmentVariable('Path', $Scope) + $parts = @() + if (-not [string]::IsNullOrWhiteSpace($current)) { + $parts = $current.Split(';') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + } + + if ($parts -contains $PathToAdd) { + return + } + + $newValue = if ($parts.Count -eq 0) { $PathToAdd } else { "$current;$PathToAdd" } + [Environment]::SetEnvironmentVariable('Path', $newValue, $Scope) +} + +function Install-LinuxPackageFromAsset { + param( + [Parameter(Mandatory = $true)][hashtable]$Asset, + [Parameter(Mandatory = $true)][string]$TempDir + ) + + $debCmd = Get-CommandExists -Name 'dpkg' + $rpmCmd = Get-CommandExists -Name 'rpm' + + if (-not $debCmd -and -not $rpmCmd) { + return $null + } + + if ($debCmd -and $Asset.ContainsKey('deb')) { + $pkg = $Asset.deb + $pkgPath = Join-Path $TempDir $pkg + Invoke-ArtifactDownload -Url (Get-DownloadUrl -FileName $pkg -SourceName $Source) -OutFile $pkgPath + & dpkg -i $pkgPath + if ($LASTEXITCODE -ne 0) { + throw 'dpkg install failed.' + } + return @{ + manager = 'dpkg' + package = $pkgPath + } + } + + if ($rpmCmd -and $Asset.ContainsKey('rpm')) { + $pkg = $Asset.rpm + $pkgPath = Join-Path $TempDir $pkg + Invoke-ArtifactDownload -Url (Get-DownloadUrl -FileName $pkg -SourceName $Source) -OutFile $pkgPath + & rpm -Uvh --replacepkgs $pkgPath + if ($LASTEXITCODE -ne 0) { + throw 'rpm install failed.' + } + return @{ + manager = 'rpm' + package = $pkgPath + } + } + + return $null +} + +function Install-LinksOrCopies { + param( + [Parameter(Mandatory = $true)][string]$InstallRoot, + [Parameter(Mandatory = $true)][string]$LinkDir, + [Parameter(Mandatory = $true)][string]$Mode + ) + + New-DirectoryIfMissing -Path $LinkDir + + if ($Mode -eq 'copy-executables') { + $copied = @() + foreach ($name in $ExpectedBins) { + $binPath = Get-BinaryPath -Root $InstallRoot -Name $name + if ($null -eq $binPath) { + continue + } + + $dest = Join-Path $LinkDir (Split-Path -Path $binPath -Leaf) + Copy-Item -LiteralPath $binPath -Destination $dest -Force + $copied += $dest + } + + return @{ + type = 'copied' + paths = $copied + } + } + + $linked = @() + foreach ($name in $ExpectedBins) { + $binPath = Get-BinaryPath -Root $InstallRoot -Name $name + if ($null -eq $binPath) { + continue + } + + $target = Join-Path $LinkDir $name + if (Test-Path -LiteralPath $target) { + Remove-Item -LiteralPath $target -Force + } + + if ($IsWindows) { + Copy-Item -LiteralPath $binPath -Destination $target -Force + } + else { + & ln -s $binPath $target + if ($LASTEXITCODE -ne 0) { + throw "Failed to create symlink: $target" + } + } + $linked += $target + } + + return @{ + type = 'linked' + paths = $linked + } +} + +function Write-MissingUserLocalWarning { + param([Parameter(Mandatory = $true)][string]$OsId) + + if ($InstallMode -ne 'user' -or $OsId -eq 'windows') { + return + } + + $localRoot = Join-Path $HOME '.local' + if (-not (Test-Path -LiteralPath $localRoot)) { + Write-Host "Warning: $localRoot does not exist. The installer will create it automatically." -ForegroundColor Yellow + } +} + +$osId = Get-OsId +$archId = Resolve-Arch -OsId $osId +Assert-SystemInstallPrivileges -OsId $osId +Write-MissingUserLocalWarning -OsId $osId + +if (-not $Assets.ContainsKey($osId)) { + throw "OS '$osId' is not included in assets map." +} + +$osAssets = $Assets[$osId] +if (-not $osAssets.ContainsKey($archId)) { + $available = ($osAssets.Keys | Sort-Object) -join ', ' + throw "Architecture '$archId' is not available for '$osId'. Available: $available" +} + +$asset = $osAssets[$archId] +$layout = Get-InstallLayout -OsId $osId + +$tempDir = Join-Path ([IO.Path]::GetTempPath()) ("picoclaw-install-$([Guid]::NewGuid().ToString('N'))") +New-DirectoryIfMissing -Path $tempDir + +try { + Write-Host "Install mode: $InstallMode" + Write-Host "OS/Arch: $osId/$archId" + + if ($osId -eq 'linux' -and $InstallMode -eq 'system') { + $pkgInfo = Install-LinuxPackageFromAsset -Asset $asset -TempDir $tempDir + if ($null -ne $pkgInfo) { + Write-Host "Installed via package manager: $($pkgInfo.manager)" + Write-Host "Package file: $($pkgInfo.package)" + Write-Host 'Install location is managed by the package manager.' + return + } + } + + $archiveName = $asset.file + $archivePath = Join-Path $tempDir $archiveName + $downloadUrl = Get-DownloadUrl -FileName $archiveName -SourceName $Source + Invoke-ArtifactDownload -Url $downloadUrl -OutFile $archivePath + + New-DirectoryIfMissing -Path $layout.installRoot + Expand-Artifact -ArchivePath $archivePath -Destination $layout.installRoot + + if ($osId -eq 'windows') { + $pathScope = if ($InstallMode -eq 'system') { 'Machine' } else { 'User' } + Add-PathVariable -PathToAdd $layout.installRoot -Scope $pathScope + Write-Host "Installed to: $($layout.installRoot)" + Write-Host "PATH scope updated: $pathScope" + Write-Host 'Please restart your terminal or log out and back in for PATH changes to take effect.' + return + } + + $linkSummary = Install-LinksOrCopies -InstallRoot $layout.installRoot -LinkDir $layout.linkDir -Mode $layout.mode + + Write-Host "Extracted to: $($layout.installRoot)" + if ($linkSummary.paths.Count -gt 0) { + if ($linkSummary.type -eq 'copied') { + Write-Host "Copied executables to: $($layout.linkDir)" + } + else { + Write-Host "Created links in: $($layout.linkDir)" + } + $linkSummary.paths | ForEach-Object { Write-Host " - $_" } + } + else { + Write-Host 'No launcher binaries were found for link/copy step.' + } + + if ($InstallMode -eq 'user' -and $layout.linkDir) { + if ($osId -ne 'windows') { + $shellRcUpdated = Set-UnixUserPathExport -HomeDir $HOME -BinDir $layout.linkDir + if ($shellRcUpdated.Count -gt 0) { + Write-Host 'Updated shell profile files for PATH:' + $shellRcUpdated | ForEach-Object { Write-Host " - $_" } + } + } + Write-Host "User-local binary directory: $($layout.linkDir)" + Write-Host 'Ensure your shell PATH contains the directory above.' -ForegroundColor Yellow + } +} +finally { + if (Test-Path -LiteralPath $tempDir) { + try { + Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction Stop + } + catch { + Write-Host "Warning: failed to remove temp directory $tempDir . Please remove it manually." -ForegroundColor Yellow + } + } +} \ No newline at end of file diff --git a/static/scripts/picoclaw/install-picoclaw.sh b/static/scripts/picoclaw/install-picoclaw.sh new file mode 100644 index 0000000..58caebc --- /dev/null +++ b/static/scripts/picoclaw/install-picoclaw.sh @@ -0,0 +1,453 @@ +#!/usr/bin/env bash + +if [ -z "${BASH_VERSION:-}" ]; then + printf 'Error: this installer must run with GNU bash (ash/busybox are not supported).\n' >&2 + exit 1 +fi + +if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then + printf 'Error: GNU bash 4+ is required. Current version: %s\n' "$BASH_VERSION" >&2 + exit 1 +fi + +if ! bash --version 2>/dev/null | head -n 1 | grep -qi 'gnu bash'; then + printf 'Error: this installer requires a full GNU bash runtime.\n' >&2 + exit 1 +fi + +set -euo pipefail + +INSTALL_MODE="user" +SOURCE="github" +ARCH_OVERRIDE="" + +BASE_GITHUB="https://github.com/sipeed/picoclaw/releases/latest/download/" +BASE_CDN="https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/" + +EXPECTED_BINS=("picoclaw" "picoclaw-launcher" "picoclaw-launcher-tui") + +usage() { + cat <<'EOF' +Usage: + install-picoclaw.sh [--mode system|user] [--source github|cdn] [--arch ARCH] + +Examples: + ./install-picoclaw.sh --mode user + sudo ./install-picoclaw.sh --mode system --source cdn +EOF +} + +log() { + printf '%s\n' "$*" +} + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +create_secure_tmp_dir() { + local prefix="${1:-picoclaw}" + local d + + if d="$(mktemp -d "/tmp/${prefix}.XXXXXX" 2>/dev/null)"; then + printf '%s\n' "$d" + return 0 + fi + + local i candidate + for i in 1 2 3 4 5 6 7 8 9 10; do + candidate="/tmp/${prefix}.$$.${RANDOM}.$(date +%s)" + if mkdir -m 700 "$candidate" 2>/dev/null; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +detect_os() { + local u + u="$(uname -s | tr '[:upper:]' '[:lower:]')" + case "$u" in + linux) echo "linux" ;; + darwin) echo "macos" ;; + freebsd) echo "freebsd" ;; + netbsd) echo "netbsd" ;; + mingw*|msys*|cygwin*) + die "Windows shell detected ($u). Please run the PowerShell installer instead: pwsh ./static/scripts/picoclaw/install-picoclaw.ps1 -InstallMode user" + ;; + *) die "Unsupported OS: $u" ;; + esac +} + +detect_arch() { + if [[ -n "$ARCH_OVERRIDE" ]]; then + echo "$ARCH_OVERRIDE" + return + fi + + local m + m="$(uname -m | tr '[:upper:]' '[:lower:]')" + case "$m" in + x86_64|amd64) echo "x86_64" ;; + aarch64|arm64) echo "arm64" ;; + armv7l|armv7) echo "armv7" ;; + armv6l|armv6) echo "armv6" ;; + riscv64) echo "riscv64" ;; + mipsel|mipsle) echo "mipsle" ;; + s390x) echo "s390x" ;; + *) die "Unsupported architecture: $m (use --arch to override)" ;; + esac +} + +assert_privileges() { + if [[ "$INSTALL_MODE" != "system" ]]; then + return + fi + + if [[ "$(id -u)" -ne 0 ]]; then + die "System install requires root privileges (sudo/root)." + fi +} + +download_file() { + local url="$1" + local out="$2" + + log "Downloading: $url" + if command_exists curl; then + curl -fsSL "$url" -o "$out" + return + fi + if command_exists wget; then + wget -qO "$out" "$url" + return + fi + die "Neither curl nor wget is available." +} + +base_url() { + case "$SOURCE" in + github) echo "$BASE_GITHUB" ;; + cdn) echo "$BASE_CDN" ;; + *) die "Invalid source: $SOURCE" ;; + esac +} + +asset_for() { + local os="$1" + local arch="$2" + + TAR_FILE="" + DEB_FILE="" + RPM_FILE="" + + case "$os/$arch" in + windows/x86_64) TAR_FILE="picoclaw_Windows_x86_64.zip" ;; + windows/arm64) TAR_FILE="picoclaw_Windows_arm64.zip" ;; + + linux/x86_64) + TAR_FILE="picoclaw_Linux_x86_64.tar.gz" + DEB_FILE="picoclaw_x86_64.deb" + RPM_FILE="picoclaw_x86_64.rpm" + ;; + linux/arm64) + TAR_FILE="picoclaw_Linux_arm64.tar.gz" + DEB_FILE="picoclaw_aarch64.deb" + RPM_FILE="picoclaw_aarch64.rpm" + ;; + linux/armv7) + TAR_FILE="picoclaw_Linux_armv7.tar.gz" + DEB_FILE="picoclaw_armv7.deb" + RPM_FILE="picoclaw_armv7.rpm" + ;; + linux/armv6) + TAR_FILE="picoclaw_Linux_armv6.tar.gz" + DEB_FILE="picoclaw_armv6.deb" + RPM_FILE="picoclaw_armv6.rpm" + ;; + linux/riscv64) + TAR_FILE="picoclaw_Linux_riscv64.tar.gz" + DEB_FILE="picoclaw_riscv64.deb" + RPM_FILE="picoclaw_riscv64.rpm" + ;; + linux/mipsle) + TAR_FILE="picoclaw_Linux_mipsle.tar.gz" + DEB_FILE="picoclaw_mipsle.deb" + RPM_FILE="picoclaw_mipsle.rpm" + ;; + linux/s390x) + TAR_FILE="picoclaw_Linux_s390x.tar.gz" + DEB_FILE="picoclaw_s390x.deb" + RPM_FILE="picoclaw_s390x.rpm" + ;; + + macos/x86_64) TAR_FILE="picoclaw_Darwin_x86_64.tar.gz" ;; + macos/arm64) TAR_FILE="picoclaw_Darwin_arm64.tar.gz" ;; + + freebsd/x86_64) TAR_FILE="picoclaw_Freebsd_x86_64.tar.gz" ;; + freebsd/arm64) TAR_FILE="picoclaw_Freebsd_arm64.tar.gz" ;; + freebsd/armv7) TAR_FILE="picoclaw_Freebsd_armv7.tar.gz" ;; + + netbsd/x86_64) TAR_FILE="picoclaw_Netbsd_x86_64.tar.gz" ;; + netbsd/arm64) TAR_FILE="picoclaw_Netbsd_arm64.tar.gz" ;; + + *) die "No asset mapping for $os/$arch" ;; + esac +} + +resolve_unix_system_layout() { + LAYOUT_MODE="symlink" + if [[ -d "/usr/local/share" ]]; then + INSTALL_ROOT="/usr/local/share/picoclaw" + LINK_DIR="/usr/local/bin" + return + fi + + if [[ -d "/usr/share" ]]; then + INSTALL_ROOT="/usr/share/picoclaw" + LINK_DIR="/usr/bin" + return + fi + + INSTALL_ROOT="$(create_secure_tmp_dir "picoclaw-install" || true)" + [[ -n "$INSTALL_ROOT" ]] || die "Failed to create secure temporary install directory under /tmp" + LINK_DIR="/usr/bin" + LAYOUT_MODE="copy-executables" +} + +resolve_layout() { + local os="$1" + + if [[ "$INSTALL_MODE" == "user" ]]; then + INSTALL_ROOT="$HOME/.local/share/picoclaw" + LINK_DIR="$HOME/.local/bin" + LAYOUT_MODE="user" + return + fi + + case "$os" in + linux|macos|freebsd|netbsd) + resolve_unix_system_layout + ;; + *) + die "Bash installer does not support this OS: $os" + ;; + esac +} + +install_via_package_manager_linux() { + local tmp_dir="$1" + local base + base="$(base_url)" + + if [[ -n "$DEB_FILE" ]] && command_exists dpkg; then + local deb_path="$tmp_dir/$DEB_FILE" + download_file "${base}${DEB_FILE}" "$deb_path" + dpkg -i "$deb_path" + PM_RESULT="dpkg:$deb_path" + return 0 + fi + + if [[ -n "$RPM_FILE" ]] && command_exists rpm; then + local rpm_path="$tmp_dir/$RPM_FILE" + download_file "${base}${RPM_FILE}" "$rpm_path" + rpm -Uvh --replacepkgs "$rpm_path" + PM_RESULT="rpm:$rpm_path" + return 0 + fi + + PM_RESULT="" + return 1 +} + +extract_archive() { + local archive="$1" + local dst="$2" + mkdir -p "$dst" + + case "$archive" in + *.tar.gz) + tar -xzf "$archive" -C "$dst" + ;; + *.zip) + if command_exists unzip; then + unzip -o "$archive" -d "$dst" >/dev/null + else + die "unzip is required for zip archives" + fi + ;; + *) + die "Unsupported archive: $archive" + ;; + esac +} + +resolve_flat_binary_path() { + local root="$1" + local name="$2" + local candidate="$root/$name" + + if [[ -f "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + + return 1 +} + +link_expected_bins() { + local root="$1" + local link_dir="$2" + + mkdir -p "$link_dir" + LINKED_PATHS=() + + local name bin target + for name in "${EXPECTED_BINS[@]}"; do + bin="$(resolve_flat_binary_path "$root" "$name" || true)" + if [[ -z "$bin" ]]; then + continue + fi + target="$link_dir/$name" + rm -f "$target" + ln -s "$bin" "$target" + LINKED_PATHS+=("$target") + done +} + +copy_executables_to_bin() { + local root="$1" + local bin_dir="$2" + + mkdir -p "$bin_dir" + COPIED_PATHS=() + + local name file dest + for name in "${EXPECTED_BINS[@]}"; do + file="$(resolve_flat_binary_path "$root" "$name" || true)" + if [[ -z "$file" ]]; then + continue + fi + if [[ ! -x "$file" ]]; then + continue + fi + + dest="$bin_dir/$name" + cp -f "$file" "$dest" + chmod +x "$dest" || true + COPIED_PATHS+=("$dest") + done +} + +ensure_user_path_export() { + local line='export PATH="$HOME/.local/bin:$PATH"' + local profile + + if [[ -f "$HOME/.bashrc" ]]; then + profile="$HOME/.bashrc" + elif [[ -f "$HOME/.bash_profile" ]]; then + profile="$HOME/.bash_profile" + else + profile="$HOME/.profile" + fi + + touch "$profile" + if ! grep -Fq "$line" "$profile"; then + printf '\n%s\n' "$line" >> "$profile" + fi + USER_PROFILE="$profile" +} + +warn_missing_user_local() { + if [[ "$INSTALL_MODE" != "user" ]]; then + return + fi + if [[ ! -d "$HOME/.local" ]]; then + printf '\033[33mWarning: %s/.local does not exist. The installer will create it automatically.\033[0m\n' "$HOME" + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + INSTALL_MODE="${2:-}" + shift 2 + ;; + --source) + SOURCE="${2:-}" + shift 2 + ;; + --arch) + ARCH_OVERRIDE="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown argument: $1" + ;; + esac +done + +[[ "$INSTALL_MODE" == "system" || "$INSTALL_MODE" == "user" ]] || die "--mode must be system or user" +[[ "$SOURCE" == "github" || "$SOURCE" == "cdn" ]] || die "--source must be github or cdn" + +OS_ID="$(detect_os)" +ARCH_ID="$(detect_arch)" +assert_privileges +warn_missing_user_local + +asset_for "$OS_ID" "$ARCH_ID" +resolve_layout "$OS_ID" + +TMP_DIR="$(create_secure_tmp_dir "picoclaw-download")" +[[ -n "$TMP_DIR" ]] || die "Failed to create temporary working directory" +trap 'rm -rf "$TMP_DIR"' EXIT + +log "Install mode: $INSTALL_MODE" +log "OS/Arch: $OS_ID/$ARCH_ID" + +if [[ "$OS_ID" == "linux" && "$INSTALL_MODE" == "system" ]]; then + if install_via_package_manager_linux "$TMP_DIR"; then + log "Installed via package manager: ${PM_RESULT%%:*}" + log "Package file: ${PM_RESULT#*:}" + log "Install layout is managed by the package manager." + exit 0 + fi +fi + +ARCHIVE_PATH="$TMP_DIR/$TAR_FILE" +download_file "$(base_url)${TAR_FILE}" "$ARCHIVE_PATH" + +mkdir -p "$INSTALL_ROOT" +extract_archive "$ARCHIVE_PATH" "$INSTALL_ROOT" + +if [[ "$LAYOUT_MODE" == "copy-executables" ]]; then + copy_executables_to_bin "$INSTALL_ROOT" "$LINK_DIR" + log "Extracted to: $INSTALL_ROOT" + log "Copied executable files to: $LINK_DIR" + for p in "${COPIED_PATHS[@]:-}"; do + [[ -n "$p" ]] && log " - $p" + done +elif [[ "$LAYOUT_MODE" == "symlink" || "$LAYOUT_MODE" == "user" ]]; then + link_expected_bins "$INSTALL_ROOT" "$LINK_DIR" + log "Extracted to: $INSTALL_ROOT" + log "Created links in: $LINK_DIR" + for p in "${LINKED_PATHS[@]:-}"; do + [[ -n "$p" ]] && log " - $p" + done +fi + +if [[ "$INSTALL_MODE" == "user" ]]; then + ensure_user_path_export + log "PATH export ensured in: $USER_PROFILE" +fi