commit 25ad154eac727053bd2960b85af5182307d82d45 Author: mathias Date: Sun Jul 19 08:29:23 2026 +0000 Initial commit diff --git a/.aider.conf.yml b/.aider.conf.yml new file mode 100644 index 0000000..a16f762 --- /dev/null +++ b/.aider.conf.yml @@ -0,0 +1,2 @@ +read: .aider.conventions.md +auto-commits: false diff --git a/.aider.conventions.md b/.aider.conventions.md new file mode 100644 index 0000000..5b2b175 --- /dev/null +++ b/.aider.conventions.md @@ -0,0 +1,13 @@ +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. diff --git a/.context/PROJECT.md b/.context/PROJECT.md new file mode 100644 index 0000000..5b2b175 --- /dev/null +++ b/.context/PROJECT.md @@ -0,0 +1,13 @@ +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. diff --git a/.context/mcp.json b/.context/mcp.json new file mode 100644 index 0000000..cd0f1bc --- /dev/null +++ b/.context/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "knowledge": { + "url": "http://localhost:3100/mcp", + "description": "Project knowledge base — vector + graph retrieval" + } + } +} diff --git a/.context/skills/go-patterns.md b/.context/skills/go-patterns.md new file mode 100644 index 0000000..cd87180 --- /dev/null +++ b/.context/skills/go-patterns.md @@ -0,0 +1,37 @@ +# Skill: Go project patterns + +## New endpoint checklist +1. Define request/response types in `types.go` +2. Write handler in `handlers.go` using `http.HandlerFunc` +3. Add route in `routes.go` +4. Write table-driven test in `handlers_test.go` +5. Run `task check` before committing + +## Error handling pattern +```go +if err != nil { + return fmt.Errorf("descriptiveOperation: %w", err) +} +``` +Never log and return — do one or the other. + +## HTMX response pattern +```go +func (h *Handler) ListItems(w http.ResponseWriter, r *http.Request) { + items, err := h.store.List(r.Context()) + if err != nil { + http.Error(w, "failed to list items", http.StatusInternalServerError) + return + } + if r.Header.Get("HX-Request") == "true" { + h.templates.Render(w, "items/_list", items) + return + } + h.templates.Render(w, "items/index", items) +} +``` + +## Dependency policy +- Prefer stdlib: `net/http`, `encoding/json`, `database/sql` +- Allowed without justification: `testify`, `slog`, `templ`, `sqlc` +- Needs justification in commit message: anything else diff --git a/.context/skills/htmx-patterns.md b/.context/skills/htmx-patterns.md new file mode 100644 index 0000000..fe2f224 --- /dev/null +++ b/.context/skills/htmx-patterns.md @@ -0,0 +1,26 @@ +# Skill: HTMX patterns + +## Default attributes +Always include on interactive elements: +- `hx-indicator` for loading states +- `hx-swap="innerHTML"` as default (explicit over implicit) +- `hx-target` pointing to a specific ID, never `this` in production + +## Form pattern +```html +
+ + + ... +
+``` + +## Server-sent validation errors +Return 422 with the error fragment, swap into the form's error container: +```html +hx-target-422="#form-errors" +``` + +## Prefer hypermedia over JSON +If the endpoint returns data for display, return an HTML fragment. +Only use JSON for machine-to-machine APIs or when a non-browser client needs it. diff --git a/.context/system-prompt.txt b/.context/system-prompt.txt new file mode 100644 index 0000000..00a6186 --- /dev/null +++ b/.context/system-prompt.txt @@ -0,0 +1,20 @@ +You are a coding assistant working on a specific project. +Follow all conventions from both the root agent context and project context. + +--- + +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. + +--- diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..2fd6be3 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,16 @@ +# Cursor rules — auto-generated +# Do not edit. Run: task context:sync + +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..b0ec31d --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -0,0 +1,132 @@ +name: CD + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +env: + IMAGE: __PROJECT_NAME__ + +jobs: + guard: + name: Detect unsubstituted template + runs-on: self-hosted + outputs: + is_template: ${{ steps.detect.outputs.is_template }} + steps: + - uses: actions/checkout@v4 + - id: detect + run: | + if grep -q '__MODULE_PATH__' go.mod; then + echo "is_template=true" >> "$GITHUB_OUTPUT" + else + echo "is_template=false" >> "$GITHUB_OUTPUT" + fi + + check: + name: Lint / Test / Vet + needs: guard + if: needs.guard.outputs.is_template != 'true' + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: false + + - name: Install toolchain + run: | + go version + go install github.com/a-h/templ/cmd/templ@latest + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh \ + | sh -s -- -b "$(go env GOPATH)/bin" v2.11.4 + + - name: Run checks + run: task check + + build: + name: Build & Import + needs: [guard, check] + if: needs.guard.outputs.is_template != 'true' && github.event_name != 'pull_request' + runs-on: self-hosted + outputs: + image-tag: ${{ steps.meta.outputs.sha-tag }} + steps: + - uses: actions/checkout@v4 + + - name: Derive image tags + id: meta + run: | + SHA=$(git rev-parse --short HEAD) + echo "sha-tag=${SHA}" >> "$GITHUB_OUTPUT" + + - name: Build and push to local registry + run: | + REGISTRY="localhost:5000" + REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" + buildah build \ + --label "org.opencontainers.image.revision=${{ github.sha }}" \ + -t ${REF} \ + -t ${REGISTRY}/${{ env.IMAGE }}:latest \ + . + buildah push --tls-verify=false ${REF} + buildah push --tls-verify=false ${REGISTRY}/${{ env.IMAGE }}:latest + echo "✓ Image pushed to ${REF}" + + deploy: + name: Deploy via GitOps + needs: [guard, build] + if: needs.guard.outputs.is_template != 'true' && github.ref == 'refs/heads/main' && github.event_name == 'push' + runs-on: self-hosted + steps: + - name: Update image tag in infra repo + env: + IMAGE_TAG: ${{ needs.build.outputs.image-tag }} + DEPLOY_KEY: ${{ secrets.INFRA_DEPLOY_KEY }} + run: | + set -euo pipefail + mkdir -p ~/.ssh + echo "$DEPLOY_KEY" > ~/.ssh/id_infra + chmod 600 ~/.ssh/id_infra + ssh-keyscan -p 30022 10.0.1.20 >> ~/.ssh/known_hosts 2>/dev/null + export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_infra -o IdentitiesOnly=yes" + rm -rf /tmp/infra + git clone -b main ssh://git@10.0.1.20:30022/mathias/infra.git /tmp/infra + cd /tmp/infra + DEPLOYMENT="k3s/apps/__PROJECT_NAME__/deployment.yaml" + sed -i "s|image: localhost:5000/__PROJECT_NAME__:.*|image: localhost:5000/__PROJECT_NAME__:${IMAGE_TAG}|" "$DEPLOYMENT" + grep -q "localhost:5000/__PROJECT_NAME__:${IMAGE_TAG}" "$DEPLOYMENT" \ + || { echo "✗ image tag patch failed"; exit 1; } + if git diff --quiet "$DEPLOYMENT"; then + echo "ℹ image tag unchanged — skipping push" + else + git -c user.name="__PROJECT_NAME__ CI" \ + -c user.email="ci@__PROJECT_NAME__.local" \ + commit -m "chore(deploy): __PROJECT_NAME__ → ${IMAGE_TAG}" "$DEPLOYMENT" + git push origin main + echo "✓ pushed to infra repo" + fi + shred -u ~/.ssh/id_infra + + - name: Trigger Flux reconcile + run: | + kubectl -n flux-system annotate gitrepository flux-system \ + reconcile.fluxcd.io/requestedAt="$(date +%s)" --overwrite + kubectl -n flux-system annotate kustomization apps \ + reconcile.fluxcd.io/requestedAt="$(date +%s)" --overwrite + + - name: Verify rollout + run: | + kubectl rollout status deployment/__PROJECT_NAME__ \ + --namespace __PROJECT_NAME__ \ + --timeout=120s \ + || { + kubectl get pods -n __PROJECT_NAME__ -o wide + kubectl get events -n __PROJECT_NAME__ --sort-by='.lastTimestamp' | tail -20 + exit 1 + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..902b7e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Project-specific +bin/ +*.templ.go diff --git a/.skills/go-patterns/SKILL.md b/.skills/go-patterns/SKILL.md new file mode 100644 index 0000000..e423120 --- /dev/null +++ b/.skills/go-patterns/SKILL.md @@ -0,0 +1,42 @@ +--- +name: go-patterns +description: Go project patterns — endpoint checklist, error handling, HTMX responses, dependency policy. Use when writing Go code, adding endpoints, or reviewing Go PRs. +--- + +# Go project patterns + +## New endpoint checklist +1. Define request/response types in `types.go` +2. Write handler in `handlers.go` using `http.HandlerFunc` +3. Add route in `routes.go` +4. Write table-driven test in `handlers_test.go` +5. Run `task check` before committing + +## Error handling pattern +```go +if err != nil { + return fmt.Errorf("descriptiveOperation: %w", err) +} +``` +Never log and return — do one or the other. + +## HTMX response pattern +```go +func (h *Handler) ListItems(w http.ResponseWriter, r *http.Request) { + items, err := h.store.List(r.Context()) + if err != nil { + http.Error(w, "failed to list items", http.StatusInternalServerError) + return + } + if r.Header.Get("HX-Request") == "true" { + h.templates.Render(w, "items/_list", items) + return + } + h.templates.Render(w, "items/index", items) +} +``` + +## Dependency policy +- Prefer stdlib: `net/http`, `encoding/json`, `database/sql` +- Allowed without justification: `testify`, `slog`, `templ`, `sqlc` +- Needs justification in commit message: anything else diff --git a/.skills/htmx-patterns/SKILL.md b/.skills/htmx-patterns/SKILL.md new file mode 100644 index 0000000..3c28a68 --- /dev/null +++ b/.skills/htmx-patterns/SKILL.md @@ -0,0 +1,31 @@ +--- +name: htmx-patterns +description: HTMX conventions — default attributes, form patterns, validation errors, hypermedia-first API design. Use when writing HTMX templates or Go handlers that return HTML fragments. +--- + +# HTMX patterns + +## Default attributes +Always include on interactive elements: +- `hx-indicator` for loading states +- `hx-swap="innerHTML"` as default (explicit over implicit) +- `hx-target` pointing to a specific ID, never `this` in production + +## Form pattern +```html +
+ + + ... +
+``` + +## Server-sent validation errors +Return 422 with the error fragment, swap into the form's error container: +```html +hx-target-422="#form-errors" +``` + +## Prefer hypermedia over JSON +If the endpoint returns data for display, return an HTML fragment. +Only use JSON for machine-to-machine APIs or when a non-browser client needs it. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5b2b175 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5b2b175 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +# __PROJECT_NAME__ + +## Identity + +- **Name**: __PROJECT_NAME__ +- **Owner**: Mathias +- **Client**: personal +- **Repo**: __MODULE_PATH__ +- **Status**: active + +## Stack + +Go + Templ + HTMX + CDN Tailwind. See `~/dev/.context/AGENT.md` for cross-project conventions. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b32c16e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.26-alpine AS build +WORKDIR /src +RUN apk add --no-cache git +RUN go install github.com/a-h/templ/cmd/templ@latest +COPY go.mod ./ +RUN go mod download +COPY . . +RUN templ generate && CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/app ./cmd/__PROJECT_NAME__ + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /out/app /app +USER nonroot:nonroot +EXPOSE 8080 +ENTRYPOINT ["/app"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e2c67c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# __PROJECT_NAME__ + +> Generated from `mathias/template-go-web`. + +## Bootstrap + +After creating from template, run: + +```bash +go mod tidy # regenerate go.sum with real module path +task generate # generate templ files +task build # build the binary +``` diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..23e9e31 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,37 @@ +version: '3' + +tasks: + generate: + desc: Run templ generate + cmds: [templ generate] + build: + desc: Build the binary + deps: [generate] + cmds: [go build -o bin/__PROJECT_NAME__ ./cmd/__PROJECT_NAME__] + run: + deps: [build] + cmds: [./bin/__PROJECT_NAME__] + test: + desc: Run all tests + deps: [generate] + cmds: [go test ./... -race] + lint: + cmds: [golangci-lint run ./...] + check: + desc: Lint, vet, and test (used by CI) + deps: [generate] + cmds: + - golangci-lint run ./... + - go vet ./... + - go test ./... -race -count=1 + + context:sync: + desc: Regenerate all harness-specific context files + cmds: + - bash scripts/context-sync.sh + context:sync:claude: + cmds: [bash scripts/context-sync.sh claude] + context:sync:agents: + cmds: [bash scripts/context-sync.sh agents] + context:sync:cursor: + cmds: [bash scripts/context-sync.sh cursor] diff --git a/cmd/__PROJECT_NAME__/main.go b/cmd/__PROJECT_NAME__/main.go new file mode 100644 index 0000000..e8959bf --- /dev/null +++ b/cmd/__PROJECT_NAME__/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log/slog" + "net/http" + "os" + + "__MODULE_PATH__/internal/web" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + mux := http.NewServeMux() + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + mux.Handle("/", web.NewHandler()) + + addr := ":8080" + logger.Info("__PROJECT_NAME__ starting", "addr", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + logger.Error("server stopped", "err", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..198b4ef --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module __MODULE_PATH__ + +go 1.26 + +require ( + github.com/a-h/templ v0.2.778 +) diff --git a/internal/web/handler.go b/internal/web/handler.go new file mode 100644 index 0000000..92cf8a1 --- /dev/null +++ b/internal/web/handler.go @@ -0,0 +1,17 @@ +package web + +import ( + "context" + "net/http" +) + +func NewHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _ = Index().Render(context.Background(), w) + }) + mux.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) { + _ = Hello("world").Render(context.Background(), w) + }) + return mux +} diff --git a/internal/web/index.templ b/internal/web/index.templ new file mode 100644 index 0000000..7a86488 --- /dev/null +++ b/internal/web/index.templ @@ -0,0 +1,16 @@ +package web + +templ Index() { + @Layout("__PROJECT_NAME__") { +

