aegis-gateway-cli 2.0.0a0__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.
Files changed (26) hide show
  1. aegis_gateway_cli-2.0.0a0/.gitignore +100 -0
  2. aegis_gateway_cli-2.0.0a0/PKG-INFO +14 -0
  3. aegis_gateway_cli-2.0.0a0/pyproject.toml +34 -0
  4. aegis_gateway_cli-2.0.0a0/src/aegis_cli/__init__.py +3 -0
  5. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/__init__.py +1 -0
  6. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/chat.py +65 -0
  7. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/config.py +77 -0
  8. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/doctor.py +246 -0
  9. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/init.py +122 -0
  10. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/keys.py +87 -0
  11. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/plugin.py +131 -0
  12. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/policy.py +293 -0
  13. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/provider.py +171 -0
  14. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/rag.py +124 -0
  15. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/runs.py +106 -0
  16. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/scaffold.py +331 -0
  17. aegis_gateway_cli-2.0.0a0/src/aegis_cli/commands/serve.py +66 -0
  18. aegis_gateway_cli-2.0.0a0/src/aegis_cli/main.py +62 -0
  19. aegis_gateway_cli-2.0.0a0/tests/test_config_commands.py +120 -0
  20. aegis_gateway_cli-2.0.0a0/tests/test_doctor.py +198 -0
  21. aegis_gateway_cli-2.0.0a0/tests/test_errors.py +186 -0
  22. aegis_gateway_cli-2.0.0a0/tests/test_init.py +118 -0
  23. aegis_gateway_cli-2.0.0a0/tests/test_placeholder.py +17 -0
  24. aegis_gateway_cli-2.0.0a0/tests/test_policy_commands.py +249 -0
  25. aegis_gateway_cli-2.0.0a0/tests/test_runs_commands.py +180 -0
  26. aegis_gateway_cli-2.0.0a0/tests/test_scaffold.py +143 -0
