Metadata-Version: 2.4
Name: edotconfig
Version: 0.20260319.1
Summary: Encrypted environment configuration cascade manager for .env files
Author: Eric Busboom
License: MIT License
        
        Copyright (c) 2026 Eric Busboom
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Keywords: dotenv,config,environment,sops,secrets
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: detect-secrets>=1.5.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# dotconfig

**Environment configuration cascade manager for `.env` files.**

`dotconfig` assembles a single `.env` file from multiple layered source
files — public config, SOPS-encrypted secrets, and per-developer local
overrides — and can round-trip it back.  It is designed for teams where:

- Different developers have different local settings (Docker context, domain
  names, key paths).
- Secrets live in SOPS-encrypted files alongside public config.
- Multiple named environments (dev, prod, test, staging, CI, …) share the
  same layout.
- Every tool (Docker, dotenv, IDEs) still reads a single `.env` file.

---

## Table of Contents

- [Installation](#installation)
- [Quick start](#quick-start)
- [Directory structure](#directory-structure)
- [Generated `.env` format](#generated-env-format)
- [Commands](#commands)
  - [`dotconfig init`](#dotconfig-init)
  - [`dotconfig load`](#dotconfig-load)
  - [`dotconfig save`](#dotconfig-save)
  - [`dotconfig keys`](#dotconfig-keys)
  - [`dotconfig audit`](#dotconfig-audit)
  - [`dotconfig config`](#dotconfig-config)
  - [`dotconfig install-hooks`](#dotconfig-install-hooks)
  - [`dotconfig agent`](#dotconfig-agent)
- [SOPS integration](#sops-integration)
- [Workflow](#workflow)
- [Adding a new deployment](#adding-a-new-deployment)
- [Adding a new developer](#adding-a-new-developer)
- [Design decisions](#design-decisions)

---

## Installation

Install with [pipx](https://pipx.pypa.io) so the tool is globally available
without polluting any project's virtual environment:

```bash
pipx install edotconfig
```

Or into a project's own virtual environment:

```bash
pip install edotconfig
```

Verify the install:

```bash
dotconfig --version
```

---

## Quick start

```
your-project/
  config/
    sops.yaml        ← SOPS encryption rules (generated by dotconfig init)
    dev/
      public.env     ← create this (public dev vars)
      secrets.env    ← SOPS-encrypted dev secrets
    prod/
      public.env     ← create this (public prod vars)
      secrets.env    ← SOPS-encrypted prod secrets
    local/
      yourname/
        public.env   ← create this (your personal overrides)
        secrets.env  ← optional personal encrypted secrets
  .env               ← generated; do not edit directly (add to .gitignore)
```

**Load** config into `.env`:

```bash
dotconfig load -d dev -l yourname   # dev deployment + your local overrides
dotconfig load -d prod              # prod deployment, no local overrides
```

**Edit** `.env` directly if you need to tweak a value, then **save** it
back to the source files:

```bash
dotconfig save
```

**Load/save specific files** (YAML, JSON, etc.):

```bash
dotconfig load -d dev --file app.yaml --stdout   # print to stdout
dotconfig save -d dev --file app.yaml            # store into config/dev/
```

---

## Directory structure

```
config/
  sops.yaml                      # SOPS encryption rules (non-dotfile, in config/)
  dev/
    public.env                   # Public config for the "dev" deployment
    secrets.env                  # SOPS-encrypted secrets for "dev"
  prod/
    public.env                   # Public config for the "prod" deployment
    secrets.env                  # SOPS-encrypted secrets for "prod"
  local/
    alice/
      public.env                 # Public local overrides for Alice
      secrets.env                # SOPS-encrypted local secrets (optional)
    bob/
      public.env                 # Another developer's overrides
```

Deployment names are open-ended — use any string that works as a directory
name (`dev`, `prod`, `test`, `staging`, `ci`, …).

---

## Generated `.env` format

`dotconfig load` produces a `.env` file with **marked sections** that map
back to the source files:

```bash
# CONFIG_DEPLOY=dev
# CONFIG_LOCAL=ericbusboom

#@dotconfig: public (dev)
APP_DOMAIN=inventory.example.com
NODE_ENV=development
PORT=3000
DEPLOYMENT=dev
DATABASE_URL=postgresql://app:devpassword@localhost:5433/app
DO_SPACES_ENDPOINT=https://sfo3.digitaloceanspaces.com
DO_SPACES_BUCKET=my-bucket
DO_SPACES_REGION=sfo3

#@dotconfig: secrets (dev)
SESSION_SECRET=abc123...
GITHUB_CLIENT_ID=...
GOOGLE_CLIENT_ID=...

#@dotconfig: public-local (ericbusboom)
DEV_DOCKER_CONTEXT=orbstack
PROD_DOCKER_CONTEXT=swarm1
QR_DOMAIN=http://192.168.1.40:5173/
SOPS_AGE_KEY_FILE=/Users/ericbusboom/.config/sops/age/keys.txt

#@dotconfig: secrets-local (ericbusboom)
```

**Last-write-wins**: when the file is shell-sourced (`set -a; . .env; set +a`),
later sections override earlier ones.  Local overrides deployment; secrets
override public.

The `#@dotconfig:` markers are unique to dotconfig — do not use this prefix
in your own comments.  The two metadata comments (`CONFIG_DEPLOY`,
`CONFIG_LOCAL`) tell `dotconfig save` where to write each section back.

---

## Commands

### `dotconfig init`

```
Usage: dotconfig init [OPTIONS]

  Initialise the config directory structure and set up age encryption.

Options:
  --config-dir TEXT  Root config directory to create.  [default: config]
  --help             Show this message and exit.
```

Creates `config/`, `config/local/`, empty env files for dev/prod
deployments and the current user, and configures SOPS age encryption.
Safe to run multiple times.

**Examples:**

```bash
dotconfig init
dotconfig init --config-dir myconfig
```

---

### `dotconfig load`

```
Usage: dotconfig load [OPTIONS]

  Assemble config files into .env, or load a specific file.

Options:
  -d, --deploy TEXT      Deployment / environment name (e.g. dev, prod).
  -l, --local TEXT       Local / developer name for personal overrides.
  -c, --config-dir TEXT  Root config directory.  [default: config]
  -o, --output TEXT      Destination file.  [default: .env or the --file name]
  -f, --file TEXT        Load a specific file instead of assembling .env.
  --stdout               Print to stdout instead of writing to a file.
  --help                 Show this message and exit.
```

**Examples:**

```bash
# Load dev deployment with Eric's local overrides
dotconfig load -d dev -l ericbusboom

# Load prod deployment, no local overrides
dotconfig load -d prod

# Write to a file other than .env
dotconfig load -d dev -o .env.dev

# Load a specific YAML file from the dev deployment
dotconfig load -d dev --file app.yaml

# Print a file to stdout (useful for agents / piping)
dotconfig load -d dev --file app.yaml --stdout
```

When using `--file`, specify either `-d` or `-l` (not both) — the file
lives in one location only.

**What it reads (without `--file`):**

| Source file | Section in `.env` |
|---|---|
| `config/{deploy}/public.env` | `#@dotconfig: public ({deploy})` |
| `config/{deploy}/secrets.env` (SOPS-encrypted) | `#@dotconfig: secrets ({deploy})` |
| `config/local/{local}/public.env` | `#@dotconfig: public-local ({local})` |
| `config/local/{local}/secrets.env` (SOPS-encrypted) | `#@dotconfig: secrets-local ({local})` |

If a secrets file is absent or SOPS is unavailable, the section is written
as empty with a warning — the command does not abort.

If a local file is absent, a warning is printed and the section is written
as empty — useful when a new developer clones the repo before creating their
own local overrides.

---

### `dotconfig save`

```
Usage: dotconfig save [OPTIONS]

  Save .env sections back to config/ source files, or store a file.

Options:
  -d, --deploy TEXT      Target deployment (overrides .env metadata).
  -l, --local TEXT       Target local / developer name (overrides .env metadata).
  --env-file TEXT        .env file to read and save.  [default: .env]
  -c, --config-dir TEXT  Root config directory.  [default: config]
  -f, --file TEXT        Save a specific file into the config directory.
  -e, --encrypt          Encrypt the file with SOPS (only with --file).
  --help                 Show this message and exit.
```

**Examples:**

```bash
# Save all sections from .env back to config/
dotconfig save

# Save to a different deployment
dotconfig save -d staging

# Save a YAML file into the dev deployment
dotconfig save -d dev --file app.yaml

# Save and encrypt a file with SOPS
dotconfig save --file secrets.yaml -d dev --encrypt

# Save a JSON file into a local config directory
dotconfig save -l alice --file settings.json
```

When using `--file`, specify either `-d` or `-l` (not both) — the file
lives in one location only.

**What it writes (without `--file`):**

| Section in `.env` | Destination file |
|---|---|
| `#@dotconfig: public ({deploy})` | `config/{deploy}/public.env` (plaintext) |
| `#@dotconfig: secrets ({deploy})` | `config/{deploy}/secrets.env` (SOPS-encrypted) |
| `#@dotconfig: public-local ({local})` | `config/local/{local}/public.env` (plaintext) |
| `#@dotconfig: secrets-local ({local})` | `config/local/{local}/secrets.env` (SOPS-encrypted, only if non-empty) |

`dotconfig save` requires a dotconfig-managed `.env` (one that was produced
by `dotconfig load`) because it relies on the `CONFIG_DEPLOY` metadata
comment to know where to write the files back.

---

### `dotconfig keys`

```
Usage: dotconfig keys [OPTIONS]

  Show age encryption key status and configuration.

Options:
  --help  Show this message and exit.
```

Reports where your age private key lives, the derived public key, and
environment variable export statements for configuring SOPS.

---

### `dotconfig audit`

```
Usage: dotconfig audit [OPTIONS]

  Scan config/ for unencrypted secrets at rest.

Options:
  -c, --config-dir TEXT  Root config directory.  [default: auto-discovered or 'config']
  --help                 Show this message and exit.
```

Walks the config directory looking for values whose key names suggest they
are secrets but are stored in plaintext rather than SOPS-encrypted.  Exits
with code 0 if clean, code 1 if findings exist (useful for CI and git hooks).

**Examples:**

```bash
dotconfig audit
dotconfig audit -c /path/to/config
```

---

### `dotconfig config`

```
Usage: dotconfig config [OPTIONS]

  Show dotconfig configuration and discovered paths.

Options:
  --help  Show this message and exit.
```

Reports the installed version, config directory name (from `DOTCONFIG_NAME`
or the default `config`), and where the config directory was found.

---

### `dotconfig install-hooks`

```
Usage: dotconfig install-hooks [OPTIONS]

  Install a git pre-commit hook that runs dotconfig audit.

Options:
  --help  Show this message and exit.
```

Installs a pre-commit hook that blocks commits when unencrypted secrets are
detected in the config directory.  Safe to run multiple times.

---

### `dotconfig agent`

```
Usage: dotconfig agent [OPTIONS]

  Print full operational instructions for AI agents.

Options:
  --help  Show this message and exit.
```

Outputs a comprehensive markdown document describing how dotconfig works,
all commands, the directory layout, the `.env` format, and rules for agents.

---

## SOPS integration

`dotconfig` delegates all encryption and decryption to
[sops](https://github.com/getsops/sops).  You must install sops separately.

**SOPS is optional for loading public config.**  If sops is not installed,
or a secrets file is missing, the secrets section is left empty and a warning
is printed.

**Key discovery** follows the standard sops precedence:

1. `SOPS_AGE_KEY_FILE` environment variable (path to an age private key file)
2. `SOPS_AGE_KEY` environment variable (inline age private key)
3. `sops.yaml` specified via `--config` flag or `SOPS_CONFIG` environment variable

If `SOPS_AGE_KEY_FILE` is defined inside `.env` itself (e.g. in the
public-local section), `dotconfig save` reads it from the file before
invoking sops, so you do not need to export it manually.

**Recommended `config/sops.yaml`:**

```yaml
creation_rules:
  # Secrets companion files (app.secrets.yaml, etc.)
  - path_regex: '.+\.secrets\.(?:env|json|yaml|yml|txt|conf)$'
    age: >-
      age1v3f2rn...,age1h02a69...
  # Legacy secrets files (secrets.env, etc.)
  - path_regex: '.+/secrets\.(?:env|json|yaml|yml|txt|conf)$'
    age: >-
      age1v3f2rn...,age1h02a69...
  # Catch-all for any file dotconfig encrypts (private keys, etc.)
  - path_regex: '.+'
    age: >-
      age1v3f2rn...,age1h02a69...
```

Because `sops.yaml` is not a dotfile, SOPS will not auto-discover it.
Specify it explicitly when invoking sops directly:

```bash
SOPS_CONFIG=config/sops.yaml sops --encrypt --in-place config/dev/secrets.env
# or
sops --config config/sops.yaml --encrypt --in-place config/dev/secrets.env
```

---

## Workflow

### First-time setup (new developer)

```bash
git clone <repo>
# Create your personal local overrides
cp -r config/local/ericbusboom config/local/yourname
# Edit it with your values
$EDITOR config/local/yourname/public.env
# Load dev config
dotconfig load -d dev -l yourname
```

### Daily development

```bash
# Reload if someone changed config files
dotconfig load -d dev -l yourname

# Make an ad-hoc change in .env directly, then save it back
$EDITOR .env
dotconfig save
```

### Keeping `.env` out of version control

Add `.env` to `.gitignore` — it is a generated file:

```gitignore
# Generated by dotconfig load — do not commit
.env
```

The source files in `config/` are committed.  Encrypted secrets files
(`config/secrets/`) are safe to commit because they are SOPS-encrypted.

---

## Adding a new deployment

1. Create the deployment directory and its public config:
   ```bash
   mkdir -p config/{name}
   $EDITOR config/{name}/public.env      # add public variables
   ```
2. Load the new deployment to generate `.env`:
   ```bash
   dotconfig load -d {name}
   ```
3. Add any secrets to the secrets section in `.env`, then save back:
   ```bash
   $EDITOR .env                          # add values under the secrets section
   dotconfig save                        # encrypts secrets via SOPS automatically
   ```

---

## Adding a new developer

1. Ask the developer to generate an age key pair:
   ```bash
   age-keygen -o ~/.config/sops/age/keys.txt
   # Share the PUBLIC key (age1...) with the team
   ```
2. Add their public key to `config/sops.yaml` and re-encrypt secrets:
   ```bash
   SOPS_CONFIG=config/sops.yaml sops updatekeys config/dev/secrets.env
   SOPS_CONFIG=config/sops.yaml sops updatekeys config/prod/secrets.env
   ```
3. The developer creates their local overrides:
   ```bash
   cp -r config/local/ericbusboom config/local/theirname
   $EDITOR config/local/theirname/public.env
   dotconfig load -d dev -l theirname
   ```

---

## Design decisions

| Decision | Rationale |
|---|---|
| **Single `.env` file** | Tools (dotenv, Docker, IDEs) read one file — no cascade-compatibility issues. |
| **Marked sections** | Enable round-tripping between `.env` and `config/` source files without extra metadata files. |
| **Open deployment names** | Not limited to `dev`/`prod`; supports `test`, `ci`, `staging`, or any custom name. |
| **Last-write-wins ordering** | Later sections override earlier when shell-sourced; local overrides deployment. |
| **SOPS optional at load time** | Developers without SOPS access can still load public config; secrets are skipped with a warning. |
| **No shell variable expansion** | Values are literal strings — no `$VAR` interpolation. Use a local override to change a value for a specific machine. |
| **Local secrets are optional** | `config/local/{user}/secrets.env` is supported but most developers won't need it. |

