diff --git a/INSTALL.md b/INSTALL.md index 6136efd..96f4d9d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,13 +5,15 @@ ## 1. 推荐:二进制安装(无需 Node) ```bash -curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash +curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash ``` +可选:`BAILIAN_CHANNEL`(默认 `latest`)、`BAILIAN_VERSION`(钉版本)、`BAILIAN_CLI_CDN`(覆盖 CDN 根)。 + Windows PowerShell: ```powershell -irm https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.ps1 | iex +irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex ``` 校验: diff --git a/README.md b/README.md index 0bdd1b6..5d7fcd1 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,10 @@ No timeline scrubbing. No frame-by-frame editing. Just one sentence → one vide ```bash # Recommended — no Node required -curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash +curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash # Windows (PowerShell) -irm https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.ps1 | iex +irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex # Node users / developers (Node.js >= 18.17) npm install -g bailian-cli diff --git a/README.zh.md b/README.zh.md index b726a9e..5ec6326 100644 --- a/README.zh.md +++ b/README.zh.md @@ -76,10 +76,10 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_ ```bash # 推荐 — 无需本机 Node.js -curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash +curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash # Windows(PowerShell) -irm https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.ps1 | iex +irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex # Node 用户 / 开发者(需要 Node.js >= 18.17) npm install -g bailian-cli diff --git a/docs/agents/publish.md b/docs/agents/publish.md index 6532b9d..95c77de 100644 --- a/docs/agents/publish.md +++ b/docs/agents/publish.md @@ -19,10 +19,10 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口 └─ binary(lib/binary-release → binary-build → gh-release - → oss-sync-webhook) + → oss-direct-upload) ``` -`tools/release/lib/binary-release.mjs` 等是实现,一般不要单独当发版入口(调试可用)。详细约定见 [binary-distribution](binary-distribution.md)。 +`tools/release/lib/binary-release.mjs` 等是实现,一般不要单独当发版入口(调试可用)。 两种模式: @@ -108,7 +108,7 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run - [ ] 验证 npm 上能装:`npm view bailian-cli@ version`;如发布 `knowledge-studio-cli`,同时 `npm view knowledge-studio-cli@ version` - [ ] 试装一次:`npm i -g bailian-cli@ && bl --version`;如发布 `knowledge-studio-cli`,同时 `npm i -g knowledge-studio-cli@ && kscli --version` -- [ ] (若本次含二进制)GitHub Release 可见 `bl-*.zip` + `SHA256SUMS`;OSS 侧 `channels/latest.json` 与 install 脚本由 FC/仓外维护;清单见 [binary-distribution.md](binary-distribution.md) +- [ ] (若本次含二进制)GitHub Release 可见 `bl-*.zip` + `SHA256SUMS`;OSS 侧 `latest.json` / `.json`、`v/*` 与 `install.sh`/`install.ps1` 已由 CI 直传 ## 常见漏点(基于历史踩坑) diff --git a/packages/cli/README.md b/packages/cli/README.md index 0bdd1b6..5d7fcd1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -78,10 +78,10 @@ No timeline scrubbing. No frame-by-frame editing. Just one sentence → one vide ```bash # Recommended — no Node required -curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash +curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash # Windows (PowerShell) -irm https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.ps1 | iex +irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex # Node users / developers (Node.js >= 18.17) npm install -g bailian-cli diff --git a/packages/cli/README.zh.md b/packages/cli/README.zh.md index b726a9e..5ec6326 100644 --- a/packages/cli/README.zh.md +++ b/packages/cli/README.zh.md @@ -76,10 +76,10 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_ ```bash # 推荐 — 无需本机 Node.js -curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash +curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash # Windows(PowerShell) -irm https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.ps1 | iex +irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex # Node 用户 / 开发者(需要 Node.js >= 18.17) npm install -g bailian-cli diff --git a/packaging/install.ps1 b/packaging/install.ps1 new file mode 100644 index 0000000..e18c4d4 --- /dev/null +++ b/packaging/install.ps1 @@ -0,0 +1,134 @@ +# Binary install script for the OSS release layout (see packages/core/src/install/cdn.ts). +# +# irm https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.ps1 | iex +# $env:BAILIAN_CHANNEL = "sync-release"; irm ... | iex +# $env:BAILIAN_VERSION = "1.10.1"; irm ... | iex +# +# Layout: +# {CDN}/{channel}.json — rolling manifest (version + assets) +# {CDN}/v{version}/{file}.zip — per-platform zip +param() + +$ErrorActionPreference = "Stop" + +$CdnBase = if ($env:BAILIAN_CLI_CDN) { $env:BAILIAN_CLI_CDN.TrimEnd("/") } else { "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release" } +$Channel = if ($env:BAILIAN_CHANNEL) { $env:BAILIAN_CHANNEL } else { "latest" } +$Version = $env:BAILIAN_VERSION +$InstallRoot = if ($env:BAILIAN_SHARE_DIR) { $env:BAILIAN_SHARE_DIR } else { Join-Path $env:LOCALAPPDATA "bailian-cli" } +$ConfigDir = if ($env:BAILIAN_CONFIG_DIR) { $env:BAILIAN_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".bailian" } + +function Write-Log([string]$Message) { + [Console]::Error.WriteLine($Message) +} + +function Get-VersionTag([string]$Ver) { + if ($Ver.StartsWith("v")) { return $Ver } + return "v$Ver" +} + +$Arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" } +if ($Arch -eq "arm64") { + throw "windows arm64 is not supported for binary install; use: npm install -g bailian-cli" +} +$PlatformKey = "windows-$Arch" + +$ManifestUrl = "$CdnBase/$Channel.json" +Write-Log "Fetching $ManifestUrl" +$Manifest = Invoke-RestMethod -Uri $ManifestUrl + +$PinnedVersion = $Version +if ([string]::IsNullOrWhiteSpace($Version)) { + $Version = $Manifest.version +} +if ([string]::IsNullOrWhiteSpace($Version)) { + throw "Could not parse version from $Channel channel manifest" +} + +$ZipFile = $null +$ExpectedSha = $null +$InnerName = $null +$AssetMeta = $null +if ($Manifest.assets -and $Manifest.assets.$PlatformKey) { + $AssetMeta = $Manifest.assets.$PlatformKey +} + +$UseManifestAsset = $AssetMeta -and ( + [string]::IsNullOrWhiteSpace($PinnedVersion) -or ($PinnedVersion -eq $Manifest.version) +) + +if ($UseManifestAsset) { + $ZipFile = $AssetMeta.file + $ExpectedSha = $AssetMeta.sha256 + $InnerName = $AssetMeta.inner +} else { + $ZipFile = "bl-$Version-$PlatformKey.zip" + $InnerName = "bl-$Version-$PlatformKey.exe" +} + +if ([string]::IsNullOrWhiteSpace($ZipFile) -or [string]::IsNullOrWhiteSpace($InnerName)) { + throw "Manifest missing asset metadata for $PlatformKey" +} + +$Tag = Get-VersionTag $Version +$Url = "$CdnBase/$Tag/$ZipFile" +$TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("bailian-cli-install-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $TempDir | Out-Null + +try { + $ZipPath = Join-Path $TempDir $ZipFile + Write-Log "Downloading $ZipFile…" + Invoke-WebRequest -Uri $Url -OutFile $ZipPath + + $Actual = (Get-FileHash -Algorithm SHA256 -Path $ZipPath).Hash.ToLowerInvariant() + if (-not [string]::IsNullOrWhiteSpace($ExpectedSha)) { + if ($ExpectedSha.ToLowerInvariant() -ne $Actual) { + throw "Checksum mismatch for $ZipFile" + } + } else { + $SumsUrl = "$CdnBase/$Tag/SHA256SUMS" + $SumsPath = Join-Path $TempDir "SHA256SUMS" + Invoke-WebRequest -Uri $SumsUrl -OutFile $SumsPath + $Expected = $null + Get-Content $SumsPath | ForEach-Object { + if ($_ -match "^([a-fA-F0-9]+)\s+$([regex]::Escape($ZipFile))\s*$") { + $Expected = $Matches[1].ToLowerInvariant() + } + } + if (-not $Expected) { throw "Checksum for $ZipFile not found in SHA256SUMS" } + if ($Expected -ne $Actual) { throw "Checksum mismatch for $ZipFile" } + } + + Write-Log "Extracting $InnerName…" + Expand-Archive -Path $ZipPath -DestinationPath $TempDir -Force + $ExtractPath = Join-Path $TempDir $InnerName + if (-not (Test-Path -LiteralPath $ExtractPath)) { + # Some Expand-Archive versions nest a single top-level folder. + $Found = Get-ChildItem -Path $TempDir -Recurse -File | Where-Object { $_.Name -eq $InnerName } | Select-Object -First 1 + if (-not $Found) { throw "Extracted binary missing: $InnerName" } + $ExtractPath = $Found.FullName + } + + $VersionDir = Join-Path $InstallRoot "versions\$Version" + New-Item -ItemType Directory -Force -Path $VersionDir | Out-Null + New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null + $Target = Join-Path $VersionDir "bl.exe" + Copy-Item -Force $ExtractPath $Target + + $ShimDir = Join-Path $InstallRoot "bin" + New-Item -ItemType Directory -Force -Path $ShimDir | Out-Null + Copy-Item -Force $Target (Join-Path $ShimDir "bl.exe") + Copy-Item -Force $Target (Join-Path $ShimDir "bailian.exe") + Set-Content -Path (Join-Path $ConfigDir "install-method") -Value "binary" -NoNewline + + $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") + if (-not ($UserPath -split ";" | Where-Object { $_ -eq $ShimDir })) { + [Environment]::SetEnvironmentVariable("Path", "$ShimDir;$UserPath", "User") + Write-Log "Added $ShimDir to your user PATH. Open a new terminal to use bl." + } + + Write-Log "Installed: $Target (bailian-cli $Version) [$Channel]" + Write-Log "Done. Run: bl --help" +} +finally { + Remove-Item -Recurse -Force $TempDir -ErrorAction SilentlyContinue +} diff --git a/packaging/install.sh b/packaging/install.sh new file mode 100755 index 0000000..f0e98ff --- /dev/null +++ b/packaging/install.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env bash +# Binary install script for the OSS release layout (see packages/core/src/install/cdn.ts). +# +# curl -fsSL https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release/install.sh | bash +# BAILIAN_CHANNEL=latest bash install.sh +# BAILIAN_CHANNEL=sync-release bash install.sh +# BAILIAN_VERSION=1.10.1 bash install.sh +# +# Layout: +# {CDN}/{channel}.json —— rolling manifest (version + assets) +# {CDN}/v{version}/{file}.zip —— per-platform zip +set -euo pipefail + +CDN_BASE="${BAILIAN_CLI_CDN:-https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release}" +CDN_BASE="${CDN_BASE%/}" +CHANNEL="${BAILIAN_CHANNEL:-latest}" +VERSION="${BAILIAN_VERSION:-}" +BIN_DIR="${BAILIAN_BIN_DIR:-${HOME}/.local/bin}" +SHARE_DIR="${BAILIAN_SHARE_DIR:-${HOME}/.local/share/bailian-cli}" +CONFIG_DIR="${BAILIAN_CONFIG_DIR:-${HOME}/.bailian}" + +ZIP_FILE="" +ZIP_SHA256="" +INNER_NAME="" + +log() { printf '%s\n' "$*" >&2; } +die() { log "error: $*"; exit 1; } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +version_tag() { + local ver="$1" + if [[ "${ver}" == v* ]]; then + printf '%s\n' "${ver}" + else + printf 'v%s\n' "${ver}" + fi +} + +detect_os_arch() { + local uname_s uname_m + uname_s="$(uname -s)" + uname_m="$(uname -m)" + + case "${uname_s}" in + Darwin) OS="darwin" ;; + Linux) OS="linux" ;; + *) die "unsupported OS: ${uname_s}" ;; + esac + + case "${uname_m}" in + arm64|aarch64) ARCH="arm64" ;; + x86_64|amd64) ARCH="x64" ;; + *) die "unsupported architecture: ${uname_m}" ;; + esac + + # Rosetta 2: report the native arm64 binary, not the translated x64 view. + if [[ "${OS}" == "darwin" ]] && [[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" == "1" ]]; then + ARCH="arm64" + fi + + if [[ "${OS}" == "linux" && "${ARCH}" == "arm64" ]]; then + die "linux arm64 is not supported for binary install; use: npm install -g bailian-cli" + fi + + PLATFORM_KEY="${OS}-${ARCH}" +} + +# First top-level JSON string field: "key": "value" +json_string_field() { + local key="$1" + local file="$2" + sed -n "s/^[[:space:]]*\"${key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" "${file}" | head -n1 +} + +# String field inside the `"": { ... }` asset object. +json_asset_field() { + local platform_key="$1" + local field="$2" + local file="$3" + awk -v platform="${platform_key}" -v field="${field}" ' + BEGIN { in_block = 0 } + $0 ~ "\"" platform "\"[[:space:]]*:" { in_block = 1; next } + in_block && /\{/ { next } + in_block { + if ($0 ~ "\"" field "\"[[:space:]]*:") { + if (match($0, /:[[:space:]]*"[^"]*"/)) { + value = substr($0, RSTART, RLENGTH) + sub(/^:[[:space:]]*"/, "", value) + sub(/"$/, "", value) + print value + exit + } + } + if ($0 ~ /\}/) exit + } + ' "${file}" +} + +# Prints: versionfilesha256inner +# Pure POSIX-ish shell + awk/sed — no python/jq/node. +parse_manifest_asset() { + local manifest_path="$1" + local platform_key="$2" + local pinned_version="${3:-}" + local channel_version file_name sha256_value inner_name version + + channel_version="$(json_string_field version "${manifest_path}")" + [[ -n "${channel_version}" ]] || die "manifest missing version" + + if [[ -n "${pinned_version}" ]]; then + version="${pinned_version}" + else + version="${channel_version}" + fi + + if [[ -z "${pinned_version}" || "${pinned_version}" == "${channel_version}" ]]; then + file_name="$(json_asset_field "${platform_key}" file "${manifest_path}")" + sha256_value="$(json_asset_field "${platform_key}" sha256 "${manifest_path}")" + inner_name="$(json_asset_field "${platform_key}" inner "${manifest_path}")" + else + # Pinned version differs from channel tip — naming convention + SHA256SUMS. + file_name="bl-${version}-${platform_key}.zip" + sha256_value="" + if [[ "${platform_key}" == windows-* ]]; then + inner_name="bl-${version}-${platform_key}.exe" + else + inner_name="bl-${version}-${platform_key}" + fi + fi + + [[ -n "${file_name}" && -n "${inner_name}" ]] \ + || die "manifest missing asset metadata for ${platform_key}" + + printf '%s\t%s\t%s\t%s\n' "${version}" "${file_name}" "${sha256_value}" "${inner_name}" +} + +resolve_asset() { + need_cmd curl + local manifest_path parsed manifest_url + manifest_path="$(mktemp)" + manifest_url="${CDN_BASE}/${CHANNEL}.json" + log "Fetching ${manifest_url}" + if ! curl -fsSL "${manifest_url}" -o "${manifest_path}"; then + rm -f "${manifest_path}" + die "failed to download channel manifest: ${manifest_url}" + fi + + if ! parsed="$(parse_manifest_asset "${manifest_path}" "${PLATFORM_KEY}" "${VERSION}")"; then + rm -f "${manifest_path}" + die "failed to parse asset metadata from ${CHANNEL}.json for ${PLATFORM_KEY}" + fi + rm -f "${manifest_path}" + + VERSION="$(printf '%s\n' "${parsed}" | cut -f1)" + ZIP_FILE="$(printf '%s\n' "${parsed}" | cut -f2)" + ZIP_SHA256="$(printf '%s\n' "${parsed}" | cut -f3)" + INNER_NAME="$(printf '%s\n' "${parsed}" | cut -f4)" + + [[ -n "${VERSION}" && -n "${ZIP_FILE}" && -n "${INNER_NAME}" ]] \ + || die "incomplete asset metadata from ${CHANNEL}.json" +} + +sha256_file() { + local path="$1" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "${path}" | awk '{ print $1 }' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "${path}" | awk '{ print $1 }' + else + die "required command not found: shasum or sha256sum" + fi +} + +verify_checksum() { + local zip_path="$1" + local expected="$2" + local tag sums_url sums_path actual + + if [[ -n "${expected}" ]]; then + actual="$(sha256_file "${zip_path}")" + [[ "${expected}" == "${actual}" ]] || die "checksum mismatch for ${ZIP_FILE}" + return + fi + + # Pinned / convention fallback: verify against per-version SHA256SUMS. + need_cmd curl + tag="$(version_tag "${VERSION}")" + sums_url="${CDN_BASE}/${tag}/SHA256SUMS" + sums_path="$(mktemp)" + curl -fsSL "${sums_url}" -o "${sums_path}" || die "download failed: ${sums_url}" + expected="$(awk -v file="${ZIP_FILE}" '$2 == file { print $1; exit }' "${sums_path}")" + rm -f "${sums_path}" + [[ -n "${expected}" ]] || die "checksum for ${ZIP_FILE} not found in SHA256SUMS" + actual="$(sha256_file "${zip_path}")" + [[ "${expected}" == "${actual}" ]] || die "checksum mismatch for ${ZIP_FILE}" +} + +download_and_install() { + need_cmd curl + need_cmd unzip + + local tag tmp_dir version_dir zip_path extract_path + tag="$(version_tag "${VERSION}")" + tmp_dir="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '${tmp_dir}'" EXIT + + zip_path="${tmp_dir}/${ZIP_FILE}" + log "Downloading ${ZIP_FILE}…" + curl -fsSL "${CDN_BASE}/${tag}/${ZIP_FILE}" -o "${zip_path}" \ + || die "download failed: ${CDN_BASE}/${tag}/${ZIP_FILE}" + + verify_checksum "${zip_path}" "${ZIP_SHA256}" + + log "Extracting ${INNER_NAME}…" + unzip -j -o "${zip_path}" "${INNER_NAME}" -d "${tmp_dir}" >/dev/null \ + || die "failed to extract ${INNER_NAME} from ${ZIP_FILE}" + extract_path="${tmp_dir}/${INNER_NAME}" + [[ -f "${extract_path}" ]] || die "extracted binary missing: ${INNER_NAME}" + + version_dir="${SHARE_DIR}/versions/${VERSION}" + mkdir -p "${version_dir}" "${BIN_DIR}" "${CONFIG_DIR}" + install -m 755 "${extract_path}" "${version_dir}/bl" + ln -sfn "${version_dir}/bl" "${BIN_DIR}/bl" + ln -sfn "${version_dir}/bl" "${BIN_DIR}/bailian" + printf 'binary\n' > "${CONFIG_DIR}/install-method" + chmod 600 "${CONFIG_DIR}/install-method" 2>/dev/null || true +} + +warn_path() { + case ":${PATH}:" in + *":${BIN_DIR}:"*) ;; + *) + log "" + log "Add ${BIN_DIR} to your PATH, then open a new terminal:" + log " export PATH=\"${BIN_DIR}:\$PATH\"" + ;; + esac +} + +warn_npm_conflict() { + if command -v npm >/dev/null 2>&1; then + local npm_bl + npm_bl="$(npm root -g 2>/dev/null)/bailian-cli" || true + if [[ -d "${npm_bl}" ]]; then + log "warning: npm global bailian-cli is also installed; PATH may prefer one over the other." + log " Consider: npm uninstall -g bailian-cli" + fi + fi +} + +main() { + need_cmd curl + need_cmd uname + detect_os_arch + resolve_asset + log "Installing bailian-cli ${VERSION} (${PLATFORM_KEY}) from ${CDN_BASE} [${CHANNEL}]" + download_and_install + warn_npm_conflict + warn_path + log "" + log "Installed: ${BIN_DIR}/bl → bailian-cli ${VERSION}" + if command -v "${BIN_DIR}/bl" >/dev/null 2>&1 || [[ -x "${BIN_DIR}/bl" ]]; then + "${BIN_DIR}/bl" --version >&2 || true + fi + log "Done. Run: bl --help" +} + +main "$@"