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,243 @@
|
|
|
1
|
+
"""lovarch status — Inspect project state and recent runs.
|
|
2
|
+
|
|
3
|
+
Two modes:
|
|
4
|
+
- lovarch status → list all projects + their last_audit / last_dossier
|
|
5
|
+
- lovarch status <project> → drill into one project: full last_audit detail,
|
|
6
|
+
last_dossier, output/ file listing
|
|
7
|
+
|
|
8
|
+
Reads project.yaml metadata (no Lovarch backend required), so this works
|
|
9
|
+
identically in Free and Premium modes. For executions persisted via the
|
|
10
|
+
DataPersistenceClient (Story 1.3), a future revision will fetch step-level
|
|
11
|
+
status from the persistence layer; today we surface what the file system
|
|
12
|
+
+ project.yaml reveal.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
import yaml
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
|
|
28
|
+
from lovarch_cli.config import DEFAULT_HOME
|
|
29
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
err_console = Console(stderr=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_project_meta(proj_dir: Path) -> dict:
|
|
36
|
+
yaml_path = proj_dir / "project.yaml"
|
|
37
|
+
if not yaml_path.exists():
|
|
38
|
+
return {}
|
|
39
|
+
try:
|
|
40
|
+
return yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
41
|
+
except yaml.YAMLError:
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_relative_age(iso_ts: str | None) -> str:
|
|
46
|
+
"""Convert ISO 8601 → 'Xm ago' / 'Xh ago' / 'Xd ago'."""
|
|
47
|
+
if not iso_ts:
|
|
48
|
+
return "-"
|
|
49
|
+
try:
|
|
50
|
+
dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
|
|
51
|
+
except ValueError:
|
|
52
|
+
return iso_ts
|
|
53
|
+
delta = datetime.now(timezone.utc) - dt
|
|
54
|
+
seconds = int(delta.total_seconds())
|
|
55
|
+
if seconds < 0:
|
|
56
|
+
return "just now"
|
|
57
|
+
if seconds < 60:
|
|
58
|
+
return f"{seconds}s ago"
|
|
59
|
+
if seconds < 3600:
|
|
60
|
+
return f"{seconds // 60}m ago"
|
|
61
|
+
if seconds < 86_400:
|
|
62
|
+
return f"{seconds // 3600}h ago"
|
|
63
|
+
return f"{seconds // 86_400}d ago"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_VERDICT_STYLE = {
|
|
67
|
+
"PASS": "[bold green]PASS[/bold green]",
|
|
68
|
+
"CONCERNS": "[bold yellow]CONCERNS[/bold yellow]",
|
|
69
|
+
"FAIL": "[bold red]FAIL[/bold red]",
|
|
70
|
+
None: "[dim]—[/dim]",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _render_project_list(projects_root: Path, lang: str) -> Table:
|
|
75
|
+
"""Table view: all projects with their audit+dossier state."""
|
|
76
|
+
table = Table(
|
|
77
|
+
title=t("status.list_title", lang=lang),
|
|
78
|
+
title_style="bold gold1",
|
|
79
|
+
header_style="bold",
|
|
80
|
+
)
|
|
81
|
+
table.add_column(t("status.col_project", lang=lang), no_wrap=True)
|
|
82
|
+
table.add_column(t("status.col_workflow", lang=lang))
|
|
83
|
+
table.add_column(t("status.col_audit", lang=lang), no_wrap=True)
|
|
84
|
+
table.add_column(t("status.col_dossier", lang=lang), no_wrap=True)
|
|
85
|
+
table.add_column(t("status.col_age", lang=lang), no_wrap=True)
|
|
86
|
+
|
|
87
|
+
for proj_dir in sorted(projects_root.iterdir()):
|
|
88
|
+
if not proj_dir.is_dir():
|
|
89
|
+
continue
|
|
90
|
+
meta = _load_project_meta(proj_dir)
|
|
91
|
+
last_audit = meta.get("last_audit") or {}
|
|
92
|
+
last_dossier = meta.get("last_dossier") or {}
|
|
93
|
+
verdict_label = _VERDICT_STYLE.get(
|
|
94
|
+
last_audit.get("verdict"), _VERDICT_STYLE[None]
|
|
95
|
+
)
|
|
96
|
+
dossier_label = (
|
|
97
|
+
f"[green]✓[/green] {last_dossier.get('files', 0)} files"
|
|
98
|
+
if last_dossier
|
|
99
|
+
else "[dim]—[/dim]"
|
|
100
|
+
)
|
|
101
|
+
# Age is the freshest of created_at / last_audit / last_dossier
|
|
102
|
+
most_recent = max(
|
|
103
|
+
(
|
|
104
|
+
ts for ts in [
|
|
105
|
+
meta.get("created_at"),
|
|
106
|
+
last_dossier.get("created_at"),
|
|
107
|
+
]
|
|
108
|
+
if ts
|
|
109
|
+
),
|
|
110
|
+
default=None,
|
|
111
|
+
)
|
|
112
|
+
table.add_row(
|
|
113
|
+
proj_dir.name,
|
|
114
|
+
meta.get("workflow", "-"),
|
|
115
|
+
verdict_label,
|
|
116
|
+
dossier_label,
|
|
117
|
+
_format_relative_age(most_recent),
|
|
118
|
+
)
|
|
119
|
+
return table
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _render_project_detail(proj_dir: Path, lang: str) -> None:
|
|
123
|
+
"""Drill-down view for a single project."""
|
|
124
|
+
meta = _load_project_meta(proj_dir)
|
|
125
|
+
name = proj_dir.name
|
|
126
|
+
|
|
127
|
+
body_lines = [
|
|
128
|
+
f"[dim]{t('status.label_workflow', lang=lang)}:[/dim] [bold]{meta.get('workflow', '-')}[/bold]",
|
|
129
|
+
f"[dim]{t('status.label_created', lang=lang)}:[/dim] {meta.get('created_at', '-')}",
|
|
130
|
+
f"[dim]{t('status.label_sample', lang=lang)}:[/dim] {meta.get('sample', False)}",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# Audit section
|
|
134
|
+
last_audit = meta.get("last_audit")
|
|
135
|
+
if last_audit:
|
|
136
|
+
verdict = last_audit.get("verdict")
|
|
137
|
+
body_lines.append("")
|
|
138
|
+
body_lines.append(
|
|
139
|
+
f"[bold]{t('status.section_audit', lang=lang)}:[/bold] "
|
|
140
|
+
f"{_VERDICT_STYLE.get(verdict, _VERDICT_STYLE[None])} — "
|
|
141
|
+
f"PASS={last_audit.get('pass_count', 0)} · "
|
|
142
|
+
f"CONCERNS={last_audit.get('concerns_count', 0)} · "
|
|
143
|
+
f"FAIL={last_audit.get('fail_count', 0)}"
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
body_lines.append("")
|
|
147
|
+
body_lines.append(
|
|
148
|
+
f"[bold]{t('status.section_audit', lang=lang)}:[/bold] "
|
|
149
|
+
f"[dim]{t('status.no_audit_yet', lang=lang)}[/dim]"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Dossier section
|
|
153
|
+
last_dossier = meta.get("last_dossier")
|
|
154
|
+
if last_dossier:
|
|
155
|
+
size_kb = (last_dossier.get("size_bytes", 0)) // 1024
|
|
156
|
+
body_lines.append(
|
|
157
|
+
f"[bold]{t('status.section_dossier', lang=lang)}:[/bold] "
|
|
158
|
+
f"[green]{last_dossier.get('files', 0)} files · {size_kb} KB[/green] "
|
|
159
|
+
f"[dim]({_format_relative_age(last_dossier.get('created_at'))})[/dim]"
|
|
160
|
+
)
|
|
161
|
+
body_lines.append(f" [dim]{last_dossier.get('path')}[/dim]")
|
|
162
|
+
else:
|
|
163
|
+
body_lines.append(
|
|
164
|
+
f"[bold]{t('status.section_dossier', lang=lang)}:[/bold] "
|
|
165
|
+
f"[dim]{t('status.no_dossier_yet', lang=lang)}[/dim]"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Output dir glance: count files
|
|
169
|
+
output_dir = proj_dir / "output"
|
|
170
|
+
if output_dir.is_dir():
|
|
171
|
+
n_output = sum(1 for _ in output_dir.rglob("*") if _.is_file())
|
|
172
|
+
body_lines.append("")
|
|
173
|
+
body_lines.append(
|
|
174
|
+
f"[bold]{t('status.section_output', lang=lang)}:[/bold] "
|
|
175
|
+
f"{n_output} files in [dim]{output_dir}[/dim]"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
console.print()
|
|
179
|
+
console.print(
|
|
180
|
+
Panel(
|
|
181
|
+
Text.from_markup("\n".join(body_lines)),
|
|
182
|
+
title=f"[bold gold1]📋 {t('status.detail_title', lang=lang, name=name)}[/bold gold1]",
|
|
183
|
+
border_style="gold1",
|
|
184
|
+
padding=(1, 2),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
console.print()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def status_command(
|
|
191
|
+
project: Annotated[
|
|
192
|
+
str | None,
|
|
193
|
+
typer.Argument(
|
|
194
|
+
help="Project name to inspect (omit to list all projects).",
|
|
195
|
+
),
|
|
196
|
+
] = None,
|
|
197
|
+
lang_flag: Annotated[
|
|
198
|
+
str | None,
|
|
199
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
200
|
+
] = None,
|
|
201
|
+
home_override: Annotated[
|
|
202
|
+
Path | None,
|
|
203
|
+
typer.Option(
|
|
204
|
+
"--home",
|
|
205
|
+
help="Override $HOME/.lovarch root (mainly for tests).",
|
|
206
|
+
hidden=True,
|
|
207
|
+
),
|
|
208
|
+
] = None,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Inspect project state and recent runs."""
|
|
211
|
+
if lang_flag is not None:
|
|
212
|
+
set_current_lang(lang_flag)
|
|
213
|
+
lang = current_lang()
|
|
214
|
+
|
|
215
|
+
home = home_override or DEFAULT_HOME
|
|
216
|
+
projects_root = home / "projects"
|
|
217
|
+
if not projects_root.is_dir():
|
|
218
|
+
err_console.print(
|
|
219
|
+
f"\n[yellow]{t('status.no_projects', lang=lang)}[/yellow]\n"
|
|
220
|
+
)
|
|
221
|
+
sys.exit(0)
|
|
222
|
+
|
|
223
|
+
if project is None:
|
|
224
|
+
# List all projects in a table
|
|
225
|
+
table = _render_project_list(projects_root, lang)
|
|
226
|
+
console.print()
|
|
227
|
+
console.print(table)
|
|
228
|
+
# Empty-state hint
|
|
229
|
+
if projects_root.exists() and not any(projects_root.iterdir()):
|
|
230
|
+
console.print(
|
|
231
|
+
f"\n[dim]{t('status.no_projects_hint', lang=lang)}[/dim]\n"
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
console.print()
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
proj_dir = projects_root / project
|
|
238
|
+
if not proj_dir.is_dir():
|
|
239
|
+
err_console.print(
|
|
240
|
+
f"\n[red]✗ {t('status.project_not_found', lang=lang, name=project)}[/red]\n"
|
|
241
|
+
)
|
|
242
|
+
sys.exit(2)
|
|
243
|
+
_render_project_detail(proj_dir, lang)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""arch upgrade — CTA from Free to Premium.
|
|
2
|
+
|
|
3
|
+
State-aware:
|
|
4
|
+
- none → instruct user to run `arch signup` first
|
|
5
|
+
- free → show benefits + open browser to /cli-upgrade
|
|
6
|
+
- premium → already-premium acknowledgment + open /settings/credits
|
|
7
|
+
|
|
8
|
+
This is a thin CLI surface around a web page; no backend interaction. The
|
|
9
|
+
goal is to keep the upgrade funnel discoverable in-terminal and route the
|
|
10
|
+
user to the same web flow Lovarch uses for in-app upgrades.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import webbrowser
|
|
16
|
+
from typing import Annotated
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
from lovarch_cli.config import load_credentials
|
|
24
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
err_console = Console(stderr=True)
|
|
28
|
+
|
|
29
|
+
# Same web base used by `arch login --premium` for the PKCE flow.
|
|
30
|
+
LOVARCH_WEB_BASE = "https://lovarch.com"
|
|
31
|
+
UPGRADE_PATH = "/cli-upgrade"
|
|
32
|
+
PREMIUM_DASHBOARD_PATH = "/settings/credits"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def upgrade_command(
|
|
36
|
+
no_browser: Annotated[
|
|
37
|
+
bool,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--no-browser",
|
|
40
|
+
help="Print the URL without opening the browser (for CI/SSH).",
|
|
41
|
+
),
|
|
42
|
+
] = False,
|
|
43
|
+
lang_flag: Annotated[
|
|
44
|
+
str | None,
|
|
45
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
46
|
+
] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Open the Premium upgrade flow in the browser (or print URL with --no-browser)."""
|
|
49
|
+
if lang_flag is not None:
|
|
50
|
+
set_current_lang(lang_flag)
|
|
51
|
+
lang = current_lang()
|
|
52
|
+
|
|
53
|
+
creds = load_credentials()
|
|
54
|
+
|
|
55
|
+
# Prefer the language the user signed up with — falls back to runtime lang
|
|
56
|
+
# so users who didn't sign up still get something localized.
|
|
57
|
+
user_lang = creds.language if creds.mode != "none" else lang
|
|
58
|
+
|
|
59
|
+
# ─── No account → tell user to signup first ──────────────────────────
|
|
60
|
+
if creds.mode == "none":
|
|
61
|
+
err_console.print(
|
|
62
|
+
f"\n[yellow]{t('upgrade.no_account_hint', lang=user_lang)}[/yellow]\n"
|
|
63
|
+
)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
# ─── Already Premium → acknowledge + open billing dashboard ──────────
|
|
67
|
+
if creds.mode == "premium":
|
|
68
|
+
url = f"{LOVARCH_WEB_BASE}{PREMIUM_DASHBOARD_PATH}"
|
|
69
|
+
body = Text.from_markup(
|
|
70
|
+
f"{t('upgrade.already_premium', lang=user_lang)}\n\n"
|
|
71
|
+
f"[dim cyan]{url}[/dim cyan]"
|
|
72
|
+
)
|
|
73
|
+
console.print()
|
|
74
|
+
console.print(
|
|
75
|
+
Panel(
|
|
76
|
+
body,
|
|
77
|
+
title=f"[bold green]{t('upgrade.already_premium_title', lang=user_lang)}[/bold green]",
|
|
78
|
+
border_style="green",
|
|
79
|
+
padding=(1, 2),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
if not no_browser:
|
|
83
|
+
webbrowser.open(url, new=1, autoraise=True)
|
|
84
|
+
console.print()
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# ─── Free → show benefits + open upgrade URL ─────────────────────────
|
|
88
|
+
url = f"{LOVARCH_WEB_BASE}{UPGRADE_PATH}"
|
|
89
|
+
body = Text.from_markup(
|
|
90
|
+
f"{t('upgrade.body_free', lang=user_lang)}\n\n"
|
|
91
|
+
f"[dim]{t('login.manual_url_hint', lang=user_lang)}[/dim]\n"
|
|
92
|
+
f"[dim cyan]{url}[/dim cyan]"
|
|
93
|
+
)
|
|
94
|
+
console.print()
|
|
95
|
+
console.print(
|
|
96
|
+
Panel(
|
|
97
|
+
body,
|
|
98
|
+
title=f"[bold gold1]{t('upgrade.title', lang=user_lang)}[/bold gold1]",
|
|
99
|
+
border_style="gold1",
|
|
100
|
+
padding=(1, 2),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
if not no_browser:
|
|
104
|
+
console.print(
|
|
105
|
+
f"\n[gold1]→[/gold1] {t('login.opening_browser', lang=user_lang)}"
|
|
106
|
+
)
|
|
107
|
+
webbrowser.open(url, new=1, autoraise=True)
|
|
108
|
+
console.print()
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""`lovarch verifica` — conferência de dados para profissionais.
|
|
2
|
+
|
|
3
|
+
lovarch verifica misure pianta.dxf (determinístico, grátis)
|
|
4
|
+
lovarch verifica normativa capitolato.pdf (adversarial 2 modelos, débito)
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
err_console = Console(stderr=True)
|
|
17
|
+
|
|
18
|
+
verifica_app = typer.Typer(
|
|
19
|
+
help="Verifica dati e documenti (misure DXF gratis · normativa adversarial).",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_VERDICT_STYLE = {"PASS": "green", "CONCERNS": "yellow", "REJECT": "red"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print_verdict(verdict: str) -> None:
|
|
27
|
+
style = _VERDICT_STYLE.get(verdict, "white")
|
|
28
|
+
console.print(f"\n[bold {style}]VERDETTO: {verdict}[/bold {style}]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@verifica_app.command("misure")
|
|
32
|
+
def misure_command(
|
|
33
|
+
dxf: Path = typer.Argument(..., help="File DXF da verificare."),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Verifica layer ISO, etichette ambienti e cartiglio CNAPPC (gratis)."""
|
|
36
|
+
from lovarch_cli.verify import verify_misure
|
|
37
|
+
|
|
38
|
+
report = verify_misure(dxf)
|
|
39
|
+
table = Table(title=f"verifica misure — {dxf.name}", header_style="bold gold1")
|
|
40
|
+
table.add_column("Controllo", style="cyan")
|
|
41
|
+
table.add_column("Esito")
|
|
42
|
+
table.add_row("Entità DXF", str(report.stats.get("entities", "—")))
|
|
43
|
+
table.add_row("Layer ISO presenti", f"{report.stats.get('iso_layers_present', 0)}/9")
|
|
44
|
+
table.add_row("Ambienti etichettati", ", ".join(report.stats.get("room_labels_found", [])) or "—")
|
|
45
|
+
console.print(table)
|
|
46
|
+
for f in report.findings:
|
|
47
|
+
console.print(f" [yellow]·[/yellow] {f}")
|
|
48
|
+
_print_verdict(report.verdict)
|
|
49
|
+
raise typer.Exit(0 if report.verdict == "PASS" else (2 if report.verdict == "CONCERNS" else 1))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@verifica_app.command("normativa")
|
|
53
|
+
def normativa_command(
|
|
54
|
+
documento: Path = typer.Argument(..., help="Documento da verificare (.pdf, .md, .txt)."),
|
|
55
|
+
language: str = typer.Option(None, "--language", help="Lingua del report."),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Verifica adversarial das citações normativas (2 modelos · debita créditos)."""
|
|
58
|
+
from lovarch_cli.ai import LovarchAiGateway
|
|
59
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
60
|
+
from lovarch_cli.i18n import current_lang
|
|
61
|
+
from lovarch_cli.verify import verify_normativa
|
|
62
|
+
from lovarch_cli.verify.normativa import NormativaError
|
|
63
|
+
|
|
64
|
+
session = LovarchSession.load()
|
|
65
|
+
if session is None:
|
|
66
|
+
err_console.print("[red]✗ Non autenticato. Esegui `lovarch login --premium`.[/red]")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
report = asyncio.run(verify_normativa(
|
|
71
|
+
LovarchAiGateway(session), documento, language=language or current_lang(),
|
|
72
|
+
))
|
|
73
|
+
except NormativaError as exc:
|
|
74
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
if report.canonical_found:
|
|
78
|
+
console.print(f"[dim]Riferimenti canonici rilevati: {', '.join(report.canonical_found)}[/dim]")
|
|
79
|
+
table = Table(title=f"verifica normativa — {documento.name}", header_style="bold gold1")
|
|
80
|
+
table.add_column("Riferimento", style="cyan")
|
|
81
|
+
table.add_column("Status", justify="center")
|
|
82
|
+
table.add_column("Motivo")
|
|
83
|
+
for v in report.verdicts:
|
|
84
|
+
status = str(v.get("status", "?")).lower()
|
|
85
|
+
icon = {"ok": "[green]✓[/green]", "refuted": "[red]✗[/red]", "doubt": "[yellow]?[/yellow]"}.get(status, "?")
|
|
86
|
+
table.add_row(str(v.get("reference", "—")), icon, str(v.get("reason", ""))[:100])
|
|
87
|
+
console.print(table)
|
|
88
|
+
for n in report.notes:
|
|
89
|
+
console.print(f" [yellow]·[/yellow] {n}")
|
|
90
|
+
console.print(f"[dim]Crediti addebitati: {report.credits_charged}[/dim]")
|
|
91
|
+
_print_verdict(report.verdict)
|
|
92
|
+
raise typer.Exit(0 if report.verdict == "PASS" else (2 if report.verdict == "CONCERNS" else 1))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@verifica_app.command("contratto")
|
|
96
|
+
def contratto_command(
|
|
97
|
+
documento: Path = typer.Argument(..., help="Contratto da verificare (.pdf, .md, .txt)."),
|
|
98
|
+
language: str = typer.Option(None, "--language", help="Lingua del report."),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Verifica adversarial de contrato CNAPPC (estrutura + compenso · debita créditos)."""
|
|
101
|
+
from lovarch_cli.ai import LovarchAiGateway
|
|
102
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
103
|
+
from lovarch_cli.i18n import current_lang
|
|
104
|
+
from lovarch_cli.verify import verify_contratto
|
|
105
|
+
from lovarch_cli.verify.normativa import NormativaError
|
|
106
|
+
|
|
107
|
+
session = LovarchSession.load()
|
|
108
|
+
if session is None:
|
|
109
|
+
err_console.print("[red]\u2717 Non autenticato. Esegui `lovarch login --premium`.[/red]")
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
try:
|
|
112
|
+
report = asyncio.run(verify_contratto(
|
|
113
|
+
LovarchAiGateway(session), str(documento), language=language or current_lang(),
|
|
114
|
+
))
|
|
115
|
+
except NormativaError as exc:
|
|
116
|
+
err_console.print(f"[red]\u2717 {exc}[/red]")
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
|
|
119
|
+
st = report.structure or {}
|
|
120
|
+
if st.get("client_type"):
|
|
121
|
+
compenso_amount = (st.get("compenso") or {}).get("amount") or "\u2014"
|
|
122
|
+
console.print(f"[dim]Committente: {st['client_type']} \u00b7 Compenso: {compenso_amount}[/dim]")
|
|
123
|
+
missing = st.get("sections_missing") or []
|
|
124
|
+
if missing:
|
|
125
|
+
console.print(f" [yellow]\u00b7[/yellow] Sezioni mancanti: {', '.join(str(m) for m in missing[:6])}")
|
|
126
|
+
for f in report.findings:
|
|
127
|
+
sev = str(f.get("severity", "info")).lower()
|
|
128
|
+
icon = {"critical": "[red]\u2717[/red]", "concern": "[yellow]?[/yellow]"}.get(sev, "[dim]\u00b7[/dim]")
|
|
129
|
+
console.print(f" {icon} [{f.get('area', '?')}] {str(f.get('reason', ''))[:110]}")
|
|
130
|
+
console.print(f"[dim]Crediti addebitati: {report.credits_charged}[/dim]")
|
|
131
|
+
_print_verdict(report.verdict)
|
|
132
|
+
raise typer.Exit(0 if report.verdict == "PASS" else (2 if report.verdict == "CONCERNS" else 1))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@verifica_app.command("dossier")
|
|
136
|
+
def dossier_command(
|
|
137
|
+
cartella: Path = typer.Argument(..., help="Cartella dei deliverable da verificare."),
|
|
138
|
+
language: str = typer.Option(None, "--language", help="Lingua del report."),
|
|
139
|
+
max_llm: int = typer.Option(8, "--max-llm", help="Massimo documenti verificati con AI (i DXF sono gratis)."),
|
|
140
|
+
) -> None:
|
|
141
|
+
"""QA completo standalone su una cartella (DXF gratis + documenti adversarial)."""
|
|
142
|
+
from lovarch_cli.ai import LovarchAiGateway
|
|
143
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
144
|
+
from lovarch_cli.i18n import current_lang
|
|
145
|
+
from lovarch_cli.verify import verify_dossier
|
|
146
|
+
from lovarch_cli.verify.normativa import NormativaError
|
|
147
|
+
|
|
148
|
+
session = LovarchSession.load()
|
|
149
|
+
if session is None:
|
|
150
|
+
err_console.print("[red]\u2717 Non autenticato. Esegui `lovarch login --premium`.[/red]")
|
|
151
|
+
raise typer.Exit(1)
|
|
152
|
+
try:
|
|
153
|
+
report = asyncio.run(verify_dossier(
|
|
154
|
+
LovarchAiGateway(session), cartella,
|
|
155
|
+
language=language or current_lang(), max_llm_files=max_llm,
|
|
156
|
+
))
|
|
157
|
+
except NormativaError as exc:
|
|
158
|
+
err_console.print(f"[red]\u2717 {exc}[/red]")
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
|
|
161
|
+
table = Table(title=f"verifica dossier \u2014 {cartella.name}", header_style="bold gold1")
|
|
162
|
+
table.add_column("File", style="cyan")
|
|
163
|
+
table.add_column("Tipo")
|
|
164
|
+
table.add_column("Verdetto", justify="center")
|
|
165
|
+
table.add_column("Dettaglio")
|
|
166
|
+
for f in report.files:
|
|
167
|
+
style = _VERDICT_STYLE.get(f["verdict"], "white")
|
|
168
|
+
table.add_row(f["name"], f["kind"], f"[{style}]{f['verdict']}[/{style}]", str(f["detail"])[:70])
|
|
169
|
+
console.print(table)
|
|
170
|
+
if report.skipped:
|
|
171
|
+
console.print(f" [yellow]\u00b7[/yellow] non verificati con AI (limite --max-llm): {', '.join(report.skipped)}")
|
|
172
|
+
console.print(f"[dim]Crediti addebitati: {report.credits_charged}[/dim]")
|
|
173
|
+
_print_verdict(report.verdict)
|
|
174
|
+
raise typer.Exit(0 if report.verdict == "PASS" else (2 if report.verdict == "CONCERNS" else 1))
|
lovarch_cli/config.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""lovarch-cli config + credentials management.
|
|
2
|
+
|
|
3
|
+
Stores user state in ~/.lovarch/:
|
|
4
|
+
- credentials.json — auth tokens (free_token or premium refresh_token)
|
|
5
|
+
- config.yaml — preferences (language, default workflow, API keys)
|
|
6
|
+
|
|
7
|
+
Free mode token is saved as plain text (low-sensitivity lead-tracking ID).
|
|
8
|
+
Premium mode tokens go through OS keyring (keyring lib) — see auth/credentials.py.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
DEFAULT_HOME = Path.home() / ".lovarch"
|
|
19
|
+
|
|
20
|
+
# Lovarch Supabase API endpoint.
|
|
21
|
+
#
|
|
22
|
+
# Decision 2026-05-10 (Pablo + Orion): Opção A — URL Supabase direta.
|
|
23
|
+
# Custom domain api.lovarch.com fica para fase pre-PyPI public publish
|
|
24
|
+
# (ver cli/MIGRATION-PLAN.md). Quando criado, usuários instalados continuam
|
|
25
|
+
# funcionando via env LOVARCH_API_URL override ou bump de versão.
|
|
26
|
+
#
|
|
27
|
+
# The anon key below is PUBLIC by Supabase design (role=anon, RLS-protected).
|
|
28
|
+
# Same key shipped in Lovarch web frontend bundle. Safe to embed in
|
|
29
|
+
# distributed CLI source.
|
|
30
|
+
DEFAULT_API_URL = os.environ.get(
|
|
31
|
+
"LOVARCH_API_URL", "https://cuxbydmyahjaplzkthkr.supabase.co"
|
|
32
|
+
)
|
|
33
|
+
DEFAULT_API_ANON_KEY = os.environ.get(
|
|
34
|
+
"LOVARCH_ANON_KEY",
|
|
35
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN1eGJ5ZG15YWhqYXBsemt0aGtyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIzODM3OTYsImV4cCI6MjA4Nzk1OTc5Nn0.UtHrPjSP40pwsRy6vCQseC5YA4DZ6e-hO8sXcRL8w_E",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Credentials:
|
|
41
|
+
"""User authentication state.
|
|
42
|
+
|
|
43
|
+
Free mode: free_token + lead_id (saved here in plain JSON).
|
|
44
|
+
Premium mode: only marker; actual tokens live in OS keyring.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
mode: str # 'free' | 'premium' | 'none'
|
|
48
|
+
lead_id: str | None = None
|
|
49
|
+
user_id: str | None = None
|
|
50
|
+
free_token: str | None = None
|
|
51
|
+
email: str | None = None
|
|
52
|
+
full_name: str | None = None
|
|
53
|
+
country: str | None = None
|
|
54
|
+
language: str = "it"
|
|
55
|
+
signed_up_at: str | None = None
|
|
56
|
+
upgrade_url: str | None = None
|
|
57
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def empty(cls) -> Credentials:
|
|
61
|
+
return cls(mode="none")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def credentials_path(home: Path | None = None) -> Path:
|
|
65
|
+
return (home or DEFAULT_HOME) / "credentials.json"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_credentials(home: Path | None = None) -> Credentials:
|
|
69
|
+
path = credentials_path(home)
|
|
70
|
+
if not path.exists():
|
|
71
|
+
return Credentials.empty()
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(path.read_text())
|
|
74
|
+
return Credentials(**data)
|
|
75
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
76
|
+
return Credentials.empty()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_credentials(creds: Credentials, home: Path | None = None) -> Path:
|
|
80
|
+
path = credentials_path(home)
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
path.write_text(json.dumps(asdict(creds), indent=2, ensure_ascii=False))
|
|
83
|
+
# Restrict permissions on POSIX (rw-------)
|
|
84
|
+
if os.name == "posix":
|
|
85
|
+
path.chmod(0o600)
|
|
86
|
+
return path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def clear_credentials(home: Path | None = None) -> bool:
|
|
90
|
+
path = credentials_path(home)
|
|
91
|
+
if path.exists():
|
|
92
|
+
path.unlink()
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_authenticated(home: Path | None = None) -> bool:
|
|
98
|
+
creds = load_credentials(home)
|
|
99
|
+
return creds.mode in {"free", "premium"} and (
|
|
100
|
+
creds.free_token is not None or creds.user_id is not None
|
|
101
|
+
)
|