diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ed0e3b1..00c6919 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -23,7 +23,9 @@ jobs: cache: true - name: Build - run: make build + run: | + make build + make build-windows - name: Test run: make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index abac40e..267af49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,10 @@ jobs: goarch: amd64 - goos: darwin goarch: arm64 + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 steps: - name: Checkout @@ -45,18 +49,31 @@ jobs: GOARCH: ${{ matrix.goarch }} run: | make clean - make build + if [ "$GOOS" = "windows" ]; then + make "build-windows-${GOARCH}" + else + make build + fi - name: Package + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} run: | mkdir -p dist - tar -C bin -czf "dist/ucloud-sandbox-cli_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz" ucloud-sandbox-cli + if [ "$GOOS" = "windows" ]; then + zip -j "dist/ucloud-sandbox-cli_${GOOS}_${GOARCH}.zip" "bin/windows/${GOARCH}/ucloud-sandbox-cli.exe" + else + tar -C bin -czf "dist/ucloud-sandbox-cli_${GOOS}_${GOARCH}.tar.gz" ucloud-sandbox-cli + fi - name: Upload artifact uses: actions/upload-artifact@v4 with: name: ucloud-sandbox-cli-${{ matrix.goos }}-${{ matrix.goarch }} - path: dist/*.tar.gz + path: | + dist/*.tar.gz + dist/*.zip if-no-files-found: error release: @@ -83,7 +100,7 @@ jobs: GH_REPO: ${{ github.repository }} TAG_NAME: ${{ github.ref_name }} run: | - gh release create "${TAG_NAME}" dist/*.tar.gz \ + gh release create "${TAG_NAME}" dist/*.tar.gz dist/*.zip \ --draft \ --verify-tag \ --title "${TAG_NAME}" \ diff --git a/Makefile b/Makefile index 6c009b5..07bed96 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,15 @@ build: @mkdir -p bin CGO_ENABLED=0 go build -ldflags "-X main.Version=$(VERSION) -X main.Commit=$(COMMIT)" -o bin/ucloud-sandbox-cli . +.PHONY: build-windows build-windows-amd64 build-windows-arm64 +build-windows: build-windows-amd64 build-windows-arm64 + +build-windows-amd64: WINDOWS_ARCH := amd64 +build-windows-arm64: WINDOWS_ARCH := arm64 +build-windows-amd64 build-windows-arm64: + @mkdir -p bin/windows/$(WINDOWS_ARCH) + CGO_ENABLED=0 GOOS=windows GOARCH=$(WINDOWS_ARCH) go build -ldflags "-X main.Version=$(VERSION) -X main.Commit=$(COMMIT)" -o bin/windows/$(WINDOWS_ARCH)/ucloud-sandbox-cli.exe . + .PHONY: test test: CGO_ENABLED=0 go test -v ./... diff --git a/README.md b/README.md index f5011ab..a299f70 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ npm uninstall -g @ucloud-sdks/ucloud-sandbox-cli 如果您之前从未安装过`ucloud-sandbox-cli`,或者没有使用npm安装过,可以跳过这一步。 -### 安装CLI +### 安装 CLI -使用下面的命令安装CLI: +Linux 和 macOS 使用: ```bash curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.sh | sh @@ -28,6 +28,14 @@ curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/instal 安装脚本会要求您确认安装路径(默认为`/usr/local/bin`),直接输入回车可以确认安装,或者您也可以手动输入安装路径。注意请确保安装路径在您的`$PATH`下面以可以直接使用命令行。 +Windows 在 PowerShell 中使用: + +```powershell +Invoke-RestMethod https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.ps1 | Invoke-Expression +``` + +Windows 安装脚本会自动选择 amd64 或 arm64 版本,默认安装到`%LOCALAPPDATA%\Programs\ucloud-sandbox-cli`,并将该目录加入用户`PATH`。 + ## 身份认证与配置 ### 环境注入 @@ -178,4 +186,4 @@ ucloud-sandbox-cli tpl publish --unpublish 1. 准备环境:`ucloud-sandbox-cli login` 2. 创建模板:`ucloud-sandbox-cli template init` -> 编写 `Dockerfile` 3. 业务接入:在 SDK 中使用 `Sandbox.create(template='my-agent-env')` -4. 资源回收:`ucloud-sandbox-cli sandbox kill --all` \ No newline at end of file +4. 资源回收:`ucloud-sandbox-cli sandbox kill --all` diff --git a/cmd/sandbox/connect.go b/cmd/sandbox/connect.go index 9984691..cb87fcf 100644 --- a/cmd/sandbox/connect.go +++ b/cmd/sandbox/connect.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "os" - "os/signal" - "syscall" "github.com/spf13/cobra" "github.com/ucloud/ucloud-sandbox-cli/internal/config" @@ -18,7 +16,7 @@ func newConnectCmd() *cobra.Command { Use: "connect ", Aliases: []string{"conn"}, Short: "Connect to a running sandbox", - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config.Load() if err != nil { @@ -81,17 +79,8 @@ func connectTerminal(ctx context.Context, sbx *sdk.Sandbox) error { } }() - // Handle terminal resize. - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGWINCH) - go func() { - for range sigCh { - if c, r, err := term.GetSize(fd); err == nil { - handle.Resize(ctx, sdk.PtySize{Cols: c, Rows: r}) - } - } - }() - defer signal.Stop(sigCh) + stopResize := watchTerminalResize(ctx, fd, handle, cols, rows) + defer stopResize() handle.Wait() return nil diff --git a/cmd/sandbox/connect_resize_unix.go b/cmd/sandbox/connect_resize_unix.go new file mode 100644 index 0000000..e5c680c --- /dev/null +++ b/cmd/sandbox/connect_resize_unix.go @@ -0,0 +1,39 @@ +//go:build darwin || linux + +package sandbox + +import ( + "context" + "os" + "os/signal" + "syscall" + + sdk "github.com/ucloud/ucloud-sandbox-sdk-go" + "golang.org/x/term" +) + +func watchTerminalResize(ctx context.Context, fd int, handle *sdk.PtyHandle, _, _ int) func() { + sigCh := make(chan os.Signal, 1) + done := make(chan struct{}) + signal.Notify(sigCh, syscall.SIGWINCH) + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-done: + return + case <-sigCh: + if cols, rows, err := term.GetSize(fd); err == nil { + _ = handle.Resize(ctx, sdk.PtySize{Cols: cols, Rows: rows}) + } + } + } + }() + + return func() { + signal.Stop(sigCh) + close(done) + } +} diff --git a/cmd/sandbox/connect_resize_windows.go b/cmd/sandbox/connect_resize_windows.go new file mode 100644 index 0000000..6473be3 --- /dev/null +++ b/cmd/sandbox/connect_resize_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package sandbox + +import ( + "context" + "time" + + sdk "github.com/ucloud/ucloud-sandbox-sdk-go" + "golang.org/x/term" +) + +func watchTerminalResize(ctx context.Context, fd int, handle *sdk.PtyHandle, cols, rows int) func() { + done := make(chan struct{}) + go func() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-done: + return + case <-ticker.C: + newCols, newRows, err := term.GetSize(fd) + if err != nil || (newCols == cols && newRows == rows) { + continue + } + if err := handle.Resize(ctx, sdk.PtySize{Cols: newCols, Rows: newRows}); err == nil { + cols, rows = newCols, newRows + } + } + } + }() + + return func() { close(done) } +} diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..83a27c4 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS +Downloads and installs UCloud Sandbox CLI on Windows. + +.PARAMETER Version +Release tag to install. Defaults to the latest release. + +.PARAMETER InstallDir +Directory where ucloud-sandbox-cli.exe is installed. + +.PARAMETER BaseUrl +Base URL of the GitHub releases page. + +.EXAMPLE +Invoke-RestMethod https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.ps1 | Invoke-Expression + +.EXAMPLE +.\install.ps1 -Version v1.2.3 +#> + +[CmdletBinding()] +param( + [ValidateNotNullOrEmpty()] + [string]$Version = "latest", + + [string]$InstallDir = "", + + [ValidateNotNullOrEmpty()] + [string]$BaseUrl = "https://github.com/ucloud/ucloud-sandbox-cli/releases" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$BinaryName = "ucloud-sandbox-cli" +$DocsUrl = "https://astraflow.ucloud.cn/docs/agent-sandbox/product/cli" + +function Write-Info { + param([string]$Message) + Write-Host "> $Message" +} + +function Write-Success { + param([string]$Message) + Write-Host "+ $Message" -ForegroundColor Green +} + +function Get-WindowsArchitecture { + $machineArchitecture = if ($env:PROCESSOR_ARCHITEW6432) { + $env:PROCESSOR_ARCHITEW6432 + } else { + $env:PROCESSOR_ARCHITECTURE + } + + if ([string]::IsNullOrWhiteSpace($machineArchitecture)) { + throw "Unable to detect the Windows architecture." + } + + switch ($machineArchitecture.ToUpperInvariant()) { + "AMD64" { return "amd64" } + "ARM64" { return "arm64" } + default { throw "Unsupported Windows architecture: $machineArchitecture. Supported architectures are: amd64, arm64." } + } +} + +function Add-InstallDirToPath { + param([string]$Directory) + + $normalizedDirectory = [Environment]::ExpandEnvironmentVariables($Directory).TrimEnd("\") + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $userEntries = @($userPath -split ";" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $isInUserPath = $userEntries | Where-Object { + [Environment]::ExpandEnvironmentVariables($_).Trim().TrimEnd("\") -ieq $normalizedDirectory + } + + if (-not $isInUserPath) { + $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { + $Directory + } else { + "$Directory;$userPath" + } + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + Write-Info "Added $Directory to the user PATH." + } + + $processEntries = @($env:Path -split ";" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $isInProcessPath = $processEntries | Where-Object { + [Environment]::ExpandEnvironmentVariables($_).Trim().TrimEnd("\") -ieq $normalizedDirectory + } + if (-not $isInProcessPath) { + $env:Path = "$Directory;$env:Path" + } +} + +if ($env:OS -ne "Windows_NT") { + throw "install.ps1 only supports Windows. Use install.sh on Linux or macOS." +} + +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) { + throw "LOCALAPPDATA is not set. Pass -InstallDir with a writable installation directory." + } + $InstallDir = Join-Path $env:LOCALAPPDATA "Programs\ucloud-sandbox-cli" +} +$InstallDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InstallDir) + +$architecture = Get-WindowsArchitecture +$assetName = "${BinaryName}_windows_${architecture}.zip" +$releaseBaseUrl = $BaseUrl.TrimEnd("/") +$downloadUrl = if ($Version -eq "latest") { + "$releaseBaseUrl/latest/download/$assetName" +} else { + "$releaseBaseUrl/download/$Version/$assetName" +} +$targetBinary = Join-Path $InstallDir "$BinaryName.exe" +$tempDir = Join-Path ([IO.Path]::GetTempPath()) "$BinaryName-install-$([Guid]::NewGuid().ToString('N'))" +$archivePath = Join-Path $tempDir $assetName +$extractDir = Join-Path $tempDir "extract" + +Write-Host "" +Write-Info "Welcome to the $BinaryName installer." +Write-Info "Installer configuration:" +Write-Info " Version: $Version" +Write-Info " OS: windows" +Write-Info " Arch: $architecture" +Write-Info " Install dir: $InstallDir" +Write-Info " Download URL: $downloadUrl" +Write-Host "" + +try { + New-Item -ItemType Directory -Force -Path $tempDir, $extractDir, $InstallDir | Out-Null + + # Windows PowerShell 5.1 can otherwise negotiate an obsolete TLS version. + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + + Write-Info "Downloading $BinaryName..." + Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing + + Write-Info "Installing $BinaryName to $InstallDir..." + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDir -Force + $extractedBinary = Join-Path $extractDir "$BinaryName.exe" + if (-not (Test-Path -LiteralPath $extractedBinary -PathType Leaf)) { + throw "Archive did not contain $BinaryName.exe." + } + + Copy-Item -LiteralPath $extractedBinary -Destination $targetBinary -Force + Unblock-File -LiteralPath $targetBinary -ErrorAction SilentlyContinue + Add-InstallDirToPath -Directory $InstallDir + + Write-Success "$BinaryName installed successfully." + Write-Host "" + Write-Info "Installed binary version:" + & $targetBinary version + if ($LASTEXITCODE -ne 0) { + throw "$BinaryName version exited with code $LASTEXITCODE." + } +} finally { + if (Test-Path -LiteralPath $tempDir) { + Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +Write-Host "" +Write-Info "Documentation: $DocsUrl" +Write-Info "Run '$BinaryName login' first, then start using the CLI." diff --git a/skills/ucloud-sandbox-site/SKILL.md b/skills/ucloud-sandbox-site/SKILL.md index 3a90eb0..aa0e281 100644 --- a/skills/ucloud-sandbox-site/SKILL.md +++ b/skills/ucloud-sandbox-site/SKILL.md @@ -1,12 +1,18 @@ --- name: ucloud-sandbox-site -description: 当用户提供以 `site_` 开头的 UCloud 站点空间 ID,并要求连接或操作站点空间时使用。适用于通过 ucloud-sandbox-cli 验证站点连接、执行命令、浏览与管理文件、上传或下载代码、读取站点环境变量,以及生成、构建、部署和排查运行在 80 端口的网站服务;同时遵守站点凭证的单沙箱权限边界和敏感变量脱敏要求。 +description: 当用户提供以 `site_` 开头的 UCloud 站点空间 ID,并要求连接或操作站点空间时使用。适用于在 Linux、macOS 或 Windows 中通过 ucloud-sandbox-cli 验证站点连接、执行命令、浏览与管理文件、上传或下载代码、读取站点环境变量,以及生成、构建、部署和排查运行在 80 端口的网站服务;同时遵守站点凭证的单沙箱权限边界和敏感变量脱敏要求。 --- # UCloud 站点空间 使用 `ucloud-sandbox-cli` 操作用户已经创建的站点空间。站点空间底层是一个沙箱;本技能只负责连接和维护该站点,不创建站点或沙箱。 +## 前置检查和平台 + +执行 CLI 安装或调用站点 API 前,先确认当前环境允许访问公网。需要网络权限审批时先申请授权;未获授权时停止并说明原因。 + +在 Windows 或 PowerShell 环境中,执行安装、连接、文件传输或部署前,先完整阅读并遵循 [Windows PowerShell 指南](references/windows.md)。本页的 Bash 安装、凭证注入和本地打包命令仅适用于 Linux 和 macOS;传给 `sandbox exec` 的命令仍在远端 Linux Shell 中执行。 + ## 核心概念 - 站点 ID 格式为 `site_`。例如 `site_iy1qen6gs2835o0udufdz` 对应沙箱 ID `iy1qen6gs2835o0udufdz`。 @@ -32,7 +38,7 @@ description: 当用户提供以 `site_` 开头的 UCloud 站点空间 ID,并 ## 准备并验证 CLI -每次准备执行真实站点操作前,先检查是否存在旧 npm 版 CLI,并验证当前命令: +每次准备执行真实站点操作前,先检查是否存在旧 npm 版 CLI,并验证当前命令。Linux 和 macOS 使用以下流程: ```bash OLD_NPM_PACKAGE="@ucloud-sdks/ucloud-sandbox-cli" @@ -87,7 +93,7 @@ ucloud-sandbox-cli version 如果用户还没有提供站点 ID,只向用户索取 `site_...` 格式的站点 ID;不要索取普通 UCloud API Key。先按照上一节完成 CLI 安装和验证。 -在同一个 shell 调用中设置凭证并派生沙箱 ID: +Linux 和 macOS 在同一个 shell 调用中设置凭证并派生沙箱 ID: ```bash SITE_ID='site_' diff --git a/skills/ucloud-sandbox-site/references/windows.md b/skills/ucloud-sandbox-site/references/windows.md new file mode 100644 index 0000000..b1ff448 --- /dev/null +++ b/skills/ucloud-sandbox-site/references/windows.md @@ -0,0 +1,142 @@ +# Windows PowerShell 指南 + +在 Windows 中使用 `ucloud-sandbox-site` 连接、维护和部署站点空间时遵循本指南,同时继续遵守主 `SKILL.md` 的权限边界、凭证保护和部署要求。 + +## 目录 + +- [本地与远端边界](#本地与远端边界) +- [检查并安装 CLI](#检查并安装-cli) +- [设置站点凭证并验证连接](#设置站点凭证并验证连接) +- [常用文件和命令操作](#常用文件和命令操作) +- [打包并上传目录](#打包并上传目录) +- [执行多行远端命令](#执行多行远端命令) +- [故障处理](#故障处理) + +## 本地与远端边界 + +- 本地使用 PowerShell 和 Windows `.exe`,不要执行主 `SKILL.md` 中的 Bash 安装、凭证注入或本地打包命令。 +- `sandbox exec` 的命令在远端 Linux 沙箱中运行,继续使用 `/home/user/...`、`source`、`sudo -n` 等 Linux 语法。 +- 本地路径使用 `C:\...`;远端路径使用 `/`。远端端点写成 `${SandboxId}:/path`,避免 PowerShell 把冒号解析成变量作用域。 +- 把完整 `site_...` 视为凭证,不要输出、记录或写入配置文件。每次开启新的 PowerShell 调用时重新设置凭证。 +- 执行需要公网的操作前,先按主 `SKILL.md` 申请并确认网络权限。 + +## 检查并安装 CLI + +检查并卸载旧 npm 版;仅在命令不存在时调用独立 `install.ps1`。卸载需要管理员权限时,让用户在真实终端处理: + +```powershell +if (Get-Command npm -ErrorAction SilentlyContinue) { + npm list -g "@ucloud-sdks/ucloud-sandbox-cli" --depth=0 *> $null + if ($LASTEXITCODE -eq 0) { + npm uninstall -g "@ucloud-sdks/ucloud-sandbox-cli" + if ($LASTEXITCODE -ne 0) { throw "Failed to uninstall the old npm CLI." } + } +} + +if (-not (Get-Command ucloud-sandbox-cli -CommandType Application -ErrorAction SilentlyContinue)) { + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + $InstallerUrl = "https://raw.githubusercontent.com/Piwriw/ucloud-sandbox-cli/feat/windows/install.ps1" + & ([scriptblock]::Create((Invoke-RestMethod -Uri $InstallerUrl -UseBasicParsing -ErrorAction Stop))) +} + +ucloud-sandbox-cli version +if ($LASTEXITCODE -ne 0) { throw "ucloud-sandbox-cli verification failed." } +``` + +安装脚本默认安装到 `%LOCALAPPDATA%\Programs\ucloud-sandbox-cli`,并更新当前进程和用户 `PATH`。不要自动更新已经可正常运行的 CLI。 + +## 设置站点凭证并验证连接 + +只在当前 PowerShell 进程中设置完整站点 ID,并派生去掉 `site_` 前缀的沙箱 ID: + +```powershell +$SiteId = "site_" + +if (-not $SiteId.StartsWith("site_", [StringComparison]::Ordinal) -or $SiteId.Length -le 5) { + throw "Invalid site ID. Expected site_." +} + +$SandboxId = $SiteId.Substring(5) +$env:UCLOUD_SANDBOX_API_KEY = $SiteId + +ucloud-sandbox-cli sandbox exec $SandboxId "printf 'SITE_CONNECTED\n'; pwd" +if ($LASTEXITCODE -ne 0) { throw "Site connection verification failed." } +``` + +不要用 `sandbox list` 验证连接。地域或 API 域名仍沿用已有 CLI 配置;需要临时指定时使用 `$env:UCLOUD_SANDBOX_REGION` 和 `$env:UCLOUD_SANDBOX_DOMAIN`,没有证据时不要擅自切换。 + +## 常用文件和命令操作 + +主 `SKILL.md` 示例中的 `$SANDBOX_ID` 在 PowerShell 中统一写成 `$SandboxId`: + +```powershell +ucloud-sandbox-cli sandbox exec $SandboxId "pwd && ls -la /home/user" +ucloud-sandbox-cli fs ls $SandboxId /home/user --format json +ucloud-sandbox-cli fs mkdir $SandboxId /home/user/site + +# 上传 +ucloud-sandbox-cli fs cp "C:\work\index.html" "${SandboxId}:/home/user/site/index.html" + +# 下载 +ucloud-sandbox-cli fs cp "${SandboxId}:/home/user/site/service.log" "C:\work\service.log" +``` + +读取文件、删除文件、部署服务和查看环境变量时,继续遵守主 `SKILL.md` 的敏感信息与破坏性操作限制。 + +## 打包并上传目录 + +Windows 自带或已安装 `tar` 时,可以在本地 PowerShell 打包;排除凭证、依赖和版本控制目录: + +```powershell +$LocalProjectDir = "C:\work\site" +$Archive = Join-Path ([IO.Path]::GetTempPath()) "site-release-$PID.tgz" + +if (-not (Test-Path -LiteralPath $LocalProjectDir -PathType Container)) { + throw "Local project directory does not exist: $LocalProjectDir" +} + +try { + tar --exclude=.git --exclude=.env --exclude=.env.* --exclude=.site.env --exclude=node_modules ` + -czf $Archive -C $LocalProjectDir . + if ($LASTEXITCODE -ne 0) { throw "Failed to create deployment archive." } + + ucloud-sandbox-cli fs cp $Archive "${SandboxId}:/tmp/site-release.tgz" + if ($LASTEXITCODE -ne 0) { throw "Failed to upload deployment archive." } + + ucloud-sandbox-cli sandbox exec $SandboxId ` + "mkdir -p /home/user/site && tar -xzf /tmp/site-release.tgz -C /home/user/site && rm -f /tmp/site-release.tgz" + if ($LASTEXITCODE -ne 0) { throw "Failed to extract deployment archive." } +} finally { + Remove-Item -LiteralPath $Archive -Force -ErrorAction SilentlyContinue +} +``` + +上传前先检查目标目录;不要无条件清空已有网站。 + +## 执行多行远端命令 + +包含 `$`、`$()`、引号或多行 Linux Shell 的远端命令使用单引号 here-string,并把 CRLF 转换为 LF: + +```powershell +$RemoteCommand = @' +set -e +cd /home/user/site +set -a +source /home/user/.site.env +set +a +npm ci +npm run build +'@ +$RemoteCommand = $RemoteCommand.Replace("`r`n", "`n") + +ucloud-sandbox-cli sandbox exec $SandboxId $RemoteCommand +``` + +主 `SKILL.md` 中的环境变量脱敏、持久启动和服务诊断命令都按此模式传入;不要让 PowerShell 在本地提前展开远端 `$HOME`、`$PID` 或 `$()`。 + +## 故障处理 + +- 命令安装成功但找不到时,将安装目录加入当前进程 `PATH`:`$env:Path = "<安装目录>;$env:Path"`,并确认安装目录已写入用户 `PATH`。 +- 出现 `Variable reference is not valid` 时,检查远端端点是否写成 `${SandboxId}:/path`。 +- 远端多行命令出现 `\r` 或语法错误时,确认 here-string 已通过 `.Replace("`r`n", "`n")` 转换换行。 +- 新的 PowerShell 调用提示缺少 API Key 时,重新设置 `$env:UCLOUD_SANDBOX_API_KEY = $SiteId`,不要把站点 ID 写入持久化配置。 diff --git a/skills/ucloud-sandbox/SKILL.md b/skills/ucloud-sandbox/SKILL.md index 37732fb..9e3e7f9 100644 --- a/skills/ucloud-sandbox/SKILL.md +++ b/skills/ucloud-sandbox/SKILL.md @@ -1,15 +1,23 @@ --- name: ucloud-sandbox -description: 当用户需要用 UCloud Sandbox CLI 操作沙箱服务时使用,包括安装或配置 ucloud-sandbox-cli、设置 API Key 和地域、创建/连接/执行/暂停/终止沙箱、浏览和管理沙箱文件、上传或下载文件、查看端口地址和监控指标、管理快照与模板,以及在 Claude Code、Codex、Gemini 等 Agent 中安装本技能。 +description: 当用户需要在 Linux、macOS 或 Windows 中用 UCloud Sandbox CLI 操作沙箱服务时使用,包括安装或配置 ucloud-sandbox-cli、设置 API Key 和地域、创建/连接/执行/暂停/终止沙箱、浏览和管理沙箱文件、上传或下载文件、查看端口地址和监控指标、管理快照与模板,以及在 Claude Code、Codex、Gemini 等 Agent 中安装本技能。 --- # UCloud Sandbox CLI 使用 `ucloud-sandbox-cli` 管理 UCloud Sandbox 沙箱、快照和模板。优先用 CLI 完成操作;如果用户只是在询问命令,给出可复制的命令即可。 +## 前置检查:公网权限 + +执行安装、更新或调用 UCloud Sandbox API 前,先确认当前环境允许访问公网。需要网络权限审批时先申请授权;未获授权时停止并说明原因。仅检查本地版本或提供命令说明时无需申请。 + +## 平台说明 + +在 Windows 或 PowerShell 环境中,执行安装、更新、认证配置、文件传输或其他 CLI 操作前,先完整阅读并遵循 [Windows PowerShell 指南](references/windows.md)。本页的 Bash 安装、更新和配置脚本仅适用于 Linux 和 macOS。 + ## 安装本技能 -仅在用户要求“安装这个 skill/技能”时执行。把 `SKILL.md` 放到目标 Agent 的技能目录。可设置 `TARGET_AGENT=codex|claude|gemini|auto`,默认自动检测: +仅在用户要求“安装这个 skill/技能”时执行。Linux 和 macOS 把 `SKILL.md` 放到目标 Agent 的技能目录。可设置 `TARGET_AGENT=codex|claude|gemini|auto`,默认自动检测: ```bash set -eu @@ -50,7 +58,7 @@ curl -fsSL "$SKILL_URL" -o "$SKILL_DIR/SKILL.md" echo "ucloud-sandbox skill installed to $SKILL_DIR" ``` -常见目录: +Linux 和 macOS 常见目录: - Codex: `TARGET_AGENT=codex`,目录为 `${CODEX_HOME:-$HOME/.codex}/skills/ucloud-sandbox` - Claude Code: `TARGET_AGENT=claude`,目录为 `$HOME/.claude/skills/ucloud-sandbox` @@ -58,7 +66,9 @@ echo "ucloud-sandbox skill installed to $SKILL_DIR" ## Step 0:确保 CLI 可用 -每次准备执行真实操作前先检查: +每次准备执行真实操作前先检查。Linux 和 macOS 使用以下 Bash 流程。 + +### Linux 和 macOS ```bash OLD_NPM_PACKAGE="@ucloud-sdks/ucloud-sandbox-cli" @@ -113,7 +123,7 @@ ucloud-sandbox-cli version 仅当用户明确要求“更新 skill/技能”或“更新 ucloud-sandbox-cli/命令行”时执行;不要在普通沙箱操作前自动更新。 -更新本技能: +Linux 和 macOS 更新本技能: ```bash set -eu @@ -154,7 +164,7 @@ curl -fsSL "$SKILL_URL" -o "$SKILL_DIR/SKILL.md" echo "ucloud-sandbox skill updated at $SKILL_DIR" ``` -更新 `ucloud-sandbox-cli` 到最新版本: +Linux 和 macOS 更新 `ucloud-sandbox-cli` 到最新版本: ```bash curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.sh | sh -s -- -y @@ -178,7 +188,7 @@ curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/instal API Key 可从星图平台密钥管理获取:`https://astraflow.ucloud.cn/modelverse/api-keys`。常用地域包括 `cn-wlcb` 和 `us-ca`;不确定时询问用户。 -持久化配置文件路径是 `~/.ucloud-sandbox-cli/config.json`,建议目录权限为 `700`、文件权限为 `600`。配置文件是 JSON,格式如下;展示或读取时必须隐藏 `api_key`: +持久化配置文件路径是 `~/.ucloud-sandbox-cli/config.json`。Linux 和 macOS 建议目录权限为 `700`、文件权限为 `600`。配置文件是 JSON,格式如下;展示或读取时必须隐藏 `api_key`: ```json { @@ -213,6 +223,8 @@ export UCLOUD_SANDBOX_REGION="cn-wlcb" 切换持久化地域时,Agent 不执行 `ucloud-sandbox-cli region`,直接修改已有配置文件的 `region` 字段。修改前先确认配置文件存在;如果不存在,提示用户先在真实终端执行 `ucloud-sandbox-cli login`。 +Linux 和 macOS: + ```bash CONFIG_FILE="$HOME/.ucloud-sandbox-cli/config.json" NEW_REGION="cn-wlcb" @@ -228,10 +240,12 @@ mv "$tmp" "$CONFIG_FILE" chmod 600 "$HOME/.ucloud-sandbox-cli/config.json" ``` -如果没有 `jq`,不要用易误伤 `api_key` 的字符串替换方案;请提示用户安装 `jq`,或让用户在真实终端运行 `ucloud-sandbox-cli region` 自行切换。 +Linux 和 macOS 如果没有 `jq`,不要用易误伤 `api_key` 的字符串替换方案;提示用户安装 `jq`,或让用户在真实终端运行 `ucloud-sandbox-cli region` 自行切换。 需要读取配置确认地域或域名时,必须隐藏 `api_key`,不要 `cat ~/.ucloud-sandbox-cli/config.json`。优先只读取必要字段: +Linux 和 macOS: + ```bash jq -r '.region // empty' "$HOME/.ucloud-sandbox-cli/config.json" jq -r '.domain // empty' "$HOME/.ucloud-sandbox-cli/config.json" @@ -243,7 +257,7 @@ jq -r '.domain // empty' "$HOME/.ucloud-sandbox-cli/config.json" jq '.api_key = if .api_key then "***hidden***" else . end' "$HOME/.ucloud-sandbox-cli/config.json" ``` -没有 `jq` 时,使用不会输出真实密钥的方式: +Linux 和 macOS 没有 `jq` 时,使用不会输出真实密钥的方式: ```bash sed -E 's/"api_key"[[:space:]]*:[[:space:]]*"[^"]*"/"api_key": "***hidden***"/' "$HOME/.ucloud-sandbox-cli/config.json" @@ -535,7 +549,7 @@ ucloud-sandbox-cli sandbox create --detach | 现象 | 处理 | | --- | --- | | `API key is required` | 提示用户在真实终端运行 `ucloud-sandbox-cli login`,或由用户自行设置 `UCLOUD_SANDBOX_API_KEY`;API Key 从星图平台 Key 管理获取 | -| 命令安装成功但找不到 | 把安装目录加入 `PATH`,例如 `export PATH="$HOME/.local/bin:$PATH"` | +| 命令安装成功但找不到 | Linux/macOS 使用 `export PATH="$HOME/.local/bin:$PATH"`;Windows 参见 [Windows 故障处理](references/windows.md#故障处理) | | 创建沙箱后卡在终端 | Agent/CI 中使用 `sandbox create ... --detach` | | `template not found` | 运行 `template list --format json` 确认模板 ID/名称 | | `sandbox not found` | 运行 `sandbox list --format json` 确认沙箱仍在运行 | diff --git a/skills/ucloud-sandbox/references/windows.md b/skills/ucloud-sandbox/references/windows.md new file mode 100644 index 0000000..f3d5b29 --- /dev/null +++ b/skills/ucloud-sandbox/references/windows.md @@ -0,0 +1,174 @@ +# Windows PowerShell 指南 + +在 Windows 中安装、更新、配置或调用 `ucloud-sandbox-cli` 时使用本指南。 + +## 目录 + +- [基本规则](#基本规则) +- [安装或更新本技能](#安装或更新本技能) +- [检查并安装 CLI](#检查并安装-cli) +- [更新 CLI](#更新-cli) +- [认证和配置](#认证和配置) +- [PowerShell 调用](#powershell-调用) +- [故障处理](#故障处理) + +## 基本规则 + +- 在本地使用 PowerShell 和 Windows `.exe`,不要执行 Bash 安装命令;沙箱内仍使用 Linux Shell。 +- 使用 `$env:NAME` 设置环境变量,本地路径使用 `C:\...`,远端路径仍使用 `/`。 +- 执行需要公网的操作前,遵守主 `SKILL.md` 的公网权限检查。 + +## 安装或更新本技能 + +仅在用户明确要求安装或更新技能时执行。把 `$TargetAgent` 设置为 `codex`、`claude` 或 `gemini`: + +```powershell +$TargetAgent = "codex" +$CodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME ".codex" } +$SkillRoot = switch ($TargetAgent) { + "codex" { Join-Path $CodexHome "skills" } + "claude" { Join-Path $HOME ".claude\skills" } + "gemini" { Join-Path $HOME ".gemini\skills" } + default { throw "TargetAgent must be codex, claude, or gemini." } +} +$SkillDir = Join-Path $SkillRoot "ucloud-sandbox" +$ReferencesDir = Join-Path $SkillDir "references" +$BaseUrl = "https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/skills/ucloud-sandbox" + +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +New-Item -ItemType Directory -Force -Path $SkillDir, $ReferencesDir | Out-Null +Invoke-WebRequest -Uri "$BaseUrl/SKILL.md" -OutFile (Join-Path $SkillDir "SKILL.md") -UseBasicParsing -ErrorAction Stop +Invoke-WebRequest -Uri "$BaseUrl/references/windows.md" -OutFile (Join-Path $ReferencesDir "windows.md") -UseBasicParsing -ErrorAction Stop +"ucloud-sandbox skill installed or updated at $SkillDir" +``` + +## 检查并安装 CLI + +检查并卸载旧 npm 版;仅在命令不存在时调用仓库根目录的独立 `install.ps1`。卸载需要管理员权限时,让用户在真实终端处理: + +```powershell +if (Get-Command npm -ErrorAction SilentlyContinue) { + npm list -g "@ucloud-sdks/ucloud-sandbox-cli" --depth=0 *> $null + if ($LASTEXITCODE -eq 0) { + npm uninstall -g "@ucloud-sdks/ucloud-sandbox-cli" + if ($LASTEXITCODE -ne 0) { throw "Failed to uninstall the old npm CLI." } + } +} + +if (-not (Get-Command ucloud-sandbox-cli -CommandType Application -ErrorAction SilentlyContinue)) { + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + & ([scriptblock]::Create((Invoke-RestMethod -Uri "https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.ps1" -UseBasicParsing -ErrorAction Stop))) +} + +ucloud-sandbox-cli version +if ($LASTEXITCODE -ne 0) { throw "ucloud-sandbox-cli verification failed." } +``` + +安装脚本默认安装到 `%LOCALAPPDATA%\Programs\ucloud-sandbox-cli`,并更新当前进程和用户 `PATH`。 + +## 更新 CLI + +仅在用户明确要求更新 CLI 时执行。更新到 latest 时保持 `$InstallArgs` 为空;指定版本或沿用自定义目录时设置相应字段,可以同时设置: + +```powershell +$InstallArgs = @{ + # Version = "v1.2.3" + # InstallDir = Join-Path $HOME "bin" +} + +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +$InstallerUrl = "https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.ps1" +& ([scriptblock]::Create((Invoke-RestMethod -Uri $InstallerUrl -UseBasicParsing -ErrorAction Stop))) @InstallArgs +ucloud-sandbox-cli version +if ($LASTEXITCODE -ne 0) { throw "ucloud-sandbox-cli verification failed." } +``` + +## 认证和配置 + +配置文件位于 `~/.ucloud-sandbox-cli/config.json`,使用用户配置目录的 ACL。不要输出真实 `api_key`。 + +临时使用环境变量时,让用户在自己的终端设置: + +```powershell +$env:UCLOUD_SANDBOX_API_KEY = "" +$env:UCLOUD_SANDBOX_REGION = "cn-wlcb" +$env:UCLOUD_SANDBOX_DOMAIN = "cn-wlcb.sandbox.ucloudai.com" +``` + +切换持久化地域时使用结构化 JSON API,不要输出 `$Config`。已有标准地域 `domain` 时同步更新,因为它优先于 `region`;检测到自定义域名时停止并先向用户确认: + +```powershell +$ConfigFile = Join-Path $HOME ".ucloud-sandbox-cli\config.json" +$NewRegion = "cn-wlcb" +$NewDomain = "$NewRegion.sandbox.ucloudai.com" + +if (-not (Test-Path -LiteralPath $ConfigFile -PathType Leaf)) { + throw "Config file not found. Run 'ucloud-sandbox-cli login' in a real terminal first." +} + +$Config = Get-Content -LiteralPath $ConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json +$Config | Add-Member -NotePropertyName "region" -NotePropertyValue $NewRegion -Force +$ExistingDomain = $Config.PSObject.Properties["domain"] +if ($ExistingDomain -and -not [string]::IsNullOrWhiteSpace([string]$ExistingDomain.Value)) { + if ([string]$ExistingDomain.Value -notmatch '^[a-z0-9-]+\.sandbox\.ucloudai\.com$') { + throw "Config uses a custom domain. Confirm it before changing the region." + } + $ExistingDomain.Value = $NewDomain +} +$Json = $Config | ConvertTo-Json -Depth 10 +$Utf8NoBom = New-Object System.Text.UTF8Encoding($false) +$TempFile = Join-Path ([IO.Path]::GetDirectoryName($ConfigFile)) "config.$PID.tmp" + +try { + [IO.File]::WriteAllText($TempFile, $Json, $Utf8NoBom) + [IO.File]::Replace($TempFile, $ConfigFile, $null) +} finally { + Remove-Item -LiteralPath $TempFile -Force -ErrorAction SilentlyContinue +} +``` + +只读取地域和域名时,不要输出整个配置: + +```powershell +$ConfigFile = Join-Path $HOME ".ucloud-sandbox-cli\config.json" +$Config = Get-Content -LiteralPath $ConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json +$Region = $Config.PSObject.Properties["region"] +$Domain = $Config.PSObject.Properties["domain"] +if ($Region) { "region=$($Region.Value)" } else { "region=" } +if ($Domain) { "domain=$($Domain.Value)" } else { "domain=" } +``` + +必须展示配置摘要时先脱敏: + +```powershell +$ConfigFile = Join-Path $HOME ".ucloud-sandbox-cli\config.json" +$Summary = Get-Content -LiteralPath $ConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json +$ApiKey = $Summary.PSObject.Properties["api_key"] +if ($ApiKey) { $ApiKey.Value = "***hidden***" } +$Summary | ConvertTo-Json -Depth 10 +``` + +## PowerShell 调用 + +- 变量后紧跟远端端点冒号时写成 `${SandboxId}:/path`,避免 PowerShell 把冒号解析为变量作用域。 +- 包含 `$`、`$()` 或多行 Shell 的远端命令使用单引号 here-string,并把 CRLF 转换为 LF。 + +```powershell +$SandboxId = "" +ucloud-sandbox-cli sandbox exec $SandboxId "pwd && ls -la" +ucloud-sandbox-cli fs cp "C:\work\index.html" "${SandboxId}:/home/user/app/index.html" + +$RemoteCommand = @' +printf 'HOME=%s\n' "$HOME" +'@ +$RemoteCommand = $RemoteCommand.Replace("`r`n", "`n") +ucloud-sandbox-cli sandbox exec $SandboxId $RemoteCommand +``` + +## 故障处理 + +命令安装成功但找不到时,将安装目录加入当前进程 `PATH`,并确认安装目录已经写入用户 `PATH`: + +```powershell +$env:Path = "<安装目录>;$env:Path" +```