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.
Files changed (114) hide show
  1. cognitivesystems/__init__.py +8 -0
  2. cognitivesystems/__main__.py +7 -0
  3. cognitivesystems/bootstrap.py +10 -0
  4. cognitivesystems/catalog.py +65 -0
  5. cognitivesystems/cli.py +540 -0
  6. cognitivesystems/config.py +104 -0
  7. cognitivesystems/core/__init__.py +8 -0
  8. cognitivesystems/core/capabilities.py +149 -0
  9. cognitivesystems/core/dockerctl.py +627 -0
  10. cognitivesystems/core/ops/__init__.py +14 -0
  11. cognitivesystems/core/ops/config.py +350 -0
  12. cognitivesystems/core/ops/custom.py +279 -0
  13. cognitivesystems/core/ops/docker.py +356 -0
  14. cognitivesystems/core/ops/jobs.py +60 -0
  15. cognitivesystems/core/ops/network.py +257 -0
  16. cognitivesystems/core/ops/overview.py +48 -0
  17. cognitivesystems/core/ops/services.py +475 -0
  18. cognitivesystems/core/ops/system.py +149 -0
  19. cognitivesystems/core/registry.py +42 -0
  20. cognitivesystems/core/runner.py +364 -0
  21. cognitivesystems/credentials.py +502 -0
  22. cognitivesystems/data/profiles/README.md +72 -0
  23. cognitivesystems/data/profiles/homeserver.yml +46 -0
  24. cognitivesystems/data/profiles/localdev.yml +41 -0
  25. cognitivesystems/data/services/adguard/service.yml +19 -0
  26. cognitivesystems/data/services/autobrr/service.yml +18 -0
  27. cognitivesystems/data/services/bazarr/service.yml +21 -0
  28. cognitivesystems/data/services/betterdesk/service.yml +106 -0
  29. cognitivesystems/data/services/byparr/service.yml +25 -0
  30. cognitivesystems/data/services/chrome/service.yml +29 -0
  31. cognitivesystems/data/services/code-server/service.yml +23 -0
  32. cognitivesystems/data/services/crontabui/service.yml +22 -0
  33. cognitivesystems/data/services/crossseed/service.yml +26 -0
  34. cognitivesystems/data/services/firefox/service.yml +29 -0
  35. cognitivesystems/data/services/flaresolverr/service.yml +23 -0
  36. cognitivesystems/data/services/heimdall/service.yml +21 -0
  37. cognitivesystems/data/services/install_order.yml +8 -0
  38. cognitivesystems/data/services/jackett/service.yml +20 -0
  39. cognitivesystems/data/services/maintainerr/service.yml +19 -0
  40. cognitivesystems/data/services/mariadb/service.yml +26 -0
  41. cognitivesystems/data/services/mongodb/service.yml +19 -0
  42. cognitivesystems/data/services/mqtt-broker/service.yml +22 -0
  43. cognitivesystems/data/services/mysql/service.yml +19 -0
  44. cognitivesystems/data/services/ollama/service.yml +19 -0
  45. cognitivesystems/data/services/openwebui/service.yml +24 -0
  46. cognitivesystems/data/services/paperless/service.yml +74 -0
  47. cognitivesystems/data/services/plex/service.yml +28 -0
  48. cognitivesystems/data/services/portainer/service.yml +25 -0
  49. cognitivesystems/data/services/profilarr/service.yml +18 -0
  50. cognitivesystems/data/services/prowlarr/service.yml +20 -0
  51. cognitivesystems/data/services/qbittorrent/service.yml +28 -0
  52. cognitivesystems/data/services/qbittorrent/sidecars/ext_webhooks.sh +59 -0
  53. cognitivesystems/data/services/radarr/service.yml +21 -0
  54. cognitivesystems/data/services/rarrnomore/service.yml +22 -0
  55. cognitivesystems/data/services/seerr/service.yml +18 -0
  56. cognitivesystems/data/services/sftpgo/service.yml +33 -0
  57. cognitivesystems/data/services/snappymail/service.yml +18 -0
  58. cognitivesystems/data/services/sonarr/service.yml +21 -0
  59. cognitivesystems/data/services/stalwart/service.yml +50 -0
  60. cognitivesystems/data/services/stash/service.yml +28 -0
  61. cognitivesystems/data/services/tautulli/service.yml +21 -0
  62. cognitivesystems/data/services/threadfin/service.yml +20 -0
  63. cognitivesystems/data/services/traefik/service.yml +116 -0
  64. cognitivesystems/data/services/traefik/sidecars/dynamic_config/tls.yml +6 -0
  65. cognitivesystems/data/services/uploadarr/service.yml +22 -0
  66. cognitivesystems/data/services/vaultwarden/service.yml +24 -0
  67. cognitivesystems/data/services/watchtower/service.yml +23 -0
  68. cognitivesystems/data/services/wg-easy/service.yml +31 -0
  69. cognitivesystems/data/services/whisparr/service.yml +21 -0
  70. cognitivesystems/data/services/wizarr/service.yml +18 -0
  71. cognitivesystems/deploy.py +453 -0
  72. cognitivesystems/hooks.py +335 -0
  73. cognitivesystems/hydrate.py +64 -0
  74. cognitivesystems/render.py +1058 -0
  75. cognitivesystems/state.py +393 -0
  76. cognitivesystems/system.py +351 -0
  77. cognitivesystems/tui/__init__.py +13 -0
  78. cognitivesystems/tui/app.py +405 -0
  79. cognitivesystems/tui/screens/__init__.py +13 -0
  80. cognitivesystems/tui/screens/config.py +97 -0
  81. cognitivesystems/tui/screens/custom.py +102 -0
  82. cognitivesystems/tui/screens/docker.py +154 -0
  83. cognitivesystems/tui/screens/jobs.py +72 -0
  84. cognitivesystems/tui/screens/network.py +130 -0
  85. cognitivesystems/tui/screens/overview.py +76 -0
  86. cognitivesystems/tui/screens/registries.py +107 -0
  87. cognitivesystems/tui/screens/secrets.py +123 -0
  88. cognitivesystems/tui/screens/services.py +228 -0
  89. cognitivesystems/tui/screens/system.py +135 -0
  90. cognitivesystems/web/__init__.py +7 -0
  91. cognitivesystems/web/app.py +128 -0
  92. cognitivesystems/web/auth.py +167 -0
  93. cognitivesystems/web/deps.py +187 -0
  94. cognitivesystems/web/routers/__init__.py +12 -0
  95. cognitivesystems/web/routers/config.py +319 -0
  96. cognitivesystems/web/routers/custom.py +263 -0
  97. cognitivesystems/web/routers/docker.py +501 -0
  98. cognitivesystems/web/routers/jobs.py +100 -0
  99. cognitivesystems/web/routers/networks.py +236 -0
  100. cognitivesystems/web/routers/overview.py +31 -0
  101. cognitivesystems/web/routers/services.py +342 -0
  102. cognitivesystems/web/routers/settings.py +70 -0
  103. cognitivesystems/web/routers/system.py +202 -0
  104. cognitivesystems/web/schemas.py +54 -0
  105. cognitivesystems/web/security.py +235 -0
  106. cognitivesystems/web/server.py +72 -0
  107. cognitivesystems/web/settings.py +60 -0
  108. cognitivesystems/web/static/assets/index-CRdsikO7.js +71 -0
  109. cognitivesystems/web/static/assets/index-DRZ00vpW.css +1 -0
  110. cognitivesystems/web/static/index.html +13 -0
  111. cognitivesystems-0.0.2.dist-info/METADATA +229 -0
  112. cognitivesystems-0.0.2.dist-info/RECORD +114 -0
  113. cognitivesystems-0.0.2.dist-info/WHEEL +4 -0
  114. cognitivesystems-0.0.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,8 @@
1
+ """CognitiveSystems — catalog-driven Docker homeserver deployer."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("cognitivesystems")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1,7 @@
1
+ """``python -m cognitivesystems`` entry point."""
2
+ import sys
3
+
4
+ from cognitivesystems.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
@@ -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())
@@ -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)