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,218 @@
|
|
|
1
|
+
"""lovarch init — Create a new project directory.
|
|
2
|
+
|
|
3
|
+
Creates ~/.lovarch/projects/<project>/ with input/ and output/ subdirs and
|
|
4
|
+
a project.yaml metadata file. With --sample copies the bundled villa-chianti
|
|
5
|
+
sample-input as a starter pack the user can run end-to-end with no extra work.
|
|
6
|
+
|
|
7
|
+
Project layout after `lovarch init my-villa --sample`:
|
|
8
|
+
|
|
9
|
+
~/.lovarch/projects/my-villa/
|
|
10
|
+
├── project.yaml # name, created_at, workflow, mode
|
|
11
|
+
├── input/ # 18 input files (briefing, photos, DXF, etc.)
|
|
12
|
+
│ ├── architetto-info.json
|
|
13
|
+
│ ├── briefing-cliente.md
|
|
14
|
+
│ ├── foto/
|
|
15
|
+
│ ├── pinterest-references/
|
|
16
|
+
│ ├── README.md
|
|
17
|
+
│ ├── stato-attuale.dxf
|
|
18
|
+
│ ├── stato-attuale.png
|
|
19
|
+
│ └── visura-catastale.pdf
|
|
20
|
+
└── output/ # populated by `lovarch run`
|
|
21
|
+
|
|
22
|
+
The sample comes from the bundled squad payload (lovarch_cli/squad/), so it
|
|
23
|
+
ships with the pip package — no separate download needed.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import shutil
|
|
29
|
+
import sys
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Annotated
|
|
33
|
+
|
|
34
|
+
import typer
|
|
35
|
+
import yaml
|
|
36
|
+
from rich.console import Console
|
|
37
|
+
from rich.panel import Panel
|
|
38
|
+
from rich.text import Text
|
|
39
|
+
|
|
40
|
+
from lovarch_cli.config import DEFAULT_HOME
|
|
41
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
err_console = Console(stderr=True)
|
|
45
|
+
|
|
46
|
+
# Valid project name pattern: lowercase letters, digits, hyphens.
|
|
47
|
+
# Forbids path-traversal characters and shell-unfriendly chars.
|
|
48
|
+
PROJECT_NAME_RX = re.compile(r"^[a-z0-9][a-z0-9-]{1,62}$")
|
|
49
|
+
|
|
50
|
+
# Sample-input villa-chianti is NOT bundled in the wheel (49MB of photos/DXF/
|
|
51
|
+
# PDF — too heavy). It ships as a GitHub Releases asset and lovarch_cli.
|
|
52
|
+
# sample_downloader.resolve_sample_source() handles bundled→cache→download
|
|
53
|
+
# resolution transparently.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _project_dir(home: Path, name: str) -> Path:
|
|
57
|
+
return home / "projects" / name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def init_command(
|
|
61
|
+
project: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
typer.Argument(
|
|
64
|
+
help="Project name (lowercase letters/digits/hyphens, 2-63 chars).",
|
|
65
|
+
),
|
|
66
|
+
],
|
|
67
|
+
sample: Annotated[
|
|
68
|
+
bool,
|
|
69
|
+
typer.Option(
|
|
70
|
+
"--sample",
|
|
71
|
+
help="Copy the bundled villa-chianti sample-input as a starter.",
|
|
72
|
+
),
|
|
73
|
+
] = False,
|
|
74
|
+
workflow: Annotated[
|
|
75
|
+
str,
|
|
76
|
+
typer.Option(
|
|
77
|
+
"--workflow",
|
|
78
|
+
help="Target workflow this project will run against.",
|
|
79
|
+
),
|
|
80
|
+
] = "dal-brief-al-cantiere",
|
|
81
|
+
force: Annotated[
|
|
82
|
+
bool,
|
|
83
|
+
typer.Option(
|
|
84
|
+
"--force",
|
|
85
|
+
help="Overwrite an existing project directory (dangerous).",
|
|
86
|
+
),
|
|
87
|
+
] = False,
|
|
88
|
+
lang_flag: Annotated[
|
|
89
|
+
str | None,
|
|
90
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
91
|
+
] = None,
|
|
92
|
+
home_override: Annotated[
|
|
93
|
+
Path | None,
|
|
94
|
+
typer.Option(
|
|
95
|
+
"--home",
|
|
96
|
+
help="Override $HOME/.lovarch root (mainly for tests).",
|
|
97
|
+
hidden=True,
|
|
98
|
+
),
|
|
99
|
+
] = None,
|
|
100
|
+
squad_src: Annotated[
|
|
101
|
+
Path | None,
|
|
102
|
+
typer.Option(
|
|
103
|
+
"--squad-src",
|
|
104
|
+
help=(
|
|
105
|
+
"Path to a squad-architettura-progetto source dir. Also reads "
|
|
106
|
+
"$LOVARCH_SQUAD_SRC. When set, --sample reads the villa-"
|
|
107
|
+
"chianti sample from this path instead of downloading."
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
] = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Initialize a new project directory."""
|
|
113
|
+
if lang_flag is not None:
|
|
114
|
+
set_current_lang(lang_flag)
|
|
115
|
+
lang = current_lang()
|
|
116
|
+
|
|
117
|
+
# ─── Validate project name ───────────────────────────────────────────
|
|
118
|
+
if not PROJECT_NAME_RX.match(project):
|
|
119
|
+
err_console.print(
|
|
120
|
+
f"\n[red]✗ {t('init.invalid_name', lang=lang, name=project)}[/red]\n"
|
|
121
|
+
)
|
|
122
|
+
sys.exit(2)
|
|
123
|
+
|
|
124
|
+
home = home_override or DEFAULT_HOME
|
|
125
|
+
proj_dir = _project_dir(home, project)
|
|
126
|
+
|
|
127
|
+
# ─── Handle existing project ─────────────────────────────────────────
|
|
128
|
+
if proj_dir.exists():
|
|
129
|
+
if not force:
|
|
130
|
+
err_console.print(
|
|
131
|
+
f"\n[yellow]{t('init.already_exists', lang=lang, name=project, path=str(proj_dir))}[/yellow]\n"
|
|
132
|
+
)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
shutil.rmtree(proj_dir)
|
|
135
|
+
|
|
136
|
+
# ─── Create dir tree ─────────────────────────────────────────────────
|
|
137
|
+
input_dir = proj_dir / "input"
|
|
138
|
+
output_dir = proj_dir / "output"
|
|
139
|
+
input_dir.mkdir(parents=True)
|
|
140
|
+
output_dir.mkdir()
|
|
141
|
+
|
|
142
|
+
# ─── Optional: resolve + copy sample-input (lazy download if needed) ─
|
|
143
|
+
sample_copied_count = 0
|
|
144
|
+
sample_origin: str | None = None
|
|
145
|
+
if sample:
|
|
146
|
+
from lovarch_cli.sample_downloader import (
|
|
147
|
+
SampleDownloadError,
|
|
148
|
+
resolve_sample_source,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
resolved = resolve_sample_source(
|
|
153
|
+
console=console,
|
|
154
|
+
lang=lang,
|
|
155
|
+
home=home,
|
|
156
|
+
squad_src=squad_src,
|
|
157
|
+
)
|
|
158
|
+
src = resolved.path
|
|
159
|
+
sample_origin = resolved.origin
|
|
160
|
+
except SampleDownloadError as exc:
|
|
161
|
+
err_console.print(f"\n[red]✗ {exc}[/red]")
|
|
162
|
+
err_console.print(
|
|
163
|
+
f"[dim]{t('init.sample_hint', lang=lang)}[/dim]\n"
|
|
164
|
+
)
|
|
165
|
+
# Don't bail: project dir exists, user can still add own input.
|
|
166
|
+
else:
|
|
167
|
+
for item in src.iterdir():
|
|
168
|
+
if item.is_dir():
|
|
169
|
+
shutil.copytree(item, input_dir / item.name)
|
|
170
|
+
else:
|
|
171
|
+
shutil.copy2(item, input_dir / item.name)
|
|
172
|
+
sample_copied_count = sum(1 for _ in input_dir.rglob("*") if _.is_file())
|
|
173
|
+
|
|
174
|
+
# ─── Write project.yaml metadata ─────────────────────────────────────
|
|
175
|
+
metadata = {
|
|
176
|
+
"name": project,
|
|
177
|
+
"created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
178
|
+
"workflow": workflow,
|
|
179
|
+
"sample": sample,
|
|
180
|
+
"cli_version": _cli_version(),
|
|
181
|
+
}
|
|
182
|
+
(proj_dir / "project.yaml").write_text(
|
|
183
|
+
yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True),
|
|
184
|
+
encoding="utf-8",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ─── Success panel ───────────────────────────────────────────────────
|
|
188
|
+
body_lines: list[str] = [
|
|
189
|
+
t("init.created", lang=lang, path=str(proj_dir)),
|
|
190
|
+
]
|
|
191
|
+
if sample and sample_copied_count > 0:
|
|
192
|
+
body_lines.append(
|
|
193
|
+
t("init.sample_copied", lang=lang, count=sample_copied_count)
|
|
194
|
+
)
|
|
195
|
+
if sample_origin in {"cache", "download"}:
|
|
196
|
+
body_lines.append(
|
|
197
|
+
f"[dim]{t(f'init.sample_origin_{sample_origin}', lang=lang)}[/dim]"
|
|
198
|
+
)
|
|
199
|
+
body_lines.append("")
|
|
200
|
+
body_lines.append(t("init.next_steps", lang=lang, name=project))
|
|
201
|
+
|
|
202
|
+
console.print()
|
|
203
|
+
console.print(
|
|
204
|
+
Panel(
|
|
205
|
+
Text.from_markup("\n".join(body_lines)),
|
|
206
|
+
title=f"[bold gold1]🌱 {t('init.title', lang=lang, name=project)}[/bold gold1]",
|
|
207
|
+
border_style="gold1",
|
|
208
|
+
padding=(1, 2),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
console.print()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _cli_version() -> str:
|
|
215
|
+
"""Read version lazily — avoids circular import at module load."""
|
|
216
|
+
from lovarch_cli.version import __version__
|
|
217
|
+
|
|
218
|
+
return __version__
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""`lovarch jobs` — async platform jobs (video, Shotstack export, upscale).
|
|
2
|
+
|
|
3
|
+
lovarch jobs list
|
|
4
|
+
lovarch jobs status <id>
|
|
5
|
+
|
|
6
|
+
Reads content_video_jobs via the user's own session (owner RLS). Costs are
|
|
7
|
+
shown only as the user's credits.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
err_console = Console(stderr=True)
|
|
19
|
+
|
|
20
|
+
jobs_app = typer.Typer(
|
|
21
|
+
help="Job asincroni della piattaforma (video, export, upscale).",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_STATUS_ICON = {"done": "✅", "completed": "✅", "failed": "❌", "error": "❌",
|
|
26
|
+
"running": "⏳", "pending": "⏳", "queued": "⏳"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _workflows():
|
|
30
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
31
|
+
from lovarch_cli.workflows import PlatformWorkflows
|
|
32
|
+
|
|
33
|
+
session = LovarchSession.load()
|
|
34
|
+
if session is None:
|
|
35
|
+
err_console.print("[red]✗ Non autenticato. Esegui `lovarch login --premium`.[/red]")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
return PlatformWorkflows(session)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@jobs_app.command("list")
|
|
41
|
+
def list_command(
|
|
42
|
+
limit: int = typer.Option(10, "--limit", "-n", help="Quanti job mostrare."),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Ultimi job asincroni dell'account."""
|
|
45
|
+
from lovarch_cli.workflows import WorkflowError
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
jobs = asyncio.run(_workflows().jobs_list(limit=limit))
|
|
49
|
+
except WorkflowError as exc:
|
|
50
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
if not jobs:
|
|
53
|
+
console.print("[dim]Nessun job trovato.[/dim]")
|
|
54
|
+
return
|
|
55
|
+
table = Table(title="Job asincroni", header_style="bold gold1")
|
|
56
|
+
for col in ("Stato", "Engine", "Modello", "Crediti", "Creato", "ID"):
|
|
57
|
+
table.add_column(col)
|
|
58
|
+
for j in jobs:
|
|
59
|
+
status = str(j.get("status", "?"))
|
|
60
|
+
table.add_row(
|
|
61
|
+
f"{_STATUS_ICON.get(status, '·')} {status}",
|
|
62
|
+
str(j.get("engine", "—")), str(j.get("model", "—")),
|
|
63
|
+
str(j.get("credits_charged", "—")),
|
|
64
|
+
str(j.get("created_at", ""))[:16], str(j.get("id", ""))[:8],
|
|
65
|
+
)
|
|
66
|
+
console.print(table)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@jobs_app.command("status")
|
|
70
|
+
def status_command(
|
|
71
|
+
job_id: str = typer.Argument(..., help="ID del job."),
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Stato dettagliato di un job."""
|
|
74
|
+
from lovarch_cli.workflows import WorkflowError
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
j = asyncio.run(_workflows().job_status(job_id))
|
|
78
|
+
except WorkflowError as exc:
|
|
79
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
status = str(j.get("status", "?"))
|
|
82
|
+
console.print(f"{_STATUS_ICON.get(status, '·')} [bold]{status}[/bold] · {j.get('engine')} · {j.get('model')}")
|
|
83
|
+
if j.get("prompt"):
|
|
84
|
+
console.print(f"[dim]{str(j['prompt'])[:120]}[/dim]")
|
|
85
|
+
if j.get("output_url"):
|
|
86
|
+
console.print(f"[green]Output:[/green] {j['output_url']}")
|
|
87
|
+
if j.get("error_message"):
|
|
88
|
+
console.print(f"[red]Errore:[/red] {j['error_message']}")
|
|
89
|
+
charged = j.get("credits_charged")
|
|
90
|
+
refunded = j.get("credits_refunded")
|
|
91
|
+
if charged is not None:
|
|
92
|
+
line = f"Crediti: {charged}"
|
|
93
|
+
if refunded:
|
|
94
|
+
line += f" (rimborsati: {refunded})"
|
|
95
|
+
console.print(f"[dim]{line}[/dim]")
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""arch login — Authentication entry point (Free or Premium).
|
|
2
|
+
|
|
3
|
+
Subcommand structure:
|
|
4
|
+
arch login → interactive: choose Free or Premium
|
|
5
|
+
arch login --free → redirects to arch signup (free has no separate login)
|
|
6
|
+
arch login --premium → PKCE flow with Lovarch web
|
|
7
|
+
|
|
8
|
+
Premium PKCE flow:
|
|
9
|
+
1. Generate verifier + challenge + state via PkceParams.generate()
|
|
10
|
+
2. Spin up local HTTP server on 127.0.0.1:RANDOM/callback
|
|
11
|
+
3. Open browser to https://lovarch.com/cli-auth?...
|
|
12
|
+
4. Show "Waiting for browser..." spinner with timeout 5min
|
|
13
|
+
5. On callback: validate state matches, POST cli-auth-exchange EF
|
|
14
|
+
6. Save tokens via keyring_store.save_premium_session
|
|
15
|
+
7. Show success panel with user info
|
|
16
|
+
|
|
17
|
+
Localized in 4 languages — keys live in lovarch_cli/i18n/translations/*.json.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import sys
|
|
23
|
+
import webbrowser
|
|
24
|
+
from typing import Annotated
|
|
25
|
+
from urllib.parse import urlencode
|
|
26
|
+
|
|
27
|
+
import typer
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.panel import Panel
|
|
30
|
+
from rich.prompt import Prompt
|
|
31
|
+
from rich.text import Text
|
|
32
|
+
|
|
33
|
+
from lovarch_cli.api import ApiClient, LovarchApiError
|
|
34
|
+
from lovarch_cli.auth import PkceParams, save_premium_session
|
|
35
|
+
from lovarch_cli.auth.local_server import AuthServer
|
|
36
|
+
from lovarch_cli.config import DEFAULT_API_URL
|
|
37
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
err_console = Console(stderr=True)
|
|
41
|
+
|
|
42
|
+
# https://lovarch.com host — ALWAYS the web app for /cli-auth, NOT the
|
|
43
|
+
# Supabase API URL (which is for EF calls). This is hardcoded because the
|
|
44
|
+
# /cli-auth React page only exists on lovarch.com.
|
|
45
|
+
LOVARCH_WEB_BASE = "https://lovarch.com"
|
|
46
|
+
PKCE_TIMEOUT_SECONDS = 300.0 # 5 minutes — matches DB code TTL
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def login_command(
|
|
50
|
+
free: Annotated[
|
|
51
|
+
bool,
|
|
52
|
+
typer.Option("--free", help="Free mode (redirects to arch signup)."),
|
|
53
|
+
] = False,
|
|
54
|
+
premium: Annotated[
|
|
55
|
+
bool,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--premium",
|
|
58
|
+
help="Premium mode (PKCE flow with Lovarch login).",
|
|
59
|
+
),
|
|
60
|
+
] = False,
|
|
61
|
+
lang_flag: Annotated[
|
|
62
|
+
str | None,
|
|
63
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
64
|
+
] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Login al CLI (Free o Premium)."""
|
|
67
|
+
if lang_flag is not None:
|
|
68
|
+
set_current_lang(lang_flag)
|
|
69
|
+
lang = current_lang()
|
|
70
|
+
|
|
71
|
+
# Mutually exclusive flags
|
|
72
|
+
if free and premium:
|
|
73
|
+
err_console.print(f"[red]{t('login.modes_mutex', lang=lang)}[/red]")
|
|
74
|
+
sys.exit(2)
|
|
75
|
+
|
|
76
|
+
if not free and not premium:
|
|
77
|
+
# Interactive choice
|
|
78
|
+
choice = Prompt.ask(
|
|
79
|
+
f"[bold]{t('login.choose_mode_prompt', lang=lang)}[/bold]",
|
|
80
|
+
choices=["free", "premium"],
|
|
81
|
+
default="free",
|
|
82
|
+
)
|
|
83
|
+
free = choice == "free"
|
|
84
|
+
premium = choice == "premium"
|
|
85
|
+
|
|
86
|
+
if free:
|
|
87
|
+
console.print()
|
|
88
|
+
console.print(t("login.free_redirect", lang=lang))
|
|
89
|
+
console.print()
|
|
90
|
+
sys.exit(0)
|
|
91
|
+
|
|
92
|
+
# ─── Premium PKCE flow ───────────────────────────────────────────────
|
|
93
|
+
pkce = PkceParams.generate()
|
|
94
|
+
server = AuthServer(port=0)
|
|
95
|
+
server.start()
|
|
96
|
+
|
|
97
|
+
auth_url = (
|
|
98
|
+
f"{LOVARCH_WEB_BASE}/cli-auth?"
|
|
99
|
+
+ urlencode(
|
|
100
|
+
{
|
|
101
|
+
"state": pkce.state,
|
|
102
|
+
"code_challenge": pkce.code_challenge,
|
|
103
|
+
"code_challenge_method": pkce.code_challenge_method,
|
|
104
|
+
"redirect_uri": server.callback_url,
|
|
105
|
+
"lang": lang,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
console.print(f"[gold1]→[/gold1] {t('login.opening_browser', lang=lang)}")
|
|
112
|
+
console.print(f" [dim]{t('login.manual_url_hint', lang=lang)}[/dim]")
|
|
113
|
+
console.print(f" [dim cyan]{auth_url}[/dim cyan]")
|
|
114
|
+
console.print()
|
|
115
|
+
|
|
116
|
+
webbrowser.open(auth_url, new=1, autoraise=True)
|
|
117
|
+
|
|
118
|
+
with console.status(
|
|
119
|
+
Text.from_markup(f"[gold1]{t('login.waiting_callback', lang=lang)}[/gold1]"),
|
|
120
|
+
spinner="dots",
|
|
121
|
+
):
|
|
122
|
+
result = server.wait_for_callback(PKCE_TIMEOUT_SECONDS)
|
|
123
|
+
|
|
124
|
+
server.shutdown()
|
|
125
|
+
|
|
126
|
+
# ─── Validate callback ───────────────────────────────────────────────
|
|
127
|
+
if result.error:
|
|
128
|
+
if result.error == "timeout":
|
|
129
|
+
err_console.print(
|
|
130
|
+
f"\n[red]✗ {t('login.callback_timeout', lang=lang)}[/red]"
|
|
131
|
+
)
|
|
132
|
+
elif result.error == "access_denied":
|
|
133
|
+
err_console.print(
|
|
134
|
+
f"\n[yellow]✗ {t('login.auth_denied', lang=lang)}[/yellow]"
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
err_console.print(
|
|
138
|
+
f"\n[red]✗ {result.error}: {result.error_description or ''}[/red]"
|
|
139
|
+
)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
if not result.code or result.state != pkce.state:
|
|
143
|
+
err_console.print(f"\n[red]✗ {t('login.state_mismatch', lang=lang)}[/red]")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
# ─── Exchange code for tokens ────────────────────────────────────────
|
|
147
|
+
api = ApiClient(base_url=DEFAULT_API_URL)
|
|
148
|
+
try:
|
|
149
|
+
response = asyncio.run(
|
|
150
|
+
api.invoke_ef(
|
|
151
|
+
"cli-auth-exchange",
|
|
152
|
+
{
|
|
153
|
+
"code": result.code,
|
|
154
|
+
"code_verifier": pkce.code_verifier,
|
|
155
|
+
"language": lang,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
except LovarchApiError as exc:
|
|
160
|
+
err_console.print(f"\n[red]✗ {exc}[/red]")
|
|
161
|
+
if exc.error_code:
|
|
162
|
+
err_console.print(f"[dim](error_code: {exc.error_code})[/dim]")
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
# ─── Save session ────────────────────────────────────────────────────
|
|
166
|
+
user = response.get("user", {})
|
|
167
|
+
expires_in = int(response.get("expires_in", 3600))
|
|
168
|
+
from datetime import datetime, timedelta, timezone
|
|
169
|
+
|
|
170
|
+
expires_at = (
|
|
171
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
172
|
+
).isoformat(timespec="seconds")
|
|
173
|
+
|
|
174
|
+
used_keyring, location = save_premium_session(
|
|
175
|
+
user_id=user.get("id", ""),
|
|
176
|
+
email=user.get("email", ""),
|
|
177
|
+
full_name=user.get("full_name"),
|
|
178
|
+
access_token=response["access_token"],
|
|
179
|
+
refresh_token=response["refresh_token"],
|
|
180
|
+
expires_at=expires_at,
|
|
181
|
+
language=lang,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
body = Text.assemble(
|
|
185
|
+
("Account: ", "dim"),
|
|
186
|
+
(f"{user.get('email','')}\n", "bold"),
|
|
187
|
+
("User ID: ", "dim"),
|
|
188
|
+
(f"{user.get('id','')}\n", "bold dim"),
|
|
189
|
+
("Expires: ", "dim"),
|
|
190
|
+
(f"{expires_at}\n", "bold"),
|
|
191
|
+
("Storage: ", "dim"),
|
|
192
|
+
(location, "bold" if used_keyring else "italic yellow"),
|
|
193
|
+
)
|
|
194
|
+
console.print()
|
|
195
|
+
console.print(
|
|
196
|
+
Panel(
|
|
197
|
+
body,
|
|
198
|
+
title=f"[bold green]{t('login.login_success', lang=lang)}[/bold green]",
|
|
199
|
+
border_style="green",
|
|
200
|
+
padding=(1, 2),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""`lovarch mcp` — run the Lovarch MCP server.
|
|
2
|
+
|
|
3
|
+
The heavy ``mcp`` SDK import is lazy (inside the command) so that
|
|
4
|
+
``lovarch --help`` and every other command keep working even when the optional
|
|
5
|
+
``[mcp]`` extra is not installed.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
mcp_app = typer.Typer(
|
|
12
|
+
help="Server MCP di Lovarch (per Claude Code / IDE). Richiede l'extra [mcp].",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@mcp_app.command("serve")
|
|
18
|
+
def serve_command() -> None:
|
|
19
|
+
"""Avvia il server MCP su stdio.
|
|
20
|
+
|
|
21
|
+
Registra in Claude Code con:
|
|
22
|
+
claude mcp add lovarch -- lovarch mcp serve
|
|
23
|
+
"""
|
|
24
|
+
from lovarch_cli.mcp.server import serve
|
|
25
|
+
|
|
26
|
+
serve()
|