navig-devhost 0.1.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 (28) hide show
  1. navig_devhost-0.1.0/LICENSE +173 -0
  2. navig_devhost-0.1.0/PKG-INFO +89 -0
  3. navig_devhost-0.1.0/README.md +65 -0
  4. navig_devhost-0.1.0/navig_devhost/__init__.py +9 -0
  5. navig_devhost-0.1.0/navig_devhost/commands/__init__.py +1 -0
  6. navig_devhost-0.1.0/navig_devhost/commands/devhost.py +305 -0
  7. navig_devhost-0.1.0/navig_devhost/engine/__init__.py +7 -0
  8. navig_devhost-0.1.0/navig_devhost/engine/certs.py +80 -0
  9. navig_devhost-0.1.0/navig_devhost/engine/hosts.py +117 -0
  10. navig_devhost-0.1.0/navig_devhost/engine/net.py +48 -0
  11. navig_devhost-0.1.0/navig_devhost/engine/paths.py +29 -0
  12. navig_devhost-0.1.0/navig_devhost/engine/proxy.py +173 -0
  13. navig_devhost-0.1.0/navig_devhost/engine/registry.py +120 -0
  14. navig_devhost-0.1.0/navig_devhost/plugin.py +45 -0
  15. navig_devhost-0.1.0/navig_devhost/skills/devhost/SKILL.md +34 -0
  16. navig_devhost-0.1.0/navig_devhost.egg-info/PKG-INFO +89 -0
  17. navig_devhost-0.1.0/navig_devhost.egg-info/SOURCES.txt +26 -0
  18. navig_devhost-0.1.0/navig_devhost.egg-info/dependency_links.txt +1 -0
  19. navig_devhost-0.1.0/navig_devhost.egg-info/entry_points.txt +5 -0
  20. navig_devhost-0.1.0/navig_devhost.egg-info/requires.txt +1 -0
  21. navig_devhost-0.1.0/navig_devhost.egg-info/top_level.txt +1 -0
  22. navig_devhost-0.1.0/pyproject.toml +53 -0
  23. navig_devhost-0.1.0/setup.cfg +4 -0
  24. navig_devhost-0.1.0/tests/test_engine.py +83 -0
  25. navig_devhost-0.1.0/tests/test_hosts.py +111 -0
  26. navig_devhost-0.1.0/tests/test_registry_no_wipe.py +103 -0
  27. navig_devhost-0.1.0/tests/test_remove_command.py +89 -0
  28. navig_devhost-0.1.0/tests/test_smoke.py +28 -0
