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.
- lovarch_cli/__init__.py +16 -0
- lovarch_cli/__main__.py +10 -0
- lovarch_cli/ai/__init__.py +21 -0
- lovarch_cli/ai/gateway.py +240 -0
- lovarch_cli/api.py +111 -0
- lovarch_cli/auth/__init__.py +32 -0
- lovarch_cli/auth/keyring_store.py +214 -0
- lovarch_cli/auth/local_server.py +165 -0
- lovarch_cli/auth/pkce.py +57 -0
- lovarch_cli/auth/session.py +189 -0
- lovarch_cli/cli.py +262 -0
- lovarch_cli/clients/__init__.py +33 -0
- lovarch_cli/clients/factory.py +54 -0
- lovarch_cli/clients/local_client.py +432 -0
- lovarch_cli/clients/lovarch_storage.py +174 -0
- lovarch_cli/clients/lovarch_supabase.py +295 -0
- lovarch_cli/clients/persistence.py +166 -0
- lovarch_cli/clients/storage.py +66 -0
- lovarch_cli/commands/__init__.py +10 -0
- lovarch_cli/commands/account.py +172 -0
- lovarch_cli/commands/audit.py +394 -0
- lovarch_cli/commands/config_cmd.py +80 -0
- lovarch_cli/commands/consolidate.py +217 -0
- lovarch_cli/commands/context_cmd.py +73 -0
- lovarch_cli/commands/dev.py +287 -0
- lovarch_cli/commands/do_cmd.py +120 -0
- lovarch_cli/commands/init.py +218 -0
- lovarch_cli/commands/jobs_cmd.py +95 -0
- lovarch_cli/commands/login.py +202 -0
- lovarch_cli/commands/mcp_cmd.py +26 -0
- lovarch_cli/commands/run.py +375 -0
- lovarch_cli/commands/signup.py +185 -0
- lovarch_cli/commands/status.py +243 -0
- lovarch_cli/commands/upgrade.py +108 -0
- lovarch_cli/commands/verifica_cmd.py +174 -0
- lovarch_cli/config.py +101 -0
- lovarch_cli/config_store.py +111 -0
- lovarch_cli/credits/__init__.py +35 -0
- lovarch_cli/credits/base.py +84 -0
- lovarch_cli/credits/factory.py +36 -0
- lovarch_cli/credits/local.py +34 -0
- lovarch_cli/credits/lovarch.py +56 -0
- lovarch_cli/i18n/__init__.py +27 -0
- lovarch_cli/i18n/loader.py +121 -0
- lovarch_cli/i18n/translations/en.json +168 -0
- lovarch_cli/i18n/translations/es.json +168 -0
- lovarch_cli/i18n/translations/it.json +168 -0
- lovarch_cli/i18n/translations/pt.json +168 -0
- lovarch_cli/mcp/__init__.py +9 -0
- lovarch_cli/mcp/server.py +199 -0
- lovarch_cli/mcp/tools.py +372 -0
- lovarch_cli/sample_downloader.py +255 -0
- lovarch_cli/squad/README.md +206 -0
- lovarch_cli/squad/agents/auditor-input.md +353 -0
- lovarch_cli/squad/agents/bim-engineer.md +404 -0
- lovarch_cli/squad/agents/briefing-architect.md +249 -0
- lovarch_cli/squad/agents/cad-engineer.md +278 -0
- lovarch_cli/squad/agents/capitolato-writer.md +256 -0
- lovarch_cli/squad/agents/computo-engineer.md +258 -0
- lovarch_cli/squad/agents/concept-designer.md +399 -0
- lovarch_cli/squad/agents/contratto-architect.md +243 -0
- lovarch_cli/squad/agents/deliverable-builder.md +253 -0
- lovarch_cli/squad/agents/energy-prelim.md +388 -0
- lovarch_cli/squad/agents/pratiche-it.md +251 -0
- lovarch_cli/squad/agents/progetto-chief.md +768 -0
- lovarch_cli/squad/agents/quality-dati.md +409 -0
- lovarch_cli/squad/agents/quality-misure.md +418 -0
- lovarch_cli/squad/agents/quality-normativa.md +417 -0
- lovarch_cli/squad/agents/quality-output.md +436 -0
- lovarch_cli/squad/agents/regolatorio-it.md +278 -0
- lovarch_cli/squad/checklists/handoff-quality-gate.md +232 -0
- lovarch_cli/squad/checklists/quality-dati-checklist.md +134 -0
- lovarch_cli/squad/checklists/quality-misure-checklist.md +139 -0
- lovarch_cli/squad/checklists/quality-normativa-checklist.md +121 -0
- lovarch_cli/squad/checklists/quality-output-checklist.md +116 -0
- lovarch_cli/squad/config.yaml +408 -0
- lovarch_cli/squad/data/CHANGELOG.md +272 -0
- lovarch_cli/squad/data/agents-prd.md +428 -0
- lovarch_cli/squad/data/architettura-progetto-rules.md +328 -0
- lovarch_cli/squad/data/handoff-card-template.md +231 -0
- lovarch_cli/squad/data/mocks/catasto-visura.json +72 -0
- lovarch_cli/squad/data/mocks/firma-envelope.json +43 -0
- lovarch_cli/squad/data/prezzario-lombardia-sample.json +312 -0
- lovarch_cli/squad/scripts/api_clients.py +206 -0
- lovarch_cli/squad/scripts/architect_profile.py +276 -0
- lovarch_cli/squad/scripts/deliverable_generators.py +844 -0
- lovarch_cli/squad/scripts/generate_attico_brera_dwg.py +369 -0
- lovarch_cli/squad/scripts/generate_chianti_dxf.py +368 -0
- lovarch_cli/squad/scripts/generate_chianti_images.py +223 -0
- lovarch_cli/squad/scripts/generate_real_sample_images.py +189 -0
- lovarch_cli/squad/scripts/generate_sample_assets.py +382 -0
- lovarch_cli/squad/scripts/lovarch_client.py +1046 -0
- lovarch_cli/squad/scripts/pipeline_runner.py +2095 -0
- lovarch_cli/squad/scripts/render_dxf_to_png.py +57 -0
- lovarch_cli/squad/scripts/run_palestra_demo.sh +277 -0
- lovarch_cli/squad/scripts/simulate_squad_execution.py +515 -0
- lovarch_cli/squad/scripts/validate-squad.py +383 -0
- lovarch_cli/squad/tasks/audit-input.md +146 -0
- lovarch_cli/squad/tasks/compute-metric.md +105 -0
- lovarch_cli/squad/tasks/consolidate-dossier.md +187 -0
- lovarch_cli/squad/tasks/generate-cad-plan.md +120 -0
- lovarch_cli/squad/tasks/generate-ifc-model.md +108 -0
- lovarch_cli/squad/tasks/write-capitolato.md +100 -0
- lovarch_cli/squad/templates/asseverazione-tecnica.md +126 -0
- lovarch_cli/squad/templates/capitolato-uni-11337.md +235 -0
- lovarch_cli/squad/templates/cila-comune-milano.md +177 -0
- lovarch_cli/squad/templates/contratto-cnappc.md +220 -0
- lovarch_cli/squad/workflows/dal-brief-al-cantiere.yaml +218 -0
- lovarch_cli/squad_loader.py +114 -0
- lovarch_cli/verify/__init__.py +15 -0
- lovarch_cli/verify/contratto.py +110 -0
- lovarch_cli/verify/dossier.py +97 -0
- lovarch_cli/verify/misure.py +83 -0
- lovarch_cli/verify/normativa.py +178 -0
- lovarch_cli/version.py +13 -0
- lovarch_cli/workflows/__init__.py +9 -0
- lovarch_cli/workflows/platform.py +212 -0
- lovarch_cli-0.2.1.dist-info/METADATA +232 -0
- lovarch_cli-0.2.1.dist-info/RECORD +122 -0
- lovarch_cli-0.2.1.dist-info/WHEEL +4 -0
- lovarch_cli-0.2.1.dist-info/entry_points.txt +3 -0
- 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)
|
lovarch_cli/auth/pkce.py
ADDED
|
@@ -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()
|