From 6ec55a5326ef6b89b3ad40ad7d7f637cff19dbc5 Mon Sep 17 00:00:00 2001 From: joohwan Date: Fri, 24 Jul 2026 14:21:30 +0800 Subject: [PATCH] feat: add Windows support --- .github/workflows/check.yml | 4 +- .github/workflows/release.yml | 25 +++- Makefile | 9 ++ README.md | 14 ++- cmd/sandbox/connect.go | 17 +-- cmd/sandbox/connect_resize_unix.go | 39 ++++++ cmd/sandbox/connect_resize_windows.go | 38 ++++++ install.ps1 | 165 ++++++++++++++++++++++++++ skills/ucloud-sandbox/SKILL.md | 162 +++++++++++++++++++++++-- 9 files changed, 443 insertions(+), 30 deletions(-) create mode 100644 cmd/sandbox/connect_resize_unix.go create mode 100644 cmd/sandbox/connect_resize_windows.go create mode 100644 install.ps1 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/SKILL.md b/skills/ucloud-sandbox/SKILL.md index 37732fb..3ae0675 100644 --- a/skills/ucloud-sandbox/SKILL.md +++ b/skills/ucloud-sandbox/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -58,7 +58,9 @@ echo "ucloud-sandbox skill installed to $SKILL_DIR" ## Step 0:确保 CLI 可用 -每次准备执行真实操作前先检查: +每次准备执行真实操作前先检查。Linux 和 macOS 使用 Bash,Windows 使用 PowerShell。不要在 Windows 中执行 Bash 安装命令。 + +### Linux 和 macOS ```bash OLD_NPM_PACKAGE="@ucloud-sdks/ucloud-sandbox-cli" @@ -109,11 +111,35 @@ ucloud-sandbox-cli version 如果提示命令不存在,说明安装目录不在 `PATH` 中。引导用户把安装目录加入 `PATH`。 +### Windows PowerShell + +检查并卸载旧 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`。 + ## 按需更新 Skill 和 CLI 仅当用户明确要求“更新 skill/技能”或“更新 ucloud-sandbox-cli/命令行”时执行;不要在普通沙箱操作前自动更新。 -更新本技能: +Linux 和 macOS 更新本技能: ```bash set -eu @@ -154,7 +180,27 @@ curl -fsSL "$SKILL_URL" -o "$SKILL_DIR/SKILL.md" echo "ucloud-sandbox skill updated at $SKILL_DIR" ``` -更新 `ucloud-sandbox-cli` 到最新版本: +Windows PowerShell 更新本技能。把 `$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" +$SkillUrl = "https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/skills/ucloud-sandbox/SKILL.md" + +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +New-Item -ItemType Directory -Force -Path $SkillDir | Out-Null +Invoke-WebRequest -Uri $SkillUrl -OutFile (Join-Path $SkillDir "SKILL.md") -UseBasicParsing -ErrorAction Stop +"ucloud-sandbox skill updated at $SkillDir" +``` + +Linux 和 macOS 更新 `ucloud-sandbox-cli` 到最新版本: ```bash curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.sh | sh -s -- -y @@ -174,11 +220,26 @@ ucloud-sandbox-cli version curl -sS https://raw.githubusercontent.com/ucloud/ucloud-sandbox-cli/main/install.sh | sh -s -- -y -p "$HOME/.local/bin" ``` +Windows PowerShell 使用独立安装脚本更新。更新到 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." } +``` + ## Step 1:认证和地域配置 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`;Windows 使用用户配置目录的 ACL。配置文件是 JSON,格式如下;展示或读取时必须隐藏 `api_key`: ```json { @@ -211,8 +272,18 @@ export UCLOUD_SANDBOX_API_KEY="" export UCLOUD_SANDBOX_REGION="cn-wlcb" ``` +Windows PowerShell: + +```powershell +$env:UCLOUD_SANDBOX_API_KEY = "" +$env:UCLOUD_SANDBOX_REGION = "cn-wlcb" +$env:UCLOUD_SANDBOX_DOMAIN = "cn-wlcb.sandbox.ucloudai.com" +``` + 切换持久化地域时,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,22 +299,77 @@ mv "$tmp" "$CONFIG_FILE" chmod 600 "$HOME/.ucloud-sandbox-cli/config.json" ``` -如果没有 `jq`,不要用易误伤 `api_key` 的字符串替换方案;请提示用户安装 `jq`,或让用户在真实终端运行 `ucloud-sandbox-cli region` 自行切换。 +Windows PowerShell 使用结构化 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 +} +``` + +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" ``` +Windows PowerShell: + +```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=" } +``` + 如果必须展示配置摘要,先脱敏: ```bash jq '.api_key = if .api_key then "***hidden***" else . end' "$HOME/.ucloud-sandbox-cli/config.json" ``` -没有 `jq` 时,使用不会输出真实密钥的方式: +Windows PowerShell: + +```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 +``` + +Linux 和 macOS 没有 `jq` 时,使用不会输出真实密钥的方式: ```bash sed -E 's/"api_key"[[:space:]]*:[[:space:]]*"[^"]*"/"api_key": "***hidden***"/' "$HOME/.ucloud-sandbox-cli/config.json" @@ -265,6 +391,26 @@ ucloud-sandbox-cli logout - 不要在回复、日志或命令输出中泄露 API Key;读取 `~/.ucloud-sandbox-cli/config.json` 时必须隐藏 `api_key`,只展示地域、域名等必要字段。 - 用户要打开交互式终端时,建议让用户在真实终端中运行 `sandbox connect`。 +## Windows PowerShell 调用 + +- 本地使用 PowerShell 和 Windows `.exe`,沙箱内仍然是 Linux Shell。 +- 环境变量写成 `$env:NAME`,不要使用 Bash 的 `export`。 +- 本地路径可以使用 `C:\...`,远端路径仍使用 `/`。 +- 变量后紧跟远端端点冒号时写成 `${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 +``` + ## Sandbox 常用操作 创建沙箱: @@ -535,7 +681,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 PowerShell 使用 `$env:Path = "<安装目录>;$env:Path"`,并确认安装目录已写入用户 `PATH` | | 创建沙箱后卡在终端 | Agent/CI 中使用 `sandbox create ... --detach` | | `template not found` | 运行 `template list --format json` 确认模板 ID/名称 | | `sandbox not found` | 运行 `sandbox list --format json` 确认沙箱仍在运行 |