diff --git a/.gitignore b/.gitignore index ae359692b2..63834a5c92 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ jobs/** node_modules/ bench/__pycache__ .ai/ + +# Beads / Dolt files (added by bd init) +.dolt/ +.beads-credential-key diff --git a/cachyos/PKGBUILD b/cachyos/PKGBUILD new file mode 100644 index 0000000000..da54705cde --- /dev/null +++ b/cachyos/PKGBUILD @@ -0,0 +1,155 @@ +# Maintainer: CachyOS Custom Overlay +# Contributor: ForgeCode packaging +# +# PKGBUILD for Forge (https://forgecode.dev) built with CachyOS optimizations. +# Intended for use in a private/custom CachyOS overlay repository served from +# a Proxmox VM (or similar) using devtools + clean chroot (extra-x86_64-build). +# +# CachyOS makepkg.conf (or equivalent) supplies the RUSTFLAGS containing +# -C target-cpu=x86-64-v3 (or v4 for supported CPUs) +# plus any other CachyOS LTO / march / mtune settings. +# This PKGBUILD does *not* override RUSTFLAGS so that the chroot environment +# (and local makepkg.conf on CachyOS workstations) fully controls the opts. +# +# The binary is completely self-contained for the ZSH plugin: +# * shell-plugin/forge.setup.zsh , lib/*.zsh and completions are include_dir!'d +# and include_str!'d at compile time into the executable. +# * Therefore we only ship /usr/bin/forge ; no extra runtime data files required. +# * After package install users run `forge zsh setup` (or the interactive first-run +# banner) to populate the # >>> forge initialize >>> block in their ~/.zshrc. +# +# Compatible with the from-source scripts/install.sh (in the forge repo) for +# local dev/CachyOS workstation testing outside of packaging: +# RUSTFLAGS="-C target-cpu=x86-64-v3" ./scripts/install.sh --reinstall +# +# Versioning strategy for custom/rebase/feature builds: +# - pkgver is the upstream base (0.1.0 here; update when rebasing on tags) +# - pkgrel carries a local suffix (e.g. 1.cachy , 1.xai20260606) so that +# our overlay packages never accidentally satisfy an official repo version +# and we can force upgrades on re-packs. +# +# Build in clean chroot example (from the overlay checkout containing this PKGBUILD): +# extra-x86_64-build # or cachy-x86_64-build if the overlay provides a wrapper +# +# Local (dirty) test build from inside a forge source tree (for quick iteration): +# cd /path/to/forgecode +# cp cachyos/PKGBUILD . +# # (edit pkgrel if desired) +# makepkg -s --nocheck # will use current tree as "source" via the hack below +# +# Co-Authored-By: ForgeCode + +pkgname=forge +pkgver=0.1.0 +pkgrel=1.cachy +pkgdesc="AI-enabled pair programmer for 300+ models (Claude, GPT, Grok, Deepseek, Gemini...)" +arch=('x86_64') +url="https://forgecode.dev" +license=('MIT') +depends=('git' 'fd') +optdepends=( + 'bat: enhanced file previews in :doctor and the ZSH plugin' + 'zsh: to use the : prefix ZSH plugin and theme' + 'fzf: improved interactive pickers (some plugin features)' +) +makedepends=( + 'cargo' + 'cmake' + 'nasm' + 'perl' + 'pkgconf' + 'protobuf' + 'sqlite' +) +provides=('forge') +conflicts=('forge-bin') # if an official bin package ever appears +options=('!lto' '!debug') # LTO is controlled by the Cargo release profile + RUSTFLAGS +source=() +sha256sums=() +# No separate .install file: post_install / post_upgrade are defined at the bottom of +# this PKGBUILD and are executed by makepkg/pacman directly. This keeps the packaging +# artifact count to the single PKGBUILD (plus the built package). + +# When this PKGBUILD lives next to a full forge checkout (for local makepkg testing +# of CachyOS-optimized builds) we treat $startdir as the source tree. +# In a real clean-chroot / overlay build you would normally use a git source entry +# pointing at the desired branch/commit and a proper pkgver() function. +# The logic below makes both scenarios work without modification. +prepare() { + # If we were given a real source tarball/git clone by makepkg, use $srcdir. + # Otherwise fall back to the directory containing the PKGBUILD (local tree test). + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + # Last resort: assume user did `makepkg` while cwd is the forge tree + cd "$startdir" + fi + + # Ensure we have a Cargo.lock (we do in the repo) + if [ ! -f Cargo.lock ]; then + cargo fetch --locked + fi +} + +build() { + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + cd "$startdir" + fi + + echo "==> Building with CachyOS-provided RUSTFLAGS (from makepkg.conf / chroot)" + echo " RUSTFLAGS=${RUSTFLAGS:-}" + echo " APP_VERSION will be set to ${pkgver}-${pkgrel} for build.rs embedding" + + export APP_VERSION="${pkgver}-${pkgrel}" + + # --frozen for reproducibility inside the chroot (Cargo.lock is present) + cargo build --frozen --release -p forge_main --bin forge +} + +package() { + if [ -d "$srcdir/forgecode" ]; then + cd "$srcdir/forgecode" + elif [ -f "$startdir/Cargo.toml" ] && [ -d "$startdir/crates/forge_main" ]; then + cd "$startdir" + else + cd "$startdir" + fi + + install -Dm755 target/release/forge "$pkgdir/usr/bin/forge" + + # License (required by Arch packaging guidelines) + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + + # Optional: ship the original shell-plugin sources under /usr/share for reference + # or for people who want to inspect / diff the embedded code. Not required at + # runtime because everything the binary needs is compiled in via include_dir!. + # Uncomment if your overlay wants to expose them: + # + # install -d "$pkgdir/usr/share/forge/shell-plugin" + # cp -a --no-preserve=ownership shell-plugin/* "$pkgdir/usr/share/forge/shell-plugin/" +} + +# The .install file is written inline by makepkg when this PKGBUILD is processed +# (because we declared `install=forge.install` and the content follows). +# +# It only prints a friendly message after pacman -S / -U. +post_install() { + echo "==> Forge installed to /usr/bin/forge" + echo "==> To enable the fast :command ZSH plugin (recommended):" + echo " forge zsh setup" + echo " exec zsh" + echo "==> Then try :doctor or just type : followed by a prompt at your shell." + echo "==> CachyOS-optimized build (RUSTFLAGS from makepkg.conf at build time)." +} + +post_upgrade() { + post_install +} + +# vim: set ts=2 sw=2 et: diff --git a/crates/forge_infra/src/auth/strategy.rs b/crates/forge_infra/src/auth/strategy.rs index 559a365f19..3ee09a8f82 100644 --- a/crates/forge_infra/src/auth/strategy.rs +++ b/crates/forge_infra/src/auth/strategy.rs @@ -1319,6 +1319,62 @@ mod tests { assert!(matches!(actual.unwrap(), AnyAuthStrategy::CodexDevice(_))); } + #[test] + fn test_create_auth_strategy_xai_oauth_code_uses_standard() { + // xAI is neither CLAUDE_CODE nor GITHUB_COPILOT, so the OAuthCode + // (SuperGrok loopback) flow must fall through to the generic + // StandardHttpProvider with zero per-provider code. + let config = OAuthConfig { + client_id: "b1a00492-073a-47ea-816f-4c329264a828".to_string().into(), + auth_url: Url::parse("https://auth.x.ai/oauth2/authorize").unwrap(), + token_url: Url::parse("https://auth.x.ai/oauth2/token").unwrap(), + scopes: vec!["api:access".to_string()], + redirect_uri: Some("http://127.0.0.1:56121/callback".to_string()), + use_pkce: true, + token_refresh_url: None, + extra_auth_params: None, + custom_headers: None, + }; + + let factory = ForgeAuthStrategyFactory; + let actual = factory + .create_auth_strategy( + ProviderId::XAI, + forge_domain::AuthMethod::OAuthCode(config), + vec![], + ) + .unwrap(); + assert!(matches!(actual, AnyAuthStrategy::OAuthCodeStandard(_))); + } + + #[test] + fn test_create_auth_strategy_xai_oauth_device_uses_device() { + // The xAI headless device flow omits token_refresh_url, so it must + // route to the plain OAuthDevice strategy (RFC 8628), not the + // GitHub-Copilot OAuthWithApiKey hybrid. + let config = OAuthConfig { + client_id: "b1a00492-073a-47ea-816f-4c329264a828".to_string().into(), + auth_url: Url::parse("https://auth.x.ai/oauth2/device/code").unwrap(), + token_url: Url::parse("https://auth.x.ai/oauth2/token").unwrap(), + scopes: vec!["api:access".to_string()], + redirect_uri: None, + use_pkce: false, + token_refresh_url: None, + extra_auth_params: None, + custom_headers: None, + }; + + let factory = ForgeAuthStrategyFactory; + let actual = factory + .create_auth_strategy( + ProviderId::XAI, + forge_domain::AuthMethod::OAuthDevice(config), + vec![], + ) + .unwrap(); + assert!(matches!(actual, AnyAuthStrategy::OAuthDevice(_))); + } + /// Helper to build a JWT token with the given claims payload. fn build_jwt(claims: &serde_json::Value) -> String { use base64::Engine; diff --git a/crates/forge_repo/src/provider/provider.json b/crates/forge_repo/src/provider/provider.json index d4db7d3439..bdd484967f 100644 --- a/crates/forge_repo/src/provider/provider.json +++ b/crates/forge_repo/src/provider/provider.json @@ -108,7 +108,46 @@ "response_type": "OpenAI", "url": "https://api.x.ai/v1/chat/completions", "models": "https://api.x.ai/v1/models", - "auth_methods": ["api_key"] + "auth_methods": [ + { + "oauth_code": { + "auth_url": "https://auth.x.ai/oauth2/authorize", + "token_url": "https://auth.x.ai/oauth2/token", + "client_id": "b1a00492-073a-47ea-816f-4c329264a828", + "scopes": [ + "openid", + "profile", + "email", + "offline_access", + "grok-cli:access", + "api:access" + ], + "redirect_uri": "http://127.0.0.1:56121/callback", + "use_pkce": true, + "extra_auth_params": { + "plan": "generic", + "referrer": "forgecode" + } + } + }, + { + "oauth_device": { + "auth_url": "https://auth.x.ai/oauth2/device/code", + "token_url": "https://auth.x.ai/oauth2/token", + "client_id": "b1a00492-073a-47ea-816f-4c329264a828", + "scopes": [ + "openid", + "profile", + "email", + "offline_access", + "grok-cli:access", + "api:access" + ], + "use_pkce": false + } + }, + "api_key" + ] }, { "id": "openai", diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 9f9d2a5877..92ff77843c 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -713,6 +713,84 @@ mod tests { ); } + #[test] + fn test_xai_oauth_config() { + let configs = get_provider_configs(); + let config = configs.iter().find(|c| c.id == ProviderId::XAI).unwrap(); + + assert_eq!(config.id, ProviderId::XAI); + assert_eq!(config.api_key_vars, Some("XAI_API_KEY".to_string())); + assert_eq!(config.response_type, Some(ProviderResponse::OpenAI)); + assert_eq!(config.url.as_str(), "https://api.x.ai/v1/chat/completions"); + + // Three auth methods: loopback OAuth, headless device OAuth, manual key. + assert_eq!(config.auth_methods.len(), 3); + assert!(config.auth_methods.contains(&AuthMethod::ApiKey)); + + let expected_scopes = vec![ + "openid".to_string(), + "profile".to_string(), + "email".to_string(), + "offline_access".to_string(), + "grok-cli:access".to_string(), + "api:access".to_string(), + ]; + + // Loopback authorization-code + PKCE (SuperGrok subscription). + let code = config + .auth_methods + .iter() + .find_map(|m| match m { + AuthMethod::OAuthCode(cfg) => Some(cfg), + _ => None, + }) + .expect("xai should expose an oauth_code auth method"); + assert_eq!( + code.client_id.as_str(), + "b1a00492-073a-47ea-816f-4c329264a828" + ); + assert_eq!(code.auth_url.as_str(), "https://auth.x.ai/oauth2/authorize"); + assert_eq!(code.token_url.as_str(), "https://auth.x.ai/oauth2/token"); + assert_eq!(code.scopes, expected_scopes); + assert_eq!( + code.redirect_uri.as_deref(), + Some("http://127.0.0.1:56121/callback") + ); + assert!(code.use_pkce); + let extra = code + .extra_auth_params + .as_ref() + .expect("oauth_code should set extra_auth_params"); + // plan=generic is mandatory: xAI rejects loopback OAuth from + // non-allowlisted clients without it. + assert_eq!(extra.get("plan").map(String::as_str), Some("generic")); + assert_eq!(extra.get("referrer").map(String::as_str), Some("forgecode")); + + // Headless device-code (remote / VPS). auth_url MUST be the + // device-authorization endpoint, and token_refresh_url must be absent + // so the factory routes to the plain device flow. + let device = config + .auth_methods + .iter() + .find_map(|m| match m { + AuthMethod::OAuthDevice(cfg) => Some(cfg), + _ => None, + }) + .expect("xai should expose an oauth_device auth method"); + assert_eq!( + device.client_id.as_str(), + "b1a00492-073a-47ea-816f-4c329264a828" + ); + assert_eq!( + device.auth_url.as_str(), + "https://auth.x.ai/oauth2/device/code" + ); + assert_eq!(device.token_url.as_str(), "https://auth.x.ai/oauth2/token"); + assert_eq!(device.scopes, expected_scopes); + assert!(device.redirect_uri.is_none()); + assert!(device.token_refresh_url.is_none()); + } + #[test] fn test_vertex_ai_config() { let configs = get_provider_configs(); diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000000..bfa0ef2b4c --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,588 @@ +#!/usr/bin/env bash +# +# Local from-source build + install/uninstall/reinstall script for Forge. +# +# This is the equivalent of the official installer (curl -fsSL https://forgecode.dev/cli | sh) +# but for building and installing directly from a source checkout. It is intended for: +# - Local development on the feat/xai-supergrok-oauth (and similar) branches +# - CachyOS custom packaging / optimized builds before feeding into PKGBUILD + devtools chroot +# +# Features: +# - Builds with: cargo build --release -p forge_main --bin forge +# - Honors RUSTFLAGS (for CachyOS: -C target-cpu=x86-64-v3 , x86-64-v4, LTO etc.) +# - Honors APP_VERSION (baked via build.rs into CARGO_PKG_VERSION for the binary) +# - Installs the resulting binary (default: $HOME/.local/bin/forge or /usr/local/bin with sudo) +# - Replicates 'forge zsh setup' NON-INTERACTIVELY: inserts the exact marker block +# (# >>> forge initialize >>> ... # <<< forge initialize <<<) using content from +# shell-plugin/forge.setup.zsh (same as the one baked into the binary). +# - Supports uninstall: removes binary + cleans the forge initialize markers from .zshrc +# (and creates timestamped .bak like the Rust implementation). Also cleans any PATH +# markers we may have added. +# - --reinstall = uninstall + install +# - --build-only to just produce target/release/forge +# - --prefix=..., --force, --no-zsh, --help +# +# Usage examples (CachyOS optimized dev build): +# RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="0.1.0-cachy1" ./scripts/install.sh +# ./scripts/install.sh --prefix=/usr/local --force +# ./scripts/install.sh --reinstall +# ./scripts/install.sh --uninstall +# ./scripts/install.sh --build-only +# +# The script is self-contained, uses the local tree for the embedded shell-plugin content, +# and can be used inside a PKGBUILD %build / %package or for manual custom package testing. +# +# After install (for ZSH users): +# exec zsh # or open a new terminal +# # then test: +# forge --version +# :doctor +# +# IMPORTANT: This script does NOT run the interactive `forge zsh setup` (which asks about +# Nerd Fonts + editor). It performs a non-interactive default setup (no NERD_FONT=0, no +# FORGE_EDITOR override). Run `forge zsh setup` manually afterwards if you want the prompts. +# +# Follows project conventions where applicable (e.g. no new *.md docs created here). +# Errors use clear messages; the script is executable after `chmod +x`. +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +# Colors (mimic official installer style, using printf) +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Resolve repo root relative to this script (works when invoked from anywhere) +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)" + +# Defaults (can be overridden by env or flags) +PREFIX="${PREFIX:-${HOME}/.local}" +APP_VERSION="${APP_VERSION:-}" +RUSTFLAGS="${RUSTFLAGS:-}" +UNINSTALL=false +REINSTALL=false +BUILD_ONLY=false +FORCE=false +NO_ZSH=false +VERBOSE=false + +usage() { + cat <<'EOF' +Forge local from-source installer (like official curl | sh, but builds here) + +Usage: + ./scripts/install.sh [options] + +Options: + --help, -h Show this help + --prefix DIR Installation prefix (binary goes to DIR/bin/forge) + Default: $HOME/.local (so ~/.local/bin/forge) + Use --prefix=/usr/local for system-wide (may need sudo) + --uninstall Remove installed binary and clean ZSH markers from .zshrc + --reinstall Uninstall then install (useful for upgrades from source) + --build-only Only run the cargo release build; do not install or touch shell + --force Overwrite existing binary without warning + --no-zsh Skip non-interactive ZSH marker block setup/update + --verbose, -v More output during build + +Environment variables respected (for CachyOS etc.): + RUSTFLAGS Passed through (e.g. "-C target-cpu=x86-64-v3") + APP_VERSION Baked into binary via build.rs (e.g. "0.1.0-mybuild") + PREFIX Same as --prefix + +Typical CachyOS optimized flow (before or instead of full PKGBUILD): + RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="$(date +%Y%m%d)-cachy" \ + ./scripts/install.sh --reinstall + +The script will: + 1. Build target/release/forge (honoring RUSTFLAGS + APP_VERSION) + 2. Install the binary + 3. Ensure the install bin dir is mentioned in PATH (via marker in rc files) + 4. Insert/update the exact "forge initialize" marker block in .zshrc / $ZDOTDIR/.zshrc + (content taken from shell-plugin/forge.setup.zsh at build time of *this* script) + +Uninstall will: + - Delete the forge binary from common locations under the chosen/current prefix + - Remove the >>> / <<< forge initialize block (with .bak timestamp like Rust code) + - Remove any PATH marker lines we added + +See also: + - Official installer: curl -fsSL https://forgecode.dev/cli | sh + - After install: forge zsh setup (for the interactive Nerd Font / editor flow) + - ZSH doctor: :doctor or forge zsh doctor +EOF +} + +log_info() { printf "${BLUE}%s${NC}\n" "$*"; } +log_ok() { printf "${GREEN}✓ %s${NC}\n" "$*"; } +log_warn() { printf "${YELLOW}%s${NC}\n" "$*"; } +log_error() { printf "${RED}Error: %s${NC}\n" "$*" >&2; } + +# Parse command line (support --foo=bar and --foo bar) +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --help|-h) + usage + exit 0 + ;; + --prefix=*) + PREFIX="${1#*=}" + ;; + --prefix) + shift + PREFIX="${1:-}" + ;; + --uninstall) + UNINSTALL=true + ;; + --reinstall) + REINSTALL=true + ;; + --build-only) + BUILD_ONLY=true + ;; + --force) + FORCE=true + ;; + --no-zsh) + NO_ZSH=true + ;; + --verbose|-v) + VERBOSE=true + ;; + --) + shift + break + ;; + -*) + log_error "Unknown option: $1" + usage + exit 1 + ;; + *) + log_error "Unexpected argument: $1" + usage + exit 1 + ;; + esac + shift + done +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + log_error "Required command not found: $1" + exit 1 + fi +} + +# Build step (always uses release profile from Cargo.toml which has lto + opt-level=3 + strip) +do_build() { + log_info "Building Forge from source in $REPO_ROOT" + + if [ -n "$APP_VERSION" ]; then + log_info " APP_VERSION=$APP_VERSION (will be baked via build.rs)" + export APP_VERSION + fi + + if [ -n "$RUSTFLAGS" ]; then + log_info " RUSTFLAGS=$RUSTFLAGS (CachyOS / custom target-cpu etc.)" + export RUSTFLAGS + else + log_warn " RUSTFLAGS is empty. For CachyOS x86-64-v3+ optimizations run with:" + log_warn " RUSTFLAGS=\"-C target-cpu=x86-64-v3\" $0 ..." + log_warn " (or -C target-cpu=x86-64-v4 if your CPU and CachyOS makepkg.conf support it)" + fi + + require_command cargo + + # We intentionally build --release here; this script exists precisely to produce + # optimized binaries (the AGENTS.md guidance against `cargo build --release` in + # day-to-day dev does not apply to this packaging/install helper). + BUILD_CMD=(cargo build --release -p forge_main --bin forge) + if $VERBOSE; then + BUILD_CMD+=(--verbose) + fi + + log_info " ${BUILD_CMD[*]}" + (cd "$REPO_ROOT" && "${BUILD_CMD[@]}") + + local src_bin="$REPO_ROOT/target/release/forge" + if [ ! -x "$src_bin" ]; then + log_error "Build succeeded but binary not found or not executable: $src_bin" + exit 1 + fi + + log_ok "Build complete: $src_bin" + "$src_bin" --version || true +} + +# Locate the just-built binary (or fail) +get_src_bin() { + local src_bin="$REPO_ROOT/target/release/forge" + if [ ! -x "$src_bin" ]; then + log_error "No built binary at $src_bin. Run without --build-only first, or use --reinstall." + exit 1 + fi + printf '%s' "$src_bin" +} + +# Install the binary to $PREFIX/bin/forge (with sudo if the dir is not writable) +do_install_binary() { + local src_bin + src_bin="$(get_src_bin)" + + local bin_dir="$PREFIX/bin" + local dest="$bin_dir/forge" + + log_info "Installing binary to $dest" + + mkdir -p "$bin_dir" 2>/dev/null || true + + local use_sudo="" + if [ ! -w "$bin_dir" ] && [ "$(id -u)" -ne 0 ]; then + if command -v sudo >/dev/null 2>&1; then + use_sudo="sudo" + log_warn "Directory $bin_dir not writable by $(id -un); using sudo" + else + log_error "Cannot write to $bin_dir and no sudo available" + exit 1 + fi + fi + + if [ -e "$dest" ] && ! $FORCE && ! $REINSTALL; then + log_warn "Forge already exists at $dest" + log_warn "Use --force or --reinstall to overwrite" + # Still continue so that zsh setup etc. can be (re)done + fi + + $use_sudo install -Dm755 "$src_bin" "$dest" 2>/dev/null || { + # Fallback for systems without GNU install -D + $use_sudo mkdir -p "$bin_dir" + $use_sudo cp "$src_bin" "$dest" + $use_sudo chmod 755 "$dest" + } + + log_ok "forge installed: $dest" + "$dest" --version || true + + # Also ensure PATH contains the bin dir (mimics official installer behavior) + ensure_path_entry "$bin_dir" +} + +# Ensure a bin dir is on PATH in the usual shell rc files (using a marker so uninstall can clean it) +# We only touch .zshrc and .bashrc to stay close to official. +ensure_path_entry() { + local bin_dir="$1" + local path_marker="# Added by Forge (local from-source installer)" + local export_line="export PATH=\"$bin_dir:\$PATH\"" + + for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do + # Ensure file exists + if [ ! -f "$rc" ]; then + # Create with the marker + export (harmless if user never uses that shell) + { + printf '%s\n' "$path_marker" + printf '%s\n' "$export_line" + } >> "$rc" + log_info "Created $rc with PATH entry for $bin_dir" + continue + fi + + # If the exact export is already there (under our marker or otherwise), do nothing + if grep -Fq "$export_line" "$rc" 2>/dev/null; then + continue + fi + + # Remove any previous lines we added (by marker or the exact export) then prepend fresh + local tmp + tmp="$(mktemp)" + grep -vF "$path_marker" "$rc" | grep -vF "$export_line" > "$tmp" || true + + # Prepend our block at the very top (after possible shebang or first lines is ok for PATH) + { + printf '%s\n' "$path_marker" + printf '%s\n' "$export_line" + printf '\n' + cat "$tmp" + } > "$rc" + rm -f "$tmp" + + log_info "Updated $rc to ensure $bin_dir is on PATH (marker: $path_marker)" + done +} + +# Remove PATH marker lines we may have added (best effort) +clean_path_markers() { + local path_marker="# Added by Forge (local from-source installer)" + for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do + if [ -f "$rc" ] && grep -Fq "$path_marker" "$rc" 2>/dev/null; then + local tmp + tmp="$(mktemp)" + grep -vF "$path_marker" "$rc" > "$tmp" || true + # Also drop any orphan export lines that look like ours for this installer + # (we keep other PATH manipulation) + mv "$tmp" "$rc" + log_ok "Removed Forge PATH marker from $rc" + fi + done +} + +# Replicate the non-interactive equivalent of setup_zsh_integration() from +# crates/forge_main/src/zsh/plugin.rs using the same markers and the exact +# content of shell-plugin/forge.setup.zsh (no nerd-font or editor overrides). +# +# This inserts: +# # >>> forge initialize >>> +# +# # <<< forge initialize <<< +# +# And creates a timestamped backup on change, exactly like the Rust code. +do_zsh_setup() { + if $NO_ZSH; then + log_info "Skipping ZSH integration (--no-zsh)" + return 0 + fi + + log_info "Setting up ZSH integration (non-interactive 'forge zsh setup' equivalent)" + + local zdotdir="${ZDOTDIR:-$HOME}" + local zshrc="$zdotdir/.zshrc" + + local start_marker="# >>> forge initialize >>>" + local end_marker="# <<< forge initialize <<<" + + local setup_src="$REPO_ROOT/shell-plugin/forge.setup.zsh" + if [ ! -f "$setup_src" ]; then + log_error "Cannot find embedded setup block source: $setup_src" + log_error "This script must be run from inside a full forge source tree." + exit 1 + fi + + # Normalize (strip stray CRs like the Rust normalize_script does) + local forge_init_config + forge_init_config="$(sed 's/\r$//' "$setup_src")" + + # Build the full block exactly as Rust does (no extra nerd/editor lines) + local forge_config + forge_config="${start_marker} +${forge_init_config} +${end_marker}" + + # Create backup if file exists (timestamp format matches Rust: %Y-%m-%d_%H-%M-%S) + local backup_path="" + if [ -f "$zshrc" ]; then + local ts + ts="$(date +%Y-%m-%d_%H-%M-%S)" + backup_path="$zshrc.bak.$ts" + cp "$zshrc" "$backup_path" + log_info "Backup created: $backup_path" + fi + + # Use bash arrays + simple scan to replicate parse + splice logic. + # This keeps behavior very close to the Rust implementation (replace in place + # or append at end, preserving other content). + local -a lines=() + if [ -f "$zshrc" ]; then + mapfile -t lines < "$zshrc" + fi + + local start_idx=-1 + local end_idx=-1 + local i + for i in "${!lines[@]}"; do + if [ "${lines[$i]}" = "$start_marker" ]; then + start_idx=$i + fi + if [ "${lines[$i]}" = "$end_marker" ]; then + end_idx=$i + fi + done + + # Split the block into lines (preserve exact content including internal blanks) + local -a block_lines=() + while IFS= read -r line || [ -n "$line" ]; do + block_lines+=("$line") + done <<< "$forge_config" + + local -a new_lines=() + if [ $start_idx -ge 0 ] && [ $end_idx -gt $start_idx ]; then + # Valid existing block: replace it (splice) + new_lines=( "${lines[@]:0:$start_idx}" ) + new_lines+=( "${block_lines[@]}" ) + local after_start=$(( end_idx + 1 )) + if [ $after_start -le ${#lines[@]} ]; then + new_lines+=( "${lines[@]:$after_start}" ) + fi + else + # No (valid) block: append at end, with a separating blank line if needed + new_lines=( "${lines[@]}" ) + if [ ${#new_lines[@]} -gt 0 ]; then + local last="${new_lines[-1]}" + if [ -n "${last//[[:space:]]/}" ]; then + new_lines+=( "" ) + fi + fi + new_lines+=( "${block_lines[@]}" ) + fi + + # Write atomically via temp + mv + local tmp + tmp="$(mktemp)" + if [ ${#new_lines[@]} -gt 0 ]; then + printf '%s\n' "${new_lines[@]}" > "$tmp" + else + : > "$tmp" + fi + mv "$tmp" "$zshrc" + + log_ok "forge plugins added/updated in $zshrc" + if [ -n "$backup_path" ]; then + log_info "(previous version backed up)" + fi + + # Friendly next steps (mimic what the interactive on_zsh_setup prints) + log_info "Run: exec zsh (or open a new terminal) to load the updated config" + log_info "Then try: :doctor or forge zsh doctor" +} + +# Remove the forge initialize marker block (and the markers themselves) from .zshrc +# Best-effort; also handles the case where only one marker exists. +clean_zsh_markers() { + local zdotdir="${ZDOTDIR:-$HOME}" + local zshrc="$zdotdir/.zshrc" + + if [ ! -f "$zshrc" ]; then + return 0 + fi + + local start_marker="# >>> forge initialize >>>" + local end_marker="# <<< forge initialize <<<" + + if ! grep -Fq "$start_marker" "$zshrc" && ! grep -Fq "$end_marker" "$zshrc"; then + return 0 + fi + + # Backup before mutating (always, like the Rust update path) + local ts + ts="$(date +%Y-%m-%d_%H-%M-%S)" + local backup_path="$zshrc.bak.$ts" + cp "$zshrc" "$backup_path" + + # Remove the entire block (from start to end inclusive). If markers are + # mismatched we still do a best-effort removal of any lines containing them. + local tmp + tmp="$(mktemp)" + awk -v s="$start_marker" -v e="$end_marker" ' + BEGIN { skipping=0 } + $0 == s { skipping=1; next } + $0 == e { skipping=0; next } + !skipping { print } + ' "$zshrc" > "$tmp" || { + # Fallback: at least strip the literal marker lines + grep -vF "$start_marker" "$zshrc" | grep -vF "$end_marker" > "$tmp" || true + } + + mv "$tmp" "$zshrc" + log_ok "Cleaned forge initialize markers from $zshrc (backup: $backup_path)" +} + +# Full uninstall (binary + markers + path hints) +do_uninstall() { + log_info "Uninstalling Forge (local from-source)" + + local bin_dir="$PREFIX/bin" + local candidates=( + "$bin_dir/forge" + "$HOME/.local/bin/forge" + "/usr/local/bin/forge" + "$HOME/.forge/bin/forge" # in case someone used an older layout + ) + + local removed=0 + local cand + for cand in "${candidates[@]}"; do + if [ -f "$cand" ] || [ -L "$cand" ]; then + local dir + dir="$(dirname "$cand")" + if [ -w "$dir" ]; then + rm -f "$cand" + else + if command -v sudo >/dev/null 2>&1; then + sudo rm -f "$cand" + else + log_warn "Cannot remove $cand (no write permission and no sudo)" + continue + fi + fi + log_ok "Removed binary: $cand" + removed=1 + fi + done + + if [ $removed -eq 0 ]; then + log_warn "No forge binary found in common locations for prefix $PREFIX" + fi + + clean_zsh_markers + clean_path_markers + + log_ok "Uninstall complete" + log_info "Note: user config in ~/.config/forge (if any) and cloned workspaces are left untouched." + log_info "Use rm -rf ~/.config/forge if you also want a full purge (not done by default)." +} + +main() { + parse_args "$@" + + if [ "$PREFIX" = "" ]; then + log_error "--prefix cannot be empty" + exit 1 + fi + + # Always start from the repo root for cargo and for locating shell-plugin/ + cd "$REPO_ROOT" + + if $UNINSTALL && $REINSTALL; then + log_error "Cannot combine --uninstall and --reinstall" + exit 1 + fi + + if $UNINSTALL || $REINSTALL; then + do_uninstall + fi + + if ! $UNINSTALL; then + if ! $REINSTALL && [ -x "$REPO_ROOT/target/release/forge" ] && ! $FORCE && ! $BUILD_ONLY; then + log_warn "A release binary already exists at target/release/forge" + log_warn "Use --force to rebuild, or --reinstall" + fi + + do_build + + if ! $BUILD_ONLY; then + do_install_binary + do_zsh_setup + fi + fi + + if ! $UNINSTALL && ! $BUILD_ONLY; then + log_ok "Done." + local installed_bin="$PREFIX/bin/forge" + if [ -x "$installed_bin" ]; then + log_info "Binary: $installed_bin" + log_info "Run 'forge --help' (after ensuring $PREFIX/bin is on PATH)" + fi + if ! $NO_ZSH; then + log_info "ZSH users: exec zsh (or new terminal), then try ':doctor'" + fi + fi +} + +main "$@" diff --git a/scripts/setup-cachyos-build-env.sh b/scripts/setup-cachyos-build-env.sh new file mode 100755 index 0000000000..d4700276ac --- /dev/null +++ b/scripts/setup-cachyos-build-env.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +# +# Setup helper / documented procedure for a CachyOS-optimized build environment +# on a Proxmox VM (LXC or full VM) that will host clean chroots for custom +# overlay packages (Forge, and similar projects). +# +# This script does NOT create .md documentation files (per project AGENTS.md). +# Instead it *is* the script + living documentation: run it with --help (or +# without args) to see the exact steps, commands, and rationale. It can also +# perform local sanity checks (--check) when executed on a CachyOS-like host. +# +# Why a dedicated build host / chroot: +# - Reproducible packages with CachyOS performance flags (RUSTFLAGS target-cpu +# x86-64-v3 / v4 + makepkg.conf CFLAGS etc.) +# - Clean chroot prevents host contamination (devtools + extra-x86_64-build) +# - Ability to serve a small local pacman repo (nginx/caddy + repo-add) to +# Proxmox nodes / workstations / the overlay users. +# - Snapshottable storage (ZFS / Btrfs subvols) for /var/cache/pacman and chroot roots. +# +# Relationship to the other deliverables on this branch: +# - scripts/install.sh : use *outside* the chroot on the build host or on +# developer workstations for fast "from source + same RUSTFLAGS" iteration: +# RUSTFLAGS="-C target-cpu=x86-64-v3" ./scripts/install.sh --reinstall +# - cachyos/PKGBUILD : the packaging recipe consumed by makepkg inside the +# clean chroot. It respects the RUSTFLAGS coming from the chroot's +# /etc/makepkg.conf (or the one injected by devtools) and bakes APP_VERSION. +# +# Co-Authored-By: ForgeCode + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd -P)" + +usage() { + cat <<'EOM' +CachyOS optimized build environment setup for custom packages (Proxmox VM) + +This script prints (and can partially validate) the procedure to turn a +CachyOS installation (LXC or VM under Proxmox) into a build host that +produces x86-64-v3 / v4 optimized .pkg.tar.zst packages using clean chroots. + +Run on the target build machine (or just read the output anywhere): + ./scripts/setup-cachyos-build-env.sh # show full guide + ./scripts/setup-cachyos-build-env.sh --help + ./scripts/setup-cachyos-build-env.sh --check # run local sanity (if on CachyOS) + +Key produced artifacts that live in the forge repo: + - scripts/install.sh (local from-source + Cachy RUSTFLAGS + zsh) + - cachyos/PKGBUILD (the recipe for the overlay) + +The two are deliberately compatible: use the install script for quick dev +builds on the host; use the PKGBUILD + this chroot for "official" overlay pkgs. + +EOM + print_steps +} + +print_steps() { + cat <<'STEPS' + +================================================================================ +1. Base OS on the Proxmox VM / LXC +================================================================================ +- Create a CachyOS LXC (or VM) from the official CachyOS template / ISO. + Recommended: privileged LXC for easier bind-mounts of ZFS datasets, or + unprivileged with proper idmap if you prefer. + +- Update: + sudo pacman -Syu + +- Install core build tooling: + sudo pacman -S --needed base-devel devtools git pacman-contrib + + (devtools brings extra-x86_64-build, makechrootpkg, etc.) + +================================================================================ +2. Storage layout (highly recommended on Proxmox) +================================================================================ +Use ZFS or Btrfs on the build host so you can snapshot before/after big builds +and roll back the chroot or the pacman cache. + +Example ZFS (from the Proxmox host or inside the guest if it owns a dataset): + zfs create -o mountpoint=/var/lib/archbuild rpool/ARCHBUILD + zfs create -o mountpoint=/var/cache/pacman/pkg rpool/PACCACHE + zfs create -o mountpoint=/var/cache/pacman/cachyos rpool/OUR_REPO + + # Optional: separate dataset for sources + zfs create -o mountpoint=/build/sources rpool/BUILDSRC + +Inside the guest, make sure the mounts are there and have sane permissions: + sudo mkdir -p /var/lib/archbuild /var/cache/pacman/pkg + sudo chown -R builduser:builduser /var/lib/archbuild /var/cache/pacman 2>/dev/null || true + +Btrfs equivalent: subvolumes + snapshots. + +================================================================================ +3. CachyOS makepkg.conf & RUSTFLAGS (the optimization source of truth) +================================================================================ +CachyOS ships a tuned /etc/makepkg.conf (or in /usr/share/makepkg.conf.d/cachyos.conf +included by the cachyos-keyring / cachyos-mirrorlist packages). + +Typical interesting bits that the PKGBUILD relies on: + CFLAGS+=" -march=x86-64-v3 -mtune=znver3 ..." # or v4 + RUSTFLAGS+=" -C target-cpu=x86-64-v3 -C opt-level=3 ..." + LDFLAGS+=" -Wl,-O2,..." + +If you are on a stock Arch in the VM and want CachyOS-like flags, you can +drop in the relevant snippets from a real CachyOS /etc/makepkg.conf or set +them in /etc/makepkg.conf.d/99-cachyos-optimizations.conf : + + # /etc/makepkg.conf.d/99-cachyos-optimizations.conf + CARCH="x86_64" + CHOST="x86_64-pc-linux-gnu" + CFLAGS="-march=x86-64-v3 -mtune=generic -O3 -pipe ..." + CXXFLAGS="${CFLAGS}" + RUSTFLAGS="-C target-cpu=x86-64-v3 -C opt-level=3 -C codegen-units=1 -C lto ..." + MAKEFLAGS="-j$(nproc)" + +The cachyos/PKGBUILD in this repo deliberately does *not* set RUSTFLAGS +itself; it lets whatever the chroot's makepkg.conf exports win. This way +the same PKGBUILD produces v3 on a v3 host and v4 on a v4 host when the +overlay's makepkg.conf is the source of the flags. + +================================================================================ +4. Create the clean chroot (once) +================================================================================ +As your normal user (or a dedicated "build" user): + + # The first time this will download a full base chroot (~1-2 GiB) + extra-x86_64-build -- --syncdeps -- --noconfirm + +This creates /var/lib/archbuild/extra-x86_64/root + +You can also create a cachyos-specific root if your overlay has its own +mirrorlist / pacman.conf : + + # Advanced: custom pacman.conf for the chroot that includes your overlay + # and the upstream CachyOS mirrors + pacoloco cache. + +To enter a shell in the chroot for debugging: + arch-nspawn /var/lib/archbuild/extra-x86_64/root + +================================================================================ +5. Building packages (using the PKGBUILD from this repo) +================================================================================ +Typical flow inside your overlay checkout (the dir that contains the copied +or symlinked cachyos/PKGBUILD for the "forge" package): + + # From the dir containing the PKGBUILD + extra-x86_64-build + +This will: + - rsync the PKGBUILD + any needed sources into the clean chroot + - run prepare/build/package under the CachyOS-tuned makepkg.conf + - produce forge-0.1.0-1.cachy-x86_64.pkg.tar.zst in the current dir + +For even faster local iteration (outside any chroot) on the build host itself +or on a dev workstation that is already CachyOS: + cd /path/to/forge-source-checkout + RUSTFLAGS="-C target-cpu=x86-64-v3" APP_VERSION="0.1.0-cachy-test" \ + ./scripts/install.sh --reinstall --prefix=/tmp/forge-test-install + +(The install.sh and the PKGBUILD share the same build command + env contract.) + +After a successful chroot build, sign if your overlay uses signed packages: + gpg --detach-sign --default-key $KEYID *.pkg.tar.zst + +================================================================================ +6. Local repository serving (so other machines can pacman -Syu your builds) +================================================================================ +After builds: + + mkdir -p /var/cache/pacman/cachyos/forge/x86_64 + cp *.pkg.tar.zst *.pkg.tar.zst.sig /var/cache/pacman/cachyos/forge/x86_64/ + repo-add /var/cache/pacman/cachyos/forge/x86_64/forge.db.tar.gz \ + /var/cache/pacman/cachyos/forge/x86_64/*.pkg.tar.zst + +Serve it (simple static + directory index is enough): + + # Option A: caddy (one-liner) + caddy file-server --root /var/cache/pacman/cachyos --listen :8080 + + # Option B: nginx snippet + # location /cachyos/ { + # alias /var/cache/pacman/cachyos/; + # autoindex on; + # } + +On client machines (or other Proxmox containers) add to /etc/pacman.conf : + + [cachyos-forge] + SigLevel = Optional TrustAll # or proper key setup + Server = http://buildhost:8080/forge/x86_64 + + # (and also keep the real CachyOS mirrors + pacoloco if you run it) + +Then: pacman -Sy forge + +================================================================================ +7. Caching & mirrors (Pacoloco recommended) +================================================================================ +Run pacoloco on the build host (or a dedicated proxy LXC). It acts as a +transparent cache for all upstream Arch / CachyOS mirrors and dramatically +speeds up repeated chroot bootstraps and dep downloads. + +Example pacoloco config + systemd socket activation is in the CachyOS wiki / +pacoloco README. Point makepkg / chroot pacman.conf at +http://localhost:9129/repo/... + +Also consider: + paccache -rk 2 # keep only last 2 versions of cached pkgs + +================================================================================ +8. Automation / CI on the build host (optional but nice) +================================================================================ +- A small cron / systemd timer that pulls the latest feat/xai... (or main), + runs extra-x86_64-build for each package that has a PKGBUILD in the overlay, + signs, repo-add, and rsyncs to a "latest" dir. +- Or a tiny webhook receiver that reacts to GitHub "release" or push events + on the feature branch. +- Store the overlay git repo on the same ZFS dataset so you can `git pull` + inside a snapshot. + +================================================================================ +9. Pairing with the deliverables in this branch (feat/xai-supergrok-oauth) +================================================================================ +- Use scripts/install.sh for "YOLO but optimized" builds on any CachyOS box + (including the build VM itself) during development of the XAI / supergrok + OAuth changes. +- Use cachyos/PKGBUILD inside the clean chroot when you are ready to cut a + package for the overlay mirror that other machines will consume. +- The Proxmox VM becomes the single source of truth for "our" builds of Forge + with the exact CachyOS CPU targeting + the latest from the feature branch. + +================================================================================ +10. One-time first-run checklist on the new build VM +================================================================================ +[ ] CachyOS guest installed + fully updated +[ ] base-devel + devtools installed +[ ] ZFS/Btrfs datasets mounted for archbuild + paccache + our_repo +[ ] /etc/makepkg.conf.d/* shows the v3/v4 RUSTFLAGS (or you added the snippet) +[ ] extra-x86_64-build ran successfully at least once (created the root) +[ ] A test build of the forge PKGBUILD succeeded and produced a .pkg.tar.zst +[ ] repo-add + a trivial http server works from another container +[ ] (optional) pacoloco running and pacman.conf points at it +[ ] (optional) the scripts/install.sh from a fresh clone of the branch also + works with the same RUSTFLAGS and produces a runnable forge + +Happy building! + +STEPS +} + +do_check() { + echo "Running local sanity checks (best effort - works best on a real CachyOS host)..." + local ok=0 + + for cmd in pacman makepkg extra-x86_64-build arch-nspawn repo-add caddy nginx pacoloco; do + if command -v "$cmd" >/dev/null 2>&1; then + printf " ${GREEN}✓${NC} %s present\n" "$cmd" + else + printf " ${YELLOW}?${NC} %s not found in PATH (may be ok depending on role)\n" "$cmd" + fi + done + + if [ -f /etc/makepkg.conf ]; then + if grep -q 'x86-64-v3\|target-cpu' /etc/makepkg.conf /etc/makepkg.conf.d/* 2>/dev/null; then + printf " ${GREEN}✓${NC} RUSTFLAGS / target-cpu hints found in makepkg.conf\n" + else + printf " ${YELLOW}!${NC} No obvious x86-64-v3 RUSTFLAGS in makepkg.conf (you may need to add the CachyOS tuning snippet)\n" + fi + fi + + if [ -d /var/lib/archbuild ]; then + printf " ${GREEN}✓${NC} /var/lib/archbuild exists (chroots live here)\n" + else + printf " ${YELLOW}!${NC} /var/lib/archbuild missing - run extra-x86_64-build first\n" + fi + + if [ -d "$REPO_ROOT/cachyos" ] && [ -f "$REPO_ROOT/cachyos/PKGBUILD" ]; then + printf " ${GREEN}✓${NC} cachyos/PKGBUILD found relative to this script\n" + fi + if [ -f "$REPO_ROOT/scripts/install.sh" ]; then + printf " ${GREEN}✓${NC} scripts/install.sh present (the companion from-source tool)\n" + fi + + echo "Check finished." +} + +main() { + case "${1:-}" in + --help|-h) + usage + exit 0 + ;; + --check) + do_check + exit 0 + ;; + *) + usage + exit 0 + ;; + esac +} + +main "$@"