meridian-vpn 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. meridian_vpn-2.0.0/.gitignore +24 -0
  2. meridian_vpn-2.0.0/BACKLOG.md +64 -0
  3. meridian_vpn-2.0.0/CLAUDE.md +438 -0
  4. meridian_vpn-2.0.0/CONTRIBUTING.md +78 -0
  5. meridian_vpn-2.0.0/LICENSE +21 -0
  6. meridian_vpn-2.0.0/PKG-INFO +150 -0
  7. meridian_vpn-2.0.0/README.md +123 -0
  8. meridian_vpn-2.0.0/SECURITY.md +24 -0
  9. meridian_vpn-2.0.0/VERSION +1 -0
  10. meridian_vpn-2.0.0/install.sh +156 -0
  11. meridian_vpn-2.0.0/inventory.yml.example +23 -0
  12. meridian_vpn-2.0.0/pyproject.toml +60 -0
  13. meridian_vpn-2.0.0/setup.sh +75 -0
  14. meridian_vpn-2.0.0/src/meridian/__init__.py +12 -0
  15. meridian_vpn-2.0.0/src/meridian/__main__.py +5 -0
  16. meridian_vpn-2.0.0/src/meridian/ai.py +87 -0
  17. meridian_vpn-2.0.0/src/meridian/ansible.py +200 -0
  18. meridian_vpn-2.0.0/src/meridian/cli.py +241 -0
  19. meridian_vpn-2.0.0/src/meridian/commands/__init__.py +0 -0
  20. meridian_vpn-2.0.0/src/meridian/commands/check.py +250 -0
  21. meridian_vpn-2.0.0/src/meridian/commands/client.py +188 -0
  22. meridian_vpn-2.0.0/src/meridian/commands/diagnostics.py +199 -0
  23. meridian_vpn-2.0.0/src/meridian/commands/ping.py +208 -0
  24. meridian_vpn-2.0.0/src/meridian/commands/resolve.py +152 -0
  25. meridian_vpn-2.0.0/src/meridian/commands/scan.py +142 -0
  26. meridian_vpn-2.0.0/src/meridian/commands/server.py +79 -0
  27. meridian_vpn-2.0.0/src/meridian/commands/setup.py +205 -0
  28. meridian_vpn-2.0.0/src/meridian/commands/uninstall.py +83 -0
  29. meridian_vpn-2.0.0/src/meridian/config.py +41 -0
  30. meridian_vpn-2.0.0/src/meridian/console.py +73 -0
  31. meridian_vpn-2.0.0/src/meridian/credentials.py +86 -0
  32. meridian_vpn-2.0.0/src/meridian/playbooks/.ansible-lint +17 -0
  33. meridian_vpn-2.0.0/src/meridian/playbooks/ansible.cfg +24 -0
  34. meridian_vpn-2.0.0/src/meridian/playbooks/group_vars/all.yml +116 -0
  35. meridian_vpn-2.0.0/src/meridian/playbooks/group_vars/exit.yml +15 -0
  36. meridian_vpn-2.0.0/src/meridian/playbooks/group_vars/relay.yml +11 -0
  37. meridian_vpn-2.0.0/src/meridian/playbooks/inventory-chain.yml.example +30 -0
  38. meridian_vpn-2.0.0/src/meridian/playbooks/playbook-chain.yml +168 -0
  39. meridian_vpn-2.0.0/src/meridian/playbooks/playbook-client.yml +71 -0
  40. meridian_vpn-2.0.0/src/meridian/playbooks/playbook-uninstall.yml +130 -0
  41. meridian_vpn-2.0.0/src/meridian/playbooks/playbook.yml +87 -0
  42. meridian_vpn-2.0.0/src/meridian/playbooks/requirements.yml +9 -0
  43. meridian_vpn-2.0.0/src/meridian/playbooks/roles/caddy/handlers/main.yml +5 -0
  44. meridian_vpn-2.0.0/src/meridian/playbooks/roles/caddy/tasks/main.yml +170 -0
  45. meridian_vpn-2.0.0/src/meridian/playbooks/roles/caddy/templates/Caddyfile.j2 +59 -0
  46. meridian_vpn-2.0.0/src/meridian/playbooks/roles/caddy/templates/connection-info.html.j2 +278 -0
  47. meridian_vpn-2.0.0/src/meridian/playbooks/roles/caddy/templates/update-stats.py.j2 +111 -0
  48. meridian_vpn-2.0.0/src/meridian/playbooks/roles/client_management/tasks/add_client.yml +250 -0
  49. meridian_vpn-2.0.0/src/meridian/playbooks/roles/client_management/tasks/list_clients.yml +42 -0
  50. meridian_vpn-2.0.0/src/meridian/playbooks/roles/client_management/tasks/main.yml +94 -0
  51. meridian_vpn-2.0.0/src/meridian/playbooks/roles/client_management/tasks/remove_client.yml +138 -0
  52. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/handlers/main.yml +5 -0
  53. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/tasks/firewall.yml +66 -0
  54. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/tasks/hardening.yml +19 -0
  55. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/tasks/main.yml +15 -0
  56. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/tasks/packages.yml +38 -0
  57. meridian_vpn-2.0.0/src/meridian/playbooks/roles/common/tasks/sysctl.yml +14 -0
  58. meridian_vpn-2.0.0/src/meridian/playbooks/roles/docker/tasks/main.yml +106 -0
  59. meridian_vpn-2.0.0/src/meridian/playbooks/roles/haproxy/handlers/main.yml +5 -0
  60. meridian_vpn-2.0.0/src/meridian/playbooks/roles/haproxy/tasks/main.yml +25 -0
  61. meridian_vpn-2.0.0/src/meridian/playbooks/roles/haproxy/templates/haproxy.cfg.j2 +52 -0
  62. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output/tasks/main.yml +170 -0
  63. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output/templates/connection-info.html.j2 +249 -0
  64. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output/templates/connection-summary-client.txt.j2 +33 -0
  65. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output/templates/connection-summary.txt.j2 +69 -0
  66. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output_relay/tasks/main.yml +94 -0
  67. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output_relay/templates/connection-info.html.j2 +196 -0
  68. meridian_vpn-2.0.0/src/meridian/playbooks/roles/output_relay/templates/connection-summary.txt.j2 +49 -0
  69. meridian_vpn-2.0.0/src/meridian/playbooks/roles/shared/tasks/check_qrencode.yml +18 -0
  70. meridian_vpn-2.0.0/src/meridian/playbooks/roles/shared/tasks/generate_client_output.yml +110 -0
  71. meridian_vpn-2.0.0/src/meridian/playbooks/roles/shared/tasks/load_credentials.yml +30 -0
  72. meridian_vpn-2.0.0/src/meridian/playbooks/roles/shared/tasks/resolve_ip.yml +30 -0
  73. meridian_vpn-2.0.0/src/meridian/playbooks/roles/shared/tasks/verify_xray.yml +31 -0
  74. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/apply_panel_settings.yml +129 -0
  75. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/configure_panel.yml +177 -0
  76. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/configure_reality.yml +141 -0
  77. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/configure_reality_xhttp.yml +127 -0
  78. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/configure_wss.yml +91 -0
  79. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/deploy.yml +97 -0
  80. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/main.yml +25 -0
  81. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/tasks/maintenance.yml +20 -0
  82. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray/templates/docker-compose.yml.j2 +21 -0
  83. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray_relay/tasks/configure_inbound.yml +86 -0
  84. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray_relay/tasks/configure_outbound.yml +159 -0
  85. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray_relay/tasks/configure_panel.yml +76 -0
  86. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray_relay/tasks/deploy.yml +73 -0
  87. meridian_vpn-2.0.0/src/meridian/playbooks/roles/xray_relay/tasks/main.yml +21 -0
  88. meridian_vpn-2.0.0/src/meridian/servers.py +98 -0
  89. meridian_vpn-2.0.0/src/meridian/ssh.py +191 -0
  90. meridian_vpn-2.0.0/src/meridian/update.py +138 -0
  91. meridian_vpn-2.0.0/tests/__init__.py +0 -0
  92. meridian_vpn-2.0.0/tests/conftest.py +66 -0
  93. meridian_vpn-2.0.0/tests/render_templates.py +174 -0
  94. meridian_vpn-2.0.0/tests/test_ansible.py +66 -0
  95. meridian_vpn-2.0.0/tests/test_cli.py +81 -0
  96. meridian_vpn-2.0.0/tests/test_credentials.py +103 -0
  97. meridian_vpn-2.0.0/tests/test_servers.py +115 -0
  98. meridian_vpn-2.0.0/tests/test_update.py +59 -0
