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.
- navig_devhost-0.1.0/LICENSE +173 -0
- navig_devhost-0.1.0/PKG-INFO +89 -0
- navig_devhost-0.1.0/README.md +65 -0
- navig_devhost-0.1.0/navig_devhost/__init__.py +9 -0
- navig_devhost-0.1.0/navig_devhost/commands/__init__.py +1 -0
- navig_devhost-0.1.0/navig_devhost/commands/devhost.py +305 -0
- navig_devhost-0.1.0/navig_devhost/engine/__init__.py +7 -0
- navig_devhost-0.1.0/navig_devhost/engine/certs.py +80 -0
- navig_devhost-0.1.0/navig_devhost/engine/hosts.py +117 -0
- navig_devhost-0.1.0/navig_devhost/engine/net.py +48 -0
- navig_devhost-0.1.0/navig_devhost/engine/paths.py +29 -0
- navig_devhost-0.1.0/navig_devhost/engine/proxy.py +173 -0
- navig_devhost-0.1.0/navig_devhost/engine/registry.py +120 -0
- navig_devhost-0.1.0/navig_devhost/plugin.py +45 -0
- navig_devhost-0.1.0/navig_devhost/skills/devhost/SKILL.md +34 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/PKG-INFO +89 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/SOURCES.txt +26 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/dependency_links.txt +1 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/entry_points.txt +5 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/requires.txt +1 -0
- navig_devhost-0.1.0/navig_devhost.egg-info/top_level.txt +1 -0
- navig_devhost-0.1.0/pyproject.toml +53 -0
- navig_devhost-0.1.0/setup.cfg +4 -0
- navig_devhost-0.1.0/tests/test_engine.py +83 -0
- navig_devhost-0.1.0/tests/test_hosts.py +111 -0
- navig_devhost-0.1.0/tests/test_registry_no_wipe.py +103 -0
- navig_devhost-0.1.0/tests/test_remove_command.py +89 -0
- 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"]
|