mad-cli 0.4.0__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 (54) hide show
  1. mad_cli/__init__.py +3 -0
  2. mad_cli/__main__.py +6 -0
  3. mad_cli/app.py +77 -0
  4. mad_cli/commands/__init__.py +5 -0
  5. mad_cli/commands/_adapt.py +41 -0
  6. mad_cli/commands/_common.py +12 -0
  7. mad_cli/commands/config.py +94 -0
  8. mad_cli/commands/install.py +504 -0
  9. mad_cli/commands/instances.py +102 -0
  10. mad_cli/commands/keys.py +126 -0
  11. mad_cli/commands/lifecycle.py +69 -0
  12. mad_cli/commands/profiles.py +238 -0
  13. mad_cli/commands/service.py +220 -0
  14. mad_cli/commands/versions.py +61 -0
  15. mad_cli/core/__init__.py +4 -0
  16. mad_cli/core/claude_creds.py +31 -0
  17. mad_cli/core/compose.py +145 -0
  18. mad_cli/core/docker_check.py +89 -0
  19. mad_cli/core/envfile.py +140 -0
  20. mad_cli/core/instance.py +110 -0
  21. mad_cli/core/keyspec.py +98 -0
  22. mad_cli/core/paths.py +40 -0
  23. mad_cli/core/profiles.py +93 -0
  24. mad_cli/core/pypi.py +29 -0
  25. mad_cli/core/templates.py +91 -0
  26. mad_cli/core/usecases/__init__.py +11 -0
  27. mad_cli/core/usecases/adopt.py +55 -0
  28. mad_cli/core/usecases/configvals.py +94 -0
  29. mad_cli/core/usecases/errors.py +57 -0
  30. mad_cli/core/usecases/install.py +263 -0
  31. mad_cli/core/usecases/instances.py +156 -0
  32. mad_cli/core/usecases/keys.py +169 -0
  33. mad_cli/core/usecases/lifecycle.py +76 -0
  34. mad_cli/core/usecases/service.py +269 -0
  35. mad_cli/core/usecases/versions.py +126 -0
  36. mad_cli/py.typed +0 -0
  37. mad_cli/server/__init__.py +13 -0
  38. mad_cli/server/app.py +260 -0
  39. mad_cli/server/auth.py +41 -0
  40. mad_cli/server/models.py +156 -0
  41. mad_cli/templates/Dockerfile.tmpl +66 -0
  42. mad_cli/templates/__init__.py +6 -0
  43. mad_cli/templates/com.mad-core.mad-cli.plist.tmpl +28 -0
  44. mad_cli/templates/compose.yml.tmpl +29 -0
  45. mad_cli/templates/entrypoint.sh.tmpl +11 -0
  46. mad_cli/templates/mad-cli.service.tmpl +15 -0
  47. mad_cli/ui/__init__.py +5 -0
  48. mad_cli/ui/console.py +65 -0
  49. mad_cli/ui/prompts.py +83 -0
  50. mad_cli-0.4.0.dist-info/METADATA +167 -0
  51. mad_cli-0.4.0.dist-info/RECORD +54 -0
  52. mad_cli-0.4.0.dist-info/WHEEL +4 -0
  53. mad_cli-0.4.0.dist-info/entry_points.txt +2 -0
  54. mad_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
