lovarch-cli 0.2.1__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 (122) hide show
  1. lovarch_cli/__init__.py +16 -0
  2. lovarch_cli/__main__.py +10 -0
  3. lovarch_cli/ai/__init__.py +21 -0
  4. lovarch_cli/ai/gateway.py +240 -0
  5. lovarch_cli/api.py +111 -0
  6. lovarch_cli/auth/__init__.py +32 -0
  7. lovarch_cli/auth/keyring_store.py +214 -0
  8. lovarch_cli/auth/local_server.py +165 -0
  9. lovarch_cli/auth/pkce.py +57 -0
  10. lovarch_cli/auth/session.py +189 -0
  11. lovarch_cli/cli.py +262 -0
  12. lovarch_cli/clients/__init__.py +33 -0
  13. lovarch_cli/clients/factory.py +54 -0
  14. lovarch_cli/clients/local_client.py +432 -0
  15. lovarch_cli/clients/lovarch_storage.py +174 -0
  16. lovarch_cli/clients/lovarch_supabase.py +295 -0
  17. lovarch_cli/clients/persistence.py +166 -0
  18. lovarch_cli/clients/storage.py +66 -0
  19. lovarch_cli/commands/__init__.py +10 -0
  20. lovarch_cli/commands/account.py +172 -0
  21. lovarch_cli/commands/audit.py +394 -0
  22. lovarch_cli/commands/config_cmd.py +80 -0
  23. lovarch_cli/commands/consolidate.py +217 -0
  24. lovarch_cli/commands/context_cmd.py +73 -0
  25. lovarch_cli/commands/dev.py +287 -0
  26. lovarch_cli/commands/do_cmd.py +120 -0
  27. lovarch_cli/commands/init.py +218 -0
  28. lovarch_cli/commands/jobs_cmd.py +95 -0
  29. lovarch_cli/commands/login.py +202 -0
  30. lovarch_cli/commands/mcp_cmd.py +26 -0
  31. lovarch_cli/commands/run.py +375 -0
  32. lovarch_cli/commands/signup.py +185 -0
  33. lovarch_cli/commands/status.py +243 -0
  34. lovarch_cli/commands/upgrade.py +108 -0
  35. lovarch_cli/commands/verifica_cmd.py +174 -0
  36. lovarch_cli/config.py +101 -0
  37. lovarch_cli/config_store.py +111 -0
  38. lovarch_cli/credits/__init__.py +35 -0
  39. lovarch_cli/credits/base.py +84 -0
  40. lovarch_cli/credits/factory.py +36 -0
  41. lovarch_cli/credits/local.py +34 -0
  42. lovarch_cli/credits/lovarch.py +56 -0
  43. lovarch_cli/i18n/__init__.py +27 -0
  44. lovarch_cli/i18n/loader.py +121 -0
  45. lovarch_cli/i18n/translations/en.json +168 -0
  46. lovarch_cli/i18n/translations/es.json +168 -0
  47. lovarch_cli/i18n/translations/it.json +168 -0
  48. lovarch_cli/i18n/translations/pt.json +168 -0
  49. lovarch_cli/mcp/__init__.py +9 -0
  50. lovarch_cli/mcp/server.py +199 -0
  51. lovarch_cli/mcp/tools.py +372 -0
  52. lovarch_cli/sample_downloader.py +255 -0
  53. lovarch_cli/squad/README.md +206 -0
  54. lovarch_cli/squad/agents/auditor-input.md +353 -0
  55. lovarch_cli/squad/agents/bim-engineer.md +404 -0
  56. lovarch_cli/squad/agents/briefing-architect.md +249 -0
  57. lovarch_cli/squad/agents/cad-engineer.md +278 -0
  58. lovarch_cli/squad/agents/capitolato-writer.md +256 -0
  59. lovarch_cli/squad/agents/computo-engineer.md +258 -0
  60. lovarch_cli/squad/agents/concept-designer.md +399 -0
  61. lovarch_cli/squad/agents/contratto-architect.md +243 -0
  62. lovarch_cli/squad/agents/deliverable-builder.md +253 -0
  63. lovarch_cli/squad/agents/energy-prelim.md +388 -0
  64. lovarch_cli/squad/agents/pratiche-it.md +251 -0
  65. lovarch_cli/squad/agents/progetto-chief.md +768 -0
  66. lovarch_cli/squad/agents/quality-dati.md +409 -0
  67. lovarch_cli/squad/agents/quality-misure.md +418 -0
  68. lovarch_cli/squad/agents/quality-normativa.md +417 -0
  69. lovarch_cli/squad/agents/quality-output.md +436 -0
  70. lovarch_cli/squad/agents/regolatorio-it.md +278 -0
  71. lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
  72. lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
  73. lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
  74. lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
  75. lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
  76. lovarch_cli/squad/config.yaml +408 -0
  77. lovarch_cli/squad/data/CHANGELOG.md +272 -0
  78. lovarch_cli/squad/data/agents-prd.md +428 -0
  79. lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
  80. lovarch_cli/squad/data/handoff-card-template.md +231 -0
  81. lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
  82. lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
  83. lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
  84. lovarch_cli/squad/scripts/api_clients.py +206 -0
  85. lovarch_cli/squad/scripts/architect_profile.py +276 -0
  86. lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
  87. lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
  88. lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
  89. lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
  90. lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
  91. lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
  92. lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
  93. lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
  94. lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
  95. lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
  96. lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
  97. lovarch_cli/squad/scripts/validate-squad.py +383 -0
  98. lovarch_cli/squad/tasks/audit-input.md +146 -0
  99. lovarch_cli/squad/tasks/compute-metric.md +105 -0
  100. lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
  101. lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
  102. lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
  103. lovarch_cli/squad/tasks/write-capitolato.md +100 -0
  104. lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
  105. lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
  106. lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
  107. lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
  108. lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
  109. lovarch_cli/squad_loader.py +114 -0
  110. lovarch_cli/verify/__init__.py +15 -0
  111. lovarch_cli/verify/contratto.py +110 -0
  112. lovarch_cli/verify/dossier.py +97 -0
  113. lovarch_cli/verify/misure.py +83 -0
  114. lovarch_cli/verify/normativa.py +178 -0
  115. lovarch_cli/version.py +13 -0
  116. lovarch_cli/workflows/__init__.py +9 -0
  117. lovarch_cli/workflows/platform.py +212 -0
  118. lovarch_cli-0.2.1.dist-info/METADATA +232 -0
  119. lovarch_cli-0.2.1.dist-info/RECORD +122 -0
  120. lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
  121. lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
  122. lovarch_cli-0.2.1.dist-info/licenses/LICENSE +38 -0
