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