@@ -0,0 +1,173 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+
135
+ 6. Trademarks. This License does not grant permission to use the trade
136
+ names, trademarks, service marks, or product names of the Licensor,
137
+ except as required for reasonable and customary use in describing the
138
+ origin of the Work and reproducing the content of the NOTICE file.
139
+
140
+ 7. Disclaimer of Warranty. Unless required by applicable law or
141
+ agreed to in writing, Licensor provides the Work (and each
142
+ Contributor provides its Contributions) on an "AS IS" BASIS,
143
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
144
+ implied, including, without limitation, any warranties or conditions
145
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
146
+ PARTICULAR PURPOSE. You are solely responsible for determining the
147
+ appropriateness of using or redistributing the Work and assume any
148
+ risks associated with Your exercise of permissions under this License.
149
+
150
+ 8. Limitation of Liability. In no event and under no legal theory,
151
+ whether in tort (including negligence), contract, or otherwise,
152
+ unless required by applicable law (such as deliberate and grossly
153
+ negligent acts) or agreed to in writing, shall any Contributor be
154
+ liable to You for damages, including any direct, indirect, special,
155
+ incidental, or consequential damages of any character arising as a
156
+ result of this License or out of the use or inability to use the
157
+ Work (including but not limited to damages for loss of goodwill,
158
+ work stoppage, computer failure or malfunction, or any and all
159
+ other commercial damages or losses), even if such Contributor
160
+ has been advised of the possibility of such damages.
161
+
162
+ 9. Accepting Warranty or Additional Liability. While redistributing
163
+ the Work or Derivative Works thereof, You may choose to offer,
164
+ and charge a fee for, acceptance of support, warranty, indemnity,
165
+ or other liability obligations and/or rights consistent with this
166
+ License. However, in accepting such obligations, You may act only
167
+ on Your own behalf and on Your sole responsibility, not on behalf
168
+ of any other Contributor, and only if You agree to indemnify,
169
+ defend, and hold each Contributor harmless for any liability
170
+ incurred by, or claims asserted against, such Contributor by reason
171
+ of your accepting any such warranty or additional liability.
172
+
173
+ END OF TERMS AND CONDITIONS
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: navig-devhost
3
+ Version: 0.1.0
4
+ Summary: NAVIG Dev Host module — give any local dev server a real .test domain over trusted HTTPS in one command (hosts + mkcert + raw TLS relay), wired into `navig devhost`. A first-party navig plugin (free, toggleable).
5
+ Author-email: NAVIG Development Team <opensource@navig.run>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://navig.run
8
+ Project-URL: Documentation, https://navig.run/docs
9
+ Keywords: navig,cli,agent,automation,plugin,devhost,https,tls,mkcert,localhost,dns
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: navig>=3.24.0
23
+ Dynamic: license-file
24
+
25
+ # navig-devhost
26
+
27
+ **Local `.test` domains with trusted HTTPS for any dev server — one command, any project.**
28
+
29
+ Turn `http://localhost:7645` into **`https://cybesis.test`** without per-project proxy scripts.
30
+ A first-party [navig](../../README.md) plugin (free, toggleable) that wires together the three
31
+ pieces every local dev domain needs:
32
+
33
+ 1. a **hosts entry** on a dedicated loopback (coexists with your other `.test` sites on `:443`),
34
+ 2. a **trusted mkcert certificate** (real padlock, no browser warnings),
35
+ 3. a **raw TLS relay** in front of your plain-HTTP dev server.
36
+
37
+ No Playwright/nginx/Caddy — the relay is pure stdlib `ssl`/`socket`, and it terminates TLS then
38
+ pipes bytes verbatim, so keep-alive, SSE, and **WebSocket/HMR pass through untouched**.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ py -3.13 -m pip install -e plugins/navig-devhost # into navig's Python (Windows)
44
+ python3 -m pip install -e plugins/navig-devhost # macOS/Linux
45
+ navig plugin list # → navig-devhost … + wired
46
+ navig devhost doctor # check mkcert + admin
47
+ ```
48
+
49
+ Prereq: [mkcert](https://github.com/FiloSottile/mkcert) (`winget install FiloSottile.mkcert`), then
50
+ `mkcert -install` once so its CA is trusted.
51
+
52
+ ## Use
53
+
54
+ ```bash
55
+ # 1) register (adds hosts entry on next free 127.0.0.x + issues a cert) — needs admin
56
+ navig devhost add cybesis.test --port 7645
57
+
58
+ # 2) start your dev server however you normally do (→ http://localhost:7645)
59
+
60
+ # 3) run the relay (foreground; Ctrl+C to stop)
61
+ navig devhost up cybesis.test # or: navig devhost up (serves all registered)
62
+ # 🔒 https://cybesis.test → http://127.0.0.1:7645
63
+ ```
64
+
65
+ Open **https://cybesis.test**.
66
+
67
+ ## Commands
68
+
69
+ | Command | What it does |
70
+ |---|---|
71
+ | `navig devhost add <domain> --port N` | Register: dedicated loopback + hosts entry + mkcert cert. `--ip`, `--no-tls`, `--target-host`. |
72
+ | `navig devhost up [domain] [--all]` | Run the HTTPS relay (foreground). No domain → all registered. |
73
+ | `navig devhost list` / `status` | Table: hosts ok · cert ok · dev-server up · currently serving. |
74
+ | `navig devhost remove <domain>` | Remove hosts entry + cert + registry record. `--keep-cert`. |
75
+ | `navig devhost doctor` | Check mkcert, its CA, and admin for hosts edits. |
76
+
77
+ Everything supports `--json` for scripting.
78
+
79
+ ## Notes / gotchas
80
+
81
+ - **Dedicated loopback per site** (`127.0.0.2`, `.3`, …) so each `.test` can own `:443` and coexist —
82
+ matching the established house convention. `add` auto-picks the next free one.
83
+ - **Admin** is needed only for `add`/`remove` (they edit the system hosts file). `up`/`list`/`status`
84
+ need no elevation — Windows doesn't gate ports < 1024.
85
+ - **Next.js dev**: to silence the cross-origin dev warning under the new host, add
86
+ `allowedDevOrigins: ['<domain>']` to `next.config.js`. (Vite needs nothing.)
87
+ - State lives in `<navig config>/devhost/` (`registry.json` + `certs/`).
88
+
89
+ <sub>First-party navig plugin · Apache-2.0 · reuses navig's hosts plumbing; zero Python deps.</sub>
@@ -0,0 +1,65 @@
1
+ # navig-devhost
2
+
3
+ **Local `.test` domains with trusted HTTPS for any dev server — one command, any project.**
4
+
5
+ Turn `http://localhost:7645` into **`https://cybesis.test`** without per-project proxy scripts.
6
+ A first-party [navig](../../README.md) plugin (free, toggleable) that wires together the three
7
+ pieces every local dev domain needs:
8
+
9
+ 1. a **hosts entry** on a dedicated loopback (coexists with your other `.test` sites on `:443`),
10
+ 2. a **trusted mkcert certificate** (real padlock, no browser warnings),
11
+ 3. a **raw TLS relay** in front of your plain-HTTP dev server.
12
+
13
+ No Playwright/nginx/Caddy — the relay is pure stdlib `ssl`/`socket`, and it terminates TLS then
14
+ pipes bytes verbatim, so keep-alive, SSE, and **WebSocket/HMR pass through untouched**.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ py -3.13 -m pip install -e plugins/navig-devhost # into navig's Python (Windows)
20
+ python3 -m pip install -e plugins/navig-devhost # macOS/Linux
21
+ navig plugin list # → navig-devhost … + wired
22
+ navig devhost doctor # check mkcert + admin
23
+ ```
24
+
25
+ Prereq: [mkcert](https://github.com/FiloSottile/mkcert) (`winget install FiloSottile.mkcert`), then
26
+ `mkcert -install` once so its CA is trusted.
27
+
28
+ ## Use
29
+
30
+ ```bash
31
+ # 1) register (adds hosts entry on next free 127.0.0.x + issues a cert) — needs admin
32
+ navig devhost add cybesis.test --port 7645
33
+
34
+ # 2) start your dev server however you normally do (→ http://localhost:7645)
35
+
36
+ # 3) run the relay (foreground; Ctrl+C to stop)
37
+ navig devhost up cybesis.test # or: navig devhost up (serves all registered)
38
+ # 🔒 https://cybesis.test → http://127.0.0.1:7645
39
+ ```
40
+
41
+ Open **https://cybesis.test**.
42
+
43
+ ## Commands
44
+
45
+ | Command | What it does |
46
+ |---|---|
47
+ | `navig devhost add <domain> --port N` | Register: dedicated loopback + hosts entry + mkcert cert. `--ip`, `--no-tls`, `--target-host`. |
48
+ | `navig devhost up [domain] [--all]` | Run the HTTPS relay (foreground). No domain → all registered. |
49
+ | `navig devhost list` / `status` | Table: hosts ok · cert ok · dev-server up · currently serving. |
50
+ | `navig devhost remove <domain>` | Remove hosts entry + cert + registry record. `--keep-cert`. |
51
+ | `navig devhost doctor` | Check mkcert, its CA, and admin for hosts edits. |
52
+
53
+ Everything supports `--json` for scripting.
54
+
55
+ ## Notes / gotchas
56
+
57
+ - **Dedicated loopback per site** (`127.0.0.2`, `.3`, …) so each `.test` can own `:443` and coexist —
58
+ matching the established house convention. `add` auto-picks the next free one.
59
+ - **Admin** is needed only for `add`/`remove` (they edit the system hosts file). `up`/`list`/`status`
60
+ need no elevation — Windows doesn't gate ports < 1024.
61
+ - **Next.js dev**: to silence the cross-origin dev warning under the new host, add
62
+ `allowedDevOrigins: ['<domain>']` to `next.config.js`. (Vite needs nothing.)
63
+ - State lives in `<navig config>/devhost/` (`registry.json` + `certs/`).
64
+
65
+ <sub>First-party navig plugin · Apache-2.0 · reuses navig's hosts plumbing; zero Python deps.</sub>
@@ -0,0 +1,9 @@
1
+ """navig-devhost — local dev domains with trusted HTTPS, from `navig devhost`.
2
+
3
+ Give any local dev server a real `.test` domain over trusted HTTPS in one command:
4
+ `navig devhost add cybesis.test --port 7645` then `navig devhost up`. A first-party
5
+ navig plugin (free, toggleable). Hosts entry + mkcert cert + a raw TLS relay — no
6
+ per-project proxy scripts.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """navig-devhost CLI commands."""
@@ -0,0 +1,305 @@
1
+ """navig devhost — local dev domains with trusted HTTPS.
2
+
3
+ Give any local dev server a real `.test` domain over trusted HTTPS in one command:
4
+
5
+ navig devhost add cybesis.test --port 7645
6
+ navig devhost up # → https://cybesis.test
7
+
8
+ devhost adds the hosts entry (on a dedicated loopback, coexisting with your other
9
+ .test sites), issues a trusted mkcert certificate, and runs a raw TLS relay in
10
+ front of your plain-HTTP dev server. No per-project proxy scripts.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json as _json
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ import typer
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+
25
+ from navig_devhost.engine import certs, hosts, net
26
+ from navig_devhost.engine.paths import registry_path
27
+ from navig_devhost.engine.proxy import Relay, Site
28
+ from navig_devhost.engine.registry import DevHost, JsonReadError, Registry
29
+
30
+ console = Console()
31
+ err = Console(stderr=True)
32
+
33
+ devhost_app = typer.Typer(
34
+ name="devhost",
35
+ help="🌐 Dev Host: local .test domains with trusted HTTPS for any dev server.",
36
+ no_args_is_help=True,
37
+ )
38
+
39
+
40
+ # ── helpers ─────────────────────────────────────────────────────────────────
41
+ def _now() -> str:
42
+ return time.strftime("%Y-%m-%dT%H:%M:%S")
43
+
44
+
45
+ def _load_for_write(json_out: bool) -> Registry:
46
+ """Load the registry for a read-MODIFY-write (add / remove).
47
+
48
+ Refuses to continue if registry.json exists but is transiently unreadable —
49
+ saving after a failed read would persist an empty registry over every other
50
+ host (the config-wipe class). The read-only views use ``Registry.load()``.
51
+ """
52
+ try:
53
+ return Registry.load_for_update()
54
+ except JsonReadError as e:
55
+ _fail(json_out, f"registry unreadable — not saving, to avoid wiping hosts: {e}")
56
+ raise typer.Exit(2) from e
57
+
58
+
59
+ def _sites_from(entries: list[DevHost]) -> tuple[list[Site], list[str]]:
60
+ """Build relay Sites from registry entries; collect skip reasons."""
61
+ sites, skipped = [], []
62
+ for dh in entries:
63
+ if not dh.tls:
64
+ skipped.append(f"{dh.domain}: TLS disabled")
65
+ continue
66
+ if not dh.cert or not dh.key or not Path(dh.cert).exists() or not Path(dh.key).exists():
67
+ skipped.append(f"{dh.domain}: cert missing (re-run `navig devhost add {dh.domain}`)")
68
+ continue
69
+ sites.append(Site(dh.domain, dh.ip, dh.https_port, dh.target_host, dh.target_port, dh.cert, dh.key))
70
+ return sites, skipped
71
+
72
+
73
+ # ── add ─────────────────────────────────────────────────────────────────────
74
+ @devhost_app.command()
75
+ def add(
76
+ domain: str = typer.Argument(..., help="The .test domain, e.g. cybesis.test"),
77
+ port: int = typer.Option(..., "--port", "-p", help="Dev server port to proxy to."),
78
+ ip: Optional[str] = typer.Option(None, "--ip", help="Loopback IP (default: next free 127.0.0.x)."),
79
+ target_host: str = typer.Option("127.0.0.1", "--target-host", help="Where the dev server listens."),
80
+ tls: bool = typer.Option(True, "--tls/--no-tls", help="Issue an mkcert cert and serve HTTPS."),
81
+ json_out: bool = typer.Option(False, "--json", help="Emit JSON."),
82
+ ) -> None:
83
+ """Register a dev domain: hosts entry + (optional) trusted cert."""
84
+ domain = domain.strip().lower()
85
+ reg = _load_for_write(json_out)
86
+
87
+ # resolve the loopback IP: explicit → existing hosts mapping → existing registry → next free
88
+ used_ips = [d.ip for d in reg.all()]
89
+ if ip:
90
+ chosen_ip = ip
91
+ elif (existing := hosts.entry_for(domain)):
92
+ chosen_ip = existing
93
+ elif reg.get(domain):
94
+ chosen_ip = reg.get(domain).ip # type: ignore[union-attr]
95
+ else:
96
+ chosen_ip = net.next_free_loopback(hosts.read(), *used_ips)
97
+
98
+ # hosts entry (idempotent; needs admin only if a write is required)
99
+ hres = hosts.add(chosen_ip, domain)
100
+ if not hres.ok:
101
+ _fail(json_out, f"hosts: {hres.message}")
102
+ raise typer.Exit(2)
103
+
104
+ # certificate
105
+ cert = key = None
106
+ cert_msg = "TLS disabled"
107
+ if tls:
108
+ cres = certs.generate(domain, chosen_ip)
109
+ if not cres.ok:
110
+ _fail(json_out, f"cert: {cres.message}")
111
+ raise typer.Exit(2)
112
+ cert, key, cert_msg = cres.cert, cres.key, cres.message
113
+ if not certs.ca_installed():
114
+ (err if not json_out else console).print(
115
+ "[yellow]![/yellow] mkcert CA not detected — run [cyan]mkcert -install[/cyan] once so browsers trust it."
116
+ )
117
+
118
+ dh = DevHost(
119
+ domain=domain, ip=chosen_ip, target_host=target_host, target_port=port,
120
+ tls=tls, cert=cert, key=key, created=(reg.get(domain).created if reg.get(domain) else _now()),
121
+ )
122
+ reg.put(dh)
123
+ reg.save()
124
+
125
+ if json_out:
126
+ console.print_json(_json.dumps({"ok": True, "domain": domain, "ip": chosen_ip,
127
+ "url": dh.url, "target": dh.target, "tls": tls, "cert": cert}))
128
+ return
129
+ console.print(Panel.fit(
130
+ f"[bold]{dh.url}[/bold] → {dh.target}\n"
131
+ f"[dim]{chosen_ip} · {hres.message} · {cert_msg}[/dim]",
132
+ title="devhost added", border_style="green",
133
+ ))
134
+ console.print(f"Start it: [cyan]navig devhost up {domain}[/cyan] (run your dev server on :{port} first)")
135
+
136
+
137
+ # ── list ────────────────────────────────────────────────────────────────────
138
+ @devhost_app.command("list")
139
+ def list_cmd(json_out: bool = typer.Option(False, "--json", help="Emit JSON.")) -> None:
140
+ """List registered dev domains and their live status."""
141
+ reg = Registry.load()
142
+ entries = reg.all()
143
+ if json_out:
144
+ console.print_json(_json.dumps({"domains": [
145
+ {"domain": d.domain, "url": d.url, "ip": d.ip, "target": d.target, "tls": d.tls,
146
+ "hosts_ok": hosts.entry_for(d.domain) == d.ip,
147
+ "cert_ok": bool(d.cert and Path(d.cert).exists()),
148
+ "target_up": net.target_reachable(d.target_host, d.target_port),
149
+ "serving": not net.can_bind(d.ip, d.https_port)} for d in entries]}))
150
+ return
151
+ if not entries:
152
+ console.print("[dim]No dev domains yet.[/dim] Add one: [cyan]navig devhost add app.test --port 3000[/cyan]")
153
+ return
154
+ table = Table(title="devhost domains", header_style="bold cyan")
155
+ for col in ("Domain", "URL", "→ Target", "Hosts", "Cert", "Dev up", "Serving"):
156
+ table.add_column(col)
157
+ for d in entries:
158
+ table.add_row(
159
+ d.domain, d.url, d.target,
160
+ _yn(hosts.entry_for(d.domain) == d.ip),
161
+ _yn(bool(d.cert and Path(d.cert).exists())) if d.tls else "[dim]—[/dim]",
162
+ _yn(net.target_reachable(d.target_host, d.target_port)),
163
+ _yn(not net.can_bind(d.ip, d.https_port)),
164
+ )
165
+ console.print(table)
166
+
167
+
168
+ # ── up ──────────────────────────────────────────────────────────────────────
169
+ @devhost_app.command()
170
+ def up(
171
+ domain: Optional[str] = typer.Argument(None, help="Domain to serve (default: all registered)."),
172
+ all_: bool = typer.Option(False, "--all", help="Serve every registered domain."),
173
+ ) -> None:
174
+ """Run the HTTPS relay (foreground). Ctrl+C to stop."""
175
+ reg = Registry.load()
176
+ if domain:
177
+ dh = reg.get(domain.strip().lower())
178
+ if not dh:
179
+ err.print(f"[red]✗[/red] '{domain}' is not registered. Add it: navig devhost add {domain} --port <PORT>")
180
+ raise typer.Exit(2)
181
+ entries = [dh]
182
+ else:
183
+ entries = reg.all()
184
+ if not entries:
185
+ err.print("[red]✗[/red] no domains registered. Add one: navig devhost add app.test --port 3000")
186
+ raise typer.Exit(2)
187
+
188
+ sites, skipped = _sites_from(entries)
189
+ for reason in skipped:
190
+ err.print(f"[yellow]skip[/yellow] {reason}")
191
+ if not sites:
192
+ err.print("[red]✗[/red] nothing to serve (no TLS domains with valid certs).")
193
+ raise typer.Exit(2)
194
+
195
+ relay = Relay(sites, on_log=lambda m: console.print(m))
196
+ console.print(Panel.fit("devhost relay — [dim]Ctrl+C to stop[/dim]", border_style="cyan"))
197
+ errors = relay.start()
198
+ for e in errors:
199
+ err.print(f"[red]✗[/red] {e}")
200
+ if len(errors) == len(sites):
201
+ raise typer.Exit(1)
202
+ for s in sites:
203
+ if not net.target_reachable(s.target_host, s.target_port):
204
+ err.print(f"[yellow]![/yellow] {s.domain}: dev server not up yet on :{s.target_port} "
205
+ f"— start it; the relay is ready and will connect on reload.")
206
+ relay.serve_forever()
207
+ console.print("\n[dim]devhost relay stopped.[/dim]")
208
+
209
+
210
+ # ── status ──────────────────────────────────────────────────────────────────
211
+ @devhost_app.command()
212
+ def status(json_out: bool = typer.Option(False, "--json", help="Emit JSON.")) -> None:
213
+ """One-line health per domain (hosts · cert · dev-up · serving)."""
214
+ list_cmd(json_out=json_out)
215
+
216
+
217
+ # ── remove ──────────────────────────────────────────────────────────────────
218
+ @devhost_app.command()
219
+ def remove(
220
+ domain: str = typer.Argument(..., help="Domain to remove."),
221
+ keep_cert: bool = typer.Option(False, "--keep-cert", help="Leave the mkcert files on disk."),
222
+ json_out: bool = typer.Option(False, "--json", help="Emit JSON."),
223
+ ) -> None:
224
+ """Remove a dev domain: hosts entry + cert + registry record."""
225
+ domain = domain.strip().lower()
226
+ reg = _load_for_write(json_out)
227
+ dh = reg.get(domain)
228
+
229
+ hres = hosts.remove(domain)
230
+ if not hres.ok:
231
+ # The hosts entry could not be removed (needs admin, or a write error) — do NOT
232
+ # delete the cert files or drop the registry record. Tearing those down while the
233
+ # live hosts entry survives orphans the domain (it still resolves to the loopback)
234
+ # and throws away the state needed to retry. `add` aborts on a hosts failure too;
235
+ # remove must mirror it. Re-run in an elevated terminal to complete the removal.
236
+ _fail(json_out, f"hosts: {hres.message}")
237
+ raise typer.Exit(2)
238
+
239
+ removed_certs = []
240
+ if dh and not keep_cert:
241
+ for p in (dh.cert, dh.key):
242
+ if p and Path(p).exists():
243
+ try:
244
+ Path(p).unlink()
245
+ removed_certs.append(p)
246
+ except OSError:
247
+ pass
248
+ reg.remove(domain)
249
+ reg.save()
250
+
251
+ if json_out:
252
+ console.print_json(_json.dumps({"ok": hres.ok, "domain": domain, "hosts": hres.message,
253
+ "certs_removed": removed_certs}))
254
+ return
255
+ mark = "[green]✓[/green]" if hres.ok else "[yellow]![/yellow]"
256
+ console.print(f"{mark} {domain} — {hres.message}"
257
+ + (f"; removed {len(removed_certs)} cert file(s)" if removed_certs else ""))
258
+
259
+
260
+ # ── doctor ──────────────────────────────────────────────────────────────────
261
+ @devhost_app.command()
262
+ def doctor() -> None:
263
+ """Check prerequisites: mkcert, its CA, admin for hosts edits."""
264
+ console.print(Panel.fit("navig devhost · environment check", border_style="cyan"))
265
+ ok = True
266
+
267
+ exe = certs.find_mkcert()
268
+ if exe:
269
+ console.print(f"[green]✓[/green] mkcert: [dim]{exe}[/dim]")
270
+ else:
271
+ console.print("[red]✗[/red] mkcert not found — [cyan]winget install FiloSottile.mkcert[/cyan]")
272
+ ok = False
273
+
274
+ if certs.ca_installed():
275
+ console.print(f"[green]✓[/green] local CA present: [dim]{certs.caroot()}[/dim]")
276
+ else:
277
+ console.print("[yellow]![/yellow] local CA not detected — run [cyan]mkcert -install[/cyan] once")
278
+
279
+ if hosts.can_edit():
280
+ console.print("[green]✓[/green] can edit hosts file (admin) — needed for add/remove")
281
+ else:
282
+ console.print("[yellow]![/yellow] not elevated — run add/remove in an Administrator terminal")
283
+
284
+ console.print(f"[green]✓[/green] registry: [dim]{registry_path()}[/dim]")
285
+ console.print()
286
+ if ok:
287
+ console.print("[bold green]Ready.[/bold green] Try: [cyan]navig devhost add app.test --port 3000[/cyan]")
288
+ else:
289
+ raise typer.Exit(1)
290
+
291
+
292
+ # ── tiny formatters ─────────────────────────────────────────────────────────
293
+ def _yn(v: bool) -> str:
294
+ return "[green]✓[/green]" if v else "[red]✗[/red]"
295
+
296
+
297
+ def _fail(json_out: bool, msg: str) -> None:
298
+ if json_out:
299
+ console.print_json(_json.dumps({"ok": False, "error": msg}))
300
+ else:
301
+ err.print(f"[red]✗[/red] {msg}")
302
+
303
+
304
+ if __name__ == "__main__": # python -m navig_devhost.commands.devhost
305
+ devhost_app()
@@ -0,0 +1,7 @@
1
+ """navig-devhost engine — local dev domains with trusted HTTPS."""
2
+
3
+ from navig_devhost.engine import certs, hosts, net
4
+ from navig_devhost.engine.proxy import Relay, Site
5
+ from navig_devhost.engine.registry import DevHost, Registry
6
+
7
+ __all__ = ["certs", "hosts", "net", "Relay", "Site", "DevHost", "Registry"]