__PROJECT_NAME__

+ +
+ } +} + +templ Hello(name string) { +

Hello, { name }!

+} diff --git a/internal/web/layout.templ b/internal/web/layout.templ new file mode 100644 index 0000000..dfedd10 --- /dev/null +++ b/internal/web/layout.templ @@ -0,0 +1,19 @@ +package web + +templ Layout(title string) { + + + + + + { title } + + + + +
+ { children... } +
+ + +} diff --git a/scripts/context-sync.sh b/scripts/context-sync.sh new file mode 100644 index 0000000..4f7300e --- /dev/null +++ b/scripts/context-sync.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# Generates harness-specific context files from .context/PROJECT.md +# Project-level script — run from a project directory. +# +# For Claude Code: generates project-only CLAUDE.md (it inherits root via tree walk) +# For everything else: concatenates root AGENT.md + project PROJECT.md +# +# Usage: ./scripts/context-sync.sh [--force] [adapter...] +# Task: task context:sync +# +# Override root context: ROOT_CONTEXT=~/dev/.context/AGENT.md ./scripts/context-sync.sh + +set -euo pipefail + +# Parse --force flag and collect adapter names separately +FORCE=false +ADAPTERS=() +for _arg in "$@"; do + case "$_arg" in + --force) FORCE=true ;; + *) ADAPTERS+=("$_arg") ;; + esac +done + +PROJECT_FILE=".context/PROJECT.md" + +# Walk up to find root .context/AGENT.md +find_root_context() { + local dir + dir="$(pwd)" + while [ "$dir" != "/" ]; do + dir="$(dirname "$dir")" + if [ -f "$dir/.context/AGENT.md" ]; then + echo "$dir/.context/AGENT.md" + return + fi + done + echo "" +} + +ROOT_CONTEXT="${ROOT_CONTEXT:-$(find_root_context)}" + +if [ ! -f "$PROJECT_FILE" ]; then + echo "Error: $PROJECT_FILE not found. Are you in a project root?" + exit 1 +fi + +# Pre-flight: reject unfilled {{...}} placeholders unless --force +if [ "$FORCE" = false ]; then + _placeholders=$(grep -n '{{[^}]*}}' "$PROJECT_FILE" 2>/dev/null || true) + if [ -n "$_placeholders" ]; then + echo "Error: unfilled placeholders in $PROJECT_FILE:" >&2 + while IFS= read -r _match; do + _lineno="${_match%%:*}" + _content="${_match#*:}" + _token=$(printf '%s' "$_content" | grep -o '{{[^}]*}}' | head -1) + echo " $PROJECT_FILE:$_lineno: unfilled placeholder $_token" >&2 + done <<< "$_placeholders" + echo "" >&2 + echo "Fill these placeholders, then re-run: task context:sync" >&2 + echo "To bypass validation: bash scripts/context-sync.sh --force" >&2 + exit 1 + fi +fi + +if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then + echo " Root context: $ROOT_CONTEXT" +else + echo " No root AGENT.md found (project context only)" +fi + +# Emit root context + separator +root_block() { + if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then + cat "$ROOT_CONTEXT" + echo "" + echo "---" + echo "" + fi +} + +# ── Claude Code ────────────────────────────────────────────── +# Claude Code walks up the tree — it finds ~/dev/CLAUDE.md automatically. +# Project-level CLAUDE.md only needs project-specific context. +generate_claude() { + cat "$PROJECT_FILE" > CLAUDE.md + echo " → CLAUDE.md (project-only; Claude Code inherits root)" +} + +# ── AGENTS.md (Crush, Pi, Antigravity) ────────────────────── +# These tools read AGENTS.md from cwd but don't walk up. +# Concatenate root + project. +generate_agents() { + { root_block; cat "$PROJECT_FILE"; } > AGENTS.md + echo " → AGENTS.md (root + project; Crush, Pi, Antigravity)" +} + +# ── Cursor ─────────────────────────────────────────────────── +generate_cursor() { + { + echo "# Cursor rules — auto-generated" + echo "# Do not edit. Run: task context:sync" + echo "" + root_block + cat "$PROJECT_FILE" + } > .cursorrules + echo " → .cursorrules (root + project)" +} + +# ── Aider ──────────────────────────────────────────────────── +generate_aider() { + { root_block; cat "$PROJECT_FILE"; } > .aider.conventions.md + if [ ! -f .aider.conf.yml ]; then + cat > .aider.conf.yml << 'YAML' +read: .aider.conventions.md +auto-commits: false +YAML + fi + echo " → .aider.conventions.md (root + project)" +} + +# ── Generic system prompt (Open WebUI, Mods, etc.) ────────── +generate_system_prompt() { + { + echo "You are a coding assistant working on a specific project." + echo "Follow all conventions from both the root agent context and project context." + echo "" + echo "---" + echo "" + root_block + cat "$PROJECT_FILE" + echo "" + echo "---" + } > .context/system-prompt.txt + echo " → .context/system-prompt.txt (root + project)" +} + +# ── MCP config ─────────────────────────────────────────────── +generate_mcp() { + # Ensure baseline file exists with project-specific knowledge server + if [ ! -f .context/mcp.json ]; then + cat > .context/mcp.json << 'JSON' +{ + "mcpServers": { + "knowledge": { + "url": "http://localhost:3100/mcp", + "description": "Project knowledge base — vector + graph retrieval" + } + } +} +JSON + fi + + # Merge root mcp-servers.json if found alongside root AGENT.md + local root_mcp="" + if [ -n "$ROOT_CONTEXT" ] && [ -f "$ROOT_CONTEXT" ]; then + local candidate + candidate="$(dirname "$ROOT_CONTEXT")/mcp-servers.json" + [ -f "$candidate" ] && root_mcp="$candidate" + fi + + if [ -z "$root_mcp" ]; then + echo " → .context/mcp.json (exists, no root mcp-servers.json found)" + return + fi + + # Root servers take precedence over project entries on key conflict + local root_servers count updated + root_servers=$(jq '.servers' "$root_mcp") + count=$(printf '%s' "$root_servers" | jq 'keys | length') + updated=$(jq --argjson root "$root_servers" \ + '.mcpServers = (.mcpServers + $root)' \ + .context/mcp.json) + printf '%s\n' "$updated" > .context/mcp.json + echo " → .context/mcp.json (merged $count root servers)" +} + +echo "Syncing project context from $PROJECT_FILE..." + +if [ ${#ADAPTERS[@]} -eq 0 ]; then + generate_claude + generate_agents + generate_cursor + generate_aider + generate_system_prompt + generate_mcp +else + for adapter in "${ADAPTERS[@]}"; do + case "$adapter" in + claude) generate_claude ;; + agents) generate_agents ;; + cursor) generate_cursor ;; + aider) generate_aider ;; + prompt|system|openwebui|owui|generic) generate_system_prompt ;; + mcp) generate_mcp ;; + *) echo "Unknown adapter: $adapter" >&2; exit 1 ;; + esac + done +fi + +echo "Done."