Skip to content

Commit 4e02e8a

Browse files
authored
Merge pull request #42 from ghasemzadeh-hamed/codex/implement-headless-setup-for-aion-os
chore: remove binary example bundle
2 parents 7b1590b + f6e263f commit 4e02e8a

48 files changed

Lines changed: 1653 additions & 27 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.PHONY: dev doctor bundle test-api
2+
3+
CONTROL_DIR=control
4+
CLI=PYTHONPATH=cli python -m aion.cli
5+
6+
7+
dev:
8+
cd $(CONTROL_DIR) && poetry run uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
9+
10+
doctor:
11+
$(CLI) doctor --verbose
12+
13+
bundle:
14+
@mkdir -p dist
15+
tar -czf dist/example-bundle.tgz -C deploy/bundles/example my-config
16+
17+
test-api:
18+
cd $(CONTROL_DIR) && poetry run pytest -q tests/api

cli/aion/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""AION OS command line interface package."""
2+
3+
from .cli import app
4+
5+
__all__ = ["app"]

cli/aion/cli.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Entrypoint for the ``aion`` Typer-based CLI."""
2+
from __future__ import annotations
3+
4+
import importlib
5+
import sys
6+
from typing import Optional
7+
8+
import typer
9+
10+
app = typer.Typer(help="AION Operating System command line interface.")
11+
12+
13+
# Dynamically register command groups to keep startup fast when optional
14+
# dependencies such as Textual or FastAPI are missing. Each module defines a
15+
# ``register`` function that takes the root Typer app.
16+
for module_name in (
17+
"aion.commands.init",
18+
"aion.commands.apply",
19+
"aion.commands.doctor",
20+
"aion.commands.auth",
21+
"aion.commands.tui",
22+
):
23+
try:
24+
module = importlib.import_module(module_name)
25+
except ImportError as exc: # pragma: no cover - guard for packaging issues
26+
typer.echo(f"warning: failed to import {module_name}: {exc}", err=True)
27+
continue
28+
29+
register = getattr(module, "register", None)
30+
if register is None:
31+
typer.echo(
32+
f"warning: module {module_name} does not expose a register(app) function",
33+
err=True,
34+
)
35+
continue
36+
register(app)
37+
38+
39+
def main(argv: Optional[list[str]] = None) -> None:
40+
"""Execute the Typer application."""
41+
42+
app(args=argv if argv is not None else sys.argv[1:])
43+
44+
45+
if __name__ == "__main__": # pragma: no cover
46+
main()

cli/aion/commands/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Command group namespace for the AION CLI."""
2+
3+
__all__ = []

cli/aion/commands/apply.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Implementation of the ``aion apply`` command group."""
2+
from __future__ import annotations
3+
4+
import pathlib
5+
6+
import typer
7+
8+
from ..services.bundle import BundleService, BundleSettings
9+
10+
11+
def register(app: typer.Typer) -> None:
12+
apply_app = typer.Typer(help="Apply configuration bundles to the control plane")
13+
14+
@apply_app.command()
15+
def bundle(
16+
path: pathlib.Path = typer.Argument(..., help="Path to the bundle .tgz archive."),
17+
atomic: bool = typer.Option(False, "--atomic", help="Rollback on failure."),
18+
no_browser: bool = typer.Option(
19+
False, "--no-browser", help="Skip browser based review before applying."
20+
),
21+
) -> None:
22+
"""Apply a configuration bundle."""
23+
24+
settings = BundleSettings(path=path, atomic=atomic, no_browser=no_browser)
25+
service = BundleService()
26+
report = service.apply(settings)
27+
typer.secho("Bundle applied", fg=typer.colors.GREEN)
28+
typer.echo(report)
29+
30+
app.add_typer(apply_app, name="apply")
31+
32+
33+
__all__ = ["register"]

cli/aion/commands/auth.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Authentication helper commands for ``aion``."""
2+
from __future__ import annotations
3+
4+
import typer
5+
6+
from ..services.auth import AuthService
7+
8+
9+
def register(app: typer.Typer) -> None:
10+
auth_app = typer.Typer(help="Authentication helpers")
11+
12+
@auth_app.command("token")
13+
def token(
14+
once: bool = typer.Option(False, "--once", help="Mark the token for single-use."),
15+
ttl: str = typer.Option("5m", "--ttl", help="Relative TTL, e.g. 5m or 1h."),
16+
) -> None:
17+
service = AuthService()
18+
token_value = service.issue_token(ttl=ttl, once=once)
19+
typer.echo(token_value)
20+
21+
app.add_typer(auth_app, name="auth")
22+
23+
24+
__all__ = ["register"]

cli/aion/commands/doctor.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""System diagnostics via ``aion doctor``."""
2+
from __future__ import annotations
3+
4+
import typer
5+
6+
from ..services.doctor import DoctorService
7+
8+
9+
def register(app: typer.Typer) -> None:
10+
doctor_app = typer.Typer(help="Run health diagnostics against the control API")
11+
12+
@doctor_app.command()
13+
def doctor(verbose: bool = typer.Option(False, "--verbose", help="Verbose output.")) -> None:
14+
service = DoctorService(verbose=verbose)
15+
ok, report = service.run()
16+
if ok:
17+
typer.secho("All systems healthy", fg=typer.colors.GREEN)
18+
else:
19+
typer.secho("Issues detected", fg=typer.colors.RED)
20+
typer.echo(report)
21+
raise typer.Exit(code=0 if ok else 1)
22+
23+
app.add_typer(doctor_app, name="doctor")
24+
25+
26+
__all__ = ["register"]

