Merge pull request 'fix: feat: disinto init — one-command project bootstrap (#393)' (#495) from fix/issue-393 into main
This commit is contained in:
commit
12f8623a04
1 changed files with 388 additions and 0 deletions
388
bin/disinto
Executable file
388
bin/disinto
Executable file
|
|
@ -0,0 +1,388 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# disinto — CLI entry point for the disinto code factory
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# disinto init <repo-url> [options] Bootstrap a new project
|
||||||
|
# disinto status Show factory status
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# disinto init https://codeberg.org/user/repo
|
||||||
|
# disinto init https://codeberg.org/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 <<EOF
|
||||||
|
disinto — autonomous code factory CLI
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
disinto init <repo-url> [options] Bootstrap a new project
|
||||||
|
disinto status Show factory status
|
||||||
|
|
||||||
|
Init options:
|
||||||
|
--branch <name> Primary branch (default: auto-detect)
|
||||||
|
--repo-root <path> Local clone path (default: ~/name)
|
||||||
|
--ci-id <n> Woodpecker CI repo ID (default: 0 = no CI)
|
||||||
|
--yes Skip confirmation prompts
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract org/repo slug from various URL formats.
|
||||||
|
# Accepts: https://codeberg.org/user/repo, codeberg.org/user/repo,
|
||||||
|
# user/repo, https://codeberg.org/user/repo.git
|
||||||
|
parse_repo_slug() {
|
||||||
|
local url="$1"
|
||||||
|
url="${url#https://}"
|
||||||
|
url="${url#http://}"
|
||||||
|
url="${url#codeberg.org/}"
|
||||||
|
url="${url%.git}"
|
||||||
|
url="${url%/}"
|
||||||
|
if [[ ! "$url" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
|
||||||
|
echo "Error: invalid repo URL — expected https://codeberg.org/org/repo or org/repo" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '%s' "$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a clone-able URL from a slug.
|
||||||
|
clone_url_from_slug() {
|
||||||
|
printf 'https://codeberg.org/%s.git' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate that required tokens and tools are available.
|
||||||
|
validate_env() {
|
||||||
|
local errors=0
|
||||||
|
if [ -z "${CODEBERG_TOKEN:-}" ]; then
|
||||||
|
echo "Error: CODEBERG_TOKEN is not set" >&2
|
||||||
|
echo " Set it in ${FACTORY_ROOT}/.env or export it" >&2
|
||||||
|
errors=1
|
||||||
|
fi
|
||||||
|
if ! command -v claude &>/dev/null; then
|
||||||
|
echo "Warning: claude CLI not found in PATH" >&2
|
||||||
|
fi
|
||||||
|
if [ "$errors" -gt 0 ]; then
|
||||||
|
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"
|
||||||
|
if [ -d "${target}/.git" ]; then
|
||||||
|
echo "Repo: ${target} (existing clone)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local url
|
||||||
|
url=$(clone_url_from_slug "$slug")
|
||||||
|
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/<name>.toml config file.
|
||||||
|
generate_toml() {
|
||||||
|
local path="$1" name="$2" repo="$3" root="$4" branch="$5" ci_id="$6"
|
||||||
|
cat > "$path" <<EOF
|
||||||
|
# projects/${name}.toml — Project config for ${repo}
|
||||||
|
#
|
||||||
|
# Generated by disinto init
|
||||||
|
|
||||||
|
name = "${name}"
|
||||||
|
repo = "${repo}"
|
||||||
|
repo_root = "${root}"
|
||||||
|
primary_branch = "${branch}"
|
||||||
|
|
||||||
|
[ci]
|
||||||
|
woodpecker_repo_id = ${ci_id}
|
||||||
|
stale_minutes = 60
|
||||||
|
|
||||||
|
[services]
|
||||||
|
containers = []
|
||||||
|
|
||||||
|
[monitoring]
|
||||||
|
check_prs = true
|
||||||
|
check_dev_agent = true
|
||||||
|
check_pipeline_stall = false
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create standard labels on the Codeberg repo.
|
||||||
|
create_labels() {
|
||||||
|
local repo="$1"
|
||||||
|
local api="https://codeberg.org/api/v1/repos/${repo}"
|
||||||
|
|
||||||
|
local -A labels=(
|
||||||
|
["backlog"]="#0075ca"
|
||||||
|
["in-progress"]="#e4e669"
|
||||||
|
["blocked"]="#d73a4a"
|
||||||
|
["tech-debt"]="#cfd3d7"
|
||||||
|
["underspecified"]="#fbca04"
|
||||||
|
["vision"]="#0e8a16"
|
||||||
|
["action"]="#1d76db"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Creating labels on ${repo}..."
|
||||||
|
local name color
|
||||||
|
for name in backlog in-progress blocked tech-debt underspecified vision action; do
|
||||||
|
color="${labels[$name]}"
|
||||||
|
if curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${api}/labels" \
|
||||||
|
-d "{\"name\":\"${name}\",\"color\":\"${color}\"}" >/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" <<EOF
|
||||||
|
# Vision
|
||||||
|
|
||||||
|
## What ${name} does
|
||||||
|
|
||||||
|
<!-- Describe the purpose of this project in one paragraph -->
|
||||||
|
|
||||||
|
## Who it's for
|
||||||
|
|
||||||
|
<!-- Describe the target audience -->
|
||||||
|
|
||||||
|
## Design principles
|
||||||
|
|
||||||
|
- <!-- Principle 1 -->
|
||||||
|
- <!-- Principle 2 -->
|
||||||
|
- <!-- Principle 3 -->
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
### Current
|
||||||
|
- <!-- What you're working on now -->
|
||||||
|
|
||||||
|
### Next
|
||||||
|
- <!-- What comes after -->
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── init command ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
disinto_init() {
|
||||||
|
local repo_url="${1:-}"
|
||||||
|
if [ -z "$repo_url" ]; then
|
||||||
|
echo "Error: repo URL required" >&2
|
||||||
|
echo "Usage: disinto init <repo-url>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
# Parse flags
|
||||||
|
local branch="" repo_root="" ci_id="0" auto_yes=false
|
||||||
|
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 ;;
|
||||||
|
--yes) auto_yes=true; shift ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Extract org/repo slug
|
||||||
|
local codeberg_repo
|
||||||
|
codeberg_repo=$(parse_repo_slug "$repo_url")
|
||||||
|
local project_name="${codeberg_repo##*/}"
|
||||||
|
local toml_path="${FACTORY_ROOT}/projects/${project_name}.toml"
|
||||||
|
|
||||||
|
echo "=== disinto init ==="
|
||||||
|
echo "Project: ${codeberg_repo}"
|
||||||
|
echo "Name: ${project_name}"
|
||||||
|
|
||||||
|
# Check for existing config
|
||||||
|
if [ -f "$toml_path" ]; then
|
||||||
|
echo "Error: ${toml_path} already exists" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate tokens
|
||||||
|
validate_env
|
||||||
|
|
||||||
|
# Determine repo root
|
||||||
|
repo_root="${repo_root:-/home/${USER}/${project_name}}"
|
||||||
|
|
||||||
|
# Clone or validate
|
||||||
|
clone_or_validate "$codeberg_repo" "$repo_root"
|
||||||
|
|
||||||
|
# Detect primary branch
|
||||||
|
if [ -z "$branch" ]; then
|
||||||
|
branch=$(detect_branch "$repo_root")
|
||||||
|
fi
|
||||||
|
echo "Branch: ${branch}"
|
||||||
|
|
||||||
|
# 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 project TOML
|
||||||
|
generate_toml "$toml_path" "$project_name" "$codeberg_repo" "$repo_root" "$branch" "$ci_id"
|
||||||
|
echo "Created: ${toml_path}"
|
||||||
|
|
||||||
|
# Create labels on remote
|
||||||
|
create_labels "$codeberg_repo"
|
||||||
|
|
||||||
|
# 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 " 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 and repo from TOML
|
||||||
|
local pname prepo
|
||||||
|
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
|
||||||
|
|
||||||
|
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 "${CODEBERG_TOKEN:-}" ]; then
|
||||||
|
local api="https://codeberg.org/api/v1/repos/${prepo}"
|
||||||
|
local backlog_count pr_count
|
||||||
|
|
||||||
|
backlog_count=$(curl -sf -I \
|
||||||
|
-H "Authorization: token ${CODEBERG_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 ${CODEBERG_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 CODEBERG_TOKEN)"
|
||||||
|
echo " Open PRs: (no CODEBERG_TOKEN)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$found" = false ]; then
|
||||||
|
echo "No projects configured."
|
||||||
|
echo "Run 'disinto init <repo-url>' to get started."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main dispatch ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
init) shift; disinto_init "$@" ;;
|
||||||
|
status) shift; disinto_status "$@" ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Loading…
Add table
Add a link
Reference in a new issue