Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Baudbot is designed as shared engineering infrastructure, not a single-user desk
| **CPU** | 2 vCPU | 4 vCPU |
| **Disk** | 20 GB | 40 GB+ (repos, dependencies, Docker images) |

System package dependencies (installed by `baudbot install`): `git`, `curl`, `tmux`, `iptables`, `docker`, `gh`, `jq`, `sudo`.

## Quick Start

```bash
Expand Down
60 changes: 45 additions & 15 deletions bin/baudbot
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,31 @@ if [ -z "${BAUDBOT_ROOT:-}" ]; then
# Handle relative symlinks
[[ "$SCRIPT" != /* ]] && SCRIPT="$DIR/$SCRIPT"
done
BAUDBOT_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"

CANDIDATE_ROOT="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
if [ -f "$CANDIDATE_ROOT/bin/baudbot" ] && [ -f "$CANDIDATE_ROOT/package.json" ]; then
BAUDBOT_ROOT="$CANDIDATE_ROOT"
elif [ -d /opt/baudbot/current/bin ]; then
BAUDBOT_ROOT="$(readlink -f /opt/baudbot/current 2>/dev/null || echo /opt/baudbot/current)"
else
BAUDBOT_ROOT="$CANDIDATE_ROOT"
fi
fi

json_get_string_or_empty() {
local file="$1"
local key="$2"
[ -r "$file" ] || return 0
command -v jq >/dev/null 2>&1 || return 0
jq -er --arg k "$key" 'if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' "$file" 2>/dev/null || true
}

json_get_string_stdin_or_empty() {
local key="$1"
command -v jq >/dev/null 2>&1 || return 0
jq -er --arg k "$key" 'if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end' 2>/dev/null || true
}

# Colors (disabled if not a terminal)
if [ -t 1 ]; then
BOLD='\033[1m'
Expand All @@ -34,11 +56,15 @@ else
fi

version() {
if [ -f "$BAUDBOT_ROOT/package.json" ]; then
grep '"version"' "$BAUDBOT_ROOT/package.json" | head -1 | sed 's/.*: *"\(.*\)".*/\1/'
else
echo "unknown"
local package_json="$BAUDBOT_ROOT/package.json"
local pkg_version=""

if [ -f "$package_json" ]; then
pkg_version="$(json_get_string_or_empty "$package_json" "version")"
[ -n "$pkg_version" ] && echo "$pkg_version" && return 0
fi

echo "unknown"
}

version_sha() {
Expand Down Expand Up @@ -73,7 +99,7 @@ version_sha() {
# Runtime metadata fallback
version_file="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}/.pi/agent/baudbot-version.json"
if [ -r "$version_file" ]; then
sha="$(grep -E '"sha"[[:space:]]*:' "$version_file" | head -1 | sed 's/.*"sha"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
sha="$(json_get_string_or_empty "$version_file" "sha")"
[ -n "$sha" ] && echo "${sha:0:7}" && return 0
fi

Expand Down Expand Up @@ -282,20 +308,29 @@ has_systemd() {
print_deployed_version() {
local agent_user="${BAUDBOT_AGENT_USER:-baudbot_agent}"
local version_file="/home/$agent_user/.pi/agent/baudbot-version.json"
local version_json=""
local short=""
local sha=""
local branch=""
local deployed_at=""
local line=""

if [ -r "$version_file" ]; then
version_json="$(cat "$version_file" 2>/dev/null || true)"
short="$(json_get_string_or_empty "$version_file" "short")"
sha="$(json_get_string_or_empty "$version_file" "sha")"
branch="$(json_get_string_or_empty "$version_file" "branch")"
deployed_at="$(json_get_string_or_empty "$version_file" "deployed_at")"
elif [ "$(id -u)" -eq 0 ] && id "$agent_user" >/dev/null 2>&1; then
version_json="$(sudo -u "$agent_user" cat "$version_file" 2>/dev/null || true)"
local version_json=""
version_json="$(sudo -u "$agent_user" sh -c "cat '$version_file' 2>/dev/null" || true)"
if [ -n "$version_json" ]; then
short="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "short" 2>/dev/null || true)"
sha="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "sha" 2>/dev/null || true)"
branch="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "branch" 2>/dev/null || true)"
deployed_at="$(printf '%s' "$version_json" | json_get_string_stdin_or_empty "deployed_at" 2>/dev/null || true)"
fi
fi

if [ -z "$version_json" ]; then
if [ -z "$short" ] && [ -z "$sha" ] && [ -z "$branch" ] && [ -z "$deployed_at" ]; then
local release_target=""
local release_sha=""

Expand All @@ -309,11 +344,6 @@ print_deployed_version() {
return 0
fi

short="$(printf '%s\n' "$version_json" | grep -E '"short"[[:space:]]*:' | head -1 | sed 's/.*"short"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
sha="$(printf '%s\n' "$version_json" | grep -E '"sha"[[:space:]]*:' | head -1 | sed 's/.*"sha"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
branch="$(printf '%s\n' "$version_json" | grep -E '"branch"[[:space:]]*:' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"
deployed_at="$(printf '%s\n' "$version_json" | grep -E '"deployed_at"[[:space:]]*:' | head -1 | sed 's/.*"deployed_at"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"

if [ -z "$short" ] && [ -n "$sha" ]; then
short="${sha:0:7}"
fi
Expand Down
4 changes: 2 additions & 2 deletions bin/ci/setup-arch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

set -euo pipefail

echo "=== [Arch] Installing git (needed to init test repo) ==="
pacman -Sy --noconfirm --needed git sudo 2>&1 | tail -3
echo "=== [Arch] Installing base CI deps ==="
pacman -Sy --noconfirm --needed git jq sudo 2>&1 | tail -3

echo "=== Preparing source ==="
useradd -m -s /bin/bash baudbot_admin
Expand Down
10 changes: 7 additions & 3 deletions bin/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ BAUDBOT_SRC="${BAUDBOT_SRC:-$(cd "$(dirname "$0")/.." && pwd)}"
BAUDBOT_HOME="${BAUDBOT_HOME:-/home/baudbot_agent}"
AGENT_USER="baudbot_agent"
DRY_RUN=0
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# shellcheck source=bin/lib/json-common.sh
source "$SCRIPT_DIR/lib/json-common.sh"

# Helper: run a command as baudbot_agent
as_agent() {
Expand Down Expand Up @@ -385,9 +389,9 @@ if [ "$DRY_RUN" -eq 0 ]; then
GIT_SHA_SHORT=$(cd "$BAUDBOT_SRC" && git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GIT_BRANCH=$(cd "$BAUDBOT_SRC" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
elif [ -f "$RELEASE_META_FILE" ]; then
GIT_SHA=$(grep '"sha"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true)
GIT_SHA_SHORT=$(grep '"short"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true)
GIT_BRANCH=$(grep '"branch"' "$RELEASE_META_FILE" | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true)
GIT_SHA="$(json_get_string_or_empty "$RELEASE_META_FILE" "sha")"
GIT_SHA_SHORT="$(json_get_string_or_empty "$RELEASE_META_FILE" "short")"
GIT_BRANCH="$(json_get_string_or_empty "$RELEASE_META_FILE" "branch")"
fi

[ -n "$GIT_SHA" ] || GIT_SHA="unknown"
Expand Down
6 changes: 6 additions & 0 deletions bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ else
fail "varlock not found"
fi

if command -v jq &>/dev/null; then
pass "jq is installed"
else
fail "jq not found (required for shell JSON parsing)"
fi

if command -v docker &>/dev/null; then
pass "docker is available"
else
Expand Down
53 changes: 53 additions & 0 deletions bin/lib/json-common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
# Shared JSON parsing helpers for shell scripts.
#
# jq is a required runtime dependency.
#
# Return codes:
# 0 => key found and value printed
# 1 => JSON parsed, but key missing/non-string
# 2 => JSON/file/tool error (including missing jq)

_json_filter='if (type == "object") and has($k) and (.[$k] | type == "string") then .[$k] else empty end'

json_require_jq() {
command -v jq >/dev/null 2>&1
}

json_get_string() {
local file="$1"
local key="$2"

[ -n "$file" ] || return 2
[ -n "$key" ] || return 2
[ -r "$file" ] || return 2
json_require_jq || return 2

jq -er --arg k "$key" "$_json_filter" "$file" 2>/dev/null
case "$?" in
0) return 0 ;;
1) return 1 ;;
*) return 2 ;;
esac
}

json_get_string_stdin() {
local key="$1"

[ -n "$key" ] || return 2
json_require_jq || return 2

jq -er --arg k "$key" "$_json_filter" 2>/dev/null
case "$?" in
0) return 0 ;;
1) return 1 ;;
*) return 2 ;;
esac
}

json_get_string_or_empty() {
local file="$1"
local key="$2"

json_get_string "$file" "$key" 2>/dev/null || true
}
112 changes: 112 additions & 0 deletions bin/lib/json-common.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/bin/bash
# Tests for bin/lib/json-common.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/lib/json-common.sh
source "$SCRIPT_DIR/json-common.sh"

TOTAL=0
PASSED=0
FAILED=0

run_test() {
local name="$1"
shift
local out

TOTAL=$((TOTAL + 1))
printf " %-45s " "$name"

out="$(mktemp /tmp/baudbot-json-common-test-output.XXXXXX)"
if "$@" >"$out" 2>&1; then
echo "✓"
PASSED=$((PASSED + 1))
else
echo "✗ FAILED"
tail -40 "$out" | sed 's/^/ /'
FAILED=$((FAILED + 1))
fi
rm -f "$out"
}

test_parses_string_key_with_whitespace_variations() {
(
set -euo pipefail
local tmp file value
tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

file="$tmp/meta.json"
cat > "$file" <<'EOF'
{
"sha" : "abc123",
"short": "abc123",
"nested": { "x": 1 }
}
EOF

value="$(json_get_string "$file" "sha")"
[ "$value" = "abc123" ]
)
}

test_missing_key_returns_nonzero() {
(
set -euo pipefail
local tmp file
tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

file="$tmp/meta.json"
printf '{"sha":"abc123"}\n' > "$file"

if json_get_string "$file" "branch" >/dev/null 2>&1; then
return 1
fi

[ -z "$(json_get_string_or_empty "$file" "branch")" ]
)
}

test_malformed_json_returns_nonzero() {
(
set -euo pipefail
local tmp file
tmp="$(mktemp -d /tmp/baudbot-json-common-test.XXXXXX)"
trap 'rm -rf "$tmp"' EXIT

file="$tmp/bad.json"
printf '{"sha": "abc123"\n' > "$file"

if json_get_string "$file" "sha" >/dev/null 2>&1; then
return 1
fi
)
}

test_stdin_parser_handles_whitespace() {
(
set -euo pipefail
local value

value="$(printf '{\n "deployed_at" : "2026-02-21T20:00:00Z"\n}\n' | json_get_string_stdin "deployed_at")"
[ "$value" = "2026-02-21T20:00:00Z" ]
)
}

echo "=== json-common tests ==="
echo ""

run_test "parses string key with whitespace" test_parses_string_key_with_whitespace_variations
run_test "missing key returns nonzero" test_missing_key_returns_nonzero
run_test "malformed json returns nonzero" test_malformed_json_returns_nonzero
run_test "stdin parser handles whitespace" test_stdin_parser_handles_whitespace

echo ""
echo "=== $PASSED/$TOTAL passed, $FAILED failed ==="

if [ "$FAILED" -gt 0 ]; then
exit 1
fi
6 changes: 4 additions & 2 deletions bin/rollback-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ EOF

# shellcheck source=bin/lib/release-common.sh
source "$SCRIPT_DIR/lib/release-common.sh"
# shellcheck source=bin/lib/json-common.sh
source "$SCRIPT_DIR/lib/json-common.sh"

TARGET_SPEC="${1:-previous}"
if [ "$#" -gt 0 ]; then
Expand Down Expand Up @@ -159,10 +161,10 @@ run_restart_and_health() {
local expected_sha
local deployed_sha

expected_sha=$(grep '"sha"' "$TARGET_RELEASE/baudbot-release.json" 2>/dev/null | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/' || true)
expected_sha="$(json_get_string_or_empty "$TARGET_RELEASE/baudbot-release.json" "sha")"
[ -n "$expected_sha" ] || expected_sha="$(basename "$TARGET_RELEASE")"

deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "grep '\"sha\"' '$version_file' 2>/dev/null | head -1 | sed 's/.*\"sha\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'")"
deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)"

if [ -z "$deployed_sha" ]; then
die "deployed version file missing or unreadable: $version_file"
Expand Down
11 changes: 9 additions & 2 deletions bin/security-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ set -euo pipefail
BAUDBOT_HOME="${BAUDBOT_HOME:-/home/baudbot_agent}"
# Source repo — auto-detect from this script's location, or use env override
BAUDBOT_SRC="${BAUDBOT_SRC:-$(cd "$(dirname "$0")/.." && pwd)}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# shellcheck source=bin/lib/json-common.sh
source "$SCRIPT_DIR/lib/json-common.sh"

DEEP=0
FIX=0
for arg in "$@"; do
Expand Down Expand Up @@ -224,8 +229,10 @@ VERSION_FILE="$BAUDBOT_HOME/.pi/agent/baudbot-version.json"
MANIFEST_FILE="$BAUDBOT_HOME/.pi/agent/baudbot-manifest.json"

if [ -f "$VERSION_FILE" ]; then
deploy_sha=$(grep '"short"' "$VERSION_FILE" 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "?")
deploy_ts=$(grep '"deployed_at"' "$VERSION_FILE" 2>/dev/null | sed 's/.*: *"\([^"]*\)".*/\1/' || echo "?")
deploy_sha="$(json_get_string_or_empty "$VERSION_FILE" "short")"
deploy_ts="$(json_get_string_or_empty "$VERSION_FILE" "deployed_at")"
[ -n "$deploy_sha" ] || deploy_sha="?"
[ -n "$deploy_ts" ] || deploy_ts="?"
ok "Deployed version: $deploy_sha ($deploy_ts)"
else
finding "WARN" "No version stamp found — run deploy.sh" ""
Expand Down
4 changes: 3 additions & 1 deletion bin/update-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ die() {

# shellcheck source=bin/lib/release-common.sh
source "$SCRIPT_DIR/lib/release-common.sh"
# shellcheck source=bin/lib/json-common.sh
source "$SCRIPT_DIR/lib/json-common.sh"

cleanup() {
if [ -n "$CHECKOUT_DIR" ] && [ -d "$CHECKOUT_DIR" ]; then
Expand Down Expand Up @@ -305,7 +307,7 @@ run_restart_and_health() {
local version_file="$BAUDBOT_AGENT_HOME/.pi/agent/baudbot-version.json"
local deployed_sha

deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "grep '\"sha\"' '$version_file' 2>/dev/null | head -1 | sed 's/.*\"sha\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'")"
deployed_sha="$(sudo -u "$BAUDBOT_AGENT_USER" sh -c "cat '$version_file' 2>/dev/null" | json_get_string_stdin "sha" 2>/dev/null || true)"

if [ -z "$deployed_sha" ]; then
die "deployed version file missing or unreadable: $version_file"
Expand Down
Loading