diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index a3156e00..5c37beb0 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -1,207 +1,775 @@ -name: Canary - -on: - push: - branches: - - main - paths-ignore: - - "docs/**" - - "**/*.md" - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: canary - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - timeout-minutes: 40 - env: - # Don't reference `secrets.*` in `if:` expressions; GitHub rejects that at workflow-parse time. - MACOS_SIGN_P12_B64: ${{ secrets.MACOS_SIGN_P12_B64 }} - MACOS_SIGN_P12_PASSWORD: ${{ secrets.MACOS_SIGN_P12_PASSWORD }} - MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} - strategy: - matrix: - include: - - target: x86_64-apple-darwin - os: macos-latest - - target: aarch64-apple-darwin - os: macos-latest - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Validate readme casing - run: scripts/ci/check-readme-case.sh - - - name: Cache vendored deps - uses: actions/cache@v4 - with: - path: | - .vendor/flow-vendor - lib/vendor - lib/vendor-manifest - key: ${{ runner.os }}-${{ runner.arch }}-vendor-${{ hashFiles('vendor.lock.toml') }} - - - name: Verify pinned vendor commit is published - run: scripts/vendor/vendor-repo.sh verify-pinned-origin - - - name: Hydrate vendored deps - run: scripts/vendor/vendor-repo.sh hydrate - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Cache Rust build - uses: Swatinem/rust-cache@v2 - with: - workspaces: ". -> target" - cache-on-failure: true - - - name: Install cross-compilation tools (Linux ARM) - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu - - - name: Build - run: | - if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc - fi - cargo build --release --target ${{ matrix.target }} --bin f - - - name: Import code-signing certificates (macOS) - if: runner.os == 'macOS' && env.MACOS_SIGN_P12_B64 != '' - uses: apple-actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ env.MACOS_SIGN_P12_B64 }} - p12-password: ${{ env.MACOS_SIGN_P12_PASSWORD }} - - - name: Codesign (macOS) - if: runner.os == 'macOS' && env.MACOS_SIGN_P12_B64 != '' && env.MACOS_SIGN_IDENTITY != '' - run: | - BIN="target/${{ matrix.target }}/release/f" - codesign --force --options runtime --timestamp --sign "$MACOS_SIGN_IDENTITY" "$BIN" - codesign -vvv --strict "$BIN" - - - name: Package - run: | - mkdir -p dist - cp target/${{ matrix.target }}/release/f dist/ - cd dist - tar -czvf flow-${{ matrix.target }}.tar.gz f - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: flow-${{ matrix.target }} - path: dist/flow-${{ matrix.target }}.tar.gz - - build-linux-host-simd: - timeout-minutes: 40 - runs-on: [self-hosted, linux, x64, ci-1focus] - env: - RUSTFLAGS: -C target-cpu=native - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Validate readme casing - run: scripts/ci/check-readme-case.sh - - - name: Cache vendored deps - uses: actions/cache@v4 - with: - path: | - .vendor/flow-vendor - lib/vendor - lib/vendor-manifest - key: ${{ runner.os }}-${{ runner.arch }}-vendor-${{ hashFiles('vendor.lock.toml') }} - - - name: Verify pinned vendor commit is published - run: scripts/vendor/vendor-repo.sh verify-pinned-origin - - - name: Hydrate vendored deps - run: scripts/vendor/vendor-repo.sh hydrate - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-unknown-linux-gnu - - - name: Cache Rust build - uses: Swatinem/rust-cache@v2 - with: - workspaces: ". -> target" - cache-on-failure: true - - - name: Build (Linux host SIMD) - run: cargo build --release --target x86_64-unknown-linux-gnu --features linux-host-simd-json --bin f - - - name: Package - run: | - mkdir -p dist - cp target/x86_64-unknown-linux-gnu/release/f dist/ - cd dist - tar -czvf flow-x86_64-unknown-linux-gnu-linux-host-simd.tar.gz f - cd .. - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: flow-x86_64-unknown-linux-gnu-linux-host-simd - path: dist/flow-x86_64-unknown-linux-gnu-linux-host-simd.tar.gz - - release: - needs: [build, build-linux-host-simd] - timeout-minutes: 20 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Prepare release files - run: | - mkdir -p release - find artifacts -name "*.tar.gz" -exec cp {} release/ \; - cd release - sha256sum *.tar.gz > checksums.txt - cat checksums.txt - - - name: Move canary tag to this commit - run: | - git tag -f canary "${GITHUB_SHA}" - git push origin -f refs/tags/canary - - - name: Create/Update Canary Release - uses: softprops/action-gh-release@v2 - with: - tag_name: canary - name: Canary - prerelease: true - generate_release_notes: false - files: release/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +#!/bin/sh +set -eu + +# Flow CLI installer +# Usage: curl -fsSL https://myflow.sh/install.sh | sh + +# Security posture: +# - We require SHA-256 verification by default. +# - Set FLOW_INSTALL_INSECURE=1 (or true/yes) to bypass verification. + +#region logging +if [ "${FLOW_DEBUG-}" = "true" ] || [ "${FLOW_DEBUG-}" = "1" ]; then + debug() { echo "$@" >&2; } +else + debug() { :; } +fi + +if [ "${FLOW_QUIET-}" = "1" ] || [ "${FLOW_QUIET-}" = "true" ]; then + info() { :; } +else + info() { echo "$@" >&2; } +fi + +error() { + echo "error: $@" >&2 + exit 1 +} + +is_truthy() { + case "${1:-}" in + 1|true|TRUE|yes|YES|y|Y) return 0 ;; + *) return 1 ;; + esac +} + +FLOW_SHIM_DIR_SELECTED="" + +can_execute_flow_binary() { + bin_path="$1" + if [ ! -f "$bin_path" ]; then + return 1 + fi + if [ ! -x "$bin_path" ]; then + chmod +x "$bin_path" 2>/dev/null || true + fi + "$bin_path" --version >/dev/null 2>&1 +} +#endregion + +#region platform detection +get_os() { + os="$(uname -s)" + if [ "$os" = Darwin ]; then + echo "macos" + elif [ "$os" = Linux ]; then + echo "linux" + else + error "unsupported OS: $os" + fi +} + +get_arch() { + arch="$(uname -m)" + if [ "$arch" = x86_64 ]; then + echo "x64" + elif [ "$arch" = aarch64 ] || [ "$arch" = arm64 ]; then + echo "arm64" + else + error "unsupported architecture: $arch" + fi +} + +get_target() { + os="$1" + arch="$2" + case "$os-$arch" in + macos-x64) echo "x86_64-apple-darwin" ;; + macos-arm64) echo "aarch64-apple-darwin" ;; + linux-x64) echo "x86_64-unknown-linux-gnu" ;; + linux-arm64) echo "aarch64-unknown-linux-gnu" ;; + *) error "unsupported platform: $os-$arch" ;; + esac +} + +shasum_bin() { + if command -v shasum >/dev/null 2>&1; then + echo "shasum -a 256" + elif command -v sha256sum >/dev/null 2>&1; then + echo "sha256sum" + else + echo "" + fi +} + +validate_repo() { + repo="$1" + if [ -z "${repo:-}" ]; then + error "FLOW_UPGRADE_REPO is empty" + fi + + owner="${repo%/*}" + name="${repo#*/}" + if [ "$owner" = "$repo" ] || [ "$name" = "$repo" ]; then + error "invalid repo '${repo}' (expected owner/repo)" + fi + case "$owner" in */*) error "invalid repo '${repo}' (expected owner/repo)" ;; esac + case "$name" in */*) error "invalid repo '${repo}' (expected owner/repo)" ;; esac + + case "$owner" in *[!A-Za-z0-9._-]*) + error "invalid repo owner '${owner}' (allowed: A-Z a-z 0-9 . _ -)" + ;; + esac + case "$name" in *[!A-Za-z0-9._-]*) + error "invalid repo name '${name}' (allowed: A-Z a-z 0-9 . _ -)" + ;; + esac +} + +validate_token() { + token="$1" + if [ -z "${token:-}" ]; then + error "GitHub token is empty" + fi + case "$token" in + *[!A-Za-z0-9._-]*) + error "invalid GitHub token characters (refusing to use it)" + ;; + esac +} + +validate_version() { + version="$1" + case "$version" in + v*) tag="${version#v}" ;; + *) tag="$version" ;; + esac + case "$tag" in + ""|*[!0-9A-Za-z._-]*) + error "invalid release version '${version}'" + ;; + esac +} +#endregion + +should_install_source() { + case "${FLOW_INSTALL_SOURCE:-1}" in + 0|false|FALSE|no|NO|n|N) return 1 ;; + *) return 0 ;; + esac +} + +should_install_path_shim() { + case "${FLOW_INSTALL_PATH_SHIM:-1}" in + 0|false|FALSE|no|NO|n|N) return 1 ;; + *) return 0 ;; + esac +} + +ensure_flow_source_checkout() { + if ! should_install_source; then + info "flow: skipping source checkout (FLOW_INSTALL_SOURCE=0)" + return 0 + fi + + if ! command -v git >/dev/null 2>&1; then + error "git is required to install flow source to ~/code/flow (or set FLOW_INSTALL_SOURCE=0)" + fi + + source_dir="${FLOW_SOURCE_DIR:-$HOME/code/flow}" + source_repo="${FLOW_SOURCE_REPO_URL:-https://github.com/nikivdev/flow.git}" + source_branch="${FLOW_SOURCE_BRANCH:-main}" + + mkdir -p "$(dirname "$source_dir")" + + if [ -d "$source_dir/.git" ]; then + info "flow: source checkout found at $source_dir" + + if ! git -C "$source_dir" diff --quiet >/dev/null 2>&1 || ! git -C "$source_dir" diff --cached --quiet >/dev/null 2>&1; then + info "flow: warning: source checkout has local changes; skipping auto-sync" + return 0 + fi + + if git -C "$source_dir" fetch --all --prune >/dev/null 2>&1; then + if git -C "$source_dir" show-ref --verify --quiet "refs/remotes/origin/$source_branch"; then + if ! git -C "$source_dir" checkout "$source_branch" >/dev/null 2>&1; then + info "flow: warning: failed to checkout '$source_branch'; leaving current branch" + fi + if ! git -C "$source_dir" pull --ff-only origin "$source_branch" >/dev/null 2>&1; then + info "flow: warning: failed to fast-forward source checkout; sync manually" + fi + fi + else + info "flow: warning: failed to fetch source checkout" + fi + + return 0 + fi + + if [ -e "$source_dir" ]; then + error "flow source path exists but is not a git checkout: $source_dir" + fi + + info "flow: cloning source checkout to $source_dir" + if ! git clone --branch "$source_branch" "$source_repo" "$source_dir" >/dev/null 2>&1; then + error "failed to clone flow source from $source_repo" + fi +} + +find_shim_dir() { + if [ -n "${FLOW_SHIM_DIR:-}" ]; then + if [ ! -d "$FLOW_SHIM_DIR" ]; then + mkdir -p "$FLOW_SHIM_DIR" 2>/dev/null || true + fi + if [ -d "$FLOW_SHIM_DIR" ] && [ -w "$FLOW_SHIM_DIR" ]; then + echo "$FLOW_SHIM_DIR" + return 0 + fi + fi + + old_ifs="${IFS:- }" + IFS=':' + for dir in ${PATH:-}; do + [ -n "$dir" ] || continue + [ "$dir" = "." ] && continue + [ -d "$dir" ] || continue + [ -w "$dir" ] || continue + echo "$dir" + IFS="$old_ifs" + return 0 + done + IFS="$old_ifs" + + fallback="$HOME/.local/bin" + if [ ! -d "$fallback" ]; then + mkdir -p "$fallback" 2>/dev/null || true + fi + if [ -d "$fallback" ] && [ -w "$fallback" ]; then + echo "$fallback" + return 0 + fi + + return 1 +} + +install_path_shim() { + if ! should_install_path_shim; then + return 0 + fi + + install_path="${FLOW_INSTALL_PATH:-$HOME/.flow/bin/f}" + install_dir="$(dirname "$install_path")" + shim_dir="$(find_shim_dir 2>/dev/null || true)" + + if [ -z "${shim_dir:-}" ]; then + info "flow: warning: no writable PATH directory found for immediate command shim" + return 0 + fi + + FLOW_SHIM_DIR_SELECTED="$shim_dir" + + for name in f flow; do + target="$shim_dir/$name" + # Never overwrite the installed binary with a symlink to itself. + if [ "$target" = "$install_path" ]; then + continue + fi + if [ -e "$target" ] && [ ! -L "$target" ]; then + # Do not replace existing non-symlink binaries/scripts. + continue + fi + ln -sf "$install_path" "$target" 2>/dev/null || true + done + + if [ "$shim_dir" != "$install_dir" ]; then + info "flow: command shim installed in $shim_dir" + fi +} + +ensure_line_in_file() { + file="$1" + needle="$2" + line="$3" + + parent="$(dirname "$file")" + [ -d "$parent" ] || mkdir -p "$parent" 2>/dev/null || true + [ -f "$file" ] || touch "$file" 2>/dev/null || true + + if ! grep -F -q "$needle" "$file" 2>/dev/null; then + printf '%s\n' "$line" >> "$file" + fi +} + +ensure_sh_path_entry() { + file="$1" + dir="$2" + ensure_line_in_file "$file" "$dir" "export PATH=\"$dir:\$PATH\"" +} + +#region download helpers +download_file() { + url="$1" + file="$2" + if command -v curl >/dev/null 2>&1; then + debug ">" curl -fsSL -o "$file" "$url" + if [ "${FLOW_DEBUG-}" = "true" ] || [ "${FLOW_DEBUG-}" = "1" ]; then + curl -fsSL --proto '=https' --tlsv1.2 -o "$file" "$url" + else + curl -fsSL --proto '=https' --tlsv1.2 -o "$file" "$url" 2>/dev/null + fi + elif command -v wget >/dev/null 2>&1; then + debug ">" wget -qO "$file" "$url" + wget -qO "$file" "$url" + else + error "curl or wget is required" + fi +} + +fetch_url() { + url="$1" + if command -v curl >/dev/null 2>&1; then + case "$url" in + https://api.github.com/*) + token="${GITHUB_TOKEN:-${GH_TOKEN:-${FLOW_GITHUB_TOKEN:-}}}" + if [ -n "${token:-}" ]; then + validate_token "$token" + curl -fsSL --proto '=https' --tlsv1.2 -H "Authorization: Bearer ${token}" "$url" + else + curl -fsSL --proto '=https' --tlsv1.2 "$url" + fi + ;; + *) + curl -fsSL --proto '=https' --tlsv1.2 "$url" + ;; + esac + elif command -v wget >/dev/null 2>&1; then + wget -qO- "$url" + else + error "curl or wget is required" + fi +} + +get_latest_version() { + repo="${FLOW_UPGRADE_REPO:-}" + if [ -z "${repo:-}" ] && [ -n "${FLOW_GITHUB_OWNER:-}" ] && [ -n "${FLOW_GITHUB_REPO:-}" ]; then + repo="${FLOW_GITHUB_OWNER}/${FLOW_GITHUB_REPO}" + fi + repo="${repo:-nikivdev/flow}" + validate_repo "$repo" + + url="https://api.github.com/repos/${repo}/releases/latest" + version="$(fetch_url "$url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" + validate_version "$version" + echo "$version" +} + +get_checksum() { + version="$1" + target="$2" + repo="${FLOW_UPGRADE_REPO:-}" + if [ -z "${repo:-}" ] && [ -n "${FLOW_GITHUB_OWNER:-}" ] && [ -n "${FLOW_GITHUB_REPO:-}" ]; then + repo="${FLOW_GITHUB_OWNER}/${FLOW_GITHUB_REPO}" + fi + repo="${repo:-nikivdev/flow}" + validate_repo "$repo" + + url="https://github.com/${repo}/releases/download/${version}/checksums.txt" + checksums="$(fetch_url "$url" 2>/dev/null)" || return 1 + echo "$checksums" | grep "flow-${target}.tar.gz" | awk '{print $1}' +} + +get_checksum_for_file() { + version="$1" + file="$2" + repo="${FLOW_UPGRADE_REPO:-}" + if [ -z "${repo:-}" ] && [ -n "${FLOW_GITHUB_OWNER:-}" ] && [ -n "${FLOW_GITHUB_REPO:-}" ]; then + repo="${FLOW_GITHUB_OWNER}/${FLOW_GITHUB_REPO}" + fi + repo="${repo:-nikivdev/flow}" + validate_repo "$repo" + + url="https://github.com/${repo}/releases/download/${version}/checksums.txt" + checksums="$(fetch_url "$url" 2>/dev/null)" || return 1 + # checksums.txt format: " " + echo "$checksums" | awk -v f="$file" '$2==f {print $1}' +} +#endregion + +install_flow() { + version="${FLOW_VERSION:-latest}" + os="${FLOW_OS:-$(get_os)}" + arch="${FLOW_ARCH:-$(get_arch)}" + target="$(get_target "$os" "$arch")" + install_path="${FLOW_INSTALL_PATH:-$HOME/.flow/bin/f}" + install_dir="$(dirname "$install_path")" + + info "flow: installing flow CLI..." + info "flow: platform: $os-$arch ($target)" + + # Get latest version if needed + if [ "$version" = "latest" ]; then + info "flow: fetching latest version..." + version="$(get_latest_version)" + if [ -z "$version" ]; then + error "failed to fetch latest version" + fi + fi + validate_version "$version" + info "flow: version: $version" + + # URLs - try CDN first, fallback to GitHub + cdn_url="https://cdn.myflow.sh/${version}/flow-${target}.tar.gz" + repo="${FLOW_UPGRADE_REPO:-}" + if [ -z "${repo:-}" ] && [ -n "${FLOW_GITHUB_OWNER:-}" ] && [ -n "${FLOW_GITHUB_REPO:-}" ]; then + repo="${FLOW_GITHUB_OWNER}/${FLOW_GITHUB_REPO}" + fi + repo="${repo:-nikivdev/flow}" + validate_repo "$repo" + github_url="https://github.com/${repo}/releases/download/${version}/flow-${target}.tar.gz" + + download_dir="$(mktemp -d)" + tarball="$download_dir/flow.tar.gz" + download_source="unknown" + + asset_file="flow-${target}.tar.gz" + legacy_os="$os" + if [ "$legacy_os" = "macos" ]; then + legacy_os="darwin" + fi + legacy_arch="amd64" + if [ "$arch" = "arm64" ]; then + legacy_arch="arm64" + fi + legacy_file="flow_${version}_${legacy_os}_${legacy_arch}.tar.gz" + legacy_url="https://github.com/${repo}/releases/download/${version}/${legacy_file}" + + # Try CDN first (faster) + info "flow: downloading..." + if command -v curl >/dev/null 2>&1 && curl -fsSL -o "$tarball" "$cdn_url" 2>/dev/null; then + debug "flow: downloaded from CDN" + download_source="cdn" + else + debug "flow: trying GitHub..." + if download_file "$github_url" "$tarball"; then + asset_file="flow-${target}.tar.gz" + download_source="github" + elif download_file "$legacy_url" "$tarball"; then + asset_file="$legacy_file" + download_source="legacy" + else + error "download failed" + fi + fi + + # Verify checksum if available + shasum="$(shasum_bin)" + if [ -n "$shasum" ]; then + expected="$(get_checksum_for_file "$version" "$asset_file" 2>/dev/null)" || true + if [ -z "${expected:-}" ]; then + # Back-compat: allow checksums.txt to contain either naming scheme. + if [ "$asset_file" = "$legacy_file" ]; then + expected="$(get_checksum_for_file "$version" "flow-${target}.tar.gz" 2>/dev/null)" || true + elif [ "$asset_file" = "flow-${target}.tar.gz" ]; then + expected="$(get_checksum_for_file "$version" "$legacy_file" 2>/dev/null)" || true + fi + fi + if [ -z "${expected:-}" ]; then + if is_truthy "${FLOW_INSTALL_INSECURE-}"; then + info "flow: warning: checksum not verified (FLOW_INSTALL_INSECURE=1)" + elif [ "${download_source:-}" = "cdn" ]; then + rm -rf "$download_dir" "$extract_dir" 2>/dev/null || true + error "checksum verification failed for CDN download (checksums.txt missing or entry not found). Refusing to install.\nSet FLOW_INSTALL_INSECURE=1 to bypass (not recommended)." + else + info "flow: warning: checksum not verified (checksums.txt missing or entry not found; legacy release?)" + expected="" + fi + fi + if [ -n "${expected:-}" ]; then + debug "flow: verifying checksum..." + actual="$($shasum "$tarball" | awk '{print $1}')" + if [ "$expected" != "$actual" ]; then + rm -rf "$download_dir" + error "checksum mismatch" + fi + info "flow: checksum verified" + fi + else + if is_truthy "${FLOW_INSTALL_INSECURE-}"; then + info "flow: warning: sha256 tool not found, skipping checksum verification (FLOW_INSTALL_INSECURE=1)" + else + error "sha256 tool not found (need shasum or sha256sum). Refusing to install.\nSet FLOW_INSTALL_INSECURE=1 to bypass (not recommended)." + fi + fi + + # Extract and install + mkdir -p "$install_dir" + extract_dir="$(mktemp -d)" + tar -xzf "$tarball" -C "$extract_dir" + + # Find binary + if [ -f "$extract_dir/f" ]; then + mv "$extract_dir/f" "$install_path" + else + binary="$(find "$extract_dir" -type f \( -name "f" -o -name "flow" \) 2>/dev/null | head -1)" + if [ -z "$binary" ]; then + binary="$(find "$extract_dir" -type f -perm +111 2>/dev/null | head -1)" + fi + if [ -z "$binary" ]; then + rm -rf "$download_dir" "$extract_dir" + error "binary not found in archive" + fi + mv "$binary" "$install_path" + fi + chmod +x "$install_path" + + # Provide both `f` and `flow` as entrypoints. + base="$(basename "$install_path")" + if [ "$base" = "f" ]; then + if [ -e "$install_dir/flow" ] && [ -d "$install_dir/flow" ]; then + info "flow: warning: cannot create symlink $install_dir/flow (path is a directory)" + else + ln -sf "f" "$install_dir/flow" 2>/dev/null || true + fi + elif [ "$base" = "flow" ]; then + if [ -e "$install_dir/f" ] && [ -d "$install_dir/f" ]; then + info "flow: warning: cannot create symlink $install_dir/f (path is a directory)" + else + ln -sf "flow" "$install_dir/f" 2>/dev/null || true + fi + fi + + # Cleanup + rm -rf "$download_dir" "$extract_dir" + + if ! can_execute_flow_binary "$install_path"; then + if [ "$os" = "macos" ] && ! is_truthy "${FLOW_INSTALL_RETRY_ALT_ARCH:-0}"; then + alt_arch="x64" + if [ "$arch" = "x64" ]; then + alt_arch="arm64" + fi + info "flow: installed binary failed execution; retrying with macos-$alt_arch build" + FLOW_ARCH="$alt_arch" FLOW_INSTALL_RETRY_ALT_ARCH=1 install_flow + return 0 + fi + + info "flow: diagnostic: unable to execute $install_path" + info "flow: diagnostic: $(ls -l "$install_path" 2>/dev/null || echo missing)" + if command -v file >/dev/null 2>&1; then + info "flow: diagnostic: $(file "$install_path" 2>/dev/null || true)" + fi + error "installed flow binary is not executable on this host" + fi + + info "flow: installed to $install_path" +} + +configure_shell() { + install_dir="$(dirname "${FLOW_INSTALL_PATH:-$HOME/.flow/bin/f}")" + shim_dir="${FLOW_SHIM_DIR_SELECTED:-}" + fallback_shim="$HOME/.local/bin" + registry_url="${FLOW_REGISTRY_URL:-https://myflow.sh}" + + # Fish + if [ -f "$HOME/.config/fish/config.fish" ]; then + ensure_line_in_file "$HOME/.config/fish/config.fish" "$install_dir" "fish_add_path $install_dir" + if [ -n "$shim_dir" ] && [ "$shim_dir" != "$install_dir" ]; then + ensure_line_in_file "$HOME/.config/fish/config.fish" "$shim_dir" "fish_add_path $shim_dir" + fi + ensure_line_in_file "$HOME/.config/fish/config.fish" "$fallback_shim" "fish_add_path $fallback_shim" + ensure_line_in_file "$HOME/.config/fish/config.fish" "FLOW_REGISTRY_URL" "set -gx FLOW_REGISTRY_URL \"$registry_url\"" + info "flow: updated ~/.config/fish/config.fish" + fi + + # Zsh + for zsh_rc in "$HOME/.zshrc" "$HOME/.zprofile"; do + ensure_sh_path_entry "$zsh_rc" "$install_dir" + if [ -n "$shim_dir" ] && [ "$shim_dir" != "$install_dir" ]; then + ensure_sh_path_entry "$zsh_rc" "$shim_dir" + fi + ensure_sh_path_entry "$zsh_rc" "$fallback_shim" + ensure_line_in_file "$zsh_rc" "FLOW_REGISTRY_URL" "export FLOW_REGISTRY_URL=\"$registry_url\"" + done + info "flow: updated ~/.zshrc and ~/.zprofile" + + # Bash + bash_updated=0 + for rc in "$HOME/.bashrc" "$HOME/.bash_profile"; do + if [ -f "$rc" ]; then + ensure_sh_path_entry "$rc" "$install_dir" + if [ -n "$shim_dir" ] && [ "$shim_dir" != "$install_dir" ]; then + ensure_sh_path_entry "$rc" "$shim_dir" + fi + ensure_sh_path_entry "$rc" "$fallback_shim" + ensure_line_in_file "$rc" "FLOW_REGISTRY_URL" "export FLOW_REGISTRY_URL=\"$registry_url\"" + bash_updated=1 + fi + done + if [ "$bash_updated" = "0" ]; then + rc="$HOME/.bashrc" + ensure_sh_path_entry "$rc" "$install_dir" + if [ -n "$shim_dir" ] && [ "$shim_dir" != "$install_dir" ]; then + ensure_sh_path_entry "$rc" "$shim_dir" + fi + ensure_sh_path_entry "$rc" "$fallback_shim" + ensure_line_in_file "$rc" "FLOW_REGISTRY_URL" "export FLOW_REGISTRY_URL=\"$registry_url\"" + fi +} + +after_install() { + source_dir="${FLOW_SOURCE_DIR:-$HOME/code/flow}" + install_path="${FLOW_INSTALL_PATH:-$HOME/.flow/bin/f}" + if [ ! -x "$install_path" ]; then + chmod +x "$install_path" 2>/dev/null || true + fi + + if ! can_execute_flow_binary "$install_path"; then + if command -v xattr >/dev/null 2>&1; then + xattr -d com.apple.quarantine "$install_path" >/dev/null 2>&1 || true + fi + chmod +x "$install_path" 2>/dev/null || true + fi + + if ! can_execute_flow_binary "$install_path"; then + info "flow: diagnostic: unable to execute fallback binary: $install_path" + info "flow: diagnostic: $(ls -l "$install_path" 2>/dev/null || echo missing)" + if command -v file >/dev/null 2>&1; then + info "flow: diagnostic: $(file "$install_path" 2>/dev/null || true)" + fi + error "installed flow binary is not runnable; rerun with FLOW_DEBUG=1 and share diagnostics" + fi + + info "" + info "flow: installed successfully!" + if command -v f >/dev/null 2>&1; then + info "flow: command ready: $(command -v f)" + elif [ -n "${FLOW_SHIM_DIR_SELECTED:-}" ] && [ -x "${FLOW_SHIM_DIR_SELECTED}/f" ]; then + info "flow: command shim ready: ${FLOW_SHIM_DIR_SELECTED}/f" + info "flow: open a new shell to refresh PATH (zsh: exec zsh -l)" + else + info "flow: OPEN NEW SHELL to use 'f' by name" + info "flow: immediate fallback: $install_path --help" + fi + if should_install_source; then + info "flow: source checkout: $source_dir" + fi + info "flow: then run 'f --help' to get started" + info "flow: docs: https://myflow.sh" +} + +should_setup_run() { + case "${FLOW_SETUP_RUN:-1}" in + 0|false|FALSE|no|NO|n|N) return 1 ;; + *) return 0 ;; + esac +} + +is_interactive() { + [ -t 0 ] && [ -t 1 ] +} + +prompt_value() { + label="$1" + default="$2" + env_override="$3" + if [ -n "$env_override" ]; then + echo "$env_override" + return 0 + fi + if ! is_interactive; then + echo "$default" + return 0 + fi + printf '%s [%s]: ' "$label" "$default" >&2 + read -r answer &2 + read -r answer $target_dir" + if git clone "$repo_url" "$target_dir" >/dev/null 2>&1; then + info "flow: $label ready" + else + info "flow: warning: failed to clone $label from $repo_url" + return 1 + fi +} + +setup_run_repos() { + if ! should_setup_run; then + info "flow: skipping ~/run setup (FLOW_SETUP_RUN=0)" + return 0 + fi + + if ! command -v git >/dev/null 2>&1; then + info "flow: warning: git not found; skipping ~/run setup" + return 0 + fi + + run_root="${RUN_ROOT:-$HOME/run}" + + # If both already exist, skip entirely + if [ -d "$run_root/.git" ] && [ -d "$run_root/i/.git" ]; then + info "flow: ~/run and ~/run/i already set up" + return 0 + fi + + info "" + if ! prompt_yn "Set up ~/run repos (task collections)?"; then + info "flow: skipping ~/run setup" + info "flow: you can set this up later with: f run-load" + return 0 + fi + + # ~/run (public) + if [ ! -d "$run_root/.git" ]; then + run_url="$(prompt_value "~/run repo URL (SSH or HTTPS)" "git@github.com:nikivdev/run.git" "${FLOW_RUN_REPO:-}")" + clone_if_missing "$run_root" "$run_url" "~/run" + fi + + # ~/run/i (internal/private) + if [ ! -d "$run_root/i/.git" ]; then + run_i_url="$(prompt_value "~/run/i repo URL (SSH or HTTPS, or 'skip')" "git@github.com:nikivdev/run-i.git" "${FLOW_RUN_I_REPO:-}")" + clone_if_missing "$run_root/i" "$run_i_url" "~/run/i" + fi + + info "flow: ~/run repos ready" + info "flow: run tasks with: f r (public) or f ri (internal)" +} + +install_flow +install_path_shim +ensure_flow_source_checkout +configure_shell +setup_run_repos +after_install