mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 05:02:07 +00:00
- Rename all package names from companion-* to clanker-* - Update npm scopes from @mariozechner to @harivansh-afk - Rename config directories .companion -> .clanker - Rename environment variables COMPANION_* -> CLANKER_* - Update all documentation, README files, and install scripts - Rename package directories (companion-channels, companion-grind, companion-teams) - Update GitHub URLs to harivansh-afk/clanker-agent - Preserve full git history from companion-cloud monorepo
620 lines
14 KiB
Bash
Executable file
620 lines
14 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
# Defaults
|
|
REPO="${CLANKER_REPO:-${CLANKER_MONO_REPO:-harivansh-afk/clanker-agent}}"
|
|
VERSION="${CLANKER_VERSION:-${CLANKER_MONO_VERSION:-latest}}"
|
|
INSTALL_DIR="${CLANKER_INSTALL_DIR:-${CLANKER_MONO_INSTALL_DIR:-$HOME/.clanker}}"
|
|
BIN_DIR="${CLANKER_BIN_DIR:-${CLANKER_MONO_BIN_DIR:-$HOME/.local/bin}}"
|
|
AGENT_DIR="${CLANKER_AGENT_DIR:-${CLANKER_MONO_AGENT_DIR:-$INSTALL_DIR/agent}}"
|
|
SERVICE_NAME="${CLANKER_SERVICE_NAME:-${CLANKER_MONO_SERVICE_NAME:-clanker}}"
|
|
FALLBACK_TO_SOURCE="${CLANKER_FALLBACK_TO_SOURCE:-${CLANKER_MONO_FALLBACK_TO_SOURCE:-1}}"
|
|
SKIP_REINSTALL="${CLANKER_SKIP_REINSTALL:-${CLANKER_MONO_SKIP_REINSTALL:-0}}"
|
|
RUN_INSTALL_PACKAGES="${CLANKER_INSTALL_PACKAGES:-${CLANKER_MONO_INSTALL_PACKAGES:-1}}"
|
|
SETUP_DAEMON="${CLANKER_SETUP_DAEMON:-${CLANKER_MONO_SETUP_DAEMON:-0}}"
|
|
START_DAEMON="${CLANKER_START_DAEMON:-${CLANKER_MONO_START_DAEMON:-0}}"
|
|
SKIP_SERVICE="${CLANKER_SKIP_SERVICE:-${CLANKER_MONO_SKIP_SERVICE:-0}}"
|
|
SERVICE_MANAGER=""
|
|
SERVICE_UNIT_PATH=""
|
|
SERVICE_LABEL=""
|
|
SERVICE_STDOUT_LOG=""
|
|
SERVICE_STDERR_LOG=""
|
|
|
|
DEFAULT_PACKAGES=(
|
|
"npm:@e9n/clanker-channels"
|
|
"npm:clanker-teams"
|
|
)
|
|
|
|
declare -a EXTRA_PACKAGES=()
|
|
USE_DEFAULT_PACKAGES=1
|
|
|
|
log() {
|
|
echo "==> $*"
|
|
}
|
|
|
|
fail() {
|
|
echo "ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
has() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
curl -fsSL https://raw.githubusercontent.com/harivansh-afk/clanker-agent/main/public-install.sh | bash
|
|
bash public-install.sh [options]
|
|
|
|
Options:
|
|
--repo <owner/repo> Override GitHub repo for install (default: harivansh-afk/clanker-agent)
|
|
--version <tag>|latest Release tag to install (default: latest)
|
|
--install-dir <path> Target directory for release contents (default: ~/.clanker)
|
|
--bin-dir <path> Directory for clanker launcher (default: ~/.local/bin)
|
|
--agent-dir <path> Agent config directory (default: <install-dir>/agent)
|
|
--package <pkg> Add package to installation list (repeatable)
|
|
--no-default-packages Skip default packages list
|
|
--skip-packages Skip package installation step
|
|
--daemon Install user service for long-lived mode
|
|
--start Start service after install (implies --daemon)
|
|
--skip-daemon Force skip service setup/start
|
|
--fallback-to-source <0|1> Allow source fallback when release is unavailable
|
|
--skip-reinstall Keep existing install directory
|
|
--help
|
|
|
|
Env vars:
|
|
CLANKER_INSTALL_PACKAGES=0/1
|
|
CLANKER_SETUP_DAEMON=0/1
|
|
CLANKER_START_DAEMON=0/1
|
|
CLANKER_FALLBACK_TO_SOURCE=0/1
|
|
CLANKER_SKIP_REINSTALL=1
|
|
CLANKER_SERVICE_NAME=<name>
|
|
EOF
|
|
}
|
|
|
|
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
if ! has tar; then
|
|
fail "required tool not found: tar"
|
|
fi
|
|
if ! has curl && ! has wget; then
|
|
fail "required tool not found: curl or wget"
|
|
fi
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--repo)
|
|
REPO="${2:?missing repo value}"
|
|
shift 2
|
|
;;
|
|
--version)
|
|
VERSION="${2:?missing version value}"
|
|
shift 2
|
|
;;
|
|
--install-dir)
|
|
INSTALL_DIR="${2:?missing install dir}"
|
|
shift 2
|
|
;;
|
|
--bin-dir)
|
|
BIN_DIR="${2:?missing bin dir}"
|
|
shift 2
|
|
;;
|
|
--agent-dir)
|
|
AGENT_DIR="${2:?missing agent dir}"
|
|
shift 2
|
|
;;
|
|
--package)
|
|
EXTRA_PACKAGES+=("${2:?missing package}")
|
|
shift 2
|
|
;;
|
|
--no-default-packages)
|
|
USE_DEFAULT_PACKAGES=0
|
|
shift
|
|
;;
|
|
--skip-packages)
|
|
RUN_INSTALL_PACKAGES=0
|
|
shift
|
|
;;
|
|
--daemon)
|
|
SETUP_DAEMON=1
|
|
shift
|
|
;;
|
|
--start)
|
|
START_DAEMON=1
|
|
SETUP_DAEMON=1
|
|
shift
|
|
;;
|
|
--skip-daemon)
|
|
SETUP_DAEMON=0
|
|
START_DAEMON=0
|
|
SKIP_SERVICE=1
|
|
shift
|
|
;;
|
|
--fallback-to-source)
|
|
FALLBACK_TO_SOURCE="${2:?missing fallback value}"
|
|
shift 2
|
|
;;
|
|
--skip-reinstall)
|
|
SKIP_REINSTALL=1
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$FALLBACK_TO_SOURCE" != "0" && "$FALLBACK_TO_SOURCE" != "1" ]]; then
|
|
fail "CLANKER_FALLBACK_TO_SOURCE must be 0 or 1"
|
|
fi
|
|
|
|
if [[ -d "$INSTALL_DIR" && "$SKIP_REINSTALL" != "1" ]]; then
|
|
rm -rf "$INSTALL_DIR"
|
|
fi
|
|
|
|
if [[ -z "${SERVICE_NAME:-}" ]]; then
|
|
SERVICE_NAME="clanker"
|
|
fi
|
|
|
|
download_file() {
|
|
local url="$1"
|
|
local out="$2"
|
|
if has curl; then
|
|
curl -fsSL "$url" -o "$out"
|
|
else
|
|
wget -qO "$out" "$url"
|
|
fi
|
|
}
|
|
|
|
detect_platform() {
|
|
local os
|
|
local arch
|
|
|
|
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
arch="$(uname -m)"
|
|
|
|
case "$os" in
|
|
darwin) os="darwin" ;;
|
|
linux) os="linux" ;;
|
|
mingw*|msys*|cygwin*) os="windows" ;;
|
|
*)
|
|
fail "unsupported OS: $os"
|
|
;;
|
|
esac
|
|
|
|
case "$arch" in
|
|
x86_64|amd64)
|
|
arch="x64"
|
|
;;
|
|
aarch64|arm64)
|
|
arch="arm64"
|
|
;;
|
|
*)
|
|
fail "unsupported CPU architecture: $arch"
|
|
;;
|
|
esac
|
|
|
|
PLATFORM="${os}-${arch}"
|
|
}
|
|
|
|
resolve_release_tag() {
|
|
if [[ "$VERSION" != "latest" ]]; then
|
|
echo "$VERSION"
|
|
return
|
|
fi
|
|
|
|
local api_json
|
|
api_json="$(mktemp)"
|
|
if ! download_file "https://api.github.com/repos/${REPO}/releases/latest" "$api_json"; then
|
|
rm -f "$api_json"
|
|
return 1
|
|
fi
|
|
|
|
local tag
|
|
if has jq; then
|
|
tag="$(jq -r '.tag_name // empty' "$api_json")"
|
|
else
|
|
tag="$(awk '/"tag_name":/ { gsub(/[",]/, "", $3); print $3; exit }' "$api_json")"
|
|
fi
|
|
rm -f "$api_json"
|
|
|
|
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
|
return 1
|
|
fi
|
|
echo "$tag"
|
|
}
|
|
|
|
platform_assets() {
|
|
if [[ "$PLATFORM" == "windows"* ]]; then
|
|
echo "clanker-${PLATFORM}.zip"
|
|
else
|
|
echo "clanker-${PLATFORM}.tar.gz"
|
|
fi
|
|
}
|
|
|
|
extract_archive() {
|
|
local archive="$1"
|
|
local out_dir="$2"
|
|
mkdir -p "$out_dir"
|
|
if [[ "$archive" == *.zip ]]; then
|
|
if ! has unzip; then
|
|
fail "unzip required for zip archive"
|
|
fi
|
|
unzip -q "$archive" -d "$out_dir"
|
|
else
|
|
tar -xzf "$archive" -C "$out_dir"
|
|
fi
|
|
}
|
|
|
|
collect_packages() {
|
|
local -a packages=()
|
|
if [[ "$USE_DEFAULT_PACKAGES" == "1" ]]; then
|
|
packages=("${DEFAULT_PACKAGES[@]}")
|
|
fi
|
|
if [[ "${#EXTRA_PACKAGES[@]}" -gt 0 ]]; then
|
|
packages+=("${EXTRA_PACKAGES[@]}")
|
|
fi
|
|
printf '%s\n' "${packages[@]}"
|
|
}
|
|
|
|
write_launcher() {
|
|
local output="$1"
|
|
local runtime_dir="$2"
|
|
|
|
mkdir -p "$(dirname "$output")"
|
|
cat > "$output" <<EOF
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export CLANKER_CODING_AGENT_DIR="${AGENT_DIR}"
|
|
export CLANKER_MONO_AGENT_DIR="${AGENT_DIR}"
|
|
|
|
exec "${runtime_dir}" "\$@"
|
|
EOF
|
|
chmod +x "$output"
|
|
}
|
|
|
|
ensure_agent_settings() {
|
|
mkdir -p "$AGENT_DIR"
|
|
local settings_file="$AGENT_DIR/settings.json"
|
|
if [[ -f "$settings_file" ]]; then
|
|
return
|
|
fi
|
|
|
|
local -a packages
|
|
readarray -t packages < <(collect_packages)
|
|
if [[ "${#packages[@]}" -eq 0 ]]; then
|
|
cat > "$settings_file" <<'EOF'
|
|
{
|
|
"packages": []
|
|
}
|
|
EOF
|
|
return
|
|
fi
|
|
|
|
{
|
|
echo "{"
|
|
echo ' "packages": ['
|
|
} > "$settings_file"
|
|
local idx=0
|
|
local total="${#packages[@]}"
|
|
for package in "${packages[@]}"; do
|
|
local suffix=""
|
|
if [[ "$idx" -lt $((total - 1)) ]]; then
|
|
suffix=","
|
|
fi
|
|
printf ' "%s"%s\n' "$package" "$suffix" >> "$settings_file"
|
|
idx=$((idx + 1))
|
|
done
|
|
{
|
|
echo " ]"
|
|
echo "}"
|
|
} >> "$settings_file"
|
|
}
|
|
|
|
install_packages() {
|
|
if [[ "$RUN_INSTALL_PACKAGES" != "1" ]]; then
|
|
return
|
|
fi
|
|
|
|
if ! has npm; then
|
|
log "npm not found. Skipping package installation."
|
|
return
|
|
fi
|
|
|
|
while IFS= read -r package; do
|
|
[[ -z "$package" ]] && continue
|
|
if "$BIN_DIR/clanker" install "$package" >/dev/null 2>&1; then
|
|
log "Installed package: $package"
|
|
else
|
|
log "Could not install ${package} now. It will install on first run when available."
|
|
fi
|
|
done < <(collect_packages)
|
|
}
|
|
|
|
write_service_file() {
|
|
local uname_s
|
|
uname_s="$(uname -s)"
|
|
|
|
if [[ "$uname_s" == "Darwin" ]]; then
|
|
if ! has launchctl; then
|
|
log "launchctl unavailable; skipping service setup."
|
|
return 1
|
|
fi
|
|
|
|
mkdir -p "$HOME/Library/LaunchAgents" "$INSTALL_DIR/logs"
|
|
local plist_path="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist"
|
|
local label="${SERVICE_NAME}"
|
|
local stdout_log="$INSTALL_DIR/logs/${SERVICE_NAME}.out.log"
|
|
local stderr_log="$INSTALL_DIR/logs/${SERVICE_NAME}.err.log"
|
|
|
|
cat > "$plist_path" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${label}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${BIN_DIR}/clanker</string>
|
|
<string>daemon</string>
|
|
</array>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>CLANKER_MONO_AGENT_DIR</key>
|
|
<string>${AGENT_DIR}</string>
|
|
<key>CLANKER_CODING_AGENT_DIR</key>
|
|
<string>${AGENT_DIR}</string>
|
|
</dict>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>WorkingDirectory</key>
|
|
<string>${INSTALL_DIR}</string>
|
|
<key>StandardOutPath</key>
|
|
<string>${stdout_log}</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${stderr_log}</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
SERVICE_MANAGER="launchd"
|
|
SERVICE_UNIT_PATH="$plist_path"
|
|
SERVICE_LABEL="$label"
|
|
SERVICE_STDOUT_LOG="$stdout_log"
|
|
SERVICE_STDERR_LOG="$stderr_log"
|
|
log "launch agent: $plist_path"
|
|
return 0
|
|
fi
|
|
|
|
if ! has systemctl; then
|
|
log "systemctl unavailable; skipping service setup."
|
|
return 1
|
|
fi
|
|
|
|
mkdir -p "$HOME/.config/systemd/user"
|
|
local service_path="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
|
|
cat > "$service_path" <<EOF
|
|
[Unit]
|
|
Description=clanker daemon
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
Environment=CLANKER_MONO_AGENT_DIR=${AGENT_DIR}
|
|
Environment=CLANKER_CODING_AGENT_DIR=${AGENT_DIR}
|
|
ExecStart=${BIN_DIR}/clanker daemon
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
EOF
|
|
SERVICE_MANAGER="systemd"
|
|
SERVICE_UNIT_PATH="$service_path"
|
|
SERVICE_LABEL="${SERVICE_NAME}"
|
|
log "service file: $service_path"
|
|
}
|
|
|
|
start_daemon_service() {
|
|
if [[ "$SERVICE_MANAGER" == "launchd" ]]; then
|
|
local domain_target="gui/$(id -u)"
|
|
launchctl bootout "$domain_target" "$SERVICE_UNIT_PATH" >/dev/null 2>&1 || true
|
|
launchctl bootstrap "$domain_target" "$SERVICE_UNIT_PATH"
|
|
launchctl enable "${domain_target}/${SERVICE_LABEL}"
|
|
launchctl kickstart -k "${domain_target}/${SERVICE_LABEL}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$SERVICE_MANAGER" == "systemd" ]]; then
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now "${SERVICE_NAME}.service"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
print_next_steps() {
|
|
echo
|
|
log "Installed to: $INSTALL_DIR"
|
|
log "Launcher: $BIN_DIR/clanker"
|
|
echo
|
|
echo "Run in terminal:"
|
|
echo " clanker"
|
|
echo
|
|
echo "Run always-on:"
|
|
echo " clanker daemon"
|
|
echo
|
|
if [[ "$SETUP_DAEMON" == "1" ]] && [[ "$SKIP_SERVICE" == "0" ]]; then
|
|
if [[ "$SERVICE_MANAGER" == "launchd" ]]; then
|
|
echo "Service:"
|
|
echo " launchctl print gui/$(id -u)/${SERVICE_LABEL}"
|
|
echo " launchctl kickstart -k gui/$(id -u)/${SERVICE_LABEL}"
|
|
echo
|
|
echo "Service logs:"
|
|
echo " tail -f ${SERVICE_STDOUT_LOG}"
|
|
echo " tail -f ${SERVICE_STDERR_LOG}"
|
|
elif [[ "$SERVICE_MANAGER" == "systemd" ]]; then
|
|
echo "Service:"
|
|
echo " systemctl --user status ${SERVICE_NAME}"
|
|
echo " systemctl --user restart ${SERVICE_NAME}"
|
|
echo
|
|
echo "Service logs:"
|
|
echo " journalctl --user -u ${SERVICE_NAME} -f"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
bootstrap_from_source() {
|
|
if ! has git; then
|
|
fail "git is required for source fallback."
|
|
fi
|
|
if ! has node; then
|
|
fail "node is required for source fallback."
|
|
fi
|
|
if ! has npm; then
|
|
fail "npm is required for source fallback."
|
|
fi
|
|
|
|
local source_dir="$INSTALL_DIR/source"
|
|
local ref="${1:-main}"
|
|
|
|
if [[ -d "$source_dir" && "$SKIP_REINSTALL" != "1" ]]; then
|
|
rm -rf "$source_dir"
|
|
fi
|
|
|
|
if [[ ! -d "$source_dir" ]]; then
|
|
log "Cloning ${REPO}@${ref}"
|
|
git clone --depth 1 --branch "$ref" "https://github.com/${REPO}.git" "$source_dir"
|
|
fi
|
|
|
|
log "Running source install"
|
|
(
|
|
cd "$source_dir"
|
|
CLANKER_MONO_AGENT_DIR="$AGENT_DIR" \
|
|
CLANKER_CODING_AGENT_DIR="$AGENT_DIR" \
|
|
./install.sh
|
|
)
|
|
|
|
if [[ ! -x "$source_dir/clanker" ]]; then
|
|
fail "clanker executable not found in source checkout."
|
|
fi
|
|
|
|
write_launcher "$BIN_DIR/clanker" "$source_dir/clanker"
|
|
ensure_agent_settings
|
|
install_packages
|
|
}
|
|
|
|
install_from_release() {
|
|
local tag="$1"
|
|
detect_platform
|
|
local workdir
|
|
local url
|
|
local archive
|
|
local downloaded=0
|
|
|
|
workdir="$(mktemp -d)"
|
|
while IFS= read -r asset; do
|
|
url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
|
|
archive="$workdir/$asset"
|
|
log "Trying asset: ${asset}"
|
|
if download_file "$url" "$archive"; then
|
|
downloaded=1
|
|
break
|
|
fi
|
|
done < <(platform_assets)
|
|
|
|
if [[ "$downloaded" == "0" ]]; then
|
|
rm -rf "$workdir"
|
|
return 1
|
|
fi
|
|
|
|
log "Extracting archive"
|
|
extract_archive "$archive" "$workdir"
|
|
|
|
local release_dir
|
|
local install_binary
|
|
if [[ -d "$workdir/clanker" ]]; then
|
|
release_dir="$workdir/clanker"
|
|
elif [[ -f "$workdir/clanker" ]]; then
|
|
release_dir="$workdir"
|
|
fi
|
|
|
|
if [[ -z "${release_dir:-}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
mkdir -p "$INSTALL_DIR"
|
|
rm -rf "$INSTALL_DIR"/*
|
|
cp -R "$release_dir/." "$INSTALL_DIR/"
|
|
|
|
if [[ -x "$INSTALL_DIR/clanker" ]]; then
|
|
install_binary="$INSTALL_DIR/clanker"
|
|
else
|
|
return 1
|
|
fi
|
|
|
|
# Runtime launcher with fixed agent dir env.
|
|
local launcher_target="$install_binary"
|
|
if [[ "$install_binary" != "$INSTALL_DIR/clanker" ]]; then
|
|
write_launcher "$INSTALL_DIR/clanker" "$install_binary"
|
|
launcher_target="$INSTALL_DIR/clanker"
|
|
fi
|
|
write_launcher "$BIN_DIR/clanker" "$launcher_target"
|
|
ensure_agent_settings
|
|
install_packages
|
|
rm -rf "$workdir"
|
|
}
|
|
|
|
main() {
|
|
local tag
|
|
if ! tag="$(resolve_release_tag)"; then
|
|
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
|
log "Could not resolve release tag. Falling back to source."
|
|
bootstrap_from_source "main"
|
|
return
|
|
fi
|
|
fail "could not resolve latest release tag from GitHub API"
|
|
fi
|
|
|
|
if [[ -n "$tag" ]]; then
|
|
if ! install_from_release "$tag"; then
|
|
if [[ "$FALLBACK_TO_SOURCE" == "1" ]]; then
|
|
log "Release install failed. Falling back to source."
|
|
if [[ "$VERSION" == "latest" ]]; then
|
|
bootstrap_from_source "main"
|
|
else
|
|
bootstrap_from_source "$VERSION"
|
|
fi
|
|
return
|
|
fi
|
|
fail "release asset unavailable: ${tag}"
|
|
fi
|
|
else
|
|
fail "release tag empty."
|
|
fi
|
|
}
|
|
|
|
main
|
|
print_next_steps
|
|
|
|
if [[ "$SETUP_DAEMON" == "1" && "$SKIP_SERVICE" == "0" ]]; then
|
|
if write_service_file; then
|
|
if [[ "$START_DAEMON" == "1" ]]; then
|
|
start_daemon_service
|
|
fi
|
|
fi
|
|
fi
|