401 lines
12 KiB
Bash
Executable File
401 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
|
# Copyright 2026 Alibaba Group
|
|
# Licensed under the Apache License, Version 2.0
|
|
#
|
|
# Installer for dws (DingTalk Workspace CLI).
|
|
# Downloads the pre-built binary from GitHub Releases and installs agent skills.
|
|
# No Go, Node.js, or other dependencies required.
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh
|
|
#
|
|
# Environment variables (all optional):
|
|
# DWS_INSTALL_DIR — where to put the binary (default: ~/.local/bin)
|
|
# DWS_VERSION — version to install (default: latest)
|
|
# DWS_NO_SKILLS — set to 1 to skip skills install
|
|
# DWS_SKILLS_ONLY — set to 1 to install only skills (skip binary)
|
|
|
|
set -eu
|
|
|
|
REPO="DingTalk-Real-AI/dingtalk-workspace-cli"
|
|
BIN_NAME="dws"
|
|
INSTALL_DIR="${DWS_INSTALL_DIR:-$HOME/.local/bin}"
|
|
INSTALL_NAME="${DWS_INSTALL_NAME:-$BIN_NAME}"
|
|
VERSION="${DWS_VERSION:-latest}"
|
|
NO_SKILLS="${DWS_NO_SKILLS:-0}"
|
|
SKILLS_ONLY="${DWS_SKILLS_ONLY:-0}"
|
|
SKILL_NAME="dws"
|
|
|
|
# ── Agent directory to install skills into ───────────────────────────────────
|
|
# Only install to .agents/skills — most agents can fall back to this directory.
|
|
AGENT_DIR=".agents/skills"
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
say() {
|
|
printf ' %s\n' "$@"
|
|
}
|
|
|
|
err() {
|
|
printf ' ❌ %s\n' "$@" >&2
|
|
exit 1
|
|
}
|
|
|
|
need_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
resolve_source_root() {
|
|
script_path="$0"
|
|
if [ ! -f "$script_path" ]; then
|
|
return 1
|
|
fi
|
|
|
|
script_dir="$(CDPATH= cd -- "$(dirname -- "$script_path")" && pwd)"
|
|
candidate_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
|
if [ -f "$candidate_root/go.mod" ] && [ -d "$candidate_root/cmd" ]; then
|
|
printf '%s\n' "$candidate_root"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Download a URL to a file. Uses curl or wget, whichever is available.
|
|
download() {
|
|
url="$1"
|
|
dest="$2"
|
|
if need_cmd curl; then
|
|
curl -fsSL "$url" -o "$dest"
|
|
elif need_cmd wget; then
|
|
wget -qO "$dest" "$url"
|
|
else
|
|
err "Neither curl nor wget found. Please install one and retry."
|
|
fi
|
|
}
|
|
|
|
extract_zip() {
|
|
archive="$1"
|
|
dest="$2"
|
|
if need_cmd unzip; then
|
|
unzip -q "$archive" -d "$dest"
|
|
return 0
|
|
fi
|
|
if need_cmd tar && tar -xf "$archive" -C "$dest" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Detect OS
|
|
detect_os() {
|
|
os="$(uname -s)"
|
|
case "$os" in
|
|
Linux*) echo "linux" ;;
|
|
Darwin*) echo "darwin" ;;
|
|
MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
|
|
*) err "Unsupported OS: $os. Use the PowerShell installer on Windows." ;;
|
|
esac
|
|
}
|
|
|
|
# Detect architecture
|
|
detect_arch() {
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
arm64|aarch64) echo "arm64" ;;
|
|
*) err "Unsupported architecture: $arch" ;;
|
|
esac
|
|
}
|
|
|
|
# Resolve the latest version tag from GitHub
|
|
resolve_version() {
|
|
if [ "$VERSION" = "latest" ]; then
|
|
# Follow the redirect from /releases/latest to get the tag
|
|
if need_cmd curl; then
|
|
VERSION="$(curl -fsSI "https://github.com/${REPO}/releases/latest" 2>/dev/null \
|
|
| grep -i '^location:' | sed 's|.*/tag/||;s/[[:space:]]*$//')"
|
|
elif need_cmd wget; then
|
|
VERSION="$(wget --spider --max-redirect=0 "https://github.com/${REPO}/releases/latest" 2>&1 \
|
|
| grep -i 'Location:' | sed 's|.*/tag/||;s/[[:space:]]*$//')"
|
|
fi
|
|
if [ -z "$VERSION" ]; then
|
|
err "Could not determine the latest version. Set DWS_VERSION explicitly."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ── Banner ───────────────────────────────────────────────────────────────────
|
|
|
|
print_banner() {
|
|
printf '\n'
|
|
say "┌──────────────────────────────────────┐"
|
|
say "│ DWS Installer │"
|
|
say "│ DingTalk Workspace CLI │"
|
|
say "└──────────────────────────────────────┘"
|
|
printf '\n'
|
|
}
|
|
|
|
install_binary_from_source() {
|
|
root="$1"
|
|
|
|
need_cmd go || err "Missing required command: go"
|
|
need_cmd make || err "Missing required command: make"
|
|
|
|
say "Installing dws from source checkout: ${root}"
|
|
say "Install dir: ${INSTALL_DIR}"
|
|
|
|
# Build using make (produces ./dws in the project root)
|
|
make -C "$root" build
|
|
|
|
built_bin="$root/$BIN_NAME"
|
|
if [ ! -f "$built_bin" ]; then
|
|
err "make build did not produce ${built_bin}"
|
|
fi
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
cp "$built_bin" "$INSTALL_DIR/$INSTALL_NAME"
|
|
chmod +x "$INSTALL_DIR/$INSTALL_NAME"
|
|
|
|
say "✅ Binary installed:"
|
|
say " → ${INSTALL_DIR}/${INSTALL_NAME}"
|
|
}
|
|
|
|
# ── Install Skills from Local Source ─────────────────────────────────────────
|
|
|
|
install_skills_local() {
|
|
root="$1"
|
|
skill_src="${root}/skills"
|
|
|
|
if [ ! -d "$skill_src" ]; then
|
|
say "⚠️ Local skills directory not found: ${skill_src}"
|
|
say " Skipping skills installation."
|
|
return 1
|
|
fi
|
|
|
|
say ""
|
|
say "📦 Installing agent skills from local source: ${skill_src}"
|
|
|
|
dest="$HOME/$AGENT_DIR/$SKILL_NAME"
|
|
display_path="~/$AGENT_DIR/$SKILL_NAME"
|
|
_copy_skill "$skill_src" "$dest" "$display_path"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Helper: copy skill files to a destination and print details
|
|
_copy_skill() {
|
|
_src="$1"
|
|
_dest="$2"
|
|
_label="$3"
|
|
|
|
if [ -d "$_dest" ]; then
|
|
rm -rf "$_dest"
|
|
fi
|
|
|
|
mkdir -p "$_dest"
|
|
cp -R "$_src/"* "$_dest/" 2>/dev/null || cp -r "$_src/"* "$_dest/"
|
|
file_count="$(find "$_dest" -type f | wc -l | tr -d ' ')"
|
|
|
|
say "✅ Skills → ${_label} (${file_count} files)"
|
|
|
|
for entry in "$_dest"/*; do
|
|
entry_name="$(basename "$entry")"
|
|
if [ -d "$entry" ]; then
|
|
sub_count="$(find "$entry" -type f | wc -l | tr -d ' ')"
|
|
say " 📁 ${entry_name}/ (${sub_count} files)"
|
|
else
|
|
say " 📄 ${entry_name}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── Install Binary ───────────────────────────────────────────────────────────
|
|
|
|
install_binary() {
|
|
os="$(detect_os)"
|
|
arch="$(detect_arch)"
|
|
resolve_version
|
|
|
|
archive_name="${BIN_NAME}-${os}-${arch}.tar.gz"
|
|
download_url="https://github.com/${REPO}/releases/download/${VERSION}/${archive_name}"
|
|
|
|
say "⬇ Downloading ${BIN_NAME} ${VERSION} (${os}/${arch})..."
|
|
|
|
tmpdir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmpdir"' EXIT INT TERM
|
|
|
|
download "$download_url" "$tmpdir/$archive_name"
|
|
|
|
# Download and verify SHA256 checksum
|
|
checksum_url="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt"
|
|
if download "$checksum_url" "$tmpdir/checksums.txt" 2>/dev/null; then
|
|
expected="$(awk -v file="$archive_name" '$2 == file {print $1; exit}' "$tmpdir/checksums.txt")"
|
|
if [ -n "$expected" ]; then
|
|
if need_cmd sha256sum; then
|
|
actual="$(sha256sum "$tmpdir/$archive_name" | awk '{print $1}')"
|
|
elif need_cmd shasum; then
|
|
actual="$(shasum -a 256 "$tmpdir/$archive_name" | awk '{print $1}')"
|
|
else
|
|
actual=""
|
|
fi
|
|
if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then
|
|
err "SHA256 checksum mismatch! Expected ${expected}, got ${actual}. Aborting."
|
|
fi
|
|
if [ -n "$actual" ]; then
|
|
say "✅ SHA256 checksum verified"
|
|
else
|
|
say "⚠️ Could not compute checksum (sha256sum/shasum not found); skipping verification"
|
|
fi
|
|
else
|
|
say "⚠️ Archive not found in checksums.txt; skipping verification"
|
|
fi
|
|
else
|
|
say "⚠️ Could not download checksums.txt; skipping verification"
|
|
fi
|
|
|
|
say "📦 Extracting..."
|
|
tar xzf "$tmpdir/$archive_name" -C "$tmpdir"
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
|
|
# The archive may contain a top-level directory or just the binary
|
|
if [ -f "$tmpdir/$BIN_NAME" ]; then
|
|
cp "$tmpdir/$BIN_NAME" "$INSTALL_DIR/$INSTALL_NAME"
|
|
elif [ -f "$tmpdir/${BIN_NAME}-${os}-${arch}/$BIN_NAME" ]; then
|
|
cp "$tmpdir/${BIN_NAME}-${os}-${arch}/$BIN_NAME" "$INSTALL_DIR/$INSTALL_NAME"
|
|
else
|
|
# Search for the binary
|
|
found="$(find "$tmpdir" -name "$BIN_NAME" -type f | head -1)"
|
|
if [ -n "$found" ]; then
|
|
cp "$found" "$INSTALL_DIR/$INSTALL_NAME"
|
|
else
|
|
err "Could not find the ${BIN_NAME} binary in the downloaded archive."
|
|
fi
|
|
fi
|
|
|
|
chmod +x "$INSTALL_DIR/$INSTALL_NAME"
|
|
|
|
say "✅ Binary installed: ${INSTALL_DIR}/${INSTALL_NAME}"
|
|
|
|
# Check if install dir is in PATH
|
|
case ":$PATH:" in
|
|
*":$INSTALL_DIR:"*) ;;
|
|
*)
|
|
say ""
|
|
say "⚠️ ${INSTALL_DIR} is not in your PATH."
|
|
say " Add it with:"
|
|
say " export PATH=\"${INSTALL_DIR}:\$PATH\""
|
|
say " Or add this line to your ~/.bashrc / ~/.zshrc"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ── Install Skills ───────────────────────────────────────────────────────────
|
|
|
|
install_skills() {
|
|
say ""
|
|
say "📦 Installing agent skills from GitHub Releases..."
|
|
|
|
resolve_version
|
|
skills_archive="dws-skills.zip"
|
|
download_url="https://github.com/${REPO}/releases/download/${VERSION}/${skills_archive}"
|
|
|
|
tmpdir_skills="$(mktemp -d)"
|
|
trap 'rm -rf "$tmpdir_skills"' EXIT INT TERM
|
|
|
|
if ! download "$download_url" "$tmpdir_skills/$skills_archive" 2>/dev/null; then
|
|
say "⚠️ Release asset download failed. Trying local source..."
|
|
rm -rf "$tmpdir_skills"
|
|
local_root="$(resolve_source_root || true)"
|
|
if [ -n "$local_root" ]; then
|
|
install_skills_local "$local_root"
|
|
return
|
|
else
|
|
err "Cannot download skills from GitHub and no local source checkout found."
|
|
fi
|
|
fi
|
|
|
|
extract_root="$tmpdir_skills/skills"
|
|
mkdir -p "$extract_root"
|
|
if ! extract_zip "$tmpdir_skills/$skills_archive" "$extract_root" 2>/dev/null; then
|
|
say "⚠️ Could not extract release skill archive. Install unzip, or retry from a source checkout."
|
|
rm -rf "$tmpdir_skills"
|
|
local_root="$(resolve_source_root || true)"
|
|
if [ -n "$local_root" ]; then
|
|
install_skills_local "$local_root"
|
|
return
|
|
fi
|
|
err "Cannot extract release skill archive and no local source checkout found."
|
|
fi
|
|
|
|
skill_src="$extract_root"
|
|
if [ -f "$extract_root/$SKILL_NAME/SKILL.md" ]; then
|
|
skill_src="$extract_root/$SKILL_NAME"
|
|
fi
|
|
if [ ! -f "$skill_src/SKILL.md" ]; then
|
|
say "⚠️ Skills not found in release asset. Trying local source..."
|
|
rm -rf "$tmpdir_skills"
|
|
local_root="$(resolve_source_root || true)"
|
|
if [ -n "$local_root" ]; then
|
|
install_skills_local "$local_root"
|
|
return
|
|
else
|
|
say "⚠️ No local source checkout found either. Skipping skills installation."
|
|
return
|
|
fi
|
|
fi
|
|
|
|
dest="$HOME/$AGENT_DIR/$SKILL_NAME"
|
|
display_path="~/$AGENT_DIR/$SKILL_NAME"
|
|
_copy_skill "$skill_src" "$dest" "$display_path"
|
|
|
|
rm -rf "$tmpdir_skills"
|
|
}
|
|
|
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
source_root=""
|
|
if [ "$SKILLS_ONLY" != "1" ] && [ "$VERSION" = "latest" ]; then
|
|
source_root="$(resolve_source_root || true)"
|
|
fi
|
|
|
|
print_banner
|
|
|
|
if [ -n "$source_root" ]; then
|
|
install_binary_from_source "$source_root"
|
|
if [ "$NO_SKILLS" != "1" ]; then
|
|
install_skills_local "$source_root"
|
|
fi
|
|
elif [ "$SKILLS_ONLY" = "1" ]; then
|
|
local_root="$(resolve_source_root || true)"
|
|
if [ -n "$local_root" ]; then
|
|
install_skills_local "$local_root"
|
|
else
|
|
install_skills
|
|
fi
|
|
elif [ "$NO_SKILLS" = "1" ]; then
|
|
install_binary
|
|
else
|
|
install_binary
|
|
install_skills
|
|
fi
|
|
|
|
printf '\n'
|
|
say "🎉 Installation complete!"
|
|
say ""
|
|
say "Next steps:"
|
|
if [ "$SKILLS_ONLY" != "1" ]; then
|
|
say " ${BIN_NAME} version # verify installation"
|
|
say " ${BIN_NAME} auth login # authenticate with DingTalk"
|
|
fi
|
|
say " ${BIN_NAME} --help # explore commands"
|
|
printf '\n'
|
|
}
|
|
|
|
main
|