#!/usr/bin/env bash # ============================================================================= # disinto — CLI entry point for the disinto code factory # # Commands: # disinto init [options] Bootstrap a new project # disinto status Show factory status # # Usage: # disinto init https://github.com/user/repo # disinto init user/repo --branch main --ci-id 3 # disinto status # ============================================================================= set -euo pipefail FACTORY_ROOT="$(cd "$(dirname "$0")/.." && pwd)" source "${FACTORY_ROOT}/lib/env.sh" # ── Helpers ────────────────────────────────────────────────────────────────── usage() { cat < [options] Bootstrap a new project disinto status Show factory status Init options: --branch Primary branch (default: auto-detect) --repo-root Local clone path (default: ~/name) --ci-id Woodpecker CI repo ID (default: 0 = no CI) --forge-url Forge base URL (default: http://localhost:3000) --yes Skip confirmation prompts EOF exit 1 } # Extract org/repo slug from various URL formats. # Accepts: https://github.com/user/repo, https://codeberg.org/user/repo, # http://localhost:3000/user/repo, user/repo, *.git parse_repo_slug() { local url="$1" url="${url#https://}" url="${url#http://}" # Strip any hostname (anything before the first / that contains a dot or colon) if [[ "$url" =~ ^[a-zA-Z0-9._:-]+/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+ ]]; then url="${url#*/}" # strip host part fi url="${url%.git}" url="${url%/}" if [[ ! "$url" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then echo "Error: invalid repo URL — expected https://host/org/repo or org/repo" >&2 exit 1 fi printf '%s' "$url" } # Build a clone-able URL from a slug and forge URL. clone_url_from_slug() { local slug="$1" forge_url="${2:-${FORGE_URL:-http://localhost:3000}}" printf '%s/%s.git' "$forge_url" "$slug" } # Write (or update) credentials in ~/.netrc for a given host. write_netrc() { local host="$1" login="$2" token="$3" local netrc="${HOME}/.netrc" # Remove existing entry for this host if present if [ -f "$netrc" ]; then local tmp tmp=$(mktemp) awk -v h="$host" ' $0 ~ "^machine " h { skip=1; next } /^machine / { skip=0 } !skip ' "$netrc" > "$tmp" mv "$tmp" "$netrc" fi # Append new entry printf 'machine %s\nlogin %s\npassword %s\n' "$host" "$login" "$token" >> "$netrc" chmod 600 "$netrc" } FORGEJO_DATA_DIR="${HOME}/.disinto/forgejo" # Provision or connect to a local Forgejo instance. # Creates admin + bot users, generates API tokens, stores in .env. setup_forge() { local forge_url="$1" local repo_slug="$2" echo "" echo "── Forge setup ────────────────────────────────────────" # Check if Forgejo is already running if curl -sf --max-time 5 "${forge_url}/api/v1/version" >/dev/null 2>&1; then echo "Forgejo: ${forge_url} (already running)" else echo "Forgejo not reachable at ${forge_url}" echo "Starting Forgejo via Docker..." if ! command -v docker &>/dev/null; then echo "Error: docker not found — needed to provision Forgejo" >&2 echo " Install Docker or start Forgejo manually at ${forge_url}" >&2 exit 1 fi # Create data directory mkdir -p "${FORGEJO_DATA_DIR}" # Extract port from forge_url local forge_port forge_port=$(printf '%s' "$forge_url" | sed -E 's|.*:([0-9]+)/?$|\1|') forge_port="${forge_port:-3000}" # Start Forgejo container if docker ps -a --format '{{.Names}}' | grep -q '^disinto-forgejo$'; then docker start disinto-forgejo >/dev/null 2>&1 || true else docker run -d \ --name disinto-forgejo \ --restart unless-stopped \ -p "${forge_port}:3000" \ -p 2222:22 \ -v "${FORGEJO_DATA_DIR}:/data" \ -e "FORGEJO__database__DB_TYPE=sqlite3" \ -e "FORGEJO__server__ROOT_URL=${forge_url}/" \ -e "FORGEJO__server__HTTP_PORT=3000" \ -e "FORGEJO__service__DISABLE_REGISTRATION=true" \ forgejo/forgejo:latest fi # Wait for Forgejo to become healthy echo -n "Waiting for Forgejo to start" local retries=0 while ! curl -sf --max-time 3 "${forge_url}/api/v1/version" >/dev/null 2>&1; do retries=$((retries + 1)) if [ "$retries" -gt 60 ]; then echo "" echo "Error: Forgejo did not become ready within 60s" >&2 exit 1 fi echo -n "." sleep 1 done echo " ready" fi # Create admin user if it doesn't exist local admin_user="disinto-admin" local admin_pass admin_pass="admin-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" if ! curl -sf --max-time 5 "${forge_url}/api/v1/users/${admin_user}" >/dev/null 2>&1; then echo "Creating admin user: ${admin_user}" docker exec disinto-forgejo forgejo admin user create \ --admin \ --username "${admin_user}" \ --password "${admin_pass}" \ --email "admin@disinto.local" \ --must-change-password=false 2>/dev/null || true fi # Get or create admin token local admin_token admin_token=$(curl -sf -X POST \ -u "${admin_user}:${admin_pass}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/users/${admin_user}/tokens" \ -d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \ | jq -r '.sha1 // empty') || admin_token="" if [ -z "$admin_token" ]; then # Token might already exist — try listing admin_token=$(curl -sf \ -u "${admin_user}:${admin_pass}" \ "${forge_url}/api/v1/users/${admin_user}/tokens" 2>/dev/null \ | jq -r '.[0].sha1 // empty') || admin_token="" fi # Create bot users and tokens local dev_token="" review_token="" for bot_user in dev-bot review-bot; do local bot_pass bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" if ! curl -sf --max-time 5 \ -H "Authorization: token ${admin_token}" \ "${forge_url}/api/v1/users/${bot_user}" >/dev/null 2>&1; then echo "Creating bot user: ${bot_user}" docker exec disinto-forgejo forgejo admin user create \ --username "${bot_user}" \ --password "${bot_pass}" \ --email "${bot_user}@disinto.local" \ --must-change-password=false 2>/dev/null || true fi # Generate token via API (using admin credentials for the bot) local token token=$(curl -sf -X POST \ -H "Authorization: token ${admin_token}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/users/${bot_user}/tokens" \ -d "{\"name\":\"disinto-${bot_user}-token\",\"scopes\":[\"all\"]}" 2>/dev/null \ | jq -r '.sha1 // empty') || token="" if [ -z "$token" ]; then # Token name collision — create with timestamp suffix token=$(curl -sf -X POST \ -H "Authorization: token ${admin_token}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/users/${bot_user}/tokens" \ -d "{\"name\":\"disinto-${bot_user}-$(date +%s)\",\"scopes\":[\"all\"]}" 2>/dev/null \ | jq -r '.sha1 // empty') || token="" fi if [ "$bot_user" = "dev-bot" ]; then dev_token="$token" else review_token="$token" fi done # Store tokens in .env local env_file="${FACTORY_ROOT}/.env" if [ -n "$dev_token" ]; then if grep -q '^FORGE_TOKEN=' "$env_file" 2>/dev/null; then sed -i "s|^FORGE_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file" elif grep -q '^CODEBERG_TOKEN=' "$env_file" 2>/dev/null; then sed -i "s|^CODEBERG_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file" else printf '\nFORGE_TOKEN=%s\n' "$dev_token" >> "$env_file" fi export FORGE_TOKEN="$dev_token" export CODEBERG_TOKEN="$dev_token" echo " dev-bot token saved" fi if [ -n "$review_token" ]; then if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file" 2>/dev/null; then sed -i "s|^FORGE_REVIEW_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file" elif grep -q '^REVIEW_BOT_TOKEN=' "$env_file" 2>/dev/null; then sed -i "s|^REVIEW_BOT_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file" else printf 'FORGE_REVIEW_TOKEN=%s\n' "$review_token" >> "$env_file" fi export FORGE_REVIEW_TOKEN="$review_token" export REVIEW_BOT_TOKEN="$review_token" echo " review-bot token saved" fi # Store FORGE_URL in .env if not already present if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file" fi # Create the repo on Forgejo if it doesn't exist local org_name="${repo_slug%%/*}" local repo_name="${repo_slug##*/}" # Check if repo already exists if ! curl -sf --max-time 5 \ -H "Authorization: token ${FORGE_TOKEN}" \ "${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then # Try creating org first (ignore if exists) curl -sf -X POST \ -H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/orgs" \ -d "{\"username\":\"${org_name}\",\"visibility\":\"public\"}" >/dev/null 2>&1 || true # Create repo under org if ! curl -sf -X POST \ -H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/orgs/${org_name}/repos" \ -d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1; then # Fallback: create under the dev-bot user curl -sf -X POST \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/user/repos" \ -d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true fi # Add bot users as collaborators for bot_user in dev-bot review-bot; do curl -sf -X PUT \ -H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/repos/${repo_slug}/collaborators/${bot_user}" \ -d '{"permission":"write"}' >/dev/null 2>&1 || true done echo "Repo: ${repo_slug} created on Forgejo" else echo "Repo: ${repo_slug} (already exists on Forgejo)" fi echo "Forge: ${forge_url} (ready)" } # Push local clone to the Forgejo remote. push_to_forge() { local repo_root="$1" forge_url="$2" repo_slug="$3" local remote_url="${forge_url}/${repo_slug}.git" if git -C "$repo_root" remote get-url forgejo >/dev/null 2>&1; then echo "Remote: forgejo (already configured)" else git -C "$repo_root" remote add forgejo "$remote_url" 2>/dev/null || \ git -C "$repo_root" remote set-url forgejo "$remote_url" echo "Remote: forgejo -> ${remote_url}" fi # Push all branches git -C "$repo_root" push forgejo --all 2>/dev/null || true git -C "$repo_root" push forgejo --tags 2>/dev/null || true } # Preflight check — verify all factory requirements before proceeding. preflight_check() { local repo_slug="${1:-}" local forge_url="${2:-${FORGE_URL:-http://localhost:3000}}" local errors=0 # ── Required commands ── local -A hints=( [claude]="Install: https://docs.anthropic.com/en/docs/claude-code/overview" [tmux]="Install: apt install tmux / brew install tmux" [git]="Install: apt install git / brew install git" [jq]="Install: apt install jq / brew install jq" [python3]="Install: apt install python3 / brew install python3" [curl]="Install: apt install curl / brew install curl" ) local cmd for cmd in claude tmux git jq python3 curl; do if ! command -v "$cmd" &>/dev/null; then echo "Error: ${cmd} not found" >&2 echo " ${hints[$cmd]}" >&2 errors=$((errors + 1)) fi done # ── Claude Code authentication ── if command -v claude &>/dev/null && command -v jq &>/dev/null; then local auth_json auth_stderr auth_rc=0 auth_stderr=$(claude auth status 2>&1 >/dev/null) || auth_rc=$? auth_json=$(claude auth status 2>/dev/null) || auth_json="" # Only skip check if subcommand is unrecognized (old claude version) if printf '%s' "$auth_stderr" | grep -qi "unknown command"; then : # claude version doesn't support auth status — skip elif [ -z "$auth_json" ] || [ "$auth_rc" -ne 0 ]; then echo "Error: Claude Code is not authenticated (auth check failed)" >&2 echo " Run: claude auth login" >&2 errors=$((errors + 1)) else local logged_in logged_in=$(printf '%s' "$auth_json" | jq -r '.loggedIn // false' 2>/dev/null) || logged_in="false" if [ "$logged_in" != "true" ]; then echo "Error: Claude Code is not authenticated" >&2 echo " Run: claude auth login" >&2 errors=$((errors + 1)) fi fi fi # ── Forge API check (verify the forge is reachable and token works) ── if [ -n "${FORGE_TOKEN:-}" ] && command -v curl &>/dev/null; then if ! curl -sf --max-time 10 \ -H "Authorization: token ${FORGE_TOKEN}" \ "${forge_url}/api/v1/repos/${repo_slug}" >/dev/null 2>&1; then echo "Error: Forge API auth failed at ${forge_url}" >&2 echo " Verify your FORGE_TOKEN and that Forgejo is running" >&2 errors=$((errors + 1)) fi fi # ── Optional tools (warn only) ── if ! command -v docker &>/dev/null; then echo "Warning: docker not found (needed for Forgejo provisioning)" >&2 fi if [ "$errors" -gt 0 ]; then echo "" >&2 echo "${errors} preflight error(s) — fix the above before running disinto init" >&2 exit 1 fi } # Clone the repo if the target directory doesn't exist; validate if it does. clone_or_validate() { local slug="$1" target="$2" forge_url="${3:-${FORGE_URL:-http://localhost:3000}}" if [ -d "${target}/.git" ]; then echo "Repo: ${target} (existing clone)" return fi local url url=$(clone_url_from_slug "$slug" "$forge_url") echo "Cloning: ${url} -> ${target}" git clone "$url" "$target" } # Detect the primary branch from the remote HEAD or fallback to main/master. detect_branch() { local repo_root="$1" local branch branch=$(git -C "$repo_root" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \ | sed 's|refs/remotes/origin/||') || true if [ -z "$branch" ]; then if git -C "$repo_root" show-ref --verify --quiet refs/remotes/origin/main 2>/dev/null; then branch="main" else branch="master" fi fi printf '%s' "$branch" } # Generate projects/.toml config file. generate_toml() { local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6" forge_url="$7" cat > "$path" </dev/null 2>&1; then echo " + ${name}" else echo " . ${name} (already exists)" fi done } # Generate a minimal VISION.md template in the target project. generate_vision() { local repo_root="$1" name="$2" local vision_path="${repo_root}/VISION.md" if [ -f "$vision_path" ]; then echo "VISION: ${vision_path} (already exists, skipping)" return fi cat > "$vision_path" < ## Who it's for ## Design principles - - - ## Milestones ### Current - ### Next - EOF echo "Created: ${vision_path}" echo " Commit this to your repo when ready" } # Generate and optionally install cron entries for the project agents. install_cron() { local name="$1" toml="$2" auto_yes="$3" # Use absolute path for the TOML in cron entries local abs_toml abs_toml="$(cd "$(dirname "$toml")" && pwd)/$(basename "$toml")" local cron_block cron_block="# disinto: ${name} 2,7,12,17,22,27,32,37,42,47,52,57 * * * * ${FACTORY_ROOT}/review/review-poll.sh ${abs_toml} >/dev/null 2>&1 4,9,14,19,24,29,34,39,44,49,54,59 * * * * ${FACTORY_ROOT}/dev/dev-poll.sh ${abs_toml} >/dev/null 2>&1 0 0,6,12,18 * * * cd ${FACTORY_ROOT} && bash gardener/gardener-run.sh ${abs_toml} >/dev/null 2>&1" echo "" echo "Cron entries to install:" echo "$cron_block" echo "" if [ "$auto_yes" = false ] && [ -t 0 ]; then read -rp "Install these cron entries? [y/N] " confirm if [[ ! "$confirm" =~ ^[Yy] ]]; then echo "Skipped cron install. Add manually with: crontab -e" return fi fi # Append to existing crontab { crontab -l 2>/dev/null || true; printf '%s\n' "$cron_block"; } | crontab - echo "Cron entries installed" } # Set up Woodpecker CI to use Forgejo as its forge backend. # Creates an OAuth2 app on Forgejo for Woodpecker, activates the repo. setup_woodpecker() { local forge_url="$1" repo_slug="$2" local wp_server="${WOODPECKER_SERVER:-}" if [ -z "$wp_server" ]; then echo "Woodpecker: not configured (WOODPECKER_SERVER not set), skipping" return fi # Check if Woodpecker is reachable if ! curl -sf --max-time 5 "${wp_server}/api/version" >/dev/null 2>&1; then echo "Woodpecker: not reachable at ${wp_server}, skipping" return fi echo "" echo "── Woodpecker CI setup ────────────────────────────────" echo "Server: ${wp_server}" # Create OAuth2 application on Forgejo for Woodpecker local oauth2_name="woodpecker-ci" local redirect_uri="${wp_server}/authorize" local existing_app client_id client_secret # Check if OAuth2 app already exists existing_app=$(curl -sf \ -H "Authorization: token ${FORGE_TOKEN}" \ "${forge_url}/api/v1/user/applications/oauth2" 2>/dev/null \ | jq -r --arg name "$oauth2_name" '.[] | select(.name == $name) | .client_id // empty' 2>/dev/null) || true if [ -n "$existing_app" ]; then echo "OAuth2: ${oauth2_name} (already exists, client_id=${existing_app})" client_id="$existing_app" else local oauth2_resp oauth2_resp=$(curl -sf -X POST \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/user/applications/oauth2" \ -d "{\"name\":\"${oauth2_name}\",\"redirect_uris\":[\"${redirect_uri}\"],\"confidential_client\":true}" \ 2>/dev/null) || oauth2_resp="" if [ -z "$oauth2_resp" ]; then echo "Warning: failed to create OAuth2 app on Forgejo" >&2 return fi client_id=$(printf '%s' "$oauth2_resp" | jq -r '.client_id // empty') client_secret=$(printf '%s' "$oauth2_resp" | jq -r '.client_secret // empty') if [ -z "$client_id" ]; then echo "Warning: OAuth2 app creation returned no client_id" >&2 return fi echo "OAuth2: ${oauth2_name} created (client_id=${client_id})" fi # Store Woodpecker forge config in .env local env_file="${FACTORY_ROOT}/.env" local wp_vars=( "WOODPECKER_FORGEJO=true" "WOODPECKER_FORGEJO_URL=${forge_url}" ) if [ -n "${client_id:-}" ]; then wp_vars+=("WOODPECKER_FORGEJO_CLIENT=${client_id}") fi if [ -n "${client_secret:-}" ]; then wp_vars+=("WOODPECKER_FORGEJO_SECRET=${client_secret}") fi for var_line in "${wp_vars[@]}"; do local var_name="${var_line%%=*}" if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then sed -i "s|^${var_name}=.*|${var_line}|" "$env_file" else printf '%s\n' "$var_line" >> "$env_file" fi done echo "Config: Woodpecker forge vars written to .env" # Activate repo in Woodpecker (if not already) local wp_token="${WOODPECKER_TOKEN:-}" if [ -z "$wp_token" ]; then echo "Warning: WOODPECKER_TOKEN not set — cannot activate repo" >&2 echo " Activate manually: woodpecker-cli repo add ${repo_slug}" >&2 return fi local wp_repo_id wp_repo_id=$(curl -sf \ -H "Authorization: Bearer ${wp_token}" \ "${wp_server}/api/repos/lookup/${repo_slug}" 2>/dev/null \ | jq -r '.id // empty' 2>/dev/null) || true if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then echo "Repo: ${repo_slug} already active in Woodpecker (id=${wp_repo_id})" else local activate_resp activate_resp=$(curl -sf -X POST \ -H "Authorization: Bearer ${wp_token}" \ -H "Content-Type: application/json" \ "${wp_server}/api/repos" \ -d "{\"forge_remote_id\":\"${repo_slug}\"}" 2>/dev/null) || activate_resp="" wp_repo_id=$(printf '%s' "$activate_resp" | jq -r '.id // empty' 2>/dev/null) || true if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then echo "Repo: ${repo_slug} activated in Woodpecker (id=${wp_repo_id})" else echo "Warning: could not activate repo in Woodpecker" >&2 echo " Activate manually: woodpecker-cli repo add ${repo_slug}" >&2 fi fi # Store repo ID for later TOML generation if [ -n "$wp_repo_id" ] && [ "$wp_repo_id" != "0" ]; then _WP_REPO_ID="$wp_repo_id" fi } # ── init command ───────────────────────────────────────────────────────────── disinto_init() { local repo_url="${1:-}" if [ -z "$repo_url" ]; then echo "Error: repo URL required" >&2 echo "Usage: disinto init " >&2 exit 1 fi shift # Parse flags local branch="" repo_root="" ci_id="0" auto_yes=false forge_url_flag="" while [ $# -gt 0 ]; do case "$1" in --branch) branch="$2"; shift 2 ;; --repo-root) repo_root="$2"; shift 2 ;; --ci-id) ci_id="$2"; shift 2 ;; --forge-url) forge_url_flag="$2"; shift 2 ;; --yes) auto_yes=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done # Extract org/repo slug local forge_repo forge_repo=$(parse_repo_slug "$repo_url") local project_name="${forge_repo##*/}" local toml_path="${FACTORY_ROOT}/projects/${project_name}.toml" # Determine forge URL (flag > env > default) local forge_url="${forge_url_flag:-${FORGE_URL:-http://localhost:3000}}" echo "=== disinto init ===" echo "Project: ${forge_repo}" echo "Name: ${project_name}" echo "Forge: ${forge_url}" # Check for existing config local toml_exists=false if [ -f "$toml_path" ]; then toml_exists=true echo "Config: ${toml_path} (already exists, reusing)" # Read repo_root and branch from existing TOML local existing_root existing_branch existing_root=$(python3 -c " import sys, tomllib with open(sys.argv[1], 'rb') as f: cfg = tomllib.load(f) print(cfg.get('repo_root', '')) " "$toml_path" 2>/dev/null) || existing_root="" existing_branch=$(python3 -c " import sys, tomllib with open(sys.argv[1], 'rb') as f: cfg = tomllib.load(f) print(cfg.get('primary_branch', '')) " "$toml_path" 2>/dev/null) || existing_branch="" # Use existing values as defaults if [ -n "$existing_branch" ] && [ -z "$branch" ]; then branch="$existing_branch" fi # Handle repo_root: flag overrides TOML, prompt if they differ if [ -z "$repo_root" ]; then repo_root="${existing_root:-/home/${USER}/${project_name}}" elif [ -n "$existing_root" ] && [ "$repo_root" != "$existing_root" ]; then echo "Note: --repo-root (${repo_root}) differs from TOML (${existing_root})" local update_toml=false if [ "$auto_yes" = true ]; then update_toml=true elif [ -t 0 ]; then read -rp "Update repo_root in TOML to ${repo_root}? [y/N] " confirm if [[ "$confirm" =~ ^[Yy] ]]; then update_toml=true else repo_root="$existing_root" fi fi if [ "$update_toml" = true ]; then python3 -c " import sys, re, pathlib p = pathlib.Path(sys.argv[1]) text = p.read_text() text = re.sub(r'^repo_root\s*=\s*.*$', 'repo_root = \"' + sys.argv[2] + '\"', text, flags=re.MULTILINE) p.write_text(text) " "$toml_path" "$repo_root" echo "Updated: repo_root in ${toml_path}" fi fi fi # Set up local Forgejo instance (provision if needed, create users/tokens/repo) setup_forge "$forge_url" "$forge_repo" # Preflight: verify factory requirements preflight_check "$forge_repo" "$forge_url" # Determine repo root (for new projects) repo_root="${repo_root:-/home/${USER}/${project_name}}" # Clone or validate (try origin first for initial clone from upstream) if [ ! -d "${repo_root}/.git" ]; then # For initial setup, clone from the provided URL directly echo "Cloning: ${repo_url} -> ${repo_root}" git clone "$repo_url" "$repo_root" 2>/dev/null || \ clone_or_validate "$forge_repo" "$repo_root" "$forge_url" else echo "Repo: ${repo_root} (existing clone)" fi # Push to local Forgejo push_to_forge "$repo_root" "$forge_url" "$forge_repo" # Detect primary branch if [ -z "$branch" ]; then branch=$(detect_branch "$repo_root") fi echo "Branch: ${branch}" # Generate project TOML (skip if already exists) if [ "$toml_exists" = false ]; then # Prompt for CI ID if interactive and not already set via flag if [ "$ci_id" = "0" ] && [ "$auto_yes" = false ] && [ -t 0 ]; then read -rp "Woodpecker CI repo ID (0 to skip CI): " user_ci_id ci_id="${user_ci_id:-0}" fi generate_toml "$toml_path" "$project_name" "$forge_repo" "$repo_root" "$branch" "$ci_id" "$forge_url" echo "Created: ${toml_path}" fi # Set up Woodpecker CI to use Forgejo as forge backend _WP_REPO_ID="" setup_woodpecker "$forge_url" "$forge_repo" # Use detected Woodpecker repo ID if ci_id was not explicitly set if [ "$ci_id" = "0" ] && [ -n "${_WP_REPO_ID:-}" ]; then ci_id="$_WP_REPO_ID" echo "CI ID: ${ci_id} (from Woodpecker)" # Update TOML if it already exists if [ "$toml_exists" = true ] && [ -f "$toml_path" ]; then python3 -c " import sys, re, pathlib p = pathlib.Path(sys.argv[1]) text = p.read_text() text = re.sub(r'^woodpecker_repo_id\s*=\s*.*$', 'woodpecker_repo_id = ' + sys.argv[2], text, flags=re.MULTILINE) p.write_text(text) " "$toml_path" "$ci_id" fi fi # Create labels on remote create_labels "$forge_repo" "$forge_url" # Generate VISION.md template generate_vision "$repo_root" "$project_name" # Install cron jobs install_cron "$project_name" "$toml_path" "$auto_yes" echo "" echo "Done. Project ${project_name} is ready." echo " Config: ${toml_path}" echo " Clone: ${repo_root}" echo " Forge: ${forge_url}/${forge_repo}" echo " Run 'disinto status' to verify." } # ── status command ─────────────────────────────────────────────────────────── disinto_status() { local toml_dir="${FACTORY_ROOT}/projects" local found=false for toml in "${toml_dir}"/*.toml; do [ -f "$toml" ] || continue found=true # Parse name, repo, forge_url from TOML local pname prepo pforge_url pname=$(python3 -c " import sys, tomllib with open(sys.argv[1], 'rb') as f: print(tomllib.load(f)['name']) " "$toml" 2>/dev/null) || continue prepo=$(python3 -c " import sys, tomllib with open(sys.argv[1], 'rb') as f: print(tomllib.load(f)['repo']) " "$toml" 2>/dev/null) || continue pforge_url=$(python3 -c " import sys, tomllib with open(sys.argv[1], 'rb') as f: print(tomllib.load(f).get('forge_url', '')) " "$toml" 2>/dev/null) || pforge_url="" pforge_url="${pforge_url:-${FORGE_URL:-http://localhost:3000}}" echo "== ${pname} (${prepo}) ==" # Active dev sessions local has_sessions=false for pf in /tmp/dev-session-"${pname}"-*.phase; do [ -f "$pf" ] || continue has_sessions=true local issue phase_line issue=$(basename "$pf" | sed "s/dev-session-${pname}-//;s/\.phase//") phase_line=$(head -1 "$pf" 2>/dev/null || echo "unknown") echo " Session #${issue}: ${phase_line}" done if [ "$has_sessions" = false ]; then echo " Sessions: none" fi # Backlog depth via API if [ -n "${FORGE_TOKEN:-}" ]; then local api="${pforge_url}/api/v1/repos/${prepo}" local backlog_count pr_count backlog_count=$(curl -sf -I \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api}/issues?state=open&labels=backlog&limit=1" 2>/dev/null \ | grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || backlog_count="?" echo " Backlog: ${backlog_count:-0} issues" pr_count=$(curl -sf -I \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api}/pulls?state=open&limit=1" 2>/dev/null \ | grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}') || pr_count="?" echo " Open PRs: ${pr_count:-0}" else echo " Backlog: (no FORGE_TOKEN)" echo " Open PRs: (no FORGE_TOKEN)" fi echo "" done if [ "$found" = false ]; then echo "No projects configured." echo "Run 'disinto init ' to get started." fi } # ── Main dispatch ──────────────────────────────────────────────────────────── case "${1:-}" in init) shift; disinto_init "$@" ;; status) shift; disinto_status "$@" ;; -h|--help) usage ;; *) usage ;; esac