@@ -0,0 +1,100 @@
1
+ # Node/npm local installs
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ .pnpm-store/
7
+
8
+ # Local environment files
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+
13
+ ## jj
14
+ .jj
15
+
16
+ # Codex local configuration
17
+ .codex/
18
+ AGENTS.md
19
+
20
+ # Claude Flow runtime data
21
+ .claude-flow/data/
22
+ .claude-flow/logs/
23
+ CLAUDE.md
24
+ .claude
25
+ .claude-flow
26
+
27
+ # MCP
28
+ .mcp.json
29
+ ruvector.db
30
+
31
+ # ruflo agents
32
+ .agents
33
+ .swarm
34
+ .agentdb
35
+ agentdb.db
36
+
37
+ # Environment variables
38
+ .env
39
+ .env.local
40
+ .env.*.local
41
+
42
+ # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
43
+ # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
44
+
45
+ ### VisualStudioCode ###
46
+ .vscode/*
47
+ !.vscode/settings.json
48
+ !.vscode/tasks.json
49
+ !.vscode/launch.json
50
+ !.vscode/extensions.json
51
+ !.vscode/*.code-snippets
52
+ .vscode
53
+
54
+ # Local History for Visual Studio Code
55
+ .history/
56
+
57
+ # Built Visual Studio Code Extensions
58
+ *.vsix
59
+
60
+ ### VisualStudioCode Patch ###
61
+ # Ignore all local history of files
62
+ .history
63
+ .ionide
64
+
65
+ ### Misc files
66
+ gemma4
67
+ .postman/
68
+ .openclaude/
69
+ postman/
70
+
71
+ ### OpenClaude
72
+ .openclaude-profile.json
73
+
74
+ # Python
75
+ __pycache__/
76
+ *.py[cod]
77
+ *.egg-info/
78
+ .pytest_cache/
79
+ dist/
80
+ build/
81
+ *.egg
82
+ .venv/
83
+ .uv-cache/
84
+ .tmp/
85
+ .ruff_cache/
86
+
87
+ # Docker
88
+ .docker/
89
+
90
+ ### Build plans
91
+ BUILD_PLAN.md
92
+ CLAUDE.md
93
+ PROJECT_SPEC.md
94
+ AMEND_PLAN.md
95
+ SOURCE_OF_TRUTH.md
96
+ repo-seed/
97
+ .import_linter_cache/
98
+ site/
99
+
100
+ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: aegis-gateway-cli
3
+ Version: 2.0.0a0
4
+ Summary: Aegis v2 — CLI: aegis command with Typer + Rich.
5
+ Project-URL: Homepage, https://github.com/e-choness/aegis
6
+ Project-URL: Repository, https://github.com/e-choness/aegis
7
+ Project-URL: Issues, https://github.com/e-choness/aegis/issues
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: aegis-gateway-core
11
+ Requires-Dist: aegis-gateway-sdk
12
+ Requires-Dist: aegis-gateway-server
13
+ Requires-Dist: rich>=13
14
+ Requires-Dist: typer>=0.12
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "aegis-gateway-cli"
3
+ version = "2.0.0a0"
4
+ description = "Aegis v2 — CLI: aegis command with Typer + Rich."
5
+ license = { text = "MIT" }
6
+ requires-python = ">=3.12"
7
+
8
+ dependencies = [
9
+ "aegis-gateway-core",
10
+ "aegis-gateway-sdk",
11
+ "aegis-gateway-server",
12
+ "typer>=0.12",
13
+ "rich>=13",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/e-choness/aegis"
18
+ Repository = "https://github.com/e-choness/aegis"
19
+ Issues = "https://github.com/e-choness/aegis/issues"
20
+
21
+ [project.scripts]
22
+ aegis = "aegis_cli.main:run"
23
+
24
+ [tool.uv.sources]
25
+ aegis-gateway-core = { workspace = true }
26
+ aegis-gateway-sdk = { workspace = true }
27
+ aegis-gateway-server = { workspace = true }
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/aegis_cli"]
@@ -0,0 +1,3 @@
1
+ """Aegis v2 — CLI package."""
2
+
3
+ __version__ = "2.0.0a0"
@@ -0,0 +1 @@
1
+ """Aegis CLI command modules."""
@@ -0,0 +1,65 @@
1
+ """CLI command: `aegis chat`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import uuid
8
+ from typing import Annotated
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from aegis_core.pipeline import PipelineAssembler, RunState
14
+ from aegis_core.providers.models import Message
15
+ from aegis_core.testing import FakeProvider
16
+
17
+ _console = Console()
18
+ _err_console = Console(stderr=True, style="bold red")
19
+
20
+
21
+ def chat(
22
+ message: Annotated[str, typer.Argument(help="The message to send.")],
23
+ route: Annotated[
24
+ str,
25
+ typer.Option("--route", "-r", help="Route name to use."),
26
+ ] = "default",
27
+ json_output: Annotated[
28
+ bool,
29
+ typer.Option("--json", help="Emit JSON with the full event log."),
30
+ ] = False,
31
+ ) -> None:
32
+ """Send *message* through the pipeline and print the response."""
33
+ provider = FakeProvider(
34
+ name="fake",
35
+ complete_response=f"[fake] echo: {message}",
36
+ )
37
+
38
+ assembler = PipelineAssembler()
39
+ pipeline = assembler.compile(provider=provider, route=route)
40
+
41
+ initial_state = RunState(
42
+ run_id=str(uuid.uuid4()),
43
+ route=route,
44
+ messages=[Message(role="user", content=message)],
45
+ )
46
+
47
+ result = asyncio.run(pipeline.run(initial_state))
48
+
49
+ if json_output:
50
+ output = {
51
+ "run_id": result.run_id,
52
+ "route": result.route,
53
+ "status": result.status,
54
+ "response": result.response,
55
+ "events": [e.to_dict() for e in result.events],
56
+ "usage": {
57
+ "prompt_tokens": result.usage.prompt_tokens,
58
+ "completion_tokens": result.usage.completion_tokens,
59
+ "total_tokens": result.usage.total_tokens,
60
+ "cost": result.usage.cost,
61
+ },
62
+ }
63
+ _console.print(json.dumps(output, indent=2), markup=False)
64
+ else:
65
+ _console.print(result.response or "", markup=False)
@@ -0,0 +1,77 @@
1
+ """CLI commands: `aegis config validate` and `aegis config show`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.syntax import Syntax
12
+
13
+ from aegis_core.config import load_config
14
+ from aegis_core.errors import AegisConfigError
15
+
16
+ app = typer.Typer(name="config", help="Validate and inspect Aegis configuration.")
17
+ _console = Console()
18
+ _err_console = Console(stderr=True, style="bold red")
19
+
20
+ _DEFAULT_CONFIG = Path("aegis.yaml")
21
+
22
+
23
+ @app.command("validate")
24
+ def validate(
25
+ config_path: Annotated[
26
+ Path,
27
+ typer.Argument(help="Path to aegis.yaml to validate."),
28
+ ] = _DEFAULT_CONFIG,
29
+ ) -> None:
30
+ """Validate an aegis.yaml file and report any errors."""
31
+ try:
32
+ load_config(config_path)
33
+ except AegisConfigError as exc:
34
+ _err_console.print(str(exc))
35
+ raise typer.Exit(1) from exc
36
+ except Exception as exc:
37
+ _err_console.print(f"Unexpected error: {exc}")
38
+ raise typer.Exit(1) from exc
39
+
40
+ _console.print(f"[green]✓[/green] {config_path} is valid.")
41
+
42
+
43
+ @app.command("show")
44
+ def show(
45
+ config_path: Annotated[
46
+ Path,
47
+ typer.Argument(help="Path to aegis.yaml to display."),
48
+ ] = _DEFAULT_CONFIG,
49
+ output_format: Annotated[
50
+ str,
51
+ typer.Option("--format", "-f", help="Output format: json or yaml."),
52
+ ] = "json",
53
+ ) -> None:
54
+ """Load and display the resolved configuration (secrets redacted)."""
55
+ try:
56
+ cfg = load_config(config_path)
57
+ except AegisConfigError as exc:
58
+ _err_console.print(str(exc))
59
+ raise typer.Exit(1) from exc
60
+ except Exception as exc:
61
+ _err_console.print(f"Unexpected error: {exc}")
62
+ raise typer.Exit(1) from exc
63
+
64
+ safe = cfg.safe_dict()
65
+
66
+ if output_format == "yaml":
67
+ try:
68
+ import yaml # type: ignore[import-untyped]
69
+
70
+ text = yaml.dump(safe, default_flow_style=False, sort_keys=False)
71
+ _console.print(Syntax(text, "yaml", theme="monokai"))
72
+ except ImportError:
73
+ _err_console.print("PyYAML is required for YAML output.")
74
+ raise typer.Exit(1) from None
75
+ else:
76
+ text = json.dumps(safe, indent=2, default=str)
77
+ _console.print(Syntax(text, "json", theme="monokai"))
@@ -0,0 +1,246 @@
1
+ """CLI command: `aegis doctor` — environment health checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ app = typer.Typer(name="doctor", invoke_without_command=True, help="Check Aegis environment health.")
16
+ _console = Console()
17
+ _err_console = Console(stderr=True)
18
+
19
+ _DEFAULT_CONFIG = Path("aegis.yaml")
20
+ _DEFAULT_PROVIDERS_STORE = Path.home() / ".aegis" / "providers.json"
21
+
22
+
23
+ class CheckStatus(StrEnum):
24
+ OK = "ok"
25
+ WARN = "warn"
26
+ FAIL = "fail"
27
+
28
+
29
+ @dataclass
30
+ class HealthCheck:
31
+ name: str
32
+ status: CheckStatus
33
+ detail: str
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Individual checks
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ def check_config(config_path: Path) -> HealthCheck:
42
+ """AEG-CFG: aegis.yaml exists and is valid YAML."""
43
+ if not config_path.exists():
44
+ return HealthCheck(
45
+ name="config",
46
+ status=CheckStatus.FAIL,
47
+ detail=f"{config_path} not found. Run `aegis init` to create one.",
48
+ )
49
+ try:
50
+ import yaml
51
+
52
+ with open(config_path) as f:
53
+ raw = yaml.safe_load(f)
54
+ if not isinstance(raw, (dict, type(None))):
55
+ return HealthCheck(
56
+ name="config",
57
+ status=CheckStatus.FAIL,
58
+ detail=f"{config_path} does not contain a YAML mapping.",
59
+ )
60
+ except Exception as exc:
61
+ return HealthCheck(
62
+ name="config",
63
+ status=CheckStatus.FAIL,
64
+ detail=f"{config_path} could not be parsed: {exc}",
65
+ )
66
+ return HealthCheck(name="config", status=CheckStatus.OK, detail=str(config_path))
67
+
68
+
69
+ def check_pii_extra() -> HealthCheck:
70
+ """AEG-POL: presidio-analyzer (PII extra) is installed."""
71
+ spec = None
72
+ try:
73
+ spec = importlib.util.find_spec("presidio_analyzer")
74
+ except ModuleNotFoundError:
75
+ pass
76
+ if spec is None:
77
+ return HealthCheck(
78
+ name="pii_extra",
79
+ status=CheckStatus.WARN,
80
+ detail=(
81
+ "presidio-analyzer not installed. "
82
+ "Install aegis-pack-pii[pii] to enable PII masking."
83
+ ),
84
+ )
85
+ return HealthCheck(name="pii_extra", status=CheckStatus.OK, detail="presidio-analyzer found.")
86
+
87
+
88
+ def check_rag_extra() -> HealthCheck:
89
+ """AEG-RAG: chromadb (RAG extra) is installed."""
90
+ spec = None
91
+ try:
92
+ spec = importlib.util.find_spec("chromadb")
93
+ except ModuleNotFoundError:
94
+ pass
95
+ if spec is None:
96
+ return HealthCheck(
97
+ name="rag_extra",
98
+ status=CheckStatus.WARN,
99
+ detail=(
100
+ "chromadb not installed. "
101
+ "Install aegis-core[rag] to enable RAG retrieval."
102
+ ),
103
+ )
104
+ return HealthCheck(name="rag_extra", status=CheckStatus.OK, detail="chromadb found.")
105
+
106
+
107
+ def check_provider_store(store_path: Path = _DEFAULT_PROVIDERS_STORE) -> HealthCheck:
108
+ """AEG-PRV: provider profile store file exists."""
109
+ if not store_path.exists():
110
+ return HealthCheck(
111
+ name="provider_store",
112
+ status=CheckStatus.WARN,
113
+ detail=(
114
+ f"{store_path} not found. "
115
+ "Run `aegis provider add` to create a provider profile."
116
+ ),
117
+ )
118
+ return HealthCheck(
119
+ name="provider_store",
120
+ status=CheckStatus.OK,
121
+ detail=str(store_path),
122
+ )
123
+
124
+
125
+ def check_providers_reachable(store_path: Path = _DEFAULT_PROVIDERS_STORE) -> HealthCheck:
126
+ """AEG-PRV: ping each provider in the store (opt-in)."""
127
+ if not store_path.exists():
128
+ return HealthCheck(
129
+ name="providers_reachable",
130
+ status=CheckStatus.WARN,
131
+ detail="No provider store found; skipping reachability check.",
132
+ )
133
+ try:
134
+ import json
135
+
136
+ with open(store_path) as f:
137
+ data = json.load(f)
138
+ profiles = data if isinstance(data, list) else []
139
+ if not profiles:
140
+ return HealthCheck(
141
+ name="providers_reachable",
142
+ status=CheckStatus.WARN,
143
+ detail="No provider profiles configured.",
144
+ )
145
+ # Best-effort TCP reachability for each profile with a base_url
146
+ import socket
147
+ import urllib.parse
148
+
149
+ unreachable: list[str] = []
150
+ for profile in profiles:
151
+ base_url = profile.get("base_url")
152
+ if not base_url:
153
+ continue
154
+ parsed = urllib.parse.urlparse(base_url)
155
+ host = parsed.hostname or ""
156
+ port = parsed.port or (443 if parsed.scheme == "https" else 80)
157
+ try:
158
+ with socket.create_connection((host, port), timeout=3):
159
+ pass
160
+ except OSError:
161
+ unreachable.append(profile.get("name", host))
162
+
163
+ if unreachable:
164
+ return HealthCheck(
165
+ name="providers_reachable",
166
+ status=CheckStatus.FAIL,
167
+ detail=f"Unreachable: {', '.join(unreachable)}",
168
+ )
169
+ return HealthCheck(
170
+ name="providers_reachable",
171
+ status=CheckStatus.OK,
172
+ detail=f"All {len(profiles)} provider(s) reachable.",
173
+ )
174
+ except Exception as exc:
175
+ return HealthCheck(
176
+ name="providers_reachable",
177
+ status=CheckStatus.FAIL,
178
+ detail=f"Check failed: {exc}",
179
+ )
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Public helper for tests
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def run_checks(
188
+ config_path: Path = _DEFAULT_CONFIG,
189
+ store_path: Path = _DEFAULT_PROVIDERS_STORE,
190
+ check_providers: bool = False,
191
+ ) -> list[HealthCheck]:
192
+ """Run all health checks and return the results."""
193
+ checks: list[HealthCheck] = [
194
+ check_config(config_path),
195
+ check_pii_extra(),
196
+ check_rag_extra(),
197
+ check_provider_store(store_path),
198
+ ]
199
+ if check_providers:
200
+ checks.append(check_providers_reachable(store_path))
201
+ return checks
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Command
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ @app.callback(invoke_without_command=True)
210
+ def doctor(
211
+ config_path: Annotated[
212
+ Path,
213
+ typer.Option("--config", "-c", help="Path to aegis.yaml to validate."),
214
+ ] = _DEFAULT_CONFIG,
215
+ store_path: Annotated[
216
+ Path,
217
+ typer.Option("--store", help="Path to provider profile store."),
218
+ ] = _DEFAULT_PROVIDERS_STORE,
219
+ check_providers: Annotated[
220
+ bool,
221
+ typer.Option("--check-providers", help="Ping each configured provider (opt-in)."),
222
+ ] = False,
223
+ ) -> None:
224
+ """Check Aegis environment health (config, extras, provider store)."""
225
+ checks = run_checks(config_path, store_path, check_providers)
226
+
227
+ table = Table(title="Aegis Doctor", show_header=True, header_style="bold cyan")
228
+ table.add_column("Check", style="bold")
229
+ table.add_column("Status")
230
+ table.add_column("Detail")
231
+
232
+ any_fail = False
233
+ for check in checks:
234
+ if check.status == CheckStatus.OK:
235
+ status_str = "[green]OK[/green]"
236
+ elif check.status == CheckStatus.WARN:
237
+ status_str = "[yellow]WARN[/yellow]"
238
+ else:
239
+ status_str = "[red]FAIL[/red]"
240
+ any_fail = True
241
+ table.add_row(check.name, status_str, check.detail)
242
+
243
+ _console.print(table)
244
+
245
+ if any_fail:
246
+ raise typer.Exit(1)
@@ -0,0 +1,122 @@
1
+ """CLI command: `aegis init` — generate a starter aegis.yaml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ app = typer.Typer(name="init", invoke_without_command=True, help="Generate a starter aegis.yaml.")
12
+ _console = Console()
13
+ _err_console = Console(stderr=True, style="bold red")
14
+
15
+ _DEFAULT_OUTPUT = Path("aegis.yaml")
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Template
19
+ # ---------------------------------------------------------------------------
20
+
21
+ _TEMPLATE = """\
22
+ # aegis.yaml — generated by `aegis init`
23
+ # Run `aegis config validate aegis.yaml` to validate this file.
24
+ # Run `aegis policy lint aegis.yaml` to check policy references.
25
+
26
+ # ── Providers ─────────────────────────────────────────────────────────────
27
+ # Uncomment and configure at least one provider before serving requests.
28
+ #
29
+ # providers:
30
+ # my_provider:
31
+ # type: openai_compatible
32
+ # base_url: http://localhost:11434/v1
33
+ # model: llama3
34
+ # api_key: secret://env/MY_API_KEY#value
35
+
36
+ # ── Guardrails ────────────────────────────────────────────────────────────
37
+ # The PII guard is enabled by default (requires aegis-pack-pii[pii]).
38
+ # Add more guards from available packs below.
39
+
40
+ guardrails:
41
+ pii:
42
+ pack: aegis_pack_pii
43
+ mode: mask
44
+
45
+ # llm_guard:
46
+ # pack: aegis_pack_llm_guard
47
+ # scanners: [PromptInjection]
48
+ # threshold: 0.8
49
+
50
+ # residency:
51
+ # pack: aegis_pack_residency
52
+ # region: us
53
+
54
+ # ── Pipeline ──────────────────────────────────────────────────────────────
55
+ # Guardrail names listed here must match keys in the guardrails section above.
56
+
57
+ pipeline:
58
+ ingress: [pii]
59
+ # tool_result: [pii]
60
+ # egress: [] # note: non-incremental egress guards disable true-streaming
61
+
62
+ # ── Routes ────────────────────────────────────────────────────────────────
63
+ # Uncomment and configure after adding a provider above.
64
+ #
65
+ # routes:
66
+ # default:
67
+ # provider: my_provider
68
+ # model: llama3
69
+
70
+ # ── Auth ──────────────────────────────────────────────────────────────────
71
+ # Change to `type: api_key` and use `aegis keys create` for production.
72
+
73
+ auth:
74
+ type: none
75
+ """
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Public helper for tests
80
+ # ---------------------------------------------------------------------------
81
+
82
+
83
+ def write_init_yaml(output: Path, force: bool = False) -> None:
84
+ """Write the starter aegis.yaml to *output*.
85
+
86
+ Raises:
87
+ FileExistsError: If *output* exists and *force* is False.
88
+ """
89
+ if output.exists() and not force:
90
+ raise FileExistsError(
91
+ f"{output} already exists. Use --force to overwrite."
92
+ )
93
+ output.parent.mkdir(parents=True, exist_ok=True)
94
+ output.write_text(_TEMPLATE)
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Command
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ @app.callback(invoke_without_command=True)
103
+ def init(
104
+ output: Annotated[
105
+ Path,
106
+ typer.Option("--output", "-o", help="Output path for aegis.yaml."),
107
+ ] = _DEFAULT_OUTPUT,
108
+ force: Annotated[
109
+ bool,
110
+ typer.Option("--force", "-f", help="Overwrite existing file."),
111
+ ] = False,
112
+ ) -> None:
113
+ """Generate a starter aegis.yaml with PII guarding enabled."""
114
+ try:
115
+ write_init_yaml(output, force=force)
116
+ except FileExistsError as exc:
117
+ _err_console.print(str(exc))
118
+ raise typer.Exit(1) from exc
119
+
120
+ _console.print(f"[green]Created[/green] {output}")
121
+ _console.print(" Validate: [cyan]aegis config validate aegis.yaml[/cyan]")
122
+ _console.print(" Lint: [cyan]aegis policy lint aegis.yaml[/cyan]")