feat: install shell test

This commit is contained in:
rendianmeng
2026-07-28 10:19:54 +08:00
parent ebbd173b79
commit 6465c4a78a
8 changed files with 421 additions and 13 deletions
+4 -2
View File
@@ -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
```
校验:
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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@<tag> version`;如发布 `knowledge-studio-cli`,同时 `npm view knowledge-studio-cli@<tag> version`
- [ ] 试装一次:`npm i -g bailian-cli@<tag> && bl --version`;如发布 `knowledge-studio-cli`,同时 `npm i -g knowledge-studio-cli@<tag> && 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` / `<channel>.json`、`v<ver>/*` 与 `install.sh`/`install.ps1` 已由 CI 直传
## 常见漏点(基于历史踩坑)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+134
View File
@@ -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
}
+272
View File
@@ -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 `"<platform_key>": { ... }` 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: version<TAB>file<TAB>sha256<TAB>inner
# 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 "$@"