Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Speeds up `docker build` and keeps the builder stage tidy. Everything listed
# here is excluded from the build context.

# VCS
.git
.gitignore
.gitattributes

# Local venvs / caches
.venv
venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
htmlcov

# Build artefacts
build
dist
*.egg-info

# IDE / editor
.idea
.vscode
.DS_Store

# Local-only configuration
.env
.env.*
!.env.example

# Tests, docs, examples — not needed at runtime
tests
docs
examples

# Compose / Docker artefacts (avoid recursive context)
docker-compose.override.yml
50 changes: 48 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Tests

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:

permissions:
contents: read
Expand Down Expand Up @@ -62,6 +61,18 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -e .
- name: wheel builds and bundles the data directory
# The editable install above never builds a wheel, so a broken wheel
# config (e.g. a force-include that duplicates files the package
# already ships) would pass every other step and fail only at
# release or `docker build`. Build it and check the data is inside.
run: |
python -m pip wheel --no-deps -w /tmp/wheel .
files=$(python -m zipfile -l /tmp/wheel/*.whl)
for f in gendia/data/forges.json gendia/data/standards/core.json \
gendia/data/scaffolds/bare.json gendia/data/scaffolds/_common/.gitignore; do
echo "$files" | grep -qF "$f" || { echo "missing from wheel: $f"; exit 1; }
done
- name: gendia --version
run: gendia --version
- name: gendia setup --detect (clean host returns 1)
Expand All @@ -88,3 +99,38 @@ jobs:
if [ "$rc" != "1" ]; then
echo "expected exit 1 (warnings), got $rc"; exit 1
fi

- name: standards engine — list profiles
# Should print the 16 bundled rule packs from bundled JSON resources.
# Regresses if `_BUNDLED_PROFILES` falls out of sync with the data.
working-directory: /tmp
run: |
count=$(gendia conventions --list-profiles | grep -cE '^ [a-z]')
if [ "$count" -lt 16 ]; then
echo "expected at least 16 bundled profiles, got $count"; exit 1
fi

- name: standards engine — list scaffolds
# Should print the 13 bundled scaffolds from bundled JSON resources.
working-directory: /tmp
run: |
count=$(gendia init --list-scaffolds | grep -cE '^ [a-z]')
if [ "$count" -lt 13 ]; then
echo "expected at least 13 bundled scaffolds, got $count"; exit 1
fi

- name: standards engine — --fix --dry-run on a fresh empty dir
# Regression guard for `--fix` + `--dry-run` argparse wiring and the
# bundled fix kinds. Expects at least one `would-fix` line for an
# empty repo (R001 README is missing, etc.).
run: |
rm -rf /tmp/fix-smoke && mkdir /tmp/fix-smoke
gendia conventions /tmp/fix-smoke --no-legacy --profile core --fix --dry-run \
| grep -q 'would-fix' \
|| (echo "expected '--fix --dry-run' to print 'would-fix' lines"; exit 1)

- name: bin/gendia-menu --list parses cleanly
# Bash-syntax + resolution smoke for the interactive menu.
run: |
bash -n bin/gendia-menu
bin/gendia-menu --list | grep -q '\[INSPECT\]'
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

All notable changes to `gendia` follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added — Standards engine, scaffolds, autofix

- **JSON-driven standards engine** (`src/gendia/standards.py`, ~1280 LOC): a new layer on top of the existing `conventions` verb that lints repos against rule packs declared in JSON. New CLI surface: `gendia conventions --list-profiles`, `--list-rules --profile <name>`, `--explain <RULE_ID>`, `--no-legacy` (skip the 10 hardcoded checks), `--profile <name>` (repeatable). **16 bundled rule packs / 71 rules / 7 check kinds** — `file_exists`, `file_absent`, `regex_in_file`, `directory_exists`, `license_spdx`, `glob_exists`, `one_of_files`. Plugin syntax: a rule's `check:` field may be `module:function` to load org-internal checks.
- **`gendia conventions --fix`** (with `--dry-run`): **21 fix kinds** spanning the universal essentials (README / .gitignore / .editorconfig / .gitattributes), community health (CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / CHANGELOG), GitHub workflows (CODEOWNERS / PR template / Dependabot / CodeQL / Scorecard), OpenSSF Security Insights, and **5 JSON-mutating fixes for `package.json`** (`name`, `version`, `license`, `engines.node`, `packageManager`). All fixes are idempotent; mutating fixes use atomic writes via tmpfile + `os.replace` and refuse symlinked targets. `set_package_license` auto-detects SPDX from an existing LICENSE file (`SPDX-License-Identifier:` line wins; falls back to a 17-marker body-text table).
- **`gendia init --scaffold`** (`src/gendia/scaffolds.py`, ~860 LOC): renders bundled scaffold templates with `${var}` substitution + post-actions (`git_init`, `license_text`, `audit`). **13 bundled scaffolds**: `bare`, `python-uv`, `python-poetry`, `rust`, `go`, `node-pnpm`, `node-bun`, `php-composer`, `ruby-gem`, `dotnet`, `docs-mkdocs`, `monorepo-pnpm-turbo`, `java-gradle`. **17 bundled license texts** for the `license_text` post-action. Security guards: path-traversal refusal (`is_relative_to`), symlink refusal, unresolved-variable detection.
- **`bin/gendia-menu`** + `make menu`: interactive numbered menu wrapping all 18 verbs with drill-down by category (Inspect / Release & Sync / Standards / Scaffold / Config). `--list` for non-interactive option dump. Shipped inside the Docker image so `make docker-exec` lands in a working toolkit shell.
- **Makefile extensions**: `make check` runs the full CI gate (lint + format-check + typecheck + tests), `make menu`, `make docker-up/down/ps/logs/exec/host-shell` for compose lifecycle, plus the existing `docker-build/run/push/shell`. `PYTHON` auto-detects `python3.12+` so it works on hosts where `python3` resolves to 3.10.
- **`.dockerignore`**: trims the build context (excludes `.venv/`, caches, `tests/`, `docs/`, `examples/`, build artefacts) so `docker build` is fast and tidy.
- **205 new unit tests** for the standards engine + scaffolds + autofix (running total: 389 across all changes in this Unreleased section). Coverage includes schema, every check kind, every fix kind (round-trip clears findings), every bundled rule pack, every bundled scaffold, every license text (via parametrize), security guards, JSON-mutating idempotency / malformed-JSON / missing-file / symlink-refusal, SPDX detection from LICENSE files, slugify edge cases.
- **Docs**: README sections for the standards engine, scaffolds, autofix and the interactive menu. The design driver doc and phase tracker used to plan this work are kept outside the repository.

### Added — SSH-config awareness layer

- **SSH-config awareness layer** (`src/gendia/ssh/`): parser (`SSHConfig`, `SSHHost`, `SSHHostList`) handles `Host` / `Match` / `Include` directives, multi-host lines, and per-host `IdentityFile` / `Port` / `User` / `ProxyJump`. JSON-driven forge registry (`ForgeRegistry`) with detection signals (exact hostname, substring pattern, comment hint, `User git` heuristic). Key-health auditor (`audit_keys`) flags loose permissions, DSA / weak-RSA, and shared-key reuse. Account synthesizer (`propose_accounts`) projects detected hosts onto gendia account stubs.
- **Bundled forge registry** (`src/gendia/data/forges.json`) shipped via `package_data`. Override per project with the new top-level `forges_file` key in `gendia.json`. See `examples/forges.json`.
- **Three new providers**: `gitea` (covers Gitea / Forgejo / Codeberg with one wire format), `azure` (Azure DevOps; PAT over Basic-auth), and `bitbucket-server` (Atlassian Data Center, distinct from Bitbucket Cloud). GitHub Enterprise now honoured via `Account.host` (`/api/v3` REST root, host-aware clone URLs).
- **Three new operations**: `gendia generate` (emit copy-paste git commands per detected SSH host in `text` / `script` / `json` / `markdown`; `--execute` runs the bash form), `gendia scan` (filesystem walk classifying every git repo as `clean` / `dirty` / `orphan` / `https-candidate`), and `gendia convert` (flip URLs between SSH and HTTPS, alias-aware via SSH config and `~/.gitconfig` `insteadOf` rules).
- **`gendia ssh` command group**: `ssh list`, `ssh test`, `ssh inspect`, `ssh bootstrap [--write]`. `ssh bootstrap` synthesizes account stubs from `~/.ssh/config` and merges them into `~/.config/gendia/gendia.json` on demand.
- **`gendia setup --from-ssh`**: same synthesis as `ssh bootstrap`, wired into the first-run wizard.
- **`gendia doctor`** now includes `audit_keys` findings (key permissions, weak-key types, shared-key reuse) under a new `ssh keys` section.
- **`Account.manual_repos`** schema field for repos the provider's API can't (or shouldn't) discover. JSON shape: `[{"host": "alias", "org": "scope", "repos": ["repo1", ...]}]`. Picked up by `generate` and reflected in the proposed config from `ssh bootstrap`.
- **Util helpers**: `gendia.util.url` (parse / convert SSH ↔ HTTPS, alias + `insteadOf` aware) and `gendia.util.repo_scan` (filesystem scanner returning `ScannedRepo` with classification helpers).
- **51 new unit tests for the SSH layer** (running total: 184 + 205 = 389 across all changes in this Unreleased section) covering SSH config parsing, forge detection, URL conversion, the filesystem scanner, the new providers, GitHub Enterprise host honouring, and end-to-end coverage of `generate` / `scan` / `convert`.
- **Examples**: `examples/forges.json` (forge registry overlay), `examples/ssh-bootstrap.json` (config showing `forges_file`, `manual_repos`, and the new providers).

### Notes

- Migrated from the standalone `git-helpers` script. The single-file `generate_git_commands.py` was decomposed across the new layers (parser, registry, providers, operations) with no behavioural regressions; the `git-commands.txt` artefact it produced is now reachable via `gendia generate --output git-commands.txt`. `git-helpers` is removed as a separate project; gendia is the supported replacement.

## [0.1.0] - 2026-05-06

### Added
Expand Down
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ RUN apt-get update \
COPY --from=builder /src/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl && rm -rf /tmp/*.whl

# Ship the env-loader bridge inside the image for Docker / Compose / k8s use.
COPY bin/gendia-env /usr/local/bin/gendia-env
RUN chmod +x /usr/local/bin/gendia-env
# Ship the env-loader bridge + interactive menu inside the image so
# `make docker-exec` can drop a user into a working toolkit shell.
COPY bin/gendia-env /usr/local/bin/gendia-env
COPY bin/gendia-menu /usr/local/bin/gendia-menu
RUN chmod +x /usr/local/bin/gendia-env /usr/local/bin/gendia-menu

# XDG-compliant config location; mount your config volume here.
ENV XDG_CONFIG_HOME=/config \
Expand Down
66 changes: 59 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
.PHONY: help install install-dev test lint typecheck status sync inventory release audit cleanup verify mirror init conventions docker-build docker-shell docker-push clean

PYTHON ?= python3
.PHONY: help menu install install-dev test lint format format-check typecheck check status sync inventory release audit cleanup verify mirror init conventions \
docker-build docker-shell docker-push docker-run \
docker-up docker-down docker-logs docker-ps docker-exec docker-host-shell \
clean

# Pick the first available Python 3.12+ interpreter. Some hosts ship a `python3`
# that resolves to 3.10 (e.g. MAMP, system Python on older macOS) and won't have
# pytest / mypy installed — overrideable via PYTHON=path/to/bin.
PYTHON ?= $(shell command -v python3.12 || command -v python3.13 || command -v python3)
UV ?= uv
ARGS ?=

# Container name used by docker-exec / docker-logs / docker-down. The compose
# service is `gendia`; the default container name follows Compose conventions.
COMPOSE_SERVICE ?= gendia
CONTAINER ?= gendia-$(COMPOSE_SERVICE)-1

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-20s %s\n", $$1, $$2}'
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " %-22s %s\n", $$1, $$2}'

menu: ## Launch the interactive gendia menu (bin/gendia-menu)
@./bin/gendia-menu

install: ## Install gendia from source (uv preferred, pip fallback)
@if command -v $(UV) >/dev/null 2>&1; then \
Expand All @@ -27,9 +41,17 @@ test: ## Run pytest
lint: ## Run ruff lint
$(PYTHON) -m ruff check src tests

format: ## Apply ruff format to src + tests (rewrites files)
$(PYTHON) -m ruff format src tests

format-check: ## Check ruff format compliance without writing (mirrors CI)
$(PYTHON) -m ruff format --check src tests

typecheck: ## Run mypy
$(PYTHON) -m mypy src

check: lint format-check typecheck test ## Run lint + format-check + typecheck + tests (CI gates)

# --- Operations ---------------------------------------------------------------

status: ## gendia status [ARGS=...]
Expand All @@ -46,20 +68,20 @@ release: ## REPO=myorg/core VERSION=1.0.1 make release
inventory audit cleanup verify mirror init conventions: ## gendia <target> [ARGS=...]
gendia $@ $(ARGS)

# --- Docker -------------------------------------------------------------------
# --- Docker (host-side: build / push / one-shot run) -------------------------

docker-build: ## Build the gendia Docker image
docker build -t gendia:latest .

docker-shell: ## Drop into a gendia container with config + .env mounted
docker-shell: ## Drop into a one-shot gendia container with config + .env mounted
docker run --rm -it \
--env-file "$(HOME)/.config/gendia/.env" \
-v "$(HOME)/.config/gendia:/config:ro" \
-v "$(HOME)/.ssh:/home/gendia/.ssh:ro" \
-v "$(PWD):/work" \
--entrypoint bash gendia:latest

docker-run: ## VERB=sync make docker-run — run any gendia verb in container
docker-run: ## VERB=sync make docker-run — run any gendia verb in a one-shot container
@test -n "$(VERB)" || (echo "VERB=... required (e.g. VERB=sync)"; exit 1)
docker run --rm \
--env-file "$(HOME)/.config/gendia/.env" \
Expand All @@ -73,6 +95,36 @@ docker-push: ## REGISTRY=ghcr.io/your-org make docker-push
docker tag gendia:latest $(REGISTRY)/gendia:latest
docker push $(REGISTRY)/gendia:latest

# --- Docker Compose (long-lived service: up / exec / down) -------------------

docker-up: ## Start the gendia compose service in the background
docker compose up -d

docker-down: ## Stop the gendia compose service (preserves volumes)
docker compose down

docker-ps: ## Show compose service status
docker compose ps

docker-logs: ## Follow logs for the gendia compose service
docker compose logs -f $(COMPOSE_SERVICE)

docker-exec: ## Open an interactive shell inside the running gendia container
@docker compose ps --status running --services 2>/dev/null | grep -qx "$(COMPOSE_SERVICE)" \
|| (echo "$(COMPOSE_SERVICE) service is not running. Try 'make docker-up' first."; exit 1)
docker compose exec $(COMPOSE_SERVICE) bash

docker-host-shell: ## Open a host-side shell on the Docker daemon's host (no-op on Docker Desktop)
@if command -v limactl >/dev/null 2>&1 && limactl list 2>/dev/null | grep -q running; then \
echo "Using Lima shell..."; limactl shell default; \
elif command -v multipass >/dev/null 2>&1 && multipass list 2>/dev/null | grep -q docker; then \
echo "Using Multipass shell..."; multipass shell docker; \
else \
echo "No remote Docker host detected (Lima / Multipass). On Docker Desktop the daemon"; \
echo "runs inside a Linux VM that isn't directly shell-accessible — use 'make docker-exec'"; \
echo "to enter the running gendia container instead."; \
fi

clean: ## Remove build artifacts
rm -rf build dist *.egg-info .pytest_cache .mypy_cache .ruff_cache .coverage htmlcov
find . -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true
Loading
Loading