@@ -0,0 +1,24 @@
1
+ # Credentials contain all secrets — NEVER commit these
2
+ credentials/
3
+ /meridian/
4
+ vpn-credentials/
5
+
6
+ # Ansible
7
+ *.retry
8
+
9
+ # Python
10
+ *.pyc
11
+ __pycache__/
12
+ *.egg-info/
13
+ dist/
14
+ build/
15
+ venv/
16
+ .venv/
17
+
18
+ # OS
19
+ .DS_Store
20
+ .claude/
21
+
22
+ # Environment
23
+ .env
24
+ *.log
@@ -0,0 +1,64 @@
1
+ # Backlog
2
+
3
+ **Last updated:** 2026-03-20
4
+ **Version:** 2.0.0
5
+
6
+ ---
7
+
8
+ ## P0 — Critical (done)
9
+
10
+ - [x] Replace `eval` with `printf -v` in `prompt()` to prevent code injection
11
+ - [x] Add SHA256 checksum verification to auto-update
12
+ - [x] Make shellcheck blocking in CI (was `|| true`, no-op)
13
+ - [x] Fix auto-update downgrade when running dev version (semver direction check)
14
+ - [x] Fix `eval` injection vector in `prompt()` function
15
+ - [x] **Rewrite CLI in Python** — migrated 1,727-line bash script to modular Python package (typer + rich + PyYAML)
16
+
17
+ ## P1 — High (done)
18
+
19
+ - [x] Gate CD and Release workflows on CI success (`workflow_run` chain)
20
+ - [x] Add `body_format` policy check in CI (prevent form-urlencoded on inbound/client APIs)
21
+ - [x] Auto-discover templates in render test (was manual list, missed `update-stats.py.j2`)
22
+ - [x] Add connection-info app link sync check in CI
23
+ - [x] Add Xray health check after inbound creation (catch crash loops)
24
+ - [x] Fix CLI server install for non-root users (sudo mv fallback)
25
+ - [x] Suggest `meridian uninstall` + retry on inbound creation failures
26
+ - [x] Add `apt` fallback for Ansible installation (WSL compat)
27
+ - [x] Retry Ansible collection install up to 3 times (flaky networks)
28
+ - [x] Validate VERSION format in CI — `^\d+\.\d+\.\d+$`
29
+ - [x] Extract YAML parsing into structured credentials — `ServerCredentials` dataclass replaces 10+ `grep|awk|tr` instances
30
+ - [x] Standardize flag parsing — typer decorators replace 8 duplicate `while/case` blocks
31
+
32
+ ## P1 — High (open)
33
+
34
+ - [ ] Pin ansible-lint via `gh_action_ref: "v25.5.0"` — `@main` is upstream-recommended but a breaking change could randomly fail CI
35
+ - [ ] Add `ansible.cfg` validation to CI — verify `jinja2_native = True` exists (critical for `body_format: json` integer typing)
36
+ - [ ] Add Docker integration test for 3x-ui API — spin up 3x-ui in CI, run login/create-inbound/list/verify JSON round-trip
37
+ - [ ] Configure PyPI trusted publisher — required for automated PyPI publishing from GitHub Actions
38
+ - [ ] Register `meridian-vpn` on PyPI — publish initial package to reserve name
39
+
40
+ ## P2 — Medium (open)
41
+
42
+ - [ ] Add deployed playbook version to diagnostics — write VERSION to `/etc/meridian/version` during deploy, read in `meridian diagnostics` to catch version mismatch
43
+ - [ ] Consolidate 3 connection-info HTML templates into one — biggest drift risk; use `{% if domain_mode %}` / `{% if relay_mode %}` conditionals instead of 3 copies
44
+ - [ ] Add "broke after update" issue template — capture old version, new version, timing, auto-update vs manual
45
+ - [ ] Improve dry-run CI job — remove `|| echo` suppression, use `--tags` for local-compatible tasks, add domain mode and chain mode dry-runs
46
+ - [ ] Add playbook sync automation — script or Makefile to copy root playbooks into `src/meridian/playbooks/` and verify in CI
47
+ - [ ] Add mypy type checking to CI — strict mode on `src/meridian/`
48
+
49
+ ## P3 — Low (open)
50
+
51
+ - [ ] Add VERSION semver validation in release workflow (not just CI)
52
+ - [ ] Replace `MockUndefined` in template tests with stricter undefined handling
53
+ - [ ] Add HTML validation for rendered connection-info templates
54
+ - [ ] Add docs/ drift detection in CI — verify `docs/` copies match source files
55
+ - [ ] Add lightweight opt-in telemetry — anonymous ping on setup success (version, OS, mode)
56
+
57
+ ## Future — Architecture (open)
58
+
59
+ - [ ] Proactive IP block notification — scheduled reachability check with Telegram/webhook alerts
60
+ - [ ] Self-steal mode — Reality masquerades as your own domain
61
+ - [ ] Zero-to-VPN onboarding wizard on meridian.msu.rocks
62
+ - [ ] Password-protected connection info page for family sharing
63
+ - [ ] Key/credential rotation without full uninstall/reinstall
64
+ - [ ] Shell completion support — typer has built-in completion generation
@@ -0,0 +1,438 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project overview
4
+
5
+ Ansible automation for deploying censorship-resistant VLESS+Reality proxy servers. Supports standalone single-server mode and a two-server relay chain for IP whitelist bypass.
6
+
7
+ ## Architecture
8
+
9
+ - **Standalone mode** (`playbook.yml`): Single server with VLESS+Reality. Optional domain mode adds HAProxy (SNI routing), Caddy (TLS), and VLESS+WSS (CDN fallback via Cloudflare).
10
+ - **Chain mode** (`playbook-chain.yml`): Exit node (Germany) + Relay node (Russia on whitelisted IP). User connects to relay via plain VLESS+TCP; relay forwards to exit via VLESS+Reality+XHTTP.
11
+
12
+ ### Key design decisions
13
+
14
+ - HAProxy on port 443 does TCP-level SNI routing without TLS termination, so both Reality and Caddy can coexist on port 443.
15
+ - 3x-ui panel is managed entirely via its REST API (no manual web UI steps).
16
+ - Caddy handles TLS automatically — email is optional, not required.
17
+ - Caddy config uses import pattern: Meridian writes to `/etc/caddy/conf.d/meridian.caddy`, main Caddyfile just has `import /etc/caddy/conf.d/*.caddy`. User's own Caddyfile is never overwritten.
18
+ - Credentials are persisted locally in `~/meridian/<host>.yml` for idempotent re-runs. Credentials are saved BEFORE changing the panel password to prevent lockout on failure.
19
+ - Uninstall deletes credentials from both server and local machine to prevent stale state on reinstall.
20
+ - Docker installation is skipped if Docker is already running with containers.
21
+ - In no-domain mode, the panel binds to localhost only (SSH tunnel required).
22
+ - 3x-ui Docker image is pinned to a tested version (`threexui_version` in group_vars) to prevent API breakage.
23
+ - All tasks handling secrets use `no_log: true` to keep credentials out of terminal output.
24
+
25
+ ## Project structure
26
+
27
+ ```
28
+ pyproject.toml Python package config (hatchling build, PyPI as meridian-vpn)
29
+ VERSION Version source of truth (read by hatchling + importlib.metadata)
30
+ src/meridian/ Python CLI package
31
+ __init__.py Version from importlib.metadata
32
+ __main__.py python -m meridian support
33
+ cli.py Typer app, subcommand registration, global --server flag
34
+ config.py Paths, URLs, constants
35
+ console.py Rich terminal output: info/ok/warn/fail/prompt/confirm
36
+ credentials.py ServerCredentials dataclass, YAML load/save
37
+ servers.py ServerRegistry: list/find/add/remove
38
+ ssh.py ServerConnection: run, check_ssh, detect_local_mode
39
+ ansible.py Playbook execution, Ansible bootstrap, inventory generation
40
+ update.py Auto-update via PyPI (semver: auto-patch, prompt minor/major)
41
+ ai.py AI prompt building, clipboard, doc fetching
42
+ commands/ One module per subcommand
43
+ resolve.py Server resolution logic (shared by most commands)
44
+ setup.py Interactive wizard + playbook execution
45
+ client.py client add/list/remove (list=direct API, add/remove=ansible)
46
+ server.py server add/list/remove
47
+ check.py Pre-flight validation (SNI, ports, DNS, OS, disk, clock)
48
+ scan.py RealiTLScanner download + execution + SNI selection
49
+ ping.py Reachability test (TCP, TLS, clock drift)
50
+ diagnostics.py System info collection + redaction
51
+ uninstall.py Playbook execution + credential cleanup
52
+ playbooks/ Bundled Ansible playbooks (package_data)
53
+ playbook.yml Standalone mode
54
+ playbook-chain.yml Chain mode
55
+ playbook-client.yml Client management
56
+ playbook-uninstall.yml Clean removal
57
+ ansible.cfg Ansible configuration
58
+ requirements.yml Galaxy collections
59
+ group_vars/ Shared defaults
60
+ roles/ All Ansible roles
61
+ playbook.yml Standalone mode (repo root copy, used by CI)
62
+ playbook-chain.yml Chain mode
63
+ playbook-client.yml Client management
64
+ playbook-uninstall.yml Clean removal
65
+ group_vars/ Shared defaults (repo root copy)
66
+ roles/ All Ansible roles (repo root copy)
67
+ install.sh CLI installer (bootstraps uv, installs from PyPI, migrates old bash CLI)
68
+ setup.sh Compat shim → installs CLI + forwards args
69
+ tests/
70
+ test_cli.py CliRunner smoke tests
71
+ test_credentials.py Credential dataclass tests
72
+ test_servers.py Server registry tests
73
+ test_ansible.py Inventory generation + playbook bundling tests
74
+ test_update.py Version comparison + update throttle tests
75
+ render_templates.py Jinja2 template rendering test (with Ansible filter mocks)
76
+ conftest.py Shared fixtures
77
+ docs/index.html Website hosted on meridian.msu.rocks (GitHub Pages)
78
+ docs/install.sh Installer served from website (CD sync)
79
+ docs/setup.sh Compat shim served from website (CD sync)
80
+ docs/version Version file served from website (CD sync)
81
+ docs/ping.html Web-based ping tool (server reachability test)
82
+ docs/CNAME Custom domain for GitHub Pages
83
+ .github/workflows/ci.yml CI: ansible-lint, pytest, ruff, shellcheck, dry-run
84
+ .github/workflows/cd.yml CD: sync install.sh/setup.sh/version → docs/
85
+ .github/workflows/release.yml Release: git tag + GitHub Release + PyPI publish
86
+ .ansible-lint Ansible lint configuration
87
+ SECURITY.md Vulnerability reporting policy
88
+ CONTRIBUTING.md Development setup and PR guidelines
89
+ ```
90
+
91
+ ## Implicit dependencies & cross-file relationships
92
+
93
+ These are easy to break by editing one file without updating the others:
94
+
95
+ ### CLI architecture
96
+ - `meridian` CLI is a Python package (`meridian-vpn` on PyPI), installed via `uv tool install` or `pipx install`
97
+ - Built with typer (CLI framework) + rich (terminal output) + PyYAML (credential management)
98
+ - Data directory: `~/.meridian/` (credentials, cache, servers, inventory)
99
+ - Playbooks bundled inside the Python package (`src/meridian/playbooks/`), accessed via `importlib.resources`
100
+ - Credentials cached locally in `~/.meridian/credentials/<IP>/` (per-server subdirectories)
101
+ - Server as source of truth: credentials stored at `/etc/meridian/` on the server (synced by playbook post_tasks)
102
+ - Server index: `~/.meridian/servers` (line-oriented: `host user name`, no spaces in names)
103
+ - Auto-update checks PyPI JSON API (throttled to 1x/60s); auto-patches via `uv tool upgrade` / `pipx upgrade`
104
+ - `VERSION` file at repo root is the single source of truth — read by hatchling at build time, by `importlib.metadata` at runtime
105
+ - `setup.sh` is a compat shim that installs the CLI and forwards args
106
+ - Ansible is NOT a Python dependency — installed lazily via `ensure_ansible()` (pipx → pip3 → apt cascade)
107
+
108
+ ### meridian CLI ↔ playbooks
109
+ - CLI passes `-e server_public_ip=... -e credentials_dir=...` to ansible-playbook via `ansible.run_playbook()`
110
+ - All output templates use `{{ server_public_ip }}` instead of `{{ ansible_host }}` for user-facing URLs
111
+ - CLI writes `inventory.yml` to `~/.meridian/inventory.yml` (not inside package dir — may be read-only)
112
+ - CLI sets `ANSIBLE_CONFIG` env var pointing to bundled `ansible.cfg`, runs with `cwd=playbooks_dir`
113
+ - CLI adds `ansible_connection: local` when running on the target server itself
114
+ - CLI adds `ansible_become: true` for non-root users
115
+ - Playbook post_tasks sync `credentials_dir` to `/etc/meridian/` on the server (unless already local)
116
+ - Playbooks exist in two places: repo root (for CI/development) and `src/meridian/playbooks/` (bundled in package)
117
+
118
+ ### meridian subcommands
119
+ - `meridian setup [IP] [--domain --sni --xhttp --name --user --yes]` — deploy server
120
+ - `meridian client add|list|remove NAME` — manage clients via `playbook-client.yml`
121
+ - `meridian server add|list|remove` — manage known servers
122
+ - `meridian check [IP] [--ai]` — pre-flight validation (SNI, ports, DNS, OS, disk, ASN)
123
+ - `meridian scan [IP]` — find optimal SNI targets via RealiTLScanner on server
124
+ - `meridian ping [IP]` — test proxy reachability from client device (no SSH needed)
125
+ - `meridian diagnostics [IP] [--ai]` — collect system info for bug reports
126
+ - `meridian uninstall [IP]` — remove proxy via `playbook-uninstall.yml`
127
+ - `meridian self-update` — update CLI via `uv tool upgrade` / `pipx upgrade`
128
+ - `meridian version` — show version
129
+
130
+ ### docs/index.html ↔ meridian CLI
131
+ - Website command builder has tabbed interface generating `meridian` subcommands
132
+ - Install files served from `meridian.msu.rocks/` — synced by CD workflow: `install.sh`, `setup.sh`, `version`
133
+ - CLI itself distributed via PyPI (`meridian-vpn`), not served from website
134
+ - Website references the same app download links as the HTML templates in roles
135
+ - `docs/ping.html` — standalone web ping tool, uses `fetch()` timing to test server reachability from browser. Supports URL params (`?ip=...&domain=...&name=...`) for shareable pre-filled links. Stores server history in localStorage.
136
+
137
+ ### Connection info HTML templates (3 copies)
138
+ - `roles/caddy/templates/connection-info.html.j2` — served on the server (domain mode)
139
+ - `roles/output/templates/connection-info.html.j2` — saved locally (standalone/exit)
140
+ - `roles/output_relay/templates/connection-info.html.j2` — saved locally (relay)
141
+ - All three have similar CSS/JS but different Jinja2 variables; app download links must be kept in sync across all three
142
+
143
+ ### Credential flow
144
+ - Server is source of truth: `/etc/meridian/proxy.yml` on the server
145
+ - Local cache: `~/.meridian/credentials/<IP>/proxy.yml` per server
146
+ - `meridian` CLI passes `credentials_dir=$HOME/.meridian/credentials/$SERVER_IP` (remote) or `/etc/meridian` (local mode)
147
+ - `roles/xray/tasks/configure_panel.yml` saves to `{{ credentials_file }}` which is `{{ credentials_dir }}/{{ inventory_hostname }}.yml`
148
+ - `roles/xray/tasks/configure_panel.yml` also creates `{{ credentials_dir }}/{{ inventory_hostname }}-clients.yml` with the first client
149
+ - `playbook.yml` and `playbook-client.yml` post_tasks sync `credentials_dir` to `/etc/meridian/` on the server
150
+ - `playbook-chain.yml` relay play loads EXIT node credentials from `{{ credentials_dir }}/{{ exit_node }}.yml`
151
+ - Domain is saved to credentials file for detection on re-runs
152
+ - CLI reads saved credentials to find the server IP (for client/uninstall/diagnostics commands)
153
+ - CLI fetches credentials from `/etc/meridian/` via SSH when not found locally (handles cross-machine runs)
154
+ - `meridian server add IP` fetches credentials from server, caches locally
155
+
156
+ ### Client management flow
157
+ - `playbook-client.yml` loads credentials from `proxy.yml`, reads `domain` field to detect domain mode
158
+ - Client names map to 3x-ui `email` fields: `reality-{name}`, `wss-{name}`, `xhttp-{name}` (e.g., `reality-alice`, `wss-alice`, `xhttp-alice`)
159
+ - The first client created during install uses `reality-{{ first_client_name | default('default') }}` — same naming convention
160
+ - Clients are tracked in `{{ credentials_dir }}/{{ inventory_hostname }}-clients.yml` with UUIDs and timestamps
161
+ - `roles/output/tasks/generate_client_output.yml` is shared between the `output` role and `client_management` role
162
+ - 3x-ui API: `addClient` adds to existing inbound (id in form body), `delClient/{uuid}` removes by client UUID (NOT email — email silently succeeds but doesn't delete)
163
+ - `--add-client`/`--remove-client` resolve server IP from saved credentials (same as `--uninstall`)
164
+
165
+ ### Caddy config pattern
166
+ - Meridian writes to `/etc/caddy/conf.d/meridian.caddy` (not the main Caddyfile)
167
+ - Main Caddyfile gets a single `import /etc/caddy/conf.d/*.caddy` line added via `lineinfile`
168
+ - Uninstall removes only `/etc/caddy/conf.d/meridian.caddy`, not the user's Caddyfile
169
+ - `meridian setup` interactive wizard checks saved credentials for domain suggestion
170
+
171
+ ### Port 443 pre-check allowlist
172
+ - `roles/xray/tasks/deploy.yml` checks port 443 and allows `3x-ui`, `xray`, `haproxy`, `caddy` — if a new service is added to the stack, add it here too
173
+
174
+ ### Panel health check URL
175
+ - After first run, the panel root `/` returns 404 (webBasePath is set) — health check accepts 404 as "responsive"
176
+ - Post-restart health checks in `configure_panel.yml` use `/{{ panel_web_base_path }}/` (guaranteed set at that point)
177
+ - The panel needs a `docker restart` after changing `webBasePath` (setting doesn't apply live)
178
+
179
+ ### Xray binary path
180
+ - Binary is at `/app/bin/xray-linux-*` inside the 3x-ui container (architecture-dependent)
181
+ - Discovered dynamically via `ls` glob — stored in `xray_cmd` fact
182
+ - Used in both `roles/xray/tasks/configure_panel.yml` and `roles/xray_relay/tasks/configure_panel.yml`
183
+ - x25519 output format: `PrivateKey:` and `Password:` (not `Private key:` / `Public key:` in newer Xray versions)
184
+ - Parsing uses regex with both old and new format patterns; assertion verifies keys were parsed
185
+
186
+ ### Handler flush timing
187
+ - `roles/output/tasks/main.yml` calls `meta: flush_handlers` before the port verification check
188
+ - Without this, HAProxy/Caddy handlers haven't fired yet and port 443 shows as not listening
189
+
190
+ ### Terminal output rendering
191
+ - QR codes and connection summary use `ansible.builtin.shell` with `printf`/`cat` instead of `debug msg:` — this is required because Ansible's debug module JSON-escapes ANSI codes, making QR codes unreadable
192
+ - The `ansible.cfg` must NOT have `result_format = yaml` for the same reason
193
+
194
+ ### Feedback loop
195
+ - `fail()` in meridian CLI suggests `meridian diagnostics` and links to GitHub issues
196
+ - Success output mentions feedback URL
197
+ - Ansible connection summary includes feedback section
198
+ - Website has troubleshooting with `meridian check` and `meridian diagnostics` commands
199
+ - README has troubleshooting section
200
+
201
+ ## Key API patterns
202
+
203
+ - 3x-ui login: `POST /login` (form-urlencoded) returns session cookie. Login MUST use form-urlencoded (not JSON).
204
+ - Add inbound: `POST /panel/api/inbounds/add` (JSON body with `body_format: json`). The `settings`, `streamSettings`, `sniffing` fields must be JSON **strings** (not nested objects). The Go struct uses `string` type for these fields. With `body_format: json` + `jinja2_native = True`, the `>-` YAML blocks containing mixed text+Jinja2 expressions remain strings (Jinja2 NativeEnvironment only returns native types for single-expression templates like `{{ x }}`, not mixed content).
205
+ - List inbounds: `GET /panel/api/inbounds/list` (check by remark before creating)
206
+ - **CRITICAL: Do NOT use `body_format: form-urlencoded` for inbound/client API calls.** Ansible's uri module silently corrupts inline JSON values in form-urlencoded bodies — the API returns `success: true` but stores only the first key name instead of the full JSON object. This was a production bug. Always use `body_format: json` for inbound operations.
207
+ - 3x-ui rejects duplicate ports — two inbounds cannot share the same port. XHTTP needs its own dedicated port separate from Reality TCP.
208
+ - Update settings: `POST /panel/setting/update` (JSON body)
209
+ - Update credentials: `POST /panel/setting/updateUser` (JSON body)
210
+ - Read settings: `POST /panel/setting/all`
211
+ - Xray template config: set `xrayTemplateConfig` field in `/panel/setting/update`
212
+ - Key generation: `docker exec 3x-ui sh -c '/app/bin/xray-linux-* x25519'` (parse PrivateKey/Password lines), `docker exec 3x-ui sh -c '/app/bin/xray-linux-* uuid'`
213
+
214
+ ## Build and test
215
+
216
+ ```bash
217
+ # Install CLI in editable mode with dev dependencies
218
+ pip install -e ".[dev]"
219
+
220
+ # Run Python tests
221
+ pytest tests/ -v
222
+
223
+ # Run linter
224
+ ruff check src/ tests/
225
+ ruff format --check src/ tests/
226
+
227
+ # Run template rendering test
228
+ python3 tests/render_templates.py
229
+
230
+ # Install Ansible for playbook validation
231
+ pip install ansible
232
+ ansible-galaxy collection install -r requirements.yml
233
+
234
+ # Run standalone
235
+ ansible-playbook playbook.yml
236
+
237
+ # Run chain mode
238
+ cp inventory-chain.yml.example inventory-chain.yml
239
+ ansible-playbook -i inventory-chain.yml playbook-chain.yml
240
+
241
+ # The playbook verifies ports are listening at the end.
242
+ # To fully test, import the VLESS URL into v2rayNG and check connectivity.
243
+ ```
244
+
245
+
246
+ ## Conventions
247
+
248
+ - All tasks use FQCNs (e.g., `ansible.builtin.uri`, not `uri`)
249
+ - API responses are validated with `ansible.builtin.assert` checking `.json.success`
250
+ - Tasks handling secrets use `no_log: true`
251
+ - Conditional roles use `include_tasks` (not `import_tasks`) with `when`
252
+ - Port variables use deterministic random seeds (`inventory_hostname + suffix`) for idempotency
253
+ - QR codes generated with `qrencode` — must be installed on the local machine
254
+ - Cross-platform: `base64 | tr -d '\n'` instead of `base64 -w0` (macOS compat on localhost tasks; remote tasks can use `-w0` since remote is always Linux)
255
+ - XHTTP transport doesn't support `xtls-rprx-vision` flow (must be empty string)
256
+ - Docker compose errors include `rescue` blocks with container logs and common fix suggestions
257
+ - DNS resolution check for domain mode fails hard (override with `-e skip_dns_check=true`)
258
+ - Use `ansible_facts['distribution']` not `ansible_distribution` (deprecated in 2.24)
259
+ - Docker role removes conflicting `docker.io` / `containerd` / `runc` packages only when `docker-ce` is not already installed AND no containers are running
260
+ - `reality_dest` is derived from `reality_sni` (`{{ reality_sni }}:443`) — don't hardcode separately
261
+ - **SNI target selection**: Never recommend apple.com or icloud.com (Apple-owned ASN — mismatch with VPS hosting is instantly detectable). Good choices: www.microsoft.com, www.twitch.tv, dl.google.com, github.com (global CDN, shared infrastructure). Best: run `meridian scan` for same-network targets.
262
+ - **Always use context7 MCP to check up-to-date docs** before writing or modifying code that depends on external tools/libraries (Ansible, Docker, Caddy, GitHub Actions, shellcheck, etc.) — stale patterns and outdated common knowledge cause real deployment failures. Don't rely on training data for API syntax, CLI flags, or workflow configuration — verify against current docs first.
263
+ - **curl|bash stdin trap**: in `install.sh` and `setup.sh` (compat shim), any command that reads stdin MUST have `</dev/null` — the `meridian` CLI runs directly so this isn't needed there, but `</dev/null` on SSH commands is still good practice
264
+ - **Ansible debug vs shell for terminal output**: use `shell` with `printf`/`cat` for output containing ANSI codes (QR codes); `debug msg:` JSON-escapes them
265
+ - **pip3 install on modern Debian/Ubuntu**: must handle PEP 668 "externally managed environment" — try pipx, then `--user`, then `--break-system-packages`, then apt
266
+ - **pip user bin PATH**: after pip3 install --user, add `~/.local/bin` (Linux) and `~/Library/Python/*/bin` (macOS) to PATH
267
+ - **meridian interactive prompts**: `console.prompt()` reads from `/dev/tty` for pipe safety; detect public IPv4 with `curl -4` to avoid IPv6; suggest domain from saved credentials
268
+ - **console output functions**: `info()`, `ok()`, `warn()`, `fail()` use Rich markup — pass plain text, not ANSI codes. `fail()` raises `typer.Exit(1)` and is testable with CliRunner.
269
+ - **GitHub raw CDN caching**: raw.githubusercontent.com caches for ~60-120s; can't bust with query params or headers, just wait. Serving from meridian.msu.rocks avoids this.
270
+ - **HAProxy health checks**: do NOT use `check` on TLS backends (Caddy, Xray) — TCP probes fail on TLS-only ports, causing "backend has no server available" errors. These are local systemd services, not load-balanced pools.
271
+ - **docs/ sync**: `docs/install.sh`, `docs/setup.sh`, `docs/version` are synced by CD workflow. Manual edits to docs/ copies will be overwritten. CLI is no longer synced to docs/ (distributed via PyPI).
272
+ - **AI docs**: Source files in `docs/ai/` (`context.md`, `architecture.md`, `troubleshooting.md`). CD workflow concatenates them into `docs/ai/reference.md` — the single file the CLI fetches. Edit the source files, not `reference.md`.
273
+ - **`--ai` flag**: `meridian check --ai` and `meridian diagnostics --ai` bundle AI docs + command output into a clipboard-ready prompt for ChatGPT/Claude. Docs are cached at `~/.meridian/cache/ai-reference.md`, invalidated on version change.
274
+ - **VERSION consistency**: `VERSION` file is the single source of truth. Hatchling reads it at build time; Python code uses `importlib.metadata.version("meridian-vpn")` at runtime. CI validates VERSION format (`^\d+\.\d+\.\d+$`).
275
+ - **Ecosystem cross-promotion**: Always upsell/reference related Meridian tools where contextually appropriate. When adding error messages, output, templates, issue templates, or docs — promote the relevant tool for that context. Current tools to cross-promote:
276
+ - `meridian ping` / `meridian.msu.rocks/ping` — for connection issues, reachability testing
277
+ - `meridian check` — for pre-deployment server validation
278
+ - `meridian diagnostics` — for bug reports and server-side debugging
279
+ - `meridian.msu.rocks` — for docs, setup guides, command builder
280
+ - Connection info pages — for end-user onboarding and troubleshooting
281
+ - The pattern: error/failure messages → suggest ping first (network issue?), then diagnostics (server issue?), then GitHub issues (bug?)
282
+ - **Context-sensitive upsells**: don't blindly suggest every tool — only suggest the tool that helps for the specific failure mode. Example: if ping shows port 443 is blocked, suggesting `meridian diagnostics` doesn't help (it's a firewall issue). But if ping passes and VPN still fails, diagnostics is the right next step.
283
+ - **Pre-fill URLs with known data**: when server IP and domain are available in context (templates, CLI output), always generate pre-filled `meridian.msu.rocks/ping?ip=...&domain=...` URLs so users land on a ready-to-run test.
284
+ - **CLI installation**: installed via `uv tool install meridian-vpn` (preferred) or `pipx install meridian-vpn`. Entry point is `meridian`. Location depends on the tool (typically `~/.local/bin/`).
285
+ - **Auto-update**: checks PyPI JSON API for latest version. Auto-upgrades patches via `uv tool upgrade` / `pipx upgrade`, prompts for minor/major. Uses `os.execvp()` to re-exec after auto-patch.
286
+ - **Auto-update direction**: only updates when remote version is strictly newer (`packaging.version.Version` comparison). Running a local dev build with a higher version will NOT trigger a downgrade.
287
+ - **`jinja2_native = True` in ansible.cfg**: required for `body_format: json` to send native integer types (e.g., `port`). Safe with mixed text+expression templates (`settings: >-` blocks) because Jinja2 NativeEnvironment only returns native types for single-expression templates. Do NOT remove without an alternative solution for integer typing.
288
+ - **`client list` bypasses Ansible**: uses direct curl + native Python JSON parsing for instant results instead of running a full playbook. `client add` and `client remove` still use Ansible playbooks because they modify state and benefit from idempotency.
289
+ - **Playbook bundling**: playbooks exist in two places — repo root (for CI/dev) and `src/meridian/playbooks/` (bundled in package). When editing playbooks, update both copies. A sync script or CI check should enforce this.
290
+ - **Credential management**: `ServerCredentials` dataclass in `credentials.py` replaces all `grep|awk|tr` YAML parsing. Handles special characters, preserves unknown fields, type-safe access.
291
+ - **When the user says "remember"**: save the instruction to this CLAUDE.md file so it persists across sessions. Don't use auto-memory — CLAUDE.md is the canonical place for project conventions.
292
+
293
+ ## Documentation surfaces & update checklist
294
+
295
+ When adding or changing a feature, update ALL relevant surfaces. The source of truth for each type of information is marked with ★.
296
+
297
+ ### Sources of truth
298
+
299
+ | Information | Source of Truth | Propagated To |
300
+ |---|---|---|
301
+ | **CLI commands & flags** | ★ `src/meridian/cli.py` (typer commands) | README.md commands table, docs/index.html builder, docs/ai/context.md CLI section, CLAUDE.md subcommands |
302
+ | **Architecture & modes** | ★ `CLAUDE.md` architecture section | docs/ai/architecture.md, docs/ai/context.md, README.md "How it works" |
303
+ | **SNI recommendations** | ★ `group_vars/all.yml` comments | docs/ai/troubleshooting.md SNI section, roles/output/templates/connection-summary.txt.j2 |
304
+ | **Troubleshooting guidance** | ★ `docs/ai/troubleshooting.md` | docs/index.html troubleshooting section, connection_issue.yml template |
305
+ | **App download links** | ★ `docs/index.html` apps section | 3x connection-info.html.j2 templates, README.md client apps table |
306
+ | **Version** | ★ `VERSION` file | `importlib.metadata` at runtime (hatchling reads VERSION at build), docs/version (CD sync) |
307
+ | **Error/failure guidance** | ★ `meridian` script `fail()` function | docs/ai/troubleshooting.md decision tree |
308
+
309
+ ### Surface update checklist
310
+
311
+ When adding a **new subcommand**:
312
+ - [ ] `src/meridian/commands/newcmd.py` — implement `run()` function
313
+ - [ ] `src/meridian/cli.py` — add typer command + import
314
+ - [ ] `tests/test_cli.py` — add help smoke test
315
+ - [ ] `README.md` — add to commands table
316
+ - [ ] `docs/index.html` — add tab to command builder (if user-facing)
317
+ - [ ] `docs/ai/context.md` — add to CLI Commands section
318
+ - [ ] `CLAUDE.md` — add to meridian subcommands list
319
+ - [ ] Regenerate `docs/ai/reference.md` (cat context + architecture + troubleshooting)
320
+
321
+ When adding a **new flag to setup**:
322
+ - [ ] `src/meridian/commands/setup.py` — add parameter + logic
323
+ - [ ] `src/meridian/cli.py` — add `typer.Option` to setup_cmd
324
+ - [ ] `docs/index.html` — add checkbox/input to setup command builder
325
+ - [ ] `docs/ai/context.md` — add to setup flags list
326
+ - [ ] `CLAUDE.md` — update subcommands line
327
+ - [ ] `group_vars/all.yml` — add default variable with comment
328
+ - [ ] Regenerate `docs/ai/reference.md`
329
+
330
+ When adding a **new inbound/transport type**:
331
+ - [ ] `roles/xray/tasks/` — create/update inbound task
332
+ - [ ] `roles/xray/tasks/main.yml` — add include gate
333
+ - [ ] `roles/shared/tasks/generate_client_output.yml` — VLESS URL + QR codes
334
+ - [ ] `roles/output/tasks/main.yml` — terminal output (QR + URL display)
335
+ - [ ] `roles/output/templates/connection-summary.txt.j2` — admin summary
336
+ - [ ] `roles/output/templates/connection-summary-client.txt.j2` — client summary
337
+ - [ ] `roles/output/templates/connection-info.html.j2` — local HTML page
338
+ - [ ] `roles/caddy/templates/connection-info.html.j2` — server HTML page (+ QR gen in caddy/tasks/main.yml)
339
+ - [ ] `roles/caddy/tasks/main.yml` — build URL + generate QR on server
340
+ - [ ] `roles/client_management/tasks/main.yml` — discover inbound
341
+ - [ ] `roles/client_management/tasks/add_client.yml` — add client to inbound + terminal output
342
+ - [ ] `roles/client_management/tasks/remove_client.yml` — remove client from inbound
343
+ - [ ] `roles/xray/tasks/configure_panel.yml` — save setting to credentials
344
+ - [ ] `tests/render_templates.py` — add mock variables (templates are auto-discovered)
345
+ - [ ] `docs/ai/context.md` — update port table and architecture
346
+ - [ ] `docs/ai/architecture.md` — update topology diagrams
347
+ - [ ] Regenerate `docs/ai/reference.md`
348
+
349
+ When changing **SNI recommendations**:
350
+ - [ ] `group_vars/all.yml` — update "Good choices" / "Avoid" comments
351
+ - [ ] `roles/output/templates/connection-summary.txt.j2` — update alternatives line
352
+ - [ ] `docs/ai/troubleshooting.md` — update SNI Target Selection section
353
+ - [ ] Regenerate `docs/ai/reference.md`
354
+
355
+ When changing **troubleshooting/error guidance**:
356
+ - [ ] `docs/ai/troubleshooting.md` — update symptom/fix sections
357
+ - [ ] `docs/index.html` — update troubleshooting details section
358
+ - [ ] `.github/ISSUE_TEMPLATE/connection_issue.yml` — update pre-report checklist
359
+ - [ ] Regenerate `docs/ai/reference.md`
360
+
361
+ ### Current inconsistencies to fix (tracked)
362
+
363
+ All previously tracked inconsistencies have been resolved:
364
+ - ~~`docs/index.html` command builder~~ — added `scan` tab, `--xhttp` checkbox, `--ai` mention, SNI guidance
365
+ - ~~`bug_report.yml` --rage~~ → fixed to "diagnostics output"
366
+ - ~~`CONTRIBUTING.md` --rage~~ → fixed to `meridian diagnostics`
367
+ - ~~`SECURITY.md` --rage~~ → fixed to `meridian diagnostics`
368
+ - ~~`README.md` missing scan/xhttp~~ → added to commands table
369
+ - i18n translations extracted to `docs/i18n.js` (index.html reduced from 805 to ~620 lines)
370
+
371
+ ## CI/CD pipelines
372
+
373
+ **Pipeline chain:** push → CI → CD (on CI success) → Release (on CD success)
374
+
375
+ ### CI (`.github/workflows/ci.yml`) — runs on push and PR
376
+ - **Lint**: `ansible/ansible-lint@main` — catches FQCN, YAML style, deprecated patterns
377
+ - **Python Test**: `pytest` on Python 3.10 + 3.12 — credentials, servers, CLI, ansible, update logic
378
+ - **Python Lint**: `ruff check` + `ruff format --check` — style, imports, formatting
379
+ - **Validate**: syntax check all playbooks + auto-discovered template rendering test with Ansible filter mocks
380
+ - **Shell**: `bash -n` + shellcheck on `install.sh` and `setup.sh` + VERSION format validation + `body_format` policy check + connection-info app link sync check
381
+ - **Dry run**: `ansible-playbook --check` with local connection (validates task structure)
382
+ - Skipped lint rules: `var-naming[no-role-prefix]` (would require renaming 90+ variables), `command-instead-of-module` (curl/dig used intentionally)
383
+
384
+ ### CD (`.github/workflows/cd.yml`) — triggers after CI succeeds (workflow_run)
385
+ - Syncs `install.sh` → `docs/install.sh`, `setup.sh` → `docs/setup.sh`, `VERSION` → `docs/version`
386
+ - Generates `SHA256SUMS` checksum file for install scripts
387
+ - Auto-commits with `[skip ci]`
388
+ - Ensures `meridian.msu.rocks/` always serves the latest installer and version
389
+
390
+ ## Versioning & releases
391
+
392
+ ### Semver: X.Y.Z
393
+
394
+ | Bump | When | User experience |
395
+ |------|------|----------------|
396
+ | **Z** (patch) | Bug fixes, docs, safe tweaks | Auto-updated silently (next CLI run) |
397
+ | **Y** (minor) | New features, opt-in changes (e.g., `--xhttp`, `scan`) | Prompted: "Update available", user runs `self-update` |
398
+ | **X** (major) | Breaking changes, defaults change | Prompted: "Major update available", user runs `self-update` |
399
+
400
+ ### How to release
401
+
402
+ 1. Update `VERSION` file with the new version
403
+ 2. Commit and push to main → three workflows trigger in chain:
404
+ - **CI**: validates VERSION format, runs pytest + ruff + ansible-lint
405
+ - **CD**: syncs install files to GitHub Pages (`meridian.msu.rocks/version`)
406
+ - **Release**: creates git tag `vX.Y.Z` + GitHub Release + publishes to PyPI
407
+ 3. Users on auto-patch get the update on next CLI run; others see "Update available" prompt
408
+
409
+ ### When to bump (guidance for Claude)
410
+
411
+ After completing a feature or fix, **always bump the version as part of the commit workflow**:
412
+ - Fixed a bug or updated docs? → Bump Z (patch): `1.1.0` → `1.1.1`
413
+ - Added a new command, flag, or transport? → Bump Y (minor): `1.1.0` → `1.2.0`
414
+ - Changed defaults or broke backward compat? → Bump X (major): `1.2.0` → `2.0.0`
415
+
416
+ **Do NOT skip version bumps.** Every meaningful change to the CLI or playbooks should get a version bump (just edit the `VERSION` file) so users on auto-patch get fixes and users on manual update see the prompt. If multiple features are in one session, one version bump at the end is fine.
417
+
418
+ ### Release artifacts
419
+
420
+ - **PyPI package**: `meridian-vpn` on PyPI (published by release workflow)
421
+ - **Version file**: `meridian.msu.rocks/version` (CD sync from VERSION)
422
+ - **Installer**: `meridian.msu.rocks/install.sh` (CD sync)
423
+ - **GitHub Release**: auto-created by `.github/workflows/release.yml` when VERSION changes
424
+
425
+ CI validates VERSION format (`^\d+\.\d+\.\d+$`) on every push.
426
+
427
+ ## Backlog & tech debt
428
+
429
+ See `BACKLOG.md` for the full prioritized task list with completion status.
430
+
431
+ ## GitHub community files
432
+
433
+ - `.github/ISSUE_TEMPLATE/bug_report.yml` — structured bug report with diagnostics prompt
434
+ - `.github/ISSUE_TEMPLATE/connection_issue.yml` — connection troubleshooting with --check prompt
435
+ - `.github/ISSUE_TEMPLATE/feature_request.yml` — feature requests by area
436
+ - `.github/ISSUE_TEMPLATE/config.yml` — disables blank issues, links to docs
437
+ - `SECURITY.md` — vulnerability reporting policy and security design overview
438
+ - `CONTRIBUTING.md` — development setup, PR guidelines, testing approach
@@ -0,0 +1,78 @@
1
+ # Contributing to Meridian
2
+
3
+ Thanks for your interest! Here's how to help.
4
+
5
+ ## Reporting Issues
6
+
7
+ - **Bug?** Use the [bug report template](https://github.com/uburuntu/meridian/issues/new?template=bug_report.yml) — run `meridian diagnostics` first to collect info
8
+ - **Connection not working?** Use the [connection issue template](https://github.com/uburuntu/meridian/issues/new?template=connection_issue.yml) — run `meridian ping` and `meridian check` first
9
+ - **Feature idea?** Use the [feature request template](https://github.com/uburuntu/meridian/issues/new?template=feature_request.yml)
10
+ - **Security vulnerability?** See [SECURITY.md](SECURITY.md) — do NOT open a public issue
11
+
12
+ ## Development Setup
13
+
14
+ ```bash
15
+ git clone https://github.com/uburuntu/meridian.git && cd meridian
16
+
17
+ # Install the CLI in editable mode with dev dependencies
18
+ pip install -e ".[dev]"
19
+
20
+ # Install Ansible (for playbook validation)
21
+ pip install ansible
22
+ ansible-galaxy collection install -r requirements.yml
23
+
24
+ # Run tests
25
+ pytest tests/ -v
26
+
27
+ # Run linter
28
+ ruff check src/ tests/
29
+ ruff format --check src/ tests/
30
+
31
+ # Run Ansible lint
32
+ pip install ansible-lint
33
+ ansible-lint
34
+
35
+ # Run template rendering test
36
+ python3 tests/render_templates.py
37
+ ```
38
+
39
+ ## Project Structure
40
+
41
+ The CLI is a Python package (`src/meridian/`) distributed via PyPI as `meridian-vpn`. Ansible playbooks are bundled inside the package as `src/meridian/playbooks/`.
42
+
43
+ Key modules:
44
+ - `cli.py` — Typer app, subcommand registration
45
+ - `commands/` — One module per subcommand (setup, client, check, etc.)
46
+ - `credentials.py` — `ServerCredentials` dataclass for YAML credential management
47
+ - `servers.py` — `ServerRegistry` for the known servers index
48
+ - `ansible.py` — Playbook execution via subprocess
49
+
50
+ ## Pull Requests
51
+
52
+ 1. Fork the repo and create a branch from `main`
53
+ 2. Make your changes — keep them focused and minimal
54
+ 3. Ensure CI passes: `pytest`, `ruff check`, `ruff format --check`, `ansible-lint`
55
+ 4. Test on a real server if possible — CI can't catch deployment issues
56
+ 5. Open a PR with a clear description of what and why
57
+
58
+ ## Architecture Notes
59
+
60
+ See [CLAUDE.md](CLAUDE.md) for detailed architecture, implicit dependencies, and conventions. Key points:
61
+
62
+ - **All Ansible tasks use FQCNs** (`ansible.builtin.uri`, not `uri`)
63
+ - **Secrets use `no_log: true`** — never expose credentials in output
64
+ - **Three connection-info templates must stay in sync** (output/caddy/output_relay)
65
+ - **Caddy config** goes in `/etc/caddy/conf.d/meridian.caddy`, not the main Caddyfile
66
+ - **Playbook sync** — when editing playbooks at repo root, copy changes to `src/meridian/playbooks/`
67
+
68
+ ## Testing
69
+
70
+ There's no way to fully test without a real server. The CI pipeline validates:
71
+ - Python tests (`pytest`) — credentials, servers, CLI, ansible, update logic
72
+ - Python lint (`ruff`) — style and import checks
73
+ - Ansible lint and syntax
74
+ - Jinja2 template rendering with mock variables
75
+ - Shell script syntax (`install.sh`, `setup.sh`)
76
+ - Ansible dry-run (`--check`) with local connection
77
+
78
+ For deployment testing, use a cheap VPS and run the full uninstall → install cycle.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Meridian Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.