@@ -0,0 +1,165 @@
1
+ """Ephemeral HTTP server for capturing OAuth-style PKCE redirects.
2
+
3
+ Listens on 127.0.0.1:RANDOM_PORT. The Lovarch web /cli-auth page redirects
4
+ the browser to http://127.0.0.1:PORT/callback?code=X&state=Y after the user
5
+ authorizes the CLI. This module captures those query params and shuts down.
6
+
7
+ Threading: the server runs in a background thread so the CLI's main thread
8
+ can show a "Waiting for browser..." spinner. The result (code+state OR
9
+ error) is delivered via a threading.Event + a result holder.
10
+
11
+ Security notes:
12
+ - Bound to 127.0.0.1 only (not 0.0.0.0) — only the user's machine can hit
13
+ - One-shot: serves a single /callback request then shuts down
14
+ - Random port (port=0 lets OS pick) — defeats simple port-based attacks
15
+ - Ignores any path other than /callback (returns 404)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import threading
20
+ from dataclasses import dataclass
21
+ from http.server import BaseHTTPRequestHandler, HTTPServer
22
+ from typing import Optional
23
+ from urllib.parse import parse_qs, urlparse
24
+
25
+ CALLBACK_PATH = "/callback"
26
+
27
+ SUCCESS_HTML = """<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8">
31
+ <title>lovarch-cli login</title>
32
+ <style>
33
+ body { font-family: -apple-system, system-ui, sans-serif;
34
+ display: flex; align-items: center; justify-content: center;
35
+ height: 100vh; margin: 0; background: #FAF9F7; color: #09090B; }
36
+ .card { text-align: center; padding: 2rem 3rem;
37
+ border: 1px solid rgba(0,0,0,0.08); border-radius: 12px;
38
+ background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
39
+ h1 { color: #A16207; margin: 0 0 0.5rem; font-weight: 600; }
40
+ p { margin: 0.5rem 0 0; color: #71717a; }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <div class="card">
45
+ <h1>✓ lovarch-cli</h1>
46
+ <p>Login completato. Puoi chiudere questa scheda.</p>
47
+ <p style="margin-top:1rem;font-size:0.85rem">Login complete — you can close this tab.</p>
48
+ </div>
49
+ </body>
50
+ </html>
51
+ """
52
+
53
+ ERROR_HTML = """<!doctype html>
54
+ <html lang="en">
55
+ <head><meta charset="utf-8"><title>lovarch-cli login error</title></head>
56
+ <body style="font-family:sans-serif;padding:2rem;background:#FAF9F7">
57
+ <h1 style="color:#dc2626">lovarch-cli login failed</h1>
58
+ <p>{error}</p>
59
+ <p>Torna al terminale per maggiori dettagli. Return to the terminal for details.</p>
60
+ </body>
61
+ </html>
62
+ """
63
+
64
+
65
+ @dataclass
66
+ class CallbackResult:
67
+ """Result captured from the browser redirect."""
68
+
69
+ code: Optional[str] = None
70
+ state: Optional[str] = None
71
+ error: Optional[str] = None
72
+ error_description: Optional[str] = None
73
+
74
+
75
+ class _Handler(BaseHTTPRequestHandler):
76
+ """Per-request handler. Stores result on the parent server."""
77
+
78
+ server_version = "lovarch-cli/0.1"
79
+
80
+ def log_message(self, format: str, *args: object) -> None: # type: ignore[override]
81
+ # Suppress default stderr logging — we capture via spinner
82
+ return
83
+
84
+ def do_GET(self) -> None: # noqa: N802 (BaseHTTPRequestHandler API)
85
+ parsed = urlparse(self.path)
86
+ if parsed.path != CALLBACK_PATH:
87
+ self.send_response(404)
88
+ self.end_headers()
89
+ self.wfile.write(b"Not Found")
90
+ return
91
+
92
+ params = parse_qs(parsed.query, keep_blank_values=True)
93
+ result: CallbackResult = self.server.callback_result # type: ignore[attr-defined]
94
+
95
+ if "error" in params:
96
+ result.error = params["error"][0]
97
+ result.error_description = params.get(
98
+ "error_description", [""]
99
+ )[0]
100
+ html = ERROR_HTML.format(error=result.error)
101
+ self.send_response(400)
102
+ self.send_header("Content-Type", "text/html; charset=utf-8")
103
+ self.end_headers()
104
+ self.wfile.write(html.encode("utf-8"))
105
+ elif "code" in params and "state" in params:
106
+ result.code = params["code"][0]
107
+ result.state = params["state"][0]
108
+ self.send_response(200)
109
+ self.send_header("Content-Type", "text/html; charset=utf-8")
110
+ self.end_headers()
111
+ self.wfile.write(SUCCESS_HTML.encode("utf-8"))
112
+ else:
113
+ result.error = "missing_params"
114
+ result.error_description = "neither code/state nor error returned"
115
+ self.send_response(400)
116
+ self.end_headers()
117
+ self.wfile.write(b"Missing parameters")
118
+
119
+ # Signal main thread that we got the redirect
120
+ self.server.done_event.set() # type: ignore[attr-defined]
121
+
122
+
123
+ class AuthServer:
124
+ """Single-shot localhost HTTP server for PKCE callback capture."""
125
+
126
+ def __init__(self, port: int = 0) -> None:
127
+ self._httpd = HTTPServer(("127.0.0.1", port), _Handler)
128
+ # Attach result holder + event to the server instance for handler access
129
+ self._httpd.callback_result = CallbackResult() # type: ignore[attr-defined]
130
+ self._httpd.done_event = threading.Event() # type: ignore[attr-defined]
131
+ self._thread: Optional[threading.Thread] = None
132
+
133
+ @property
134
+ def port(self) -> int:
135
+ return self._httpd.server_address[1]
136
+
137
+ @property
138
+ def callback_url(self) -> str:
139
+ return f"http://127.0.0.1:{self.port}{CALLBACK_PATH}"
140
+
141
+ def start(self) -> None:
142
+ """Start serving in a background thread."""
143
+ self._thread = threading.Thread(
144
+ target=self._httpd.serve_forever,
145
+ daemon=True,
146
+ name="lovarch-cli-auth-server",
147
+ )
148
+ self._thread.start()
149
+
150
+ def wait_for_callback(self, timeout_seconds: float) -> CallbackResult:
151
+ """Block until the browser hits /callback or timeout elapses."""
152
+ got_it = self._httpd.done_event.wait(timeout=timeout_seconds) # type: ignore[attr-defined]
153
+ if not got_it:
154
+ self._httpd.callback_result.error = "timeout" # type: ignore[attr-defined]
155
+ self._httpd.callback_result.error_description = ( # type: ignore[attr-defined]
156
+ f"No callback within {timeout_seconds}s"
157
+ )
158
+ return self._httpd.callback_result # type: ignore[attr-defined,return-value]
159
+
160
+ def shutdown(self) -> None:
161
+ """Stop the HTTP server and wait for thread to join."""
162
+ self._httpd.shutdown()
163
+ self._httpd.server_close()
164
+ if self._thread:
165
+ self._thread.join(timeout=2.0)
@@ -0,0 +1,57 @@
1
+ """PKCE (RFC 7636) parameter generation for lovarch-cli premium login.
2
+
3
+ PKCE = Proof Key for Code Exchange. Used to authenticate a CLI/native app
4
+ without storing client secrets:
5
+ - Generate random `code_verifier` (43-128 chars, URL-safe)
6
+ - Compute `code_challenge` = base64url(SHA-256(code_verifier))
7
+ - Send challenge to authorization server in initial request
8
+ - Send verifier later when exchanging code for tokens
9
+ - Server validates SHA-256(verifier) == challenge before issuing tokens
10
+
11
+ Why: a CLI cannot keep a client_secret confidential (it's bundled in the
12
+ distribution). PKCE replaces that with a per-session ephemeral secret.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import hashlib
18
+ import secrets
19
+ from dataclasses import dataclass
20
+
21
+
22
+ def _b64url(data: bytes) -> str:
23
+ """Encode bytes as base64url (no padding)."""
24
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PkceParams:
29
+ """PKCE parameters generated by the CLI for a single login attempt."""
30
+
31
+ code_verifier: str
32
+ code_challenge: str
33
+ state: str
34
+
35
+ @classmethod
36
+ def generate(cls) -> PkceParams:
37
+ """Generate a fresh PKCE param set per RFC 7636.
38
+
39
+ - code_verifier: 64 random URL-safe bytes → ~86 chars (within 43-128 spec)
40
+ - code_challenge: base64url(SHA-256(verifier))
41
+ - state: 24 random URL-safe bytes for CSRF protection (~32 chars)
42
+ """
43
+ verifier = secrets.token_urlsafe(64)
44
+ verifier_bytes = verifier.encode("ascii")
45
+ challenge_digest = hashlib.sha256(verifier_bytes).digest()
46
+ challenge = _b64url(challenge_digest)
47
+ state = secrets.token_urlsafe(24)
48
+ return cls(
49
+ code_verifier=verifier,
50
+ code_challenge=challenge,
51
+ state=state,
52
+ )
53
+
54
+ @property
55
+ def code_challenge_method(self) -> str:
56
+ """RFC 7636 challenge method identifier — always SHA-256 here."""
57
+ return "S256"
@@ -0,0 +1,189 @@
1
+ """LovarchSession — manages Supabase access/refresh tokens with auto-refresh.
2
+
3
+ Wraps httpx and the Supabase /auth/v1/token endpoint to:
4
+ - Inject Authorization: Bearer {access_token} on every call
5
+ - Detect 401 responses and automatically refresh tokens
6
+ - Persist refreshed tokens back to OS keyring + credentials.json
7
+ - Surface fatal errors (refresh failed) so callers can prompt re-login
8
+
9
+ Used by LovarchSupabaseClient (DataPersistenceClient impl) and
10
+ LovarchStorage (StorageClient impl). NOT used by free-mode clients.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from dataclasses import dataclass
16
+ from datetime import datetime, timedelta, timezone
17
+ from typing import Any
18
+
19
+ import httpx
20
+
21
+ from lovarch_cli.auth.keyring_store import (
22
+ PremiumSession,
23
+ load_premium_session,
24
+ save_premium_session,
25
+ )
26
+ from lovarch_cli.config import DEFAULT_API_ANON_KEY, DEFAULT_API_URL
27
+
28
+
29
+ class LovarchSessionError(Exception):
30
+ """Raised when token refresh fails permanently — caller should re-login."""
31
+
32
+
33
+ @dataclass
34
+ class _RefreshResult:
35
+ access_token: str
36
+ refresh_token: str
37
+ expires_in: int
38
+
39
+
40
+ class LovarchSession:
41
+ """Holds + auto-refreshes a Supabase user session."""
42
+
43
+ def __init__(
44
+ self,
45
+ session: PremiumSession,
46
+ api_url: str = DEFAULT_API_URL,
47
+ anon_key: str = DEFAULT_API_ANON_KEY,
48
+ ) -> None:
49
+ self._session = session
50
+ self._api_url = api_url.rstrip("/")
51
+ self._anon_key = anon_key
52
+ self._refresh_lock = asyncio.Lock()
53
+
54
+ # ─── Public load/save helpers ─────────────────────────────────────────
55
+
56
+ @classmethod
57
+ def load(cls) -> "LovarchSession | None":
58
+ """Load from keyring + credentials.json, return None if no session."""
59
+ ps = load_premium_session()
60
+ if ps is None:
61
+ return None
62
+ return cls(ps)
63
+
64
+ @property
65
+ def user_id(self) -> str:
66
+ return self._session.user_id
67
+
68
+ @property
69
+ def email(self) -> str:
70
+ return self._session.email
71
+
72
+ @property
73
+ def access_token(self) -> str:
74
+ return self._session.access_token
75
+
76
+ # ─── HTTP request with auto-refresh ───────────────────────────────────
77
+
78
+ async def request(
79
+ self,
80
+ method: str,
81
+ path: str,
82
+ *,
83
+ params: dict[str, Any] | None = None,
84
+ json: Any = None,
85
+ headers: dict[str, str] | None = None,
86
+ content: bytes | None = None,
87
+ timeout: float = 60.0,
88
+ ) -> httpx.Response:
89
+ """Perform HTTP request with Authorization injected; refresh on 401."""
90
+ url = f"{self._api_url}{path}" if path.startswith("/") else path
91
+
92
+ merged_headers: dict[str, str] = {
93
+ "User-Agent": "lovarch-cli/0.1.0",
94
+ "apikey": self._anon_key,
95
+ "Authorization": f"Bearer {self._session.access_token}",
96
+ }
97
+ if headers:
98
+ merged_headers.update(headers)
99
+
100
+ async with httpx.AsyncClient(timeout=timeout) as client:
101
+ response = await client.request(
102
+ method,
103
+ url,
104
+ params=params,
105
+ json=json,
106
+ headers=merged_headers,
107
+ content=content,
108
+ )
109
+
110
+ if response.status_code != 401:
111
+ return response
112
+
113
+ # ─── Try refresh + retry once ────────────────────────────────
114
+ try:
115
+ await self._refresh()
116
+ except LovarchSessionError:
117
+ return response # bubble original 401 to caller
118
+
119
+ merged_headers["Authorization"] = f"Bearer {self._session.access_token}"
120
+ response = await client.request(
121
+ method,
122
+ url,
123
+ params=params,
124
+ json=json,
125
+ headers=merged_headers,
126
+ content=content,
127
+ )
128
+ return response
129
+
130
+ # ─── Refresh logic ────────────────────────────────────────────────────
131
+
132
+ async def _refresh(self) -> None:
133
+ """Refresh access_token using refresh_token. Persist on success."""
134
+ async with self._refresh_lock:
135
+ url = f"{self._api_url}/auth/v1/token?grant_type=refresh_token"
136
+ async with httpx.AsyncClient(timeout=30.0) as client:
137
+ response = await client.post(
138
+ url,
139
+ json={"refresh_token": self._session.refresh_token},
140
+ headers={
141
+ "apikey": self._anon_key,
142
+ "Content-Type": "application/json",
143
+ },
144
+ )
145
+
146
+ if response.status_code >= 400:
147
+ msg = (
148
+ f"Token refresh failed: HTTP {response.status_code} "
149
+ f"{response.text[:200]}"
150
+ )
151
+ raise LovarchSessionError(msg)
152
+
153
+ try:
154
+ data = response.json()
155
+ except ValueError as exc:
156
+ raise LovarchSessionError(
157
+ f"Invalid JSON from refresh endpoint: {exc}"
158
+ ) from exc
159
+
160
+ access = data.get("access_token")
161
+ refresh = data.get("refresh_token") or self._session.refresh_token
162
+ expires_in = int(data.get("expires_in", 3600))
163
+
164
+ if not access:
165
+ raise LovarchSessionError("Refresh response missing access_token")
166
+
167
+ new_expires = (
168
+ datetime.now(timezone.utc) + timedelta(seconds=expires_in)
169
+ ).isoformat(timespec="seconds")
170
+
171
+ # Update in-memory + persist
172
+ self._session = PremiumSession(
173
+ user_id=self._session.user_id,
174
+ email=self._session.email,
175
+ full_name=self._session.full_name,
176
+ access_token=access,
177
+ refresh_token=refresh,
178
+ expires_at=new_expires,
179
+ metadata=self._session.metadata,
180
+ )
181
+ save_premium_session(
182
+ user_id=self._session.user_id,
183
+ email=self._session.email,
184
+ full_name=self._session.full_name,
185
+ access_token=access,
186
+ refresh_token=refresh,
187
+ expires_at=new_expires,
188
+ language=self._session.metadata.get("language", "it"),
189
+ )
lovarch_cli/cli.py ADDED
@@ -0,0 +1,262 @@
1
+ """lovarch-cli — Typer app entry point.
2
+
3
+ Subcommand structure (loaded lazily as commands are implemented):
4
+ lovarch login → Free or Premium authentication
5
+ lovarch signup → Free mode registration (name, email, phone)
6
+ lovarch config → API keys, language, storage path
7
+ lovarch init → Create new project with sample-input
8
+ lovarch audit → Run input audit (18-point checklist)
9
+ lovarch run → Execute full workflow
10
+ lovarch consolidate → Generate DOSSIER.zip
11
+ lovarch status → Inspect execution
12
+ lovarch upgrade → CTA from Free to Premium
13
+ lovarch account → GDPR right-to-erasure
14
+
15
+ Global flags:
16
+ --lang {it,pt,en,es} → override detected language
17
+ --version → print version and exit
18
+ --verbose → verbose logging
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ from typing import Annotated, Optional
24
+
25
+ import typer
26
+ from rich.console import Console
27
+ from rich.panel import Panel
28
+ from rich.text import Text
29
+
30
+ from lovarch_cli.i18n import current_lang, set_current_lang, t
31
+ from lovarch_cli.version import __version__
32
+
33
+ console = Console()
34
+ err_console = Console(stderr=True)
35
+
36
+ app = typer.Typer(
37
+ name="lovarch",
38
+ help=(
39
+ "lovarch-cli — AI-powered architectural project execution.\n\n"
40
+ "Squad di 17 agenti specializzati che genera audit, CAD, BIM/IFC, "
41
+ "computo, capitolato, pratiche IT, contratto CNAPPC, energy/LCA "
42
+ "in 14 minuti vs 3 settimane di lavoro tradizionale."
43
+ ),
44
+ no_args_is_help=True,
45
+ add_completion=True,
46
+ rich_markup_mode="rich",
47
+ context_settings={"help_option_names": ["-h", "--help"]},
48
+ )
49
+
50
+
51
+ def _version_callback(value: bool) -> None:
52
+ """Print version and exit (used by --version global flag)."""
53
+ if value:
54
+ console.print(f"lovarch-cli [bold]{__version__}[/bold]")
55
+ raise typer.Exit()
56
+
57
+
58
+ @app.callback()
59
+ def main(
60
+ ctx: typer.Context,
61
+ version: Annotated[
62
+ Optional[bool],
63
+ typer.Option(
64
+ "--version",
65
+ "-V",
66
+ callback=_version_callback,
67
+ is_eager=True,
68
+ help="Mostra la versione e esce.",
69
+ ),
70
+ ] = None,
71
+ lang: Annotated[
72
+ Optional[str],
73
+ typer.Option(
74
+ "--lang",
75
+ "-l",
76
+ help="Lingua: it, pt, en, es (default: rilevata automaticamente).",
77
+ ),
78
+ ] = None,
79
+ verbose: Annotated[
80
+ bool,
81
+ typer.Option(
82
+ "--verbose",
83
+ "-v",
84
+ help="Output dettagliato per debug.",
85
+ ),
86
+ ] = False,
87
+ ) -> None:
88
+ """lovarch-cli — global flags handler."""
89
+ # Resolve --lang precedence: explicit flag > $LOVARCH_LANG > $LANG > 'it'.
90
+ # set_current_lang() validates against VALID_LANGUAGES and falls back to
91
+ # env detection if the override is invalid.
92
+ if lang is not None:
93
+ set_current_lang(lang)
94
+ ctx.ensure_object(dict)
95
+ ctx.obj["lang"] = lang
96
+ ctx.obj["verbose"] = verbose
97
+
98
+
99
+ @app.command(name="info")
100
+ def info_cmd() -> None:
101
+ """Mostra informazioni sulla installazione corrente."""
102
+ lang = current_lang()
103
+ title = Text("lovarch-cli", style="bold gold1")
104
+ body = Text.assemble(
105
+ ("Version: ", "dim"),
106
+ (f"{__version__}\n", "bold"),
107
+ ("Python: ", "dim"),
108
+ (f"{sys.version.split()[0]}\n", "bold"),
109
+ ("Platform:", "dim"),
110
+ (f" {sys.platform}\n", "bold"),
111
+ ("Squad: ", "dim"),
112
+ ("architettura-progetto (17 agents, 6 tasks, 1 workflow)\n", "bold"),
113
+ ("Mode: ", "dim"),
114
+ (t("info.mode_not_configured", lang=lang), "italic yellow"),
115
+ )
116
+ console.print(
117
+ Panel(
118
+ Text.assemble(title, "\n\n", body),
119
+ title=f"[bold]{t('info.title', lang=lang)}[/bold]",
120
+ border_style="gold1",
121
+ padding=(1, 2),
122
+ )
123
+ )
124
+ console.print(
125
+ f"\n[dim]{t('info.powered_by', lang=lang)}[/dim]\n"
126
+ f"[dim]{t('info.course_promo', lang=lang)}[/dim]\n"
127
+ )
128
+
129
+
130
+ # ════════════════════════════════════════════════════════════════════════════
131
+ # Subcommands registration
132
+ #
133
+ # Each subcommand is either:
134
+ # - a single-action command via app.command()(fn)
135
+ # - a multi-subcommand group via app.add_typer(sub.app, name=...)
136
+ # ════════════════════════════════════════════════════════════════════════════
137
+
138
+ # Story 2.2 — Free mode signup with lead capture
139
+ from lovarch_cli.commands.signup import signup_command # noqa: E402
140
+
141
+ app.command(name="signup", help="Cadastro Free interattivo (lead capture).")(
142
+ signup_command
143
+ )
144
+
145
+ # Story 2.3 — GDPR right-to-erasure + account info
146
+ from lovarch_cli.commands import account as account_cmd # noqa: E402
147
+
148
+ app.add_typer(
149
+ account_cmd.app,
150
+ name="account",
151
+ help="Gestione account (info, delete GDPR).",
152
+ )
153
+
154
+ # Story 3.3 — premium PKCE login + free redirect
155
+ from lovarch_cli.commands.login import login_command # noqa: E402
156
+
157
+ app.command(name="login", help="Login al CLI (--free o --premium).")(
158
+ login_command
159
+ )
160
+
161
+ # Story 3.4 — CTA Free → Premium upgrade flow (opens browser)
162
+ from lovarch_cli.commands.upgrade import upgrade_command # noqa: E402
163
+
164
+ app.command(name="upgrade", help="Passa da Free a Premium (apre il browser).")(
165
+ upgrade_command
166
+ )
167
+
168
+ # Fase A.1 — Create a new project directory (+ optional sample-input)
169
+ from lovarch_cli.commands.init import init_command # noqa: E402
170
+
171
+ app.command(name="init", help="Inizializza un nuovo progetto (con --sample copia villa-chianti).")(
172
+ init_command
173
+ )
174
+
175
+ # Fase A.2 — 18-point input validation checklist
176
+ from lovarch_cli.commands.audit import audit_command # noqa: E402
177
+
178
+ app.command(name="audit", help="Audit dei 18 input prima del run (verdict PASS/CONCERNS/FAIL).")(
179
+ audit_command
180
+ )
181
+
182
+ # Fase A.3 — Execute the squad pipeline (subprocess wrapper, premium = real)
183
+ from lovarch_cli.commands.run import run_command # noqa: E402
184
+
185
+ app.command(name="run", help="Esegui il pipeline (Free=dry-run, Premium=reale).")(
186
+ run_command
187
+ )
188
+
189
+ # Fase A.4 — Bundle run output into DOSSIER.zip
190
+ from lovarch_cli.commands.consolidate import consolidate_command # noqa: E402
191
+
192
+ app.command(name="consolidate", help="Genera DOSSIER.zip dall'output di un run.")(
193
+ consolidate_command
194
+ )
195
+
196
+ # Fase A.5 — Inspect project state and recent runs
197
+ from lovarch_cli.commands.status import status_command # noqa: E402
198
+
199
+ app.command(name="status", help="Stato dei progetti e ultime esecuzioni.")(
200
+ status_command
201
+ )
202
+
203
+ # Squad-dev-loop — developer tooling (squad path resolution, vendor refresh)
204
+ from lovarch_cli.commands.dev import dev_app # noqa: E402
205
+
206
+ app.add_typer(
207
+ dev_app,
208
+ name="dev",
209
+ help="Developer tooling for the squad payload (contributors only).",
210
+ )
211
+
212
+ from lovarch_cli.commands.mcp_cmd import mcp_app # noqa: E402
213
+
214
+ app.add_typer(
215
+ mcp_app,
216
+ name="mcp",
217
+ help="Server MCP (Claude Code / IDE). Richiede l'extra [mcp].",
218
+ )
219
+
220
+ from lovarch_cli.commands.context_cmd import context_app # noqa: E402
221
+
222
+ app.add_typer(
223
+ context_app,
224
+ name="context",
225
+ help="Contesto di personalizzazione usato dagli agenti AI (premium).",
226
+ )
227
+
228
+ from lovarch_cli.commands.do_cmd import do_app # noqa: E402
229
+
230
+ app.add_typer(
231
+ do_app,
232
+ name="do",
233
+ help="Workflow della piattaforma: render, colors, copy (premium).",
234
+ )
235
+
236
+ from lovarch_cli.commands.verifica_cmd import verifica_app # noqa: E402
237
+
238
+ app.add_typer(
239
+ verifica_app,
240
+ name="verifica",
241
+ help="Verifica dati: misure DXF (gratis) · normativa adversarial (premium).",
242
+ )
243
+
244
+ from lovarch_cli.commands.jobs_cmd import jobs_app # noqa: E402
245
+
246
+ app.add_typer(
247
+ jobs_app,
248
+ name="jobs",
249
+ help="Job asincroni (video, export, upscale) — stato e output.",
250
+ )
251
+
252
+ from lovarch_cli.commands.config_cmd import config_app # noqa: E402
253
+
254
+ app.add_typer(
255
+ config_app,
256
+ name="config",
257
+ help="Configurazione utente (lingua, storage, API keys Free mode).",
258
+ )
259
+
260
+
261
+ if __name__ == "__main__":
262
+ app()