#!/usr/bin/env sh set -eu REPO_URL="${REPO_URL:-http://100.64.0.1:30087/section0/rta-handbook.git}" TARGET_DIR="${TARGET_DIR:-$HOME/Developer/Section0/rta-handbook}" COMMAND_DIR="${COMMAND_DIR:-$HOME/.local/bin}" COMMAND_NAME="${COMMAND_NAME:-section0-docs}" COMMAND_PATH="$COMMAND_DIR/$COMMAND_NAME" SERVER_URL="${SECTION0_SERVER_URL:-https://ops.virgil.info/md-to-section0-api}" STATUS_URL="${SECTION0_STATUS_URL:-https://ops.virgil.info/section0/projection-status.json}" step() { printf "\n==> %s\n" "$*" } if ! command -v git >/dev/null 2>&1; then echo "git is required but was not found on PATH" >&2 exit 1 fi step "Preparing local Markdown workspace" if [ -d "$TARGET_DIR/.git" ]; then echo "Repo already exists: $TARGET_DIR" git -C "$TARGET_DIR" remote set-url origin "$REPO_URL" git -C "$TARGET_DIR" fetch origin git -C "$TARGET_DIR" checkout main git -C "$TARGET_DIR" pull --ff-only else mkdir -p "$(dirname "$TARGET_DIR")" git clone "$REPO_URL" "$TARGET_DIR" fi git -C "$TARGET_DIR" config pull.ff only step "Installing helper command" mkdir -p "$COMMAND_DIR" cat > "$COMMAND_PATH" </dev/null 2>&1 || { echo "python3 is required for Authentik login JSON handling" >&2 exit 1 } } json_get() { need_python python3 -c 'import json,sys; data=json.load(sys.stdin); cur=data for part in sys.argv[1].split("."): cur = cur.get(part, "") if isinstance(cur, dict) else "" print(cur if cur is not None else "")' "\$1" } now_utc() { date -u +%Y-%m-%dT%H:%M:%SZ } run_id() { if command -v uuidgen >/dev/null 2>&1; then uuidgen | tr '[:upper:]' '[:lower:]' else date -u +%Y%m%d%H%M%S fi } json_log() { need_python mkdir -p "\$SESSION_DIR" event="\$1" detail="\${2:-}" current_head="\$(git -C "\$REPO_DIR" rev-parse --short HEAD 2>/dev/null || true)" current_branch="\$(git -C "\$REPO_DIR" branch --show-current 2>/dev/null || true)" remote="\$(git -C "\$REPO_DIR" remote get-url origin 2>/dev/null || true)" python3 -c 'import json,sys print(json.dumps({ "ts": sys.argv[1], "event": sys.argv[2], "detail": sys.argv[3], "repo": sys.argv[4], "branch": sys.argv[5], "head": sys.argv[6], "remote": sys.argv[7], }, separators=(",", ":")))' "\$(now_utc)" "\$event" "\$detail" "\$REPO_DIR" "\$current_branch" "\$current_head" "\$remote" >> "\$EVENTS_PATH" chmod 600 "\$EVENTS_PATH" } open_url() { if command -v open >/dev/null 2>&1; then open "\$1" >/dev/null 2>&1 || true elif command -v xdg-open >/dev/null 2>&1; then xdg-open "\$1" >/dev/null 2>&1 || true fi } credential_host() { need_python python3 -c 'from urllib.parse import urlparse; import sys url=urlparse(sys.argv[1]) print(url.netloc)' "\$1" } http_json() { curl --connect-timeout 5 --max-time 15 -fsS "\$@" } projection_status() { http_json "\$STATUS_URL" } wait_for_projection() { expected="\${1:-}" [ -n "\$expected" ] || expected="\$(git -C "\$REPO_DIR" rev-parse --short HEAD)" timeout_seconds="\${SECTION0_WAIT_TIMEOUT_SECONDS:-180}" started_at="\$(date +%s)" echo "Waiting for AFFiNE projection:" echo " commit: \$expected" echo " status: \$STATUS_URL" while :; do status_json="\$(projection_status 2>/dev/null || true)" if [ -n "\$status_json" ]; then state="\$(printf "%s" "\$status_json" | json_get state 2>/dev/null || true)" origin_commit="\$(printf "%s" "\$status_json" | json_get source.originCommit 2>/dev/null || true)" projected_commit="\$(printf "%s" "\$status_json" | json_get projected.commit 2>/dev/null || true)" completed_at="\$(printf "%s" "\$status_json" | json_get projected.completedAt 2>/dev/null || true)" readme_url="\$(printf "%s" "\$status_json" | python3 -c 'import json,sys data=json.load(sys.stdin) for doc in (data.get("projected") or {}).get("docs") or []: if doc.get("sourcePath") == "README.md": print(doc.get("url","")) break' 2>/dev/null || true)" printf " origin=%s projected=%s state=%s%s\n" "\${origin_commit:-unknown}" "\${projected_commit:-none}" "\${state:-unknown}" "\${completed_at:+ completed=\$completed_at}" if [ "\$projected_commit" = "\$expected" ]; then echo "Projected and verified in AFFiNE." [ -z "\$readme_url" ] || echo "Doc: \$readme_url" echo "If a browser tab still shows old content, reload the AFFiNE tab or open the doc URL in a fresh tab." json_log "wait.complete" "\$expected" return 0 fi else echo " projection status not reachable yet" fi now="\$(date +%s)" elapsed=\$((now - started_at)) if [ "\$elapsed" -ge "\$timeout_seconds" ]; then echo "Timed out after \${timeout_seconds}s waiting for projection of \$expected." >&2 echo "Run again later: section0-docs wait" >&2 json_log "wait.timeout" "\$expected" return 1 fi sleep "\${SECTION0_WAIT_INTERVAL_SECONDS:-5}" done } install_git_credential() { remote="\$1" username="\$2" password="\$3" host="\$(credential_host "\$remote")" protocol="\$(printf "%s" "\$remote" | sed -n "s#^\\([^:/]*\\)://.*#\\1#p")" [ -n "\$protocol" ] || protocol="http" mkdir -p "\$SESSION_DIR" credential_path="\$SESSION_DIR/git-credentials" touch "\$credential_path" chmod 600 "\$credential_path" git -C "\$REPO_DIR" config --local --replace-all credential.helper "" git -C "\$REPO_DIR" config --local --add credential.helper "store --file=\$credential_path" printf "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n\n" "\$protocol" "\$host" "\$username" "\$password" \ | git -C "\$REPO_DIR" credential approve chmod 600 "\$credential_path" } auth_login() { need_python mkdir -p "\$SESSION_DIR" login_run_id="\$(run_id)" json_log "auth.login.start" "\$login_run_id" device_json="\$(curl -fsS -X POST "\$SERVER_URL/device")" code="\$(printf "%s" "\$device_json" | json_get code)" auth_url="\$(printf "%s" "\$device_json" | json_get authUrl)" [ -n "\$code" ] && [ -n "\$auth_url" ] || { echo "login server did not return a device code" >&2 exit 1 } echo "Opening Authentik login:" echo " \$auth_url" echo "Device code:" echo " \$code" echo "Run id:" echo " \$login_run_id" echo "Token check:" echo " \$SERVER_URL/device/\$code/token" open_url "\$auth_url" if [ "\${SECTION0_AUTH_POLL:-}" != "1" ]; then echo "When the browser says login succeeded, return here and press Enter." printf "Press Enter to finish login: " IFS= read -r _section0_continue auth_finish "\$code" return fi echo "Waiting for login..." token_json="" i=0 while [ "\$i" -lt 90 ]; do token_json="\$(http_json "\$SERVER_URL/device/\$code/token" 2>/dev/null || true)" access_token="\$(printf "%s" "\${token_json:-{}}" | json_get accessToken 2>/dev/null || true)" [ -n "\$access_token" ] && break status="\$(printf "%s" "\${token_json:-{}}" | json_get status 2>/dev/null || true)" if [ \$((i % 5)) -eq 0 ]; then printf "\n still waiting%s\n" "\${status:+ (\$status)}" else printf "." fi i=\$((i + 1)) sleep 2 done printf "\n" [ -n "\${access_token:-}" ] || { echo "timed out waiting for Authentik login" >&2 echo "If the browser says login succeeded, run:" >&2 echo " section0-docs auth finish \$code" >&2 exit 1 } complete_login "\$token_json" "\$access_token" } auth_finish() { need_python mkdir -p "\$SESSION_DIR" code="\${1:-}" [ -n "\$code" ] || { echo "usage: section0-docs auth finish DEVICE_CODE" >&2 exit 1 } token_json="\$(http_json "\$SERVER_URL/device/\$code/token")" access_token="\$(printf "%s" "\$token_json" | json_get accessToken 2>/dev/null || true)" [ -n "\$access_token" ] || { echo "No completed login token for code: \$code" >&2 printf "%s\n" "\$token_json" >&2 exit 1 } complete_login "\$token_json" "\$access_token" } complete_login() { token_json="\$1" access_token="\$2" echo "Login accepted; requesting Section 0 Git access..." access_json="\$(http_json -X POST -H "Authorization: Bearer \$access_token" "\$SERVER_URL/section0/git/access")" ok="\$(printf "%s" "\$access_json" | json_get ok)" [ "\$ok" = "True" ] || [ "\$ok" = "true" ] || { echo "Git access broker did not return credentials:" >&2 printf "%s\n" "\$access_json" >&2 exit 1 } remote="\$(printf "%s" "\$access_json" | json_get remote)" git_username="\$(printf "%s" "\$access_json" | json_get git.username)" git_password="\$(printf "%s" "\$access_json" | json_get git.password)" author_name="\$(printf "%s" "\$access_json" | json_get author.name)" author_email="\$(printf "%s" "\$access_json" | json_get author.email)" [ -n "\$remote" ] && [ -n "\$git_username" ] && [ -n "\$git_password" ] || { echo "Git access broker response was incomplete" >&2 exit 1 } git -C "\$REPO_DIR" remote set-url origin "\$remote" git -C "\$REPO_DIR" config user.name "\$author_name" git -C "\$REPO_DIR" config user.email "\$author_email" echo "Storing Git credential..." install_git_credential "\$remote" "\$git_username" "\$git_password" python3 -c 'import json,sys token=json.loads(sys.argv[1]) access=json.loads(sys.argv[2]) print(json.dumps({ "authenticated": True, "serverUrl": sys.argv[3], "authentik": token.get("authentik", {}), "author": access.get("author", {}), "remote": access.get("remote", ""), "credentialUser": access.get("git", {}).get("username", "") }, indent=2))' "\$token_json" "\$access_json" "\$SERVER_URL" > "\$SESSION_PATH" chmod 600 "\$SESSION_PATH" echo "Authenticated: \$author_email" echo "Git remote: \$remote" echo "Git user: \$git_username" echo "Session: \$SESSION_PATH" json_log "auth.login.complete" "\$author_email" } case "\${1:-help}" in auth) case "\${2:-}" in login) auth_login ;; finish) auth_finish "\${3:-}" ;; status) if [ -f "\$SESSION_PATH" ]; then cat "\$SESSION_PATH" else echo "not authenticated; run: section0-docs auth login" >&2 exit 1 fi ;; *) echo "usage: section0-docs auth " >&2 exit 1 ;; esac ;; configure) current_name="\$(git -C "\$REPO_DIR" config user.name || true)" current_email="\$(git -C "\$REPO_DIR" config user.email || true)" printf "Git author name [%s]: " "\$current_name" IFS= read -r author_name printf "Git author email [%s]: " "\$current_email" IFS= read -r author_email if [ -n "\$author_name" ]; then git -C "\$REPO_DIR" config user.name "\$author_name" elif [ -z "\$current_name" ]; then echo "Git author name is required" >&2 exit 1 fi if [ -n "\$author_email" ]; then git -C "\$REPO_DIR" config user.email "\$author_email" elif [ -z "\$current_email" ]; then echo "Git author email is required" >&2 exit 1 fi ;; doctor) remote="\$(git -C "\$REPO_DIR" remote get-url origin)" branch="\$(git -C "\$REPO_DIR" branch --show-current)" author_name="\$(git -C "\$REPO_DIR" config user.name || true)" author_email="\$(git -C "\$REPO_DIR" config user.email || true)" echo "Repo: \$REPO_DIR" echo "Remote: \$remote" echo "Branch: \$branch" if [ -n "\$author_name" ] && [ -n "\$author_email" ]; then echo "Git author: \$author_name <\$author_email>" else echo "Git author: missing; run section0-docs configure" fi git -C "\$REPO_DIR" ls-remote --heads origin main >/dev/null echo "Read access: ok" ;; open) printf "%s\n" "\$REPO_DIR" ;; pull) git -C "\$REPO_DIR" pull --ff-only ;; status) git -C "\$REPO_DIR" status --short --branch ;; commit) : "\${MESSAGE:?Set MESSAGE before running commit}" json_log "commit.start" "\$MESSAGE" git -C "\$REPO_DIR" add . git -C "\$REPO_DIR" commit -m "\$MESSAGE" json_log "commit.complete" "\$(git -C "\$REPO_DIR" rev-parse --short HEAD)" ;; push) before="\$(git -C "\$REPO_DIR" rev-parse --short HEAD)" json_log "push.start" "\$before" git -C "\$REPO_DIR" push git -C "\$REPO_DIR" fetch origin >/dev/null 2>&1 || true origin_head="\$(git -C "\$REPO_DIR" rev-parse --short origin/main 2>/dev/null || true)" json_log "push.complete" "\${origin_head:-\$before}" echo "Pushed commit: \${origin_head:-\$before}" if [ "\${SECTION0_WAIT_AFTER_PUSH:-1}" = "1" ]; then wait_for_projection "\${origin_head:-\$before}" else echo "AFFiNE updates after the operator refresh runs." echo "To watch it: section0-docs wait" fi ;; wait) wait_for_projection "\${2:-}" ;; push-test) branch="section0-smoke-\${USER:-user}-\$(date +%Y%m%d%H%M%S)" git -C "\$REPO_DIR" push origin HEAD:refs/heads/"\$branch" git -C "\$REPO_DIR" push origin :refs/heads/"\$branch" echo "Write access: ok" json_log "push-test.complete" "\$branch" ;; trace) echo "Repo: \$REPO_DIR" echo "Remote: \$(git -C "\$REPO_DIR" remote get-url origin)" echo "Projection status: \$STATUS_URL" echo "Branch: \$(git -C "\$REPO_DIR" branch --show-current)" echo "Local HEAD: \$(git -C "\$REPO_DIR" rev-parse --short HEAD)" git -C "\$REPO_DIR" fetch origin >/dev/null 2>&1 || true echo "Origin HEAD: \$(git -C "\$REPO_DIR" rev-parse --short origin/main 2>/dev/null || true)" echo "Status:" git -C "\$REPO_DIR" status --short --branch echo echo "Recent helper events: \$EVENTS_PATH" if [ -f "\$EVENTS_PATH" ]; then tail -20 "\$EVENTS_PATH" else echo "no events yet" fi ;; help|--help|-h) usage ;; *) usage >&2 exit 1 ;; esac EOF chmod +x "$COMMAND_PATH" cat <