diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 05dba53..271fbee 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,8 +18,8 @@ /docker-compose*.yaml @Zzackllack # Application code and tests -/app/ @Zzackllack -/tests/ @Zzackllack +/apps/api/app/ @Zzackllack +/apps/api/tests/ @Zzackllack # Cloudflare Worker /src/ @Zzackllack diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe40cd5..132ecd1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -78,7 +78,10 @@ We follow the Conventional Commits specification: https://www.conventionalcommit ## Codeowners -The codeowners for this repository are listed in the [CODEOWNERS](/.github/CODEOWNERS) file. Please update it as necessary when making changes to the codebase. +The codeowners for this repository are listed in the [CODEOWNERS](/.github/CODEOWNERS) file. +CODEOWNERS records ongoing review responsibility; it is not a list of every contributor. +Do not add yourself only because you opened a pull request. Maintainers may add recurring +contributors when they take ownership of an area. Conventions and tips you should follow when editing the CODEOWNERS file: @@ -87,6 +90,9 @@ Conventions and tips you should follow when editing the CODEOWNERS file: - Only `CODEOWNERS`, `.github/CODEOWNERS`, or `docs/CODEOWNERS` are recognized by GitHub. - Use @org/team for teams. +External contributors who are not listed receive an informational pull request comment. The +CODEOWNERS check only blocks malformed rules or owner references that GitHub cannot resolve. + ## Pull Request Process 1. Fork the repository and create a new branch. diff --git a/.github/workflows/codeowners-review.yml b/.github/workflows/codeowners-review.yml index f93ca6b..4a4641d 100644 --- a/.github/workflows/codeowners-review.yml +++ b/.github/workflows/codeowners-review.yml @@ -11,7 +11,7 @@ on: permissions: contents: read issues: write - pull-requests: write + pull-requests: read concurrency: group: codeowners-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -28,13 +28,12 @@ jobs: - name: Validate CODEOWNERS coverage id: validate continue-on-error: true - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: - COMMENT_MARKER: "" + COMMENT_MARKER: "" with: script: | const fs = require('node:fs'); - const path = require('node:path'); const { owner, repo } = context.repo; const issue_number = context.payload.pull_request.number; const candidates = ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS']; @@ -141,58 +140,6 @@ jobs: } } - const globToRegex = (pattern) => { - let result = '^'; - for (let i = 0; i < pattern.length; i += 1) { - const char = pattern[i]; - const next = pattern[i + 1]; - if (char === '*' && next === '*') { - result += '.*'; - i += 1; - } else if (char === '*') { - result += '[^/]*'; - } else if (char === '?') { - result += '[^/]'; - } else if ('\\.[]{}()+-^$|'.includes(char)) { - result += `\\${char}`; - } else { - result += char; - } - } - result += '$'; - return new RegExp(result); - }; - - const matches = (pattern, file) => { - if (pattern === '*') { - return true; - } - - const normalized = pattern.replace(/^\/+/, ''); - if (normalized.endsWith('/')) { - return file.startsWith(normalized); - } - - return globToRegex(normalized).test(file); - }; - - const files = await github.paginate(github.rest.pulls.listFiles, { - owner, - repo, - pull_number: issue_number, - per_page: 100, - }); - - const candidatePaths = files - .filter((file) => ['added', 'renamed', 'copied'].includes(file.status)) - .map((file) => file.filename); - - const fallbackOnly = candidatePaths.filter((filename) => { - const matchedRules = rules.filter((rule) => matches(rule.pattern, filename)); - const hasSpecificRule = matchedRules.some((rule) => rule.pattern !== '*'); - return matchedRules.length > 0 && !hasSpecificRule; - }); - const findings = []; if (invalidLines.length > 0) { findings.push( @@ -211,15 +158,6 @@ jobs: findings.push(` - \`${entry.owner}\` on line ${entry.lineNumber}`); } } - if (fallbackOnly.length > 0) { - findings.push( - '- The following added or renamed paths are only covered by the global `*` fallback:', - ); - for (const filename of fallbackOnly) { - findings.push(` - \`${filename}\``); - } - } - if (findings.length === 0) { return; } @@ -231,7 +169,7 @@ jobs: '', ...findings, '', - 'If the fallback ownership is intentional, update `CODEOWNERS` to add an explicit rule or adjust this check.', + 'This check only blocks malformed or unresolved ownership rules.', ].join('\n'); fs.writeFileSync('codeowners-comment.md', `${body}\n`, 'utf8'); @@ -239,9 +177,9 @@ jobs: - name: Sync pull request CODEOWNERS comment if: always() - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: - COMMENT_MARKER: "" + COMMENT_MARKER: "" with: script: | const fs = require('node:fs'); @@ -298,6 +236,69 @@ jobs: ); } + - name: Sync new contributor ownership notice + if: always() && steps.validate.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + COMMENT_MARKER: "" + with: + script: | + const fs = require('node:fs'); + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + const marker = process.env.COMMENT_MARKER; + const author = context.payload.pull_request.user.login; + const association = context.payload.pull_request.author_association; + const trustedAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + const candidates = ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS']; + const codeownersPath = candidates.find((candidate) => fs.existsSync(candidate)); + const raw = codeownersPath ? fs.readFileSync(codeownersPath, 'utf8') : ''; + const authorRef = `@${author}`.toLowerCase(); + const listedAsOwner = raw + .split(/\r?\n/) + .filter((line) => line.trim() && !line.trim().startsWith('#')) + .some((line) => + line.trim().split(/\s+/).slice(1).some((entry) => entry.toLowerCase() === authorRef), + ); + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: issue_number, + per_page: 100, + }); + const changedCodeowners = files.some((file) => file.filename === codeownersPath); + const shouldComment = + !trustedAssociations.has(association) && !listedAsOwner && !changedCodeowners; + const body = [ + marker, + `### CODEOWNERS note for @${author}`, + '', + `Thanks for contributing. You are not currently listed in \`${codeownersPath}\`.`, + '', + 'You do **not** need to add yourself for this pull request. CODEOWNERS represents ongoing review responsibility, not a list of everyone who has contributed.', + '', + 'A maintainer can add you later if you take recurring ownership of an area.', + ].join('\n'); + + try { + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number, per_page: 100, + }); + const existing = comments.find((comment) => + comment.user?.type === 'Bot' && comment.body?.includes(marker), + ); + + if (shouldComment && existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else if (shouldComment) { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } else if (existing) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id }); + } + } catch (error) { + core.warning(`Skipping contributor ownership notice: ${error.message}`); + } + - name: Enforce CODEOWNERS validation if: steps.validate.outcome == 'failure' run: exit 1 diff --git a/.github/workflows/pr-test-feedback.yml b/.github/workflows/pr-test-feedback.yml index 409b0d8..c64c6b6 100644 --- a/.github/workflows/pr-test-feedback.yml +++ b/.github/workflows/pr-test-feedback.yml @@ -1,9 +1,10 @@ -name: QA / PR Test Feedback +name: QA / PR Feedback on: workflow_run: workflows: - QA / Tests + - QA / Pylint Score types: - completed @@ -17,8 +18,8 @@ concurrency: cancel-in-progress: false jobs: - sync-pytest-feedback: - name: Sync Pytest Feedback + sync-qa-feedback: + name: Sync QA Feedback if: github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: @@ -31,6 +32,14 @@ jobs: core.setOutput('pr_number', pullRequest ? String(pullRequest.number) : ''); core.setOutput('conclusion', context.payload.workflow_run.conclusion || ''); core.setOutput('run_url', context.payload.workflow_run.html_url || ''); + const workflowName = context.payload.workflow_run.name; + const isPylint = workflowName === 'QA / Pylint Score'; + const isPytest = workflowName === 'QA / Tests'; + if (!isPylint && !isPytest) { + throw new Error(`Unsupported QA workflow: ${workflowName}`); + } + core.setOutput('tool', isPylint ? 'Pylint' : 'Pytest'); + core.setOutput('marker', ``); if (!pullRequest) { core.setOutput('artifact_id', ''); @@ -39,7 +48,7 @@ jobs: const { owner, repo } = context.repo; const run_id = context.payload.workflow_run.id; - const artifactName = `pytest-output-pr-${pullRequest.number}`; + const artifactName = `${isPylint ? 'pylint' : 'pytest'}-output-pr-${pullRequest.number}`; const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { owner, repo, @@ -49,48 +58,54 @@ jobs: const artifact = artifacts.find((item) => item.name === artifactName); core.setOutput('artifact_id', artifact ? String(artifact.id) : ''); - - name: Download pytest output artifact + - name: Download QA output artifact if: steps.context.outputs.conclusion == 'failure' && steps.context.outputs.artifact_id != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} ARTIFACT_ID: ${{ steps.context.outputs.artifact_id }} + TOOL: ${{ steps.context.outputs.tool }} shell: bash run: | set -euo pipefail gh api \ -H "Accept: application/vnd.github+json" \ - "/repos/${REPO}/actions/artifacts/${ARTIFACT_ID}/zip" > pytest-output.zip - unzip -p pytest-output.zip pytest-output.txt > pytest-output.raw.txt - mv pytest-output.raw.txt pytest-output.txt + "/repos/${REPO}/actions/artifacts/${ARTIFACT_ID}/zip" > qa-output.zip + output_file="$(printf '%s' "${TOOL}" | tr '[:upper:]' '[:lower:]')-output.txt" + if ! unzip -p qa-output.zip "apps/api/${output_file}" > qa-output.txt; then + unzip -p qa-output.zip "${output_file}" > qa-output.txt + fi - name: Build pull request feedback body if: steps.context.outputs.pr_number != '' env: CONCLUSION: ${{ steps.context.outputs.conclusion }} RUN_URL: ${{ steps.context.outputs.run_url }} + TOOL: ${{ steps.context.outputs.tool }} + MARKER: ${{ steps.context.outputs.marker }} shell: bash run: | set -euo pipefail - marker='' { - echo "${marker}" + echo "${MARKER}" + echo "### ${TOOL} check ${CONCLUSION}" + echo if [ "${CONCLUSION}" = "failure" ]; then - echo "Pytest failed for this pull request." + echo "${TOOL} failed for this pull request. The relevant output is included below." else - echo "Pytest passed for this pull request." + echo "${TOOL} passed for this pull request." fi echo - echo "Run details: ${RUN_URL}" - if [ "${CONCLUSION}" = "failure" ] && [ -s pytest-output.txt ]; then + echo "[Open the full workflow run](${RUN_URL})" + if [ "${CONCLUSION}" = "failure" ] && [ -s qa-output.txt ]; then echo - echo "
Pytest output" + echo "
${TOOL} output" echo echo '```text' python - <<'PY' from pathlib import Path - output = Path("pytest-output.txt").read_text(encoding="utf-8", errors="replace") + output = Path("qa-output.txt").read_text(encoding="utf-8", errors="replace") max_chars = 50000 if len(output) > max_chars: print("(truncated to the last 50000 characters)") @@ -108,7 +123,7 @@ jobs: if: steps.context.outputs.pr_number != '' uses: actions/github-script@v9 env: - COMMENT_MARKER: "" + COMMENT_MARKER: ${{ steps.context.outputs.marker }} PR_NUMBER: ${{ steps.context.outputs.pr_number }} CONCLUSION: ${{ steps.context.outputs.conclusion }} with: diff --git a/.github/workflows/pr-title-conventional.yml b/.github/workflows/pr-title-conventional.yml index 28d1602..ad49a65 100644 --- a/.github/workflows/pr-title-conventional.yml +++ b/.github/workflows/pr-title-conventional.yml @@ -33,9 +33,21 @@ jobs: if [[ "${PR_TITLE}" =~ ${pattern} ]]; then echo "PR title is valid: ${PR_TITLE}" + echo "reason=" >> "${GITHUB_OUTPUT}" exit 0 fi + reason="The title does not match \`type(scope): description\`." + invalid_scope_pattern='^[a-z]+\([^)]*[,[:space:]][^)]*\)' + if [[ ! "${PR_TITLE}" =~ ^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test) ]]; then + reason="The title must start with an allowed lowercase type." + elif [[ "${PR_TITLE}" =~ ${invalid_scope_pattern} ]]; then + reason="The scope must be one lowercase token; commas and spaces are not allowed." + elif [[ ! "${PR_TITLE}" =~ ^[^:]+:\ .+ ]]; then + reason="The header must contain a colon followed by one space and a description." + fi + echo "reason=${reason}" >> "${GITHUB_OUTPUT}" + echo "Pull request title must follow Conventional Commits." echo "Received: ${PR_TITLE}" echo @@ -58,23 +70,35 @@ jobs: uses: actions/github-script@v9 env: COMMENT_MARKER: "" - COMMENT_BODY: | - - Please update this pull request title to follow the Conventional Commits specification. - - Examples: - - `feat(api): add torrent health endpoint` - - `fix: handle missing provider aliases` - - `refactor(parser)!: remove legacy AniWorld slug fallback` - - Reference: - - https://www.conventionalcommits.org/en/v1.0.0/ + PR_TITLE: ${{ github.event.pull_request.title }} + FAILURE_REASON: ${{ steps.validate.outputs.reason }} with: script: | const { owner, repo } = context.repo; const issue_number = context.payload.pull_request.number; const marker = process.env.COMMENT_MARKER; - const body = process.env.COMMENT_BODY; + const body = [ + marker, + '### Pull request title needs attention', + '', + '**Received:**', + '```text', + process.env.PR_TITLE, + '```', + '', + `**Why it failed:** ${process.env.FAILURE_REASON}`, + '', + '**Expected:** `type(scope): description` or `type: description`', + '', + 'Allowed types: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`.', + '', + 'Examples:', + '- `feat(api): add torrent health endpoint`', + '- `fix: handle missing provider aliases`', + '- `refactor(parser)!: remove legacy AniWorld slug fallback`', + '', + '[Conventional Commits reference](https://www.conventionalcommits.org/en/v1.0.0/)', + ].join('\n'); const shouldComment = '${{ steps.validate.outcome }}' === 'failure'; const { data: comments } = await github.rest.issues.listComments({ diff --git a/.github/workflows/pylint-quality.yml b/.github/workflows/pylint-quality.yml index 7f71565..17e1688 100644 --- a/.github/workflows/pylint-quality.yml +++ b/.github/workflows/pylint-quality.yml @@ -48,9 +48,11 @@ jobs: working-directory: apps/api/app run: | set -o pipefail - uv run pylint . | tee ../pylint-output.txt || true + uv run pylint . 2>&1 | tee ../pylint-output.txt || true - name: Enforce minimum pylint score + id: score + continue-on-error: true run: | set -euo pipefail SCORE=$(sed -nE 's/.*rated at ([0-9]+\.[0-9]+)\/10.*/\1/p' apps/api/pylint-output.txt | tail -n1) @@ -68,3 +70,16 @@ jobs: } echo "pylint score gate passed: score $SCORE > 7.5" + + - name: Upload pylint failure output + if: github.event_name == 'pull_request' && steps.score.outcome == 'failure' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: pylint-output-pr-${{ github.event.pull_request.number }} + path: apps/api/pylint-output.txt + if-no-files-found: error + retention-days: 7 + + - name: Enforce pylint success + if: steps.score.outcome == 'failure' + run: exit 1 diff --git a/AGENTS.md b/AGENTS.md index 51fb890..47058dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,6 @@ More guidance - [Security & Legal](internal/agents/security-legal.md) - [Onboarding & Collaboration](internal/agents/onboarding-collaboration.md) - [References](internal/agents/references.md) -- [Change Log](internal/agents/change-log.md) Maintenance notice diff --git a/internal/agents/change-log.md b/internal/agents/change-log.md deleted file mode 100644 index 7a7430d..0000000 --- a/internal/agents/change-log.md +++ /dev/null @@ -1,18 +0,0 @@ -# Change Log - -- 2026-04-13: Documented the new `app/hosts` package and clarified the naming - split between catalogue-site providers and direct video hosts. -- 2026-03-22: Documented automatic Serienstream Turnstile handling for protected - `/r?t=...` redirect tokens, including browser-like headers and configurable - backoff via `PROVIDER_CHALLENGE_BACKOFF_SECONDS`. -- 2026-03-22: Documented provider redirect retry/timeout controls for slow - `s.to` -> `VOE` resolution and noted the Sonarr `qBittorrent is reporting an - error` symptom when redirect resolution fails before download start. -- 2026-03-19: Documented the `aniworld` 4.1.1 migration, including the new - site-specific episode model split and the removal of `SpeedFiles` from the - default provider order. -- 2026-02-23: Documented Cloudflare PR preview URL requirements and enabled - `preview_urls` in `wrangler.toml`. -- 2026-01-25: Refactored AGENTS.md into progressive disclosure files under `internal/agents/`. -- 2025-12-21: Added uv guidance, improved formatting/phrasing, added `.env.example` reference, added Context7 reference, removed unused sections and optional pre-commit step. -- 2025-09-21: Rebuilt AGENTS.md to >=1000 lines, documented constitution alignment, added Cloudflare Workers/VitePress/CI/CD details, expanded env var catalog and file index. diff --git a/internal/agents/release-ci.md b/internal/agents/release-ci.md index af620e4..80c7dbb 100644 --- a/internal/agents/release-ci.md +++ b/internal/agents/release-ci.md @@ -5,8 +5,6 @@ - `scripts/local_build_release.sh` — local artifact build helper. - `scripts/local_build_release.ps1` — PowerShell equivalent. - `scripts/release/cut_release.py` — authoritative semver bump helper used by CI. -- `scripts/setup-codex-overlay.sh` — agent overlay helper. -- `scripts/startup-script.sh` — example startup script. ## Build and Release @@ -54,16 +52,17 @@ For Pull Request preview links in Cloudflare's native PR status comment, use - `tests.yml`: runs `cd apps/api && uv sync --frozen`, executes pytest, and uploads captured failure output for pull request feedback. It ignores `v*` release-tag pushes. -- `pr-test-feedback.yml`: posts or updates a pull request comment when - `tests.yml` fails on a PR, including the pytest output in a collapsed Markdown - details block, and removes the comment automatically once the test run passes. -- `codeowners-review.yml`: checks pull requests for malformed `CODEOWNERS` - rules, invalid or unresolved owner references, and newly added or renamed - paths that are covered only by the global fallback rule, then posts or - refreshes a remediation comment on the PR. -- `format-and-run.yml`: runs `cd apps/api && ruff format app tests` and auto-commits formatting changes. +- `pr-test-feedback.yml`: posts or updates tool-specific pull request comments + when `tests.yml` or `pylint-quality.yml` fails, includes captured output in a + collapsed Markdown details block, and removes each comment once its check + passes. +- `codeowners-review.yml`: blocks malformed `CODEOWNERS` rules and invalid or + unresolved owner references. It separately posts an informational notice for + external contributors who are not listed as recurring code owners; fallback + ownership alone is not treated as a failure. - `pylint-quality.yml`: enforces the pylint score gate on branch pushes and pull - requests, but ignores `v*` release-tag pushes. + requests, captures failure output for PR feedback, and ignores `v*` + release-tag pushes. - `format-and-run.yml`: runs `cd apps/api && ruff format app tests` and auto-commits formatting changes on branch pushes and pull requests, but ignores `v*` release-tag pushes. diff --git a/internal/agents/repo-map.md b/internal/agents/repo-map.md index e2b7fc6..b9efe98 100644 --- a/internal/agents/repo-map.md +++ b/internal/agents/repo-map.md @@ -56,8 +56,6 @@ - `local_build_release.sh` — local artifact build helper. - `local_build_release.ps1` — PowerShell equivalent. -- `setup-codex-overlay.sh` — agent overlay helper. -- `startup-script.sh` — example startup script. ## File Reference Index diff --git a/scripts/setup-codex-overlay.sh b/scripts/setup-codex-overlay.sh deleted file mode 100644 index 8fd38ab..0000000 --- a/scripts/setup-codex-overlay.sh +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ========================= -# UI helpers -# ========================= -color() { printf "\033[%sm%s\033[0m" "${1:-0}" "${2:-}"; } -info() { echo "$(color '1;36' '›')" "$@"; } -warn() { echo "$(color '1;33' '⚠')" "$@"; } -ok() { echo "$(color '1;32' '✓')" "$@"; } -err() { echo "$(color '1;31' '✗')" "$@"; } - -ask_yn() { # ask_yn "Question" default(no|yes) - local q="${1:-Proceed?}" d="${2:-no}" ans - case "$d" in - y|Y|yes) read -rp "$q [Y/n] " ans || true; [[ -z "${ans:-}" || "$ans" =~ ^[Yy]$ ]];; - *) read -rp "$q [y/N] " ans || true; [[ "$ans" =~ ^[Yy]$ ]];; - esac -} - -need_cmd() { command -v "$1" >/dev/null 2>&1 || { err "Required command '$1' not found in PATH."; return 1; }; } - -# ========================= -# Environment / detection -# ========================= -need_cmd ln -need_cmd awk -need_cmd sed -need_cmd date - -if ! command -v git >/dev/null 2>&1; then - warn "git not found — .gitignore guard can still work later via shell hooks." -fi - -if ! command -v codex >/dev/null 2>&1; then - warn "The 'codex' CLI was not found." - ask_yn "Continue anyway (you can install codex later)?" no || exit 1 -else - ok "codex found: $(command -v codex)" -fi - -DEFAULT_SHELL="${SHELL:-}" -[[ -z "$DEFAULT_SHELL" ]] && DEFAULT_SHELL="$(ps -p ${PPID:-$$} -o comm= 2>/dev/null || true)" -DEFAULT_SHELL_BASENAME="${DEFAULT_SHELL##*/}" - -choose_shells() { - local choices=() pick - command -v zsh >/dev/null 2>&1 && choices+=("zsh") - command -v bash >/dev/null 2>&1 && choices+=("bash") - if ((${#choices[@]}==0)); then - err "Neither zsh nor bash available. Aborting." - exit 1 - fi - info "Detected shells: ${choices[*]}" - if [[ " ${choices[*]} " == *" zsh "* ]]; then - pick="zsh" - else - pick="${choices[0]}" - fi - echo "$pick" -} - -PRIMARY_SHELL="$(choose_shells)" -info "Primary target shell: $PRIMARY_SHELL" - -RC_ZSH="${ZDOTDIR:-$HOME}/.zshrc" -RC_BASH="$HOME/.bashrc" -[[ -f "$HOME/.bashrc" || ! -f "$HOME/.bash_profile" ]] || RC_BASH="$HOME/.bash_profile" - -ZSH_SNIPPET="$HOME/.codex_overlay.zsh" -BASH_SNIPPET="$HOME/.codex_overlay.bash" -GLOBAL_HOME="$HOME/.codex" - -# ========================= -# Write snippets -# ========================= -write_zsh() { -cat >"$ZSH_SNIPPET" <<"ZSH_SNIP" -# --- Codex per-project overlay (zsh) ----------------------------------------- -# Keeps auth/logs/sessions in ~/.codex and overlays them into PROJECT/.codex -# while letting .codex/prompts (and optionally .codex/commands) stay local. - -export CODX_GLOBAL_HOME="$HOME/.codex" - -_codx_find_project_root() { - local dir="$PWD" - while [[ "$dir" != "/" ]]; do - [[ -d "$dir/.codex" || -d "$dir/.codex/prompts" || -d "$dir/.codex/commands" ]] && { print -r -- "$dir"; return 0; } - dir="${dir:h}" - done - return 1 -} - -_codx_overlay_global_into_project() { - local proj_codex="$1" - [[ -n "$proj_codex" ]] || return 1 - mkdir -p "$proj_codex" - mkdir -p "$proj_codex/prompts" - # mkdir -p "$proj_codex/commands" # uncomment if you want a local commands dir - - setopt local_options null_glob extended_glob - local src base dest - for src in "$CODX_GLOBAL_HOME"/^(prompts|commands)(N); do - base="${src:t}" - dest="$proj_codex/$base" - if [[ -e "$dest" && ! -L "$dest" ]]; then - continue - fi - ln -sfn -- "$src" "$dest" - done -} - -typeset -ga _CODX_GI_HEADER=( - "# Codex overlay: keep auth/logs in ~/.codex; only track per-project prompts/commands" - "# Ignore .codex/* but re-include .codex/prompts and .codex/commands below" -) - -typeset -ga _CODX_GI_RULES=( - ".codex/*" - "!.codex/prompts/" - "!.codex/prompts/**" - "!.codex/commands/" - "!.codex/commands/**" -) - -_codx_in_git_repo() { - command -v git >/dev/null 2>&1 || return 1 - git -C "$1" rev-parse --is-inside-work-tree >/dev/null 2>&1 -} - -_codx_gitignore_ensure_trailing_blankline() { - local gi="$1" - [[ -f "$gi" ]] || return 0 - if [[ -n "$(tail -c1 "$gi" 2>/dev/null)" ]]; then echo >> "$gi"; fi - if [[ -n "$(tail -n1 "$gi" | tr -d ' \t\r')" ]]; then echo >> "$gi"; fi -} - -_codx_gitignore_append_missing() { - local gi="$1" ; shift - [[ -f "$gi" ]] || : > "$gi" - _codx_gitignore_ensure_trailing_blankline "$gi" - local line missing=() - for line in "$@"; do - grep -Fqx -- "$line" "$gi" || missing+=("$line") - done - [[ ${#missing[@]} -eq 0 ]] && return 0 - if ! grep -Fqx -- "${_CODX_GI_HEADER[1]}" "$gi"; then - printf "%s\n" "${_CODX_GI_HEADER[@]}" >> "$gi" - echo >> "$gi" - print -u2 -- "✅ Inserted Codex header at end of $gi" - fi - for line in "${missing[@]}"; do - print -r -- "$line" >> "$gi" - done -} - -_codx_ensure_gitignore() { - local proj_root="$1" gi="$proj_root/.gitignore" - _codx_in_git_repo "$proj_root" || return 0 - local has_broad="" - if [[ -f "$gi" ]] && grep -Eq '^[[:space:]]*\.codex/?[[:space:]]*$' "$gi"; then - has_broad=1 - fi - local missing=() r - for r in "${_CODX_GI_RULES[@]}"; do - if [[ ! -f "$gi" ]] || ! grep -Fqx -- "$r" "$gi"; then - missing+=("$r") - fi - done - [[ -z "$has_broad" && ${#missing[@]} -eq 0 ]] && return 0 - - print -u2 -- "⚠️ Codex: .gitignore at $gi needs attention." - [[ -n "$has_broad" ]] && print -u2 -- " - Found a broad \".codex\" rule that prevents re-including prompts/commands." - (( ${#missing[@]} )) && print -u2 -- " - Missing recommended rules: ${missing[*]}" - - local reply - if [[ ! -f "$gi" ]]; then - read -q "reply?Create .gitignore with Codex header and rules now? [y/N] "; echo - if [[ "$reply" == [yY] ]]; then - : > "$gi" - _codx_gitignore_append_missing "$gi" "${_CODX_GI_RULES[@]}" - print -u2 -- "✅ Created $gi with Codex header and rules." - fi - return 0 - fi - - read -q "reply?Patch .gitignore (add Codex header, remove broad rule, append safe rules)? [y/N] "; echo - if [[ "$reply" == [yY] ]]; then - local ts backup tmp - ts=$(date +%Y%m%d-%H%M%S) - backup="$gi.bak_codex_$ts" - cp -p -- "$gi" "$backup" 2>/dev/null || cp -p "$gi" "$backup" - if [[ -n "$has_broad" ]]; then - tmp="$gi.tmp_codex_$ts" - awk '!/^[[:space:]]*\.codex\/?[[:space:]]*$/' "$gi" > "$tmp" && mv "$tmp" "$gi" - fi - _codx_gitignore_append_missing "$gi" "${_CODX_GI_RULES[@]}" - print -u2 -- "✅ Updated $gi (backup: $backup)" - else - print -u2 -- "ℹ️ Skipped updating $gi. Broad \".codex\" rules will hide prompts/commands from Git." - fi -} - -_codx_switch_codex_home() { - local proj_root proj_codex - if proj_root="$(_codx_find_project_root)"; then - proj_codex="$proj_root/.codex" - export CODEX_HOME="$proj_codex" - _codx_overlay_global_into_project "$proj_codex" - _codx_ensure_gitignore "$proj_root" - else - export CODEX_HOME="$CODX_GLOBAL_HOME" - fi -} - -autoload -Uz add-zsh-hook -add-zsh-hook chpwd _codx_switch_codex_home -_codx_switch_codex_home -# --- end Codex overlay (zsh) ------------------------------------------------ -ZSH_SNIP -} - -write_bash() { -cat >"$BASH_SNIPPET" <<"BASH_SNIP" -# --- Codex per-project overlay (bash) ---------------------------------------- -# Uses PROMPT_COMMAND to update CODEX_HOME on directory changes. - -export CODX_GLOBAL_HOME="$HOME/.codex" - -_codx_find_project_root() { - local dir="$PWD" - while [[ "$dir" != "/" ]]; do - if [[ -d "$dir/.codex" || -d "$dir/.codex/prompts" || -d "$dir/.codex/commands" ]]; then - printf '%s\n' "$dir"; return 0 - fi - dir="$(dirname "$dir")" - done - return 1 -} - -_codx_overlay_global_into_project() { - local proj_codex="$1" - [[ -n "$proj_codex" ]] || return 1 - mkdir -p "$proj_codex" - mkdir -p "$proj_codex/prompts" - # mkdir -p "$proj_codex/commands" # uncomment if you want a local commands dir - - shopt -s nullglob extglob - local src base dest - for src in "$CODX_GLOBAL_HOME"/!(prompts|commands); do - base="$(basename "$src")" - dest="$proj_codex/$base" - if [[ -e "$dest" && ! -L "$dest" ]]; then - continue - fi - ln -sfn -- "$src" "$dest" - done - shopt -u nullglob extglob -} - -_CODX_GI_HEADER_1="# Codex overlay: keep auth/logs in ~/.codex; only track per-project prompts/commands" -_CODX_GI_HEADER_2="# Ignore .codex/* but re-include .codex/prompts and .codex/commands below" - -declare -a _CODX_GI_RULES=( - ".codex/*" - "!.codex/prompts/" - "!.codex/prompts/**" - "!.codex/commands/" - "!.codex/commands/**" -) - -_codx_in_git_repo() { - command -v git >/dev/null 2>&1 || return 1 - git -C "$1" rev-parse --is-inside-work-tree >/dev/null 2>&1 -} - -_codx_gitignore_ensure_trailing_blankline() { - local gi="$1" - [[ -f "$gi" ]] || return 0 - if [[ -n "$(tail -c1 "$gi" 2>/dev/null || true)" ]]; then echo >> "$gi"; fi - if [[ -n "$(tail -n1 "$gi" | tr -d ' \t\r')" ]]; then echo >> "$gi"; fi -} - -_codx_gitignore_append_missing() { - local gi="$1"; shift - [[ -f "$gi" ]] || : > "$gi" - _codx_gitignore_ensure_trailing_blankline "$gi" - - local line missing=() - for line in "$@"; do - if ! grep -Fqx -- "$line" "$gi" 2>/dev/null; then - missing+=("$line") - fi - done - [[ ${#missing[@]} -eq 0 ]] && return 0 - - if ! grep -Fqx -- "$_CODX_GI_HEADER_1" "$gi" 2>/dev/null; then - printf "%s\n" "$_CODX_GI_HEADER_1" "$_CODX_GI_HEADER_2" >> "$gi" - echo >> "$gi" - >&2 echo "✅ Inserted Codex header at end of $gi" - fi - - for line in "${missing[@]}"; do - printf "%s\n" "$line" >> "$gi" - done -} - -_codx_ensure_gitignore() { - local proj_root="$1" gi="$proj_root/.gitignore" - _codx_in_git_repo "$proj_root" || return 0 - - local has_broad="" - if [[ -f "$gi" ]] && grep -Eq '^[[:space:]]*\.codex/?[[:space:]]*$' "$gi"; then - has_broad=1 - fi - - local missing=() r - for r in "${_CODX_GI_RULES[@]}"; do - if [[ ! -f "$gi" ]] || ! grep -Fqx -- "$r" "$gi"; then - missing+=("$r") - fi - done - - [[ -z "$has_broad" && ${#missing[@]} -eq 0 ]] && return 0 - - >&2 echo "⚠️ Codex: .gitignore at $gi needs attention." - [[ -n "$has_broad" ]] && >&2 echo " - Found a broad \".codex\" rule that prevents re-including prompts/commands." - (( ${#missing[@]} )) && >&2 echo " - Missing recommended rules: ${missing[*]}" - - local reply - if [[ ! -f "$gi" ]]; then - read -r -p "Create .gitignore with Codex header and rules now? [y/N] " reply - if [[ "$reply" =~ ^[Yy]$ ]]; then - : > "$gi" - _codx_gitignore_append_missing "$gi" "${_CODX_GI_RULES[@]}" - >&2 echo "✅ Created $gi with Codex header and rules." - fi - return 0 - fi - - read -r -p "Patch .gitignore (add Codex header, remove broad rule, append safe rules)? [y/N] " reply - if [[ "$reply" =~ ^[Yy]$ ]]; then - local ts backup tmp - ts="$(date +%Y%m%d-%H%M%S)" - backup="$gi.bak_codex_$ts" - cp -p -- "$gi" "$backup" 2>/dev/null || cp -p "$gi" "$backup" - if [[ -n "$has_broad" ]]; then - tmp="$gi.tmp_codex_$ts" - awk '!/^[[:space:]]*\.codex\/?[[:space:]]*$/' "$gi" > "$tmp" && mv "$tmp" "$gi" - fi - _codx_gitignore_append_missing "$gi" "${_CODX_GI_RULES[@]}" - >&2 echo "✅ Updated $gi (backup: $backup)" - else - >&2 echo "ℹ️ Skipped updating $gi. Broad \".codex\" rules will hide prompts/commands from Git." - fi -} - -_codx_switch_codex_home() { - local proj_root proj_codex - if proj_root="$(_codx_find_project_root)"; then - proj_codex="$proj_root/.codex" - export CODEX_HOME="$proj_codex" - _codx_overlay_global_into_project "$proj_codex" - _codx_ensure_gitignore "$proj_root" - else - export CODEX_HOME="$CODX_GLOBAL_HOME" - fi -} - -# Install PROMPT_COMMAND hook once -case "${PROMPT_COMMAND:-}" in - *_codx_switch_codex_home*) :;; - "") PROMPT_COMMAND="_codx_switch_codex_home";; - *) PROMPT_COMMAND="_codx_switch_codex_home; $PROMPT_COMMAND";; -esac - -# Run once for current shell -_codx_switch_codex_home -# --- end Codex overlay (bash) ----------------------------------------------- -BASH_SNIP -} - -mkdir -p "$GLOBAL_HOME" - -# Write/overwrite snippets only with consent -if [[ -f "$ZSH_SNIPPET" ]]; then - info "zsh snippet exists at $ZSH_SNIPPET" - if ask_yn "Overwrite zsh snippet with latest version?" no; then - write_zsh; ok "Updated $ZSH_SNIPPET" - fi -else - write_zsh; ok "Wrote $ZSH_SNIPPET" -fi - -if [[ -f "$BASH_SNIPPET" ]]; then - info "bash snippet exists at $BASH_SNIPPET" - if ask_yn "Overwrite bash snippet with latest version?" no; then - write_bash; ok "Updated $BASH_SNIPPET" - fi -else - write_bash; ok "Wrote $BASH_SNIPPET" -fi - -# ========================= -# Wire into RC files (ask!) -# ========================= -wire_rc() { - local rc="$1" snippet="$2" shellname="$3" - [[ -n "$rc" && -n "$snippet" ]] || return 0 - [[ -f "$snippet" ]] || return 0 - [[ -f "$rc" ]] || touch "$rc" - - # If RC already references our snippet, skip or offer to re-add - if grep -Fq "$snippet" "$rc"; then - info "$shellname RC already sources $snippet" - return 0 - fi - - # Heuristic: if RC already contains our function name (inline setup), - # warn and offer to skip adding another hook. - if grep -Fq "_codx_switch_codex_home" "$rc"; then - warn "$shellname RC already seems to contain a Codex overlay (inline)." - ask_yn "Add snippet sourcing anyway?" no || return 0 - else - ask_yn "Append snippet sourcing to $rc for $shellname?" yes || return 0 - fi - - local backup="${rc}.bak_codex_$(date +%Y%m%d-%H%M%S)" - cp -p "$rc" "$backup" 2>/dev/null || true - { - echo - echo "# Load Codex per-project overlay (installed by setup-codex-overlay.sh)" - if [[ "$shellname" == "zsh" ]]; then - echo "if [[ -f \"$snippet\" ]]; then source \"$snippet\"; fi" - else - echo "[ -f \"$snippet\" ] && source \"$snippet\"" - fi - } >> "$rc" - ok "Added source line to $rc (backup: $backup)" -} - -# Ask which shells to wire -TARGET_ZSH=false -TARGET_BASH=false - -if [[ "$PRIMARY_SHELL" == "zsh" ]]; then - TARGET_ZSH=true - ask_yn "Also install for bash RC?" no && TARGET_BASH=true -else - TARGET_BASH=true - ask_yn "Also install for zsh RC?" yes && TARGET_ZSH=true -fi - -$TARGET_ZSH && wire_rc "$RC_ZSH" "$ZSH_SNIPPET" "zsh" -$TARGET_BASH && wire_rc "$RC_BASH" "$BASH_SNIPPET" "bash" - -# ========================= -# Optional one-shot run now -# ========================= -echo -if ask_yn "Run a one-shot overlay + .gitignore check for THIS directory now?" yes; then - if [[ "$PRIMARY_SHELL" == "zsh" && $(command -v zsh) ]]; then - # Run in a separate zsh to avoid sourcing zsh code into bash - zsh -c "source '$ZSH_SNIPPET'; _codx_switch_codex_home" || true - elif [[ "$PRIMARY_SHELL" == "bash" && $(command -v bash) ]]; then - bash -c "source '$BASH_SNIPPET'; _codx_switch_codex_home" || true - else - warn "Primary shell not available for one-shot. Skipping." - fi -fi - -echo -ok "Codex per-project overlay setup finished." -info "To activate hooks in new terminals:" -echo " - zsh : run 'source \"$RC_ZSH\"' or open a new shell" -echo " - bash: run 'source \"$RC_BASH\"' or open a new shell" \ No newline at end of file diff --git a/scripts/startup-script.sh b/scripts/startup-script.sh deleted file mode 100644 index 38fa2b5..0000000 --- a/scripts/startup-script.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "🚀 AniBridge Startup Script – Initializing Environment..." -echo "--------------------------------------------" - -# Check Python -if ! command -v python3 >/dev/null 2>&1; then - echo "❌ Python3 not found. Please install Python 3.11+" - exit 1 -fi -if ! command -v uv >/dev/null 2>&1; then - echo "❌ uv not found. Install it first: https://docs.astral.sh/uv/getting-started/installation/" - exit 1 -fi - -echo "✅ Python3 detected: $(python3 --version)" - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -API_DIR="$(cd "$(dirname "$0")/../apps/api" && pwd)" - -# Ensure Python version is >= 3.14 to match apps/api/pyproject.toml requires-python. -PYTHON_VERSION_OK=$(python3 -c 'import sys; print(int(sys.version_info >= (3, 14)))' || echo "0") -if [ "$PYTHON_VERSION_OK" != "1" ]; then - echo "❌ Python 3.14+ is required, but found: $(python3 --version 2>&1 || echo "unknown version")" - exit 1 -fi - -# Create virtual environment -echo "📦 Creating virtual environment..." -( - cd "$API_DIR" - uv venv -) - -# Install dependencies -echo "📦 Installing dependencies..." -( - cd "$API_DIR" - uv sync --frozen -) - -echo "✅ Dependencies installed." - -# Export environment variables -echo "⚙️ Setting environment variables..." -export LOG_LEVEL=DEBUG -export INDEXER_API_KEY="devkey" -export INDEXER_NAME="AniBridge" -export TORZNAB_CAT_ANIME=5070 -export DATA_DIR="$REPO_ROOT/data" -export DOWNLOAD_DIR="$REPO_ROOT/data/downloads" - -echo "✅ Environment variables set." - -# Startup message to Codex / LLM -echo "💡 Hey Codex, the environment is ready." -echo "💡 Python venv is active." -echo "💡 All dependencies are installed." -echo "💡 You can now start the AniBridge FastAPI server." - -# Start FastAPI -echo "🚀 Launching AniBridge..." -( - cd "$API_DIR" - uv run python -m app.main -)