cognitivesystems 0.0.2__py3-none-any.whl
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.
- cognitivesystems/__init__.py +8 -0
- cognitivesystems/__main__.py +7 -0
- cognitivesystems/bootstrap.py +10 -0
- cognitivesystems/catalog.py +65 -0
- cognitivesystems/cli.py +540 -0
- cognitivesystems/config.py +104 -0
- cognitivesystems/core/__init__.py +8 -0
- cognitivesystems/core/capabilities.py +149 -0
- cognitivesystems/core/dockerctl.py +627 -0
- cognitivesystems/core/ops/__init__.py +14 -0
- cognitivesystems/core/ops/config.py +350 -0
- cognitivesystems/core/ops/custom.py +279 -0
- cognitivesystems/core/ops/docker.py +356 -0
- cognitivesystems/core/ops/jobs.py +60 -0
- cognitivesystems/core/ops/network.py +257 -0
- cognitivesystems/core/ops/overview.py +48 -0
- cognitivesystems/core/ops/services.py +475 -0
- cognitivesystems/core/ops/system.py +149 -0
- cognitivesystems/core/registry.py +42 -0
- cognitivesystems/core/runner.py +364 -0
- cognitivesystems/credentials.py +502 -0
- cognitivesystems/data/profiles/README.md +72 -0
- cognitivesystems/data/profiles/homeserver.yml +46 -0
- cognitivesystems/data/profiles/localdev.yml +41 -0
- cognitivesystems/data/services/adguard/service.yml +19 -0
- cognitivesystems/data/services/autobrr/service.yml +18 -0
- cognitivesystems/data/services/bazarr/service.yml +21 -0
- cognitivesystems/data/services/betterdesk/service.yml +106 -0
- cognitivesystems/data/services/byparr/service.yml +25 -0
- cognitivesystems/data/services/chrome/service.yml +29 -0
- cognitivesystems/data/services/code-server/service.yml +23 -0
- cognitivesystems/data/services/crontabui/service.yml +22 -0
- cognitivesystems/data/services/crossseed/service.yml +26 -0
- cognitivesystems/data/services/firefox/service.yml +29 -0
- cognitivesystems/data/services/flaresolverr/service.yml +23 -0
- cognitivesystems/data/services/heimdall/service.yml +21 -0
- cognitivesystems/data/services/install_order.yml +8 -0
- cognitivesystems/data/services/jackett/service.yml +20 -0
- cognitivesystems/data/services/maintainerr/service.yml +19 -0
- cognitivesystems/data/services/mariadb/service.yml +26 -0
- cognitivesystems/data/services/mongodb/service.yml +19 -0
- cognitivesystems/data/services/mqtt-broker/service.yml +22 -0
- cognitivesystems/data/services/mysql/service.yml +19 -0
- cognitivesystems/data/services/ollama/service.yml +19 -0
- cognitivesystems/data/services/openwebui/service.yml +24 -0
- cognitivesystems/data/services/paperless/service.yml +74 -0
- cognitivesystems/data/services/plex/service.yml +28 -0
- cognitivesystems/data/services/portainer/service.yml +25 -0
- cognitivesystems/data/services/profilarr/service.yml +18 -0
- cognitivesystems/data/services/prowlarr/service.yml +20 -0
- cognitivesystems/data/services/qbittorrent/service.yml +28 -0
- cognitivesystems/data/services/qbittorrent/sidecars/ext_webhooks.sh +59 -0
- cognitivesystems/data/services/radarr/service.yml +21 -0
- cognitivesystems/data/services/rarrnomore/service.yml +22 -0
- cognitivesystems/data/services/seerr/service.yml +18 -0
- cognitivesystems/data/services/sftpgo/service.yml +33 -0
- cognitivesystems/data/services/snappymail/service.yml +18 -0
- cognitivesystems/data/services/sonarr/service.yml +21 -0
- cognitivesystems/data/services/stalwart/service.yml +50 -0
- cognitivesystems/data/services/stash/service.yml +28 -0
- cognitivesystems/data/services/tautulli/service.yml +21 -0
- cognitivesystems/data/services/threadfin/service.yml +20 -0
- cognitivesystems/data/services/traefik/service.yml +116 -0
- cognitivesystems/data/services/traefik/sidecars/dynamic_config/tls.yml +6 -0
- cognitivesystems/data/services/uploadarr/service.yml +22 -0
- cognitivesystems/data/services/vaultwarden/service.yml +24 -0
- cognitivesystems/data/services/watchtower/service.yml +23 -0
- cognitivesystems/data/services/wg-easy/service.yml +31 -0
- cognitivesystems/data/services/whisparr/service.yml +21 -0
- cognitivesystems/data/services/wizarr/service.yml +18 -0
- cognitivesystems/deploy.py +453 -0
- cognitivesystems/hooks.py +335 -0
- cognitivesystems/hydrate.py +64 -0
- cognitivesystems/render.py +1058 -0
- cognitivesystems/state.py +393 -0
- cognitivesystems/system.py +351 -0
- cognitivesystems/tui/__init__.py +13 -0
- cognitivesystems/tui/app.py +405 -0
- cognitivesystems/tui/screens/__init__.py +13 -0
- cognitivesystems/tui/screens/config.py +97 -0
- cognitivesystems/tui/screens/custom.py +102 -0
- cognitivesystems/tui/screens/docker.py +154 -0
- cognitivesystems/tui/screens/jobs.py +72 -0
- cognitivesystems/tui/screens/network.py +130 -0
- cognitivesystems/tui/screens/overview.py +76 -0
- cognitivesystems/tui/screens/registries.py +107 -0
- cognitivesystems/tui/screens/secrets.py +123 -0
- cognitivesystems/tui/screens/services.py +228 -0
- cognitivesystems/tui/screens/system.py +135 -0
- cognitivesystems/web/__init__.py +7 -0
- cognitivesystems/web/app.py +128 -0
- cognitivesystems/web/auth.py +167 -0
- cognitivesystems/web/deps.py +187 -0
- cognitivesystems/web/routers/__init__.py +12 -0
- cognitivesystems/web/routers/config.py +319 -0
- cognitivesystems/web/routers/custom.py +263 -0
- cognitivesystems/web/routers/docker.py +501 -0
- cognitivesystems/web/routers/jobs.py +100 -0
- cognitivesystems/web/routers/networks.py +236 -0
- cognitivesystems/web/routers/overview.py +31 -0
- cognitivesystems/web/routers/services.py +342 -0
- cognitivesystems/web/routers/settings.py +70 -0
- cognitivesystems/web/routers/system.py +202 -0
- cognitivesystems/web/schemas.py +54 -0
- cognitivesystems/web/security.py +235 -0
- cognitivesystems/web/server.py +72 -0
- cognitivesystems/web/settings.py +60 -0
- cognitivesystems/web/static/assets/index-CRdsikO7.js +71 -0
- cognitivesystems/web/static/assets/index-DRZ00vpW.css +1 -0
- cognitivesystems/web/static/index.html +13 -0
- cognitivesystems-0.0.2.dist-info/METADATA +229 -0
- cognitivesystems-0.0.2.dist-info/RECORD +114 -0
- cognitivesystems-0.0.2.dist-info/WHEEL +4 -0
- cognitivesystems-0.0.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Bootstrap paths — default state file location (``--state`` override only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def default_state_path() -> pathlib.Path:
|
|
9
|
+
"""Return the default ``state.json`` path under the user's home directory."""
|
|
10
|
+
return pathlib.Path.home() / "cognitivesystems" / "state" / "state.json"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Catalog access — thin wrapper over the renderer's loader.
|
|
2
|
+
|
|
3
|
+
Keeps one source of truth (``render.load_catalog``) while giving the CLI/TUI a
|
|
4
|
+
small, import-friendly surface that resolves the packaged-or-repo catalog dir.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import pathlib
|
|
9
|
+
|
|
10
|
+
from . import render
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load(services_dir: pathlib.Path | None = None, *, overlay: bool = True) -> dict:
|
|
14
|
+
"""Load the service catalog (shipped services plus the optional user overlay).
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
services_dir : pathlib.Path or None, optional
|
|
19
|
+
Directory holding the per-service ``service.yml`` files. ``None`` resolves the
|
|
20
|
+
packaged-or-repo default via :func:`render.default_services_dir`.
|
|
21
|
+
overlay : bool, default: True
|
|
22
|
+
When ``True``, merge the user-catalog overlay (custom/community apps) over the
|
|
23
|
+
shipped services.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
dict
|
|
28
|
+
The merged catalog, shaped ``{"install_order": [...], "services": {...}}``.
|
|
29
|
+
"""
|
|
30
|
+
return render.load_catalog(
|
|
31
|
+
services_dir or render.default_services_dir(),
|
|
32
|
+
render.default_overlay_dir() if overlay else None) # include custom/community apps
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def meta(services_dir: pathlib.Path | None = None) -> dict:
|
|
36
|
+
"""Return the compact catalog summary consumed by the deploy/CLI/TUI layers.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
services_dir : pathlib.Path or None, optional
|
|
41
|
+
Catalog directory; ``None`` uses the packaged-or-repo default.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
dict
|
|
46
|
+
The catalog meta (display names, subdomains, host ports, auth, …) as produced
|
|
47
|
+
by :func:`render._meta`.
|
|
48
|
+
"""
|
|
49
|
+
return render._meta(load(services_dir))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def service_names(services_dir: pathlib.Path | None = None) -> list[str]:
|
|
53
|
+
"""List the catalog's service keys, sorted.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
services_dir : pathlib.Path or None, optional
|
|
58
|
+
Catalog directory; ``None`` uses the packaged-or-repo default.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
list of str
|
|
63
|
+
Sorted service keys.
|
|
64
|
+
"""
|
|
65
|
+
return sorted(load(services_dir)["services"].keys())
|
cognitivesystems/cli.py
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""CognitiveSystems CLI.
|
|
2
|
+
|
|
3
|
+
Cross-platform commands (``list`` / ``meta`` / ``render`` / ``init`` / ``configure``)
|
|
4
|
+
plus the Linux-only deploy commands (``install`` / ``update`` / ``uninstall`` / ``system``).
|
|
5
|
+
Running ``cognitivesystems`` with no subcommand launches the web dashboard, falling back
|
|
6
|
+
to the Textual TUI when the ``[web]`` extra is absent or the dashboard is disabled.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import importlib.util
|
|
12
|
+
import json
|
|
13
|
+
import pathlib
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from . import __version__, bootstrap, catalog, config, credentials, render, state
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _split(items: list[str] | None) -> list[str]:
|
|
20
|
+
"""Flatten repeated and comma-separated CLI values into a flat list.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
items : list of str or None
|
|
25
|
+
Raw argument values (each may itself be comma-separated).
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
list of str
|
|
30
|
+
The non-empty, comma-split items.
|
|
31
|
+
"""
|
|
32
|
+
return [s for grp in (items or []) for s in str(grp).split(",") if s]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ─── cross-platform ──────────────────────────────────────────────────────────
|
|
36
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
37
|
+
"""``list`` — print catalog services, marking installed ones with ``*``.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
args : argparse.Namespace
|
|
42
|
+
Parsed arguments (uses ``args.state``).
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
int
|
|
47
|
+
Process exit code.
|
|
48
|
+
"""
|
|
49
|
+
m = catalog.meta()
|
|
50
|
+
inst = (state.load(args.state).get("installed") or {}) if pathlib.Path(args.state).is_file() else {}
|
|
51
|
+
for name in sorted(m["services"]):
|
|
52
|
+
d = m["services"][name]
|
|
53
|
+
mark = "*" if name in inst else " "
|
|
54
|
+
print(f"{mark} {name:20} {d.get('display_name', name)}")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cmd_meta(args: argparse.Namespace) -> int:
|
|
59
|
+
"""``meta`` — print the catalog metadata as JSON.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
args : argparse.Namespace
|
|
64
|
+
Parsed arguments (unused).
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
int
|
|
69
|
+
Process exit code.
|
|
70
|
+
"""
|
|
71
|
+
print(json.dumps(catalog.meta(), indent=2))
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cmd_render(args: argparse.Namespace) -> int:
|
|
76
|
+
"""``render`` — render one service's compose + ``.env`` to a directory.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
args : argparse.Namespace
|
|
81
|
+
Parsed arguments (service, ``--out``, ``--services-dir``, ``--no-sidecars``, state).
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
int
|
|
86
|
+
Process exit code from the renderer.
|
|
87
|
+
"""
|
|
88
|
+
argv = [args.service, "--state", args.state, "--out-root", args.out]
|
|
89
|
+
if args.services_dir:
|
|
90
|
+
argv += ["--services-dir", args.services_dir]
|
|
91
|
+
if args.no_sidecars:
|
|
92
|
+
argv += ["--no-sidecars"]
|
|
93
|
+
return render.main(argv)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_tui(args: argparse.Namespace) -> int:
|
|
97
|
+
"""``tui`` — launch the Textual terminal UI.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
args : argparse.Namespace
|
|
102
|
+
Parsed arguments (uses ``args.state``).
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
int
|
|
107
|
+
Process exit code (``1`` if Textual is unavailable).
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
from . import tui
|
|
111
|
+
except ImportError as e: # textual missing
|
|
112
|
+
print(f"TUI unavailable ({e}). Reinstall: pip install cognitivesystems", file=sys.stderr)
|
|
113
|
+
return 1
|
|
114
|
+
return tui.run(args.state)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _cmd_web(args: argparse.Namespace) -> int:
|
|
118
|
+
"""``web`` — launch the FastAPI web dashboard.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
args : argparse.Namespace
|
|
123
|
+
Parsed arguments (``--host``, ``--port``, ``--no-open``, state).
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
int
|
|
128
|
+
Process exit code (``1`` if the ``[web]`` extra is not installed).
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
from .web import server
|
|
132
|
+
except ImportError as e: # [web] extra not installed
|
|
133
|
+
print(f"web dashboard unavailable ({e}). Install: pip install cognitivesystems[web]",
|
|
134
|
+
file=sys.stderr)
|
|
135
|
+
return 1
|
|
136
|
+
return server.serve(state_path=args.state, host=args.host, port=args.port,
|
|
137
|
+
open_browser=not getattr(args, "no_open", False))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _web_disabled(state_path: str) -> bool:
|
|
141
|
+
"""Report whether the auto-launching dashboard has been explicitly disabled.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
state_path : str
|
|
146
|
+
Path to ``state.json``.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
bool
|
|
151
|
+
``True`` if ``state.dashboard.enabled`` is ``False``.
|
|
152
|
+
"""
|
|
153
|
+
p = pathlib.Path(state_path)
|
|
154
|
+
data = state.load(p) if p.is_file() else {}
|
|
155
|
+
return (data.get("dashboard") or {}).get("enabled") is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _web_available() -> bool:
|
|
159
|
+
"""Report whether the web dashboard dependencies are importable.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
bool
|
|
164
|
+
``True`` if both ``fastapi`` and ``uvicorn`` can be found.
|
|
165
|
+
"""
|
|
166
|
+
return all(importlib.util.find_spec(m) is not None for m in ("fastapi", "uvicorn"))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cmd_default(args: argparse.Namespace) -> int:
|
|
170
|
+
"""No subcommand: launch the web dashboard, falling back to the TUI.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
args : argparse.Namespace
|
|
175
|
+
Parsed arguments.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
int
|
|
180
|
+
Process exit code from whichever interface is launched.
|
|
181
|
+
"""
|
|
182
|
+
if not _web_disabled(args.state) and _web_available():
|
|
183
|
+
return _cmd_web(args)
|
|
184
|
+
return _cmd_tui(args)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _cmd_init(args: argparse.Namespace) -> int:
|
|
188
|
+
"""``init`` — create ``state.json`` from an install profile.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
args : argparse.Namespace
|
|
193
|
+
Parsed arguments (``--profile``, state).
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
int
|
|
198
|
+
Process exit code (``1`` if the profile is not found).
|
|
199
|
+
"""
|
|
200
|
+
data_path = pathlib.Path(args.state)
|
|
201
|
+
try:
|
|
202
|
+
state.init(data_path, args.profile)
|
|
203
|
+
except FileNotFoundError as e:
|
|
204
|
+
print(f"cognitivesystems init: {e}", file=sys.stderr)
|
|
205
|
+
return 1
|
|
206
|
+
print(f"init: wrote {data_path} (profile: {args.profile})")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cmd_configure(args: argparse.Namespace) -> int:
|
|
211
|
+
"""``configure`` — interactively set credentials/secrets into ``state.json``.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
args : argparse.Namespace
|
|
216
|
+
Parsed arguments (``--profile``, ``--service``, ``--registries``, state).
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
int
|
|
221
|
+
Process exit code.
|
|
222
|
+
"""
|
|
223
|
+
data_path = pathlib.Path(args.state)
|
|
224
|
+
data = state.load(data_path) or state.default_state()
|
|
225
|
+
if not data.get("site"):
|
|
226
|
+
try:
|
|
227
|
+
state.load_profile(data, args.profile)
|
|
228
|
+
data.setdefault("site", {})["profile"] = args.profile
|
|
229
|
+
except FileNotFoundError:
|
|
230
|
+
pass
|
|
231
|
+
credentials.configure(data, catalog.meta(),
|
|
232
|
+
services=_split(args.service) or None,
|
|
233
|
+
registries=args.registries)
|
|
234
|
+
state.save(data_path, data)
|
|
235
|
+
print(f"configure: saved {data_path}")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ─── deploy (Linux) ──────────────────────────────────────────────────────────
|
|
240
|
+
def _load_for_deploy(args: argparse.Namespace):
|
|
241
|
+
"""Load state + meta and run preflight for a deploy command.
|
|
242
|
+
|
|
243
|
+
Seeds the default profile when state has no ``.site`` yet, then runs
|
|
244
|
+
:func:`deploy.preflight` (which creates the profile's data root).
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
args : argparse.Namespace
|
|
249
|
+
Parsed arguments (uses ``args.state``).
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
tuple
|
|
254
|
+
``(deploy_module, data_path, meta, data)``.
|
|
255
|
+
"""
|
|
256
|
+
from . import deploy
|
|
257
|
+
data_path = pathlib.Path(args.state)
|
|
258
|
+
meta = catalog.meta()
|
|
259
|
+
data = state.load(data_path) or state.default_state()
|
|
260
|
+
if not data.get("site"): # seed the default profile when state has no .site yet
|
|
261
|
+
try:
|
|
262
|
+
state.load_profile(data, "homeserver")
|
|
263
|
+
data.setdefault("site", {})["profile"] = "homeserver"
|
|
264
|
+
except FileNotFoundError:
|
|
265
|
+
pass
|
|
266
|
+
deploy.preflight(data) # after state load: preflight creates the profile's data root
|
|
267
|
+
return deploy, data_path, meta, data
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _cmd_install(args: argparse.Namespace) -> int:
|
|
271
|
+
"""``install`` — install services (Linux + Docker).
|
|
272
|
+
|
|
273
|
+
Applies CLI overrides, requires the needed secrets non-interactively, resolves install
|
|
274
|
+
order, checks port conflicts, then installs each service (one failure does not abort the
|
|
275
|
+
batch; failures are summarized).
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
args : argparse.Namespace
|
|
280
|
+
Parsed arguments (services, ``--mode``/``--domain``/``--wildcard``/…, state).
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
int
|
|
285
|
+
``0`` if all succeeded, ``1`` if any service or a precheck failed.
|
|
286
|
+
"""
|
|
287
|
+
from .deploy import DeployError
|
|
288
|
+
try:
|
|
289
|
+
deploy, data_path, meta, data = _load_for_deploy(args)
|
|
290
|
+
state.apply_cli_overrides(
|
|
291
|
+
data, puid=args.puid, pgid=args.pgid, mode=args.mode, domain=args.domain,
|
|
292
|
+
wildcard=(True if args.wildcard else None), cf_dns_token=args.cf_token,
|
|
293
|
+
)
|
|
294
|
+
# non-interactive: required secrets must already be in env/state (the TUI
|
|
295
|
+
# spawns install as a subprocess with no stdin, so it cannot prompt)
|
|
296
|
+
credentials.require_globals(data, interactive=False)
|
|
297
|
+
state.save(data_path, data)
|
|
298
|
+
deploy.registry_login(data)
|
|
299
|
+
ordered = deploy.resolve_deps(_split(args.services), meta)
|
|
300
|
+
# fail closed before touching anything if two services would fight over a
|
|
301
|
+
# host port (e.g. MariaDB + MySQL on 3306) — clearer than a mid-deploy abort
|
|
302
|
+
deploy.check_port_conflicts(ordered, meta, data)
|
|
303
|
+
failed: list[str] = []
|
|
304
|
+
for svc in ordered:
|
|
305
|
+
# one failing service must not abort the batch: log it, keep going
|
|
306
|
+
# (dependents of a failed dependency simply fail too and get reported),
|
|
307
|
+
# then summarize at the end and exit nonzero if anything failed
|
|
308
|
+
try:
|
|
309
|
+
if state.is_installed(data, svc):
|
|
310
|
+
if args.force:
|
|
311
|
+
deploy.uninstall_one(svc, data, meta, data_path, purge=args.purge)
|
|
312
|
+
else:
|
|
313
|
+
print(f"{svc} already installed; skipping (use --force to reinstall)")
|
|
314
|
+
continue
|
|
315
|
+
deploy.install_one(svc, data, meta, data_path, auto_update=args.auto_update)
|
|
316
|
+
except DeployError as e:
|
|
317
|
+
failed.append(svc)
|
|
318
|
+
print(f"cognitivesystems install: {svc} failed: {e}", file=sys.stderr)
|
|
319
|
+
if failed:
|
|
320
|
+
print(f"cognitivesystems install: {len(failed)}/{len(ordered)} services failed: "
|
|
321
|
+
f"{', '.join(failed)} — fix the cause and re-run the same install "
|
|
322
|
+
"(succeeded services are skipped)", file=sys.stderr)
|
|
323
|
+
return 1
|
|
324
|
+
except (DeployError, credentials.CredentialError) as e:
|
|
325
|
+
print(f"cognitivesystems install: {e}", file=sys.stderr)
|
|
326
|
+
return 1
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _cmd_update(args: argparse.Namespace) -> int:
|
|
331
|
+
"""``update`` — update the given services, or all installed when none are named.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
args : argparse.Namespace
|
|
336
|
+
Parsed arguments (optional services, state).
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
int
|
|
341
|
+
``0`` on success, ``1`` if nothing is installed or an update fails.
|
|
342
|
+
"""
|
|
343
|
+
from .deploy import DeployError
|
|
344
|
+
try:
|
|
345
|
+
deploy, data_path, meta, data = _load_for_deploy(args)
|
|
346
|
+
svcs = _split(args.services) or list((data.get("installed") or {}).keys())
|
|
347
|
+
if not svcs:
|
|
348
|
+
print("nothing installed to update.", file=sys.stderr)
|
|
349
|
+
return 1
|
|
350
|
+
for svc in svcs:
|
|
351
|
+
deploy.update_one(svc, data, meta, data_path)
|
|
352
|
+
except DeployError as e:
|
|
353
|
+
print(f"cognitivesystems update: {e}", file=sys.stderr)
|
|
354
|
+
return 1
|
|
355
|
+
return 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _cmd_uninstall(args: argparse.Namespace) -> int:
|
|
359
|
+
"""``uninstall`` — remove the given services (optionally purging their data).
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
args : argparse.Namespace
|
|
364
|
+
Parsed arguments (services, ``--purge``, state).
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
int
|
|
369
|
+
``0`` on success, ``1`` if an uninstall fails.
|
|
370
|
+
"""
|
|
371
|
+
from .deploy import DeployError
|
|
372
|
+
try:
|
|
373
|
+
deploy, data_path, meta, data = _load_for_deploy(args)
|
|
374
|
+
for svc in _split(args.services):
|
|
375
|
+
deploy.uninstall_one(svc, data, meta, data_path, purge=args.purge)
|
|
376
|
+
except DeployError as e:
|
|
377
|
+
print(f"cognitivesystems uninstall: {e}", file=sys.stderr)
|
|
378
|
+
return 1
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _cmd_system(args: argparse.Namespace) -> int:
|
|
383
|
+
"""``system`` — run a host-admin action (ssh-keygen / change-password / media-user).
|
|
384
|
+
|
|
385
|
+
Resolves the active profile's data root from state (ssh-keygen publishes under it).
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
args : argparse.Namespace
|
|
390
|
+
Parsed arguments (``action``, passthrough ``args``, state).
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
int
|
|
395
|
+
The action's exit code (``1`` on a deploy error).
|
|
396
|
+
"""
|
|
397
|
+
from . import system
|
|
398
|
+
from .deploy import DeployError
|
|
399
|
+
try:
|
|
400
|
+
# ssh-keygen publishes under the active profile's data root; resolve it from state
|
|
401
|
+
data = state.load(pathlib.Path(args.state)) or state.default_state()
|
|
402
|
+
if not data.get("site"): # seed the default profile so data_root resolves
|
|
403
|
+
try:
|
|
404
|
+
state.load_profile(data, "homeserver")
|
|
405
|
+
data.setdefault("site", {})["profile"] = "homeserver"
|
|
406
|
+
except FileNotFoundError:
|
|
407
|
+
pass
|
|
408
|
+
return system.run(args.action, args.args, data_root=config.data_root_for(data))
|
|
409
|
+
except (DeployError, config.ConfigError) as e:
|
|
410
|
+
print(f"cognitivesystems system: {e}", file=sys.stderr)
|
|
411
|
+
return 1
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ─── parser ──────────────────────────────────────────────────────────────────
|
|
415
|
+
def _add_state_arg(sp: argparse.ArgumentParser) -> None:
|
|
416
|
+
"""Add the shared ``--state`` argument to a subparser.
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
sp : argparse.ArgumentParser
|
|
421
|
+
The subparser to extend.
|
|
422
|
+
"""
|
|
423
|
+
sp.add_argument("--state", default=str(bootstrap.default_state_path()),
|
|
424
|
+
help="path to state.json (default: %(default)s)")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
428
|
+
"""Build the top-level argument parser with all subcommands.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
argparse.ArgumentParser
|
|
433
|
+
The configured parser.
|
|
434
|
+
"""
|
|
435
|
+
p = argparse.ArgumentParser(
|
|
436
|
+
prog="cognitivesystems",
|
|
437
|
+
description="CognitiveSystems - catalog-driven Docker homeserver deployer.",
|
|
438
|
+
)
|
|
439
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
440
|
+
sub = p.add_subparsers(dest="cmd")
|
|
441
|
+
|
|
442
|
+
sp = sub.add_parser("list", help="list catalog services (* = installed)")
|
|
443
|
+
_add_state_arg(sp)
|
|
444
|
+
sp.set_defaults(func=_cmd_list)
|
|
445
|
+
|
|
446
|
+
sp = sub.add_parser("meta", help="print catalog metadata as JSON")
|
|
447
|
+
sp.set_defaults(func=_cmd_meta)
|
|
448
|
+
|
|
449
|
+
sp = sub.add_parser("tui", help="launch the terminal UI")
|
|
450
|
+
_add_state_arg(sp)
|
|
451
|
+
sp.set_defaults(func=_cmd_tui)
|
|
452
|
+
|
|
453
|
+
sp = sub.add_parser("web", help="launch the web dashboard (host control plane; auto with no command)")
|
|
454
|
+
sp.add_argument("--host", default="127.0.0.1",
|
|
455
|
+
help="bind address (default loopback; off-loopback exposes full host/docker)")
|
|
456
|
+
sp.add_argument("--port", type=int, default=8800)
|
|
457
|
+
sp.add_argument("--no-open", dest="no_open", action="store_true", help="don't open a browser")
|
|
458
|
+
_add_state_arg(sp)
|
|
459
|
+
sp.set_defaults(func=_cmd_web)
|
|
460
|
+
|
|
461
|
+
sp = sub.add_parser("render", help="render a service's compose + .env")
|
|
462
|
+
sp.add_argument("service")
|
|
463
|
+
sp.add_argument("--out", default="./render-out")
|
|
464
|
+
sp.add_argument("--services-dir", default=None)
|
|
465
|
+
sp.add_argument("--no-sidecars", action="store_true")
|
|
466
|
+
_add_state_arg(sp)
|
|
467
|
+
sp.set_defaults(func=_cmd_render)
|
|
468
|
+
|
|
469
|
+
sp = sub.add_parser("init", help="create state.json from a profile")
|
|
470
|
+
sp.add_argument("--profile", default="homeserver")
|
|
471
|
+
_add_state_arg(sp)
|
|
472
|
+
sp.set_defaults(func=_cmd_init)
|
|
473
|
+
|
|
474
|
+
sp = sub.add_parser("configure",
|
|
475
|
+
help="set credentials/secrets (prompt; Enter = generate) → state.json")
|
|
476
|
+
sp.add_argument("--profile", default="homeserver")
|
|
477
|
+
sp.add_argument("--service", action="append",
|
|
478
|
+
help="limit per-service password prompts (repeatable)")
|
|
479
|
+
sp.add_argument("--registries", action="store_true",
|
|
480
|
+
help="also add/edit private registry logins")
|
|
481
|
+
_add_state_arg(sp)
|
|
482
|
+
sp.set_defaults(func=_cmd_configure)
|
|
483
|
+
|
|
484
|
+
sp = sub.add_parser("install", help="install services (Linux + Docker)")
|
|
485
|
+
sp.add_argument("services", nargs="+", help="service names (comma or space separated)")
|
|
486
|
+
sp.add_argument("--mode", choices=["public", "private", "local"], default=None)
|
|
487
|
+
sp.add_argument("--domain", default=None)
|
|
488
|
+
sp.add_argument("--puid", default=None)
|
|
489
|
+
sp.add_argument("--pgid", default=None)
|
|
490
|
+
sp.add_argument("--wildcard", action="store_true")
|
|
491
|
+
sp.add_argument("--cf-token", dest="cf_token", default=None,
|
|
492
|
+
help="scoped Cloudflare DNS API token (wildcard DNS-01)")
|
|
493
|
+
sp.add_argument("--force", action="store_true", help="reinstall if already installed")
|
|
494
|
+
sp.add_argument("--purge", action="store_true", help="also drop data dir (with --force)")
|
|
495
|
+
g = sp.add_mutually_exclusive_group()
|
|
496
|
+
g.add_argument("--auto-update", dest="auto_update", action="store_true", default=None)
|
|
497
|
+
g.add_argument("--no-auto-update", dest="auto_update", action="store_false")
|
|
498
|
+
_add_state_arg(sp)
|
|
499
|
+
sp.set_defaults(func=_cmd_install)
|
|
500
|
+
|
|
501
|
+
sp = sub.add_parser("update", help="update services (default: all installed)")
|
|
502
|
+
sp.add_argument("services", nargs="*")
|
|
503
|
+
_add_state_arg(sp)
|
|
504
|
+
sp.set_defaults(func=_cmd_update)
|
|
505
|
+
|
|
506
|
+
sp = sub.add_parser("uninstall", help="uninstall services")
|
|
507
|
+
sp.add_argument("services", nargs="+")
|
|
508
|
+
sp.add_argument("--purge", action="store_true", help="also drop data dir")
|
|
509
|
+
_add_state_arg(sp)
|
|
510
|
+
sp.set_defaults(func=_cmd_uninstall)
|
|
511
|
+
|
|
512
|
+
sp = sub.add_parser("system", help="host admin (Linux): ssh-keygen | change-password | media-user")
|
|
513
|
+
sp.add_argument("action", choices=["ssh-keygen", "change-password", "media-user"])
|
|
514
|
+
_add_state_arg(sp) # ssh-keygen resolves the profile's data_root from state; pass --state BEFORE the action
|
|
515
|
+
sp.add_argument("args", nargs=argparse.REMAINDER, help="arguments passed through to the action")
|
|
516
|
+
sp.set_defaults(func=_cmd_system)
|
|
517
|
+
|
|
518
|
+
return p
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def main(argv: list[str] | None = None) -> int:
|
|
522
|
+
"""CLI entry point: parse arguments and dispatch to the selected command.
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
argv : list of str or None, optional
|
|
527
|
+
Argument vector; ``None`` uses ``sys.argv``.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
int
|
|
532
|
+
The selected command's exit code.
|
|
533
|
+
"""
|
|
534
|
+
args = build_parser().parse_args(argv)
|
|
535
|
+
func = getattr(args, "func", None)
|
|
536
|
+
if func is None: # no subcommand → web dashboard (auto), else the TUI
|
|
537
|
+
ns = argparse.Namespace(state=str(bootstrap.default_state_path()), host="127.0.0.1",
|
|
538
|
+
port=8800, no_open=False)
|
|
539
|
+
return _cmd_default(ns)
|
|
540
|
+
return func(args)
|