cli/aion/commands/init.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Implementation of the ``aion init`` command group."""
2+
from __future__ import annotations
3+
4+
import getpass
5+
import sys
6+
from typing import Optional
7+
8+
import typer
9+
10+
from ..services.quickstart import QuickstartOptions, QuickstartService
11+
12+
13+
def _prompt_missing(options: QuickstartOptions) -> QuickstartOptions:
14+
if options.admin_email is None:
15+
options.admin_email = typer.prompt("Admin email")
16+
if options.admin_pass is None:
17+
options.admin_pass = getpass.getpass("Admin password: ")
18+
if not options.model:
19+
options.model = typer.prompt("Default model", default="gpt-4o-mini")
20+
return options
21+
22+
23+
def register(app: typer.Typer) -> None:
24+
init_app = typer.Typer(help="Bootstrap a new AION OS deployment")
25+
26+
@init_app.callback()
27+
def init_command(
28+
ctx: typer.Context,
29+
quickstart: bool = typer.Option(
30+
False, "--quickstart", help="Run the guided quickstart workflow."
31+
),
32+
no_browser: bool = typer.Option(
33+
False, "--no-browser", help="Force headless mode even if a browser exists."
34+
),
35+
admin_email: Optional[str] = typer.Option(
36+
None, "--admin-email", help="Seed admin email for the console."
37+
),
38+
admin_pass: Optional[str] = typer.Option(
39+
None, "--admin-pass", help="Seed admin password for the console."
40+
),
41+
provider: str = typer.Option(
42+
"local", "--provider", help="Default provider to configure (local|api|hybrid)."
43+
),
44+
api_key: Optional[str] = typer.Option(
45+
None, "--api-key", help="API key when provider requires one."
46+
),
47+
model: Optional[str] = typer.Option(
48+
None, "--model", help="Default model identifier to set up."
49+
),
50+
port: int = typer.Option(3000, "--port", help="Port to bind the console to."),
51+
) -> None:
52+
if not quickstart:
53+
typer.echo("Use --quickstart to launch the guided bootstrap workflow.")
54+
raise typer.Exit(code=1)
55+
56+
options = QuickstartOptions(
57+
quickstart=quickstart,
58+
no_browser=no_browser,
59+
admin_email=admin_email,
60+
admin_pass=admin_pass,
61+
provider=provider,
62+
api_key=api_key,
63+
model=model,
64+
port=port,
65+
extra_args=list(ctx.args),
66+
)
67+
if options.no_browser or not sys.stdin.isatty(): # type: ignore[attr-defined]
68+
options = _prompt_missing(options)
69+
70+
service = QuickstartService()
71+
result = service.run(options=options)
72+
typer.secho("Quickstart completed", fg=typer.colors.GREEN)
73+
for key, value in result.items():
74+
typer.echo(f"{key}: {value}")
75+
76+
app.add_typer(init_app, name="init")
77+
78+
79+
__all__ = ["register"]

cli/aion/commands/tui.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Terminal UI launcher commands."""
2+
from __future__ import annotations
3+
4+
import typer
5+
6+
from ..services.tui import TuiServerService
7+
8+
9+
def register(app: typer.Typer) -> None:
10+
tui_app = typer.Typer(help="Serve the terminal explorer UI")
11+
12+
@tui_app.command("serve")
13+
def serve(
14+
port: int = typer.Option(3030, "--port", help="Port to bind the explorer server to."),
15+
api: str = typer.Option("http://127.0.0.1:8001", "--api", help="Control API base URL."),
16+
token: str = typer.Option(..., "--token", help="Authentication token to forward."),
17+
) -> None:
18+
service = TuiServerService(port=port, api_url=api, token=token)
19+
service.run()
20+
21+
app.add_typer(tui_app, name="tui-serve")
22+
23+
24+
__all__ = ["register"]

cli/aion/services/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Service helpers used by the AION CLI commands."""
2+
3+
__all__ = []

0 commit comments

Comments
 (0)