mad_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """mad-cli — operator CLI for Mad (installs and manages mad-edge containers)."""
2
+
3
+ __version__ = "0.4.0"
mad_cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Enable ``python -m mad_cli``."""
2
+
3
+ from mad_cli.app import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
mad_cli/app.py ADDED
@@ -0,0 +1,77 @@
1
+ """Typer application wiring and the ``mad`` console-script entry point."""
2
+
3
+ import typer
4
+
5
+ from mad_cli import __version__
6
+ from mad_cli.commands import config as config_cmd
7
+ from mad_cli.commands import install as install_cmd
8
+ from mad_cli.commands import instances as instances_cmd
9
+ from mad_cli.commands import keys as keys_cmd
10
+ from mad_cli.commands import lifecycle as lifecycle_cmd
11
+ from mad_cli.commands import profiles as profiles_cmd
12
+ from mad_cli.commands import service as service_cmd
13
+ from mad_cli.commands import versions as versions_cmd
14
+
15
+ app = typer.Typer(
16
+ no_args_is_help=True,
17
+ add_completion=False,
18
+ help="Mad operator CLI — install and manage mad-edge containers.",
19
+ )
20
+
21
+
22
+ def _version_callback(value: bool) -> None:
23
+ if value:
24
+ typer.echo(__version__)
25
+ raise typer.Exit()
26
+
27
+
28
+ @app.callback()
29
+ def _root(
30
+ version: bool = typer.Option(
31
+ False,
32
+ "--version",
33
+ callback=_version_callback,
34
+ is_eager=True,
35
+ help="Show the mad-cli version and exit.",
36
+ ),
37
+ ) -> None:
38
+ """Mad operator CLI — install and manage mad-edge containers."""
39
+
40
+
41
+ # ── install ──────────────────────────────────────────────────────────────────
42
+ app.command("install")(install_cmd.install)
43
+
44
+ # ── lifecycle ────────────────────────────────────────────────────────────────
45
+ app.command("start")(lifecycle_cmd.start)
46
+ app.command("stop")(lifecycle_cmd.stop)
47
+ app.command("restart")(lifecycle_cmd.restart)
48
+ app.command("status")(lifecycle_cmd.status)
49
+ app.command("logs")(lifecycle_cmd.logs)
50
+ app.command("shell")(lifecycle_cmd.shell)
51
+
52
+ # ── inventory ────────────────────────────────────────────────────────────────
53
+ app.command("list")(instances_cmd.list_)
54
+ app.command("info")(instances_cmd.info_cmd)
55
+ app.command("adopt")(instances_cmd.adopt)
56
+
57
+ # ── versions ─────────────────────────────────────────────────────────────────
58
+ app.command("versions")(versions_cmd.versions)
59
+ app.command("update")(versions_cmd.update)
60
+
61
+ # ── keys & config ─────────────────────────────────────────────────────────────
62
+ app.add_typer(keys_cmd.keys_app, name="keys")
63
+ app.add_typer(config_cmd.config_app, name="config")
64
+ app.add_typer(profiles_cmd.profiles_app, name="profiles")
65
+
66
+ # ── service mode (HTTP API) ───────────────────────────────────────────────────
67
+ app.command("serve")(service_cmd.serve)
68
+ app.add_typer(service_cmd.service_app, name="service")
69
+
70
+
71
+ def main() -> None:
72
+ """Console-script entry point (``mad``)."""
73
+ app()
74
+
75
+
76
+ if __name__ == "__main__": # pragma: no cover
77
+ main()
@@ -0,0 +1,5 @@
1
+ """Typer command surface for the ``mad`` CLI.
2
+
3
+ Modules here own the operator-facing UX only; every side effect (docker,
4
+ filesystem, templating) is delegated to ``mad_cli.core`` per CONTRACTS.md.
5
+ """
@@ -0,0 +1,41 @@
1
+ """Shared CLI adapter helpers: instance resolution and use-case error mapping.
2
+
3
+ The command modules are thin adapters over :mod:`mad_cli.core.usecases`; this maps
4
+ the framework-free :class:`~mad_cli.core.usecases.errors.UseCaseError` vocabulary
5
+ onto the CLI idiom (an ``error(...)`` line plus ``typer.Exit(1)``) with the
6
+ operator-facing hints (``mad install`` / ``mad list``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import NoReturn
12
+
13
+ import typer
14
+
15
+ from mad_cli.core.instance import Instance
16
+ from mad_cli.core.usecases import instances as uc_instances
17
+ from mad_cli.core.usecases.errors import AmbiguousInstanceError, NotFoundError, UseCaseError
18
+ from mad_cli.ui.console import error
19
+
20
+
21
+ def die(msg: str) -> NoReturn:
22
+ """Print an error and exit non-zero."""
23
+ error(msg)
24
+ raise typer.Exit(1)
25
+
26
+
27
+ def fail(exc: UseCaseError) -> NoReturn:
28
+ """Render a use-case failure verbatim and exit non-zero."""
29
+ die(str(exc))
30
+
31
+
32
+ def resolve_or_die(name: str | None) -> Instance:
33
+ """Resolve ``name`` (or the sole instance) or exit with an actionable hint."""
34
+ try:
35
+ return uc_instances.resolve_instance(name)
36
+ except NotFoundError:
37
+ if name is not None:
38
+ die(f"Instance {name!r} not found. Run `mad list` to see available instances.")
39
+ die("No instances found. Run `mad install` first.")
40
+ except AmbiguousInstanceError as exc:
41
+ die(str(exc))
@@ -0,0 +1,12 @@
1
+ """Small presentation helpers shared across commands.
2
+
3
+ Secret detection now lives in the framework-free engine
4
+ (:func:`mad_cli.core.keyspec.is_secret_key`) so the HTTP surface and the CLI
5
+ share one masking rule; it is re-exported here for the command modules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from mad_cli.core.keyspec import is_secret_key
11
+
12
+ __all__ = ["is_secret_key"]
@@ -0,0 +1,94 @@
1
+ """``mad config get|set|unset`` — read and edit an instance's ``.env`` values.
2
+
3
+ Thin adapter over :mod:`mad_cli.core.usecases.configvals` — the general-purpose
4
+ ``.env`` editor (``mad keys`` is the credential-aware front end). Secret-looking
5
+ values are masked on read unless ``--reveal`` is passed, and a handful of known
6
+ keys are validated on write.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import typer
12
+ from rich.table import Table
13
+
14
+ from mad_cli.commands._adapt import fail, resolve_or_die
15
+ from mad_cli.core.instance import Instance
16
+ from mad_cli.core.usecases import configvals as uc
17
+ from mad_cli.core.usecases.errors import NotFoundError, ValidationError
18
+ from mad_cli.ui.console import console, error, info, ok, warn
19
+
20
+ config_app = typer.Typer(
21
+ no_args_is_help=True,
22
+ add_completion=False,
23
+ help="Read and edit an instance's .env configuration.",
24
+ )
25
+
26
+ _INSTANCE_OPTION = typer.Option(
27
+ None, "--instance", "-i", help="Instance name (optional when exactly one instance exists)."
28
+ )
29
+
30
+
31
+ def _restart_hint(instance: Instance) -> None:
32
+ info(f"Restart the instance to apply: mad restart {instance.name}")
33
+
34
+
35
+ @config_app.command("get")
36
+ def get(
37
+ key: str | None = typer.Argument(None, help="Env key to read. Omit to list every value."),
38
+ instance: str | None = _INSTANCE_OPTION,
39
+ reveal: bool = typer.Option(False, "--reveal", help="Show secret values in full."),
40
+ ) -> None:
41
+ """Print one ``.env`` value, or the whole file (secrets masked)."""
42
+ inst = resolve_or_die(instance)
43
+
44
+ if key is None:
45
+ table = Table(title=f".env — {inst.name}")
46
+ table.add_column("Key", style="bold", no_wrap=True)
47
+ table.add_column("Value")
48
+ for item in uc.list_config(inst):
49
+ table.add_row(item.key, item.display(reveal=reveal))
50
+ console.print(table)
51
+ return
52
+
53
+ try:
54
+ item = uc.get_config(inst, key)
55
+ except NotFoundError:
56
+ error(f"{key} is not set on {inst.name}.")
57
+ raise typer.Exit(1) from None
58
+ console.print(item.display(reveal=reveal), markup=False, highlight=False)
59
+
60
+
61
+ @config_app.command("set")
62
+ def set_value(
63
+ key: str = typer.Argument(..., help="Env key to write."),
64
+ value: str = typer.Argument(..., help="Value to store."),
65
+ instance: str | None = _INSTANCE_OPTION,
66
+ ) -> None:
67
+ """Set an ``.env`` value, validating a handful of known keys."""
68
+ inst = resolve_or_die(instance)
69
+ try:
70
+ item, compose_baked = uc.set_config(inst, key, value)
71
+ except ValidationError as exc:
72
+ fail(exc)
73
+
74
+ ok(f"Set {key} = {item.display()} on {inst.name}.")
75
+ if compose_baked:
76
+ warn(
77
+ f"{key} was baked into compose.yml at install time. This updates .env only; "
78
+ "a port/bind change needs the instance regenerated (v0.3+) to take effect."
79
+ )
80
+ _restart_hint(inst)
81
+
82
+
83
+ @config_app.command("unset")
84
+ def unset(
85
+ key: str = typer.Argument(..., help="Env key to remove."),
86
+ instance: str | None = _INSTANCE_OPTION,
87
+ ) -> None:
88
+ """Remove a key from the instance's ``.env``."""
89
+ inst = resolve_or_die(instance)
90
+ if not uc.unset_config(inst, key):
91
+ warn(f"{key} is not set on {inst.name}; nothing to remove.")
92
+ return
93
+ ok(f"Unset {key} on {inst.name}.")
94
+ _restart_hint(inst)