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,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
+ )