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,217 @@
|
|
|
1
|
+
"""lovarch consolidate — Generate DOSSIER.zip from a completed run.
|
|
2
|
+
|
|
3
|
+
Walks ~/.lovarch/projects/<name>/output/ and produces a structured ZIP
|
|
4
|
+
that mirrors the dossier format expected by Lovarch's downstream consumers
|
|
5
|
+
(architects sharing with clients, builders importing into BIM, etc.).
|
|
6
|
+
|
|
7
|
+
Folder convention inside the ZIP (matches the squad's deliverable layout):
|
|
8
|
+
|
|
9
|
+
DOSSIER-<project>-<YYYY-MM-DD>.zip
|
|
10
|
+
├── 00-validation/ audit + input-validation JSONs
|
|
11
|
+
├── 01-bootstrap/ project metadata, project.yaml snapshot
|
|
12
|
+
├── 02-concept/ moodboard + render images
|
|
13
|
+
├── 03-tier1/ CAD/IFC/PDF/XLSX from tier-1 agents
|
|
14
|
+
├── 04-tier2/ QA reports
|
|
15
|
+
└── 05-dossier/ briefing-architect, regolatorio, energy
|
|
16
|
+
|
|
17
|
+
Files in output/ that don't match a known prefix go into 99-other/ so we
|
|
18
|
+
never silently drop content. The user can inspect what landed there to
|
|
19
|
+
catch missing routing rules.
|
|
20
|
+
|
|
21
|
+
Free mode (no real run): we still zip whatever is in output/, even if
|
|
22
|
+
empty — useful for users who want to package their own deliverables and
|
|
23
|
+
hand them off without going through Premium.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
import zipfile
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Annotated
|
|
32
|
+
|
|
33
|
+
import typer
|
|
34
|
+
import yaml
|
|
35
|
+
from rich.console import Console
|
|
36
|
+
from rich.panel import Panel
|
|
37
|
+
from rich.text import Text
|
|
38
|
+
|
|
39
|
+
from lovarch_cli.config import DEFAULT_HOME
|
|
40
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
41
|
+
|
|
42
|
+
console = Console()
|
|
43
|
+
err_console = Console(stderr=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Routing prefixes → DOSSIER folder.
|
|
47
|
+
# The squad pipeline writes filenames with these prefixes; if a new
|
|
48
|
+
# convention shows up, add the mapping here rather than scattering it.
|
|
49
|
+
_ROUTING: list[tuple[str, str]] = [
|
|
50
|
+
("input-validation", "00-validation"),
|
|
51
|
+
("audit-", "00-validation"),
|
|
52
|
+
("project-", "01-bootstrap"),
|
|
53
|
+
("bootstrap-", "01-bootstrap"),
|
|
54
|
+
("moodboard-", "02-concept"),
|
|
55
|
+
("render-", "02-concept"),
|
|
56
|
+
("brief-", "02-concept"),
|
|
57
|
+
("dxf-", "03-tier1"),
|
|
58
|
+
("ifc-", "03-tier1"),
|
|
59
|
+
("pianta-", "03-tier1"),
|
|
60
|
+
("sezione-", "03-tier1"),
|
|
61
|
+
("computo-", "03-tier1"),
|
|
62
|
+
("capitolato-", "03-tier1"),
|
|
63
|
+
("contratto-", "03-tier1"),
|
|
64
|
+
("qa-", "04-tier2"),
|
|
65
|
+
("verifica-", "04-tier2"),
|
|
66
|
+
("briefing-architect", "05-dossier"),
|
|
67
|
+
("regolatorio-", "05-dossier"),
|
|
68
|
+
("energy-", "05-dossier"),
|
|
69
|
+
("dossier-", "05-dossier"),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _route_filename(name: str) -> str:
|
|
74
|
+
"""Map a filename prefix to its DOSSIER folder, or '99-other/' fallback."""
|
|
75
|
+
lower = name.lower()
|
|
76
|
+
for prefix, folder in _ROUTING:
|
|
77
|
+
if lower.startswith(prefix):
|
|
78
|
+
return folder
|
|
79
|
+
return "99-other"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _gather_files(output_dir: Path) -> list[tuple[Path, str]]:
|
|
83
|
+
"""Return (abs_path, arcname) tuples for every file under output_dir."""
|
|
84
|
+
items: list[tuple[Path, str]] = []
|
|
85
|
+
for path in sorted(output_dir.rglob("*")):
|
|
86
|
+
if not path.is_file():
|
|
87
|
+
continue
|
|
88
|
+
# Use the file's name relative to output_dir for routing,
|
|
89
|
+
# but preserve any subdir structure inside the routed folder.
|
|
90
|
+
rel = path.relative_to(output_dir)
|
|
91
|
+
first_component = rel.parts[0]
|
|
92
|
+
# If the user already organized into 0X-foo/, preserve it.
|
|
93
|
+
if "-" in first_component and first_component[:2].isdigit():
|
|
94
|
+
arcname = str(rel)
|
|
95
|
+
else:
|
|
96
|
+
folder = _route_filename(rel.name)
|
|
97
|
+
arcname = f"{folder}/{rel.name}"
|
|
98
|
+
items.append((path, arcname))
|
|
99
|
+
return items
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def consolidate_command(
|
|
103
|
+
project: Annotated[
|
|
104
|
+
str, typer.Argument(help="Project name to consolidate.")
|
|
105
|
+
],
|
|
106
|
+
output: Annotated[
|
|
107
|
+
Path | None,
|
|
108
|
+
typer.Option(
|
|
109
|
+
"--output",
|
|
110
|
+
"-o",
|
|
111
|
+
help="Where to write the ZIP (default: project's output/ dir).",
|
|
112
|
+
),
|
|
113
|
+
] = None,
|
|
114
|
+
lang_flag: Annotated[
|
|
115
|
+
str | None,
|
|
116
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
117
|
+
] = None,
|
|
118
|
+
home_override: Annotated[
|
|
119
|
+
Path | None,
|
|
120
|
+
typer.Option(
|
|
121
|
+
"--home",
|
|
122
|
+
help="Override $HOME/.lovarch root (mainly for tests).",
|
|
123
|
+
hidden=True,
|
|
124
|
+
),
|
|
125
|
+
] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Bundle the run output into a DOSSIER.zip."""
|
|
128
|
+
if lang_flag is not None:
|
|
129
|
+
set_current_lang(lang_flag)
|
|
130
|
+
lang = current_lang()
|
|
131
|
+
|
|
132
|
+
home = home_override or DEFAULT_HOME
|
|
133
|
+
proj_dir = home / "projects" / project
|
|
134
|
+
if not proj_dir.is_dir():
|
|
135
|
+
err_console.print(
|
|
136
|
+
f"\n[red]✗ {t('consolidate.no_project', lang=lang, name=project)}[/red]\n"
|
|
137
|
+
)
|
|
138
|
+
sys.exit(2)
|
|
139
|
+
|
|
140
|
+
output_dir = proj_dir / "output"
|
|
141
|
+
if not output_dir.is_dir():
|
|
142
|
+
err_console.print(
|
|
143
|
+
f"\n[red]✗ {t('consolidate.no_output', lang=lang, path=str(output_dir))}[/red]\n"
|
|
144
|
+
)
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
|
|
147
|
+
items = _gather_files(output_dir)
|
|
148
|
+
if not items:
|
|
149
|
+
err_console.print(
|
|
150
|
+
f"\n[yellow]⚠ {t('consolidate.empty_output', lang=lang, name=project)}[/yellow]\n"
|
|
151
|
+
)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
# ─── Choose ZIP path ─────────────────────────────────────────────────
|
|
155
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
156
|
+
zip_name = f"DOSSIER-{project}-{today}.zip"
|
|
157
|
+
zip_path = output / zip_name if output else proj_dir / zip_name
|
|
158
|
+
|
|
159
|
+
# ─── Write ZIP ───────────────────────────────────────────────────────
|
|
160
|
+
console.print(
|
|
161
|
+
f"\n[gold1]→[/gold1] {t('consolidate.zipping', lang=lang, count=len(items))}"
|
|
162
|
+
)
|
|
163
|
+
try:
|
|
164
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
165
|
+
for src, arcname in items:
|
|
166
|
+
zf.write(src, arcname=arcname)
|
|
167
|
+
# Drop a small README inside the ZIP describing the layout.
|
|
168
|
+
zf.writestr(
|
|
169
|
+
"README.md",
|
|
170
|
+
t(
|
|
171
|
+
"consolidate.zip_readme",
|
|
172
|
+
lang=lang,
|
|
173
|
+
project=project,
|
|
174
|
+
date=today,
|
|
175
|
+
count=len(items),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
except OSError as exc:
|
|
179
|
+
err_console.print(
|
|
180
|
+
f"\n[red]✗ {t('consolidate.write_failed', lang=lang, message=str(exc))}[/red]\n"
|
|
181
|
+
)
|
|
182
|
+
sys.exit(2)
|
|
183
|
+
|
|
184
|
+
# ─── Persist last_dossier in project.yaml ────────────────────────────
|
|
185
|
+
yaml_path = proj_dir / "project.yaml"
|
|
186
|
+
if yaml_path.exists():
|
|
187
|
+
try:
|
|
188
|
+
meta = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
189
|
+
except yaml.YAMLError:
|
|
190
|
+
meta = {}
|
|
191
|
+
meta["last_dossier"] = {
|
|
192
|
+
"path": str(zip_path),
|
|
193
|
+
"files": len(items),
|
|
194
|
+
"size_bytes": zip_path.stat().st_size,
|
|
195
|
+
"created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
196
|
+
}
|
|
197
|
+
yaml_path.write_text(
|
|
198
|
+
yaml.safe_dump(meta, sort_keys=False, allow_unicode=True),
|
|
199
|
+
encoding="utf-8",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# ─── Success panel ───────────────────────────────────────────────────
|
|
203
|
+
size_kb = zip_path.stat().st_size // 1024
|
|
204
|
+
body = Text.from_markup(
|
|
205
|
+
f"{t('consolidate.created', lang=lang, path=str(zip_path), size_kb=size_kb, count=len(items))}\n\n"
|
|
206
|
+
f"{t('consolidate.next_steps', lang=lang, project=project)}"
|
|
207
|
+
)
|
|
208
|
+
console.print()
|
|
209
|
+
console.print(
|
|
210
|
+
Panel(
|
|
211
|
+
body,
|
|
212
|
+
title=f"[bold green]📦 {t('consolidate.title', lang=lang)}[/bold green]",
|
|
213
|
+
border_style="green",
|
|
214
|
+
padding=(1, 2),
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
console.print()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""`lovarch context` — show the personalization bundle the AI agents use.
|
|
2
|
+
|
|
3
|
+
Displays which profile pieces the platform will inject into every generation
|
|
4
|
+
(brand, style, DISC, fiscal, professional signature, language) — the CLI
|
|
5
|
+
equivalent of the web app's "IA usa il tuo: …" chips.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
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
|
+
context_app = typer.Typer(
|
|
19
|
+
help="Contesto di personalizzazione usato dagli agenti AI.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_CHIP_LABELS = {
|
|
24
|
+
"hasBrand": "Brand",
|
|
25
|
+
"hasStyle": "Stile visivo",
|
|
26
|
+
"hasDisc": "DISC",
|
|
27
|
+
"hasSwot": "SWOT",
|
|
28
|
+
"hasAvatar": "Avatar cliente",
|
|
29
|
+
"hasFiscal": "Dati fiscali",
|
|
30
|
+
"hasCredentials": "Credenziali professionali",
|
|
31
|
+
"hasClient": "Cliente CRM",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@context_app.command("show")
|
|
36
|
+
def show_command(
|
|
37
|
+
lead_id: str = typer.Option(None, "--lead", help="ID di un lead CRM da includere."),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Mostra il bundle di personalizzazione (richiede login premium)."""
|
|
40
|
+
from lovarch_cli.ai import AiGatewayError, LovarchAiGateway
|
|
41
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
42
|
+
|
|
43
|
+
session = LovarchSession.load()
|
|
44
|
+
if session is None:
|
|
45
|
+
err_console.print("[red]✗ Non autenticato. Esegui `lovarch login --premium`.[/red]")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
bundle = asyncio.run(LovarchAiGateway(session).get_user_context(lead_id=lead_id))
|
|
50
|
+
except AiGatewayError as exc:
|
|
51
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
summary = bundle.get("context_summary", {})
|
|
55
|
+
table = Table(title="Contesto AI — cosa viene usato per personalizzare",
|
|
56
|
+
show_header=True, header_style="bold gold1")
|
|
57
|
+
table.add_column("Elemento", style="cyan")
|
|
58
|
+
table.add_column("Attivo", justify="center")
|
|
59
|
+
for key, label in _CHIP_LABELS.items():
|
|
60
|
+
table.add_row(label, "✅" if summary.get(key) else "—")
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
if bundle.get("signature_line"):
|
|
64
|
+
console.print(f"\n[bold]Firma documenti:[/bold] {bundle['signature_line']}")
|
|
65
|
+
prefs = bundle.get("preferences", {})
|
|
66
|
+
console.print(
|
|
67
|
+
f"[dim]Lingua output: {prefs.get('preferred_language', 'it')} · "
|
|
68
|
+
f"Valuta: {prefs.get('currency', 'EUR')} · "
|
|
69
|
+
f"Unità: {prefs.get('measurement_unit', 'metric')}[/dim]"
|
|
70
|
+
)
|
|
71
|
+
client = bundle.get("client")
|
|
72
|
+
if client and client.get("lead"):
|
|
73
|
+
console.print(f"[bold]Cliente CRM:[/bold] {client['lead'].get('name')}")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""lovarch dev — developer tooling for working on the squad payload.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
lovarch dev show-squad-root
|
|
6
|
+
Print which squad payload would be used by `lovarch run` right now,
|
|
7
|
+
and where the resolution chain landed (override flag / env var /
|
|
8
|
+
bundled).
|
|
9
|
+
|
|
10
|
+
lovarch dev refresh-squad [--source PATH] [--target PATH] [--dry-run]
|
|
11
|
+
Copy the squad source-of-truth (default: $LOVARCH_SQUAD_SRC or
|
|
12
|
+
`~/Lovarch/squads/architettura-progetto/`) into the standalone
|
|
13
|
+
repo's vendored copy at `lovarch_cli/squad/`. Excludes the heavy
|
|
14
|
+
sample-input directories — those ship via GitHub Releases. This is
|
|
15
|
+
the "promote dev edits to staged" step before cutting a release.
|
|
16
|
+
|
|
17
|
+
These commands are intended for Pablo / future contributors maintaining
|
|
18
|
+
the squad. Brew-installed users never need them.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Annotated
|
|
27
|
+
|
|
28
|
+
import typer
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
from rich.table import Table
|
|
32
|
+
from rich.text import Text
|
|
33
|
+
|
|
34
|
+
from lovarch_cli.squad_loader import (
|
|
35
|
+
ENV_VAR,
|
|
36
|
+
SquadNotFoundError,
|
|
37
|
+
bundled_squad_dir,
|
|
38
|
+
resolve_squad_root,
|
|
39
|
+
squad_source_label,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
console = Console()
|
|
44
|
+
err_console = Console(stderr=True)
|
|
45
|
+
|
|
46
|
+
dev_app = typer.Typer(
|
|
47
|
+
help="Developer tooling for the squad payload (Pablo / contributors only).",
|
|
48
|
+
no_args_is_help=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# What the build hook + refresh script copy. Mirror of scripts/sync_squad.py.
|
|
53
|
+
COPY_DIRS: tuple[str, ...] = (
|
|
54
|
+
"agents",
|
|
55
|
+
"tasks",
|
|
56
|
+
"workflows",
|
|
57
|
+
"checklists",
|
|
58
|
+
"templates",
|
|
59
|
+
"scripts",
|
|
60
|
+
"data",
|
|
61
|
+
)
|
|
62
|
+
COPY_FILES: tuple[str, ...] = ("README.md", "config.yaml")
|
|
63
|
+
# Heavy sample-inputs that ship via GitHub Releases instead.
|
|
64
|
+
EXCLUDE_RELATIVE: frozenset[str] = frozenset({
|
|
65
|
+
"data/sample-input",
|
|
66
|
+
"data/sample-input-villa-chianti",
|
|
67
|
+
})
|
|
68
|
+
DEFAULT_SOURCE = Path.home() / "Lovarch" / "squads" / "architettura-progetto"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dev_app.command("show-squad-root")
|
|
72
|
+
def show_squad_root_command(
|
|
73
|
+
squad_src: Annotated[
|
|
74
|
+
Path | None,
|
|
75
|
+
typer.Option(
|
|
76
|
+
"--squad-src",
|
|
77
|
+
help="Mirror the --squad-src flag passed to `lovarch run`.",
|
|
78
|
+
),
|
|
79
|
+
] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Show which squad payload `lovarch run` would currently use."""
|
|
82
|
+
try:
|
|
83
|
+
root = resolve_squad_root(override=squad_src)
|
|
84
|
+
except SquadNotFoundError as exc:
|
|
85
|
+
err_console.print(f"\n[red]✗ {exc}[/red]\n")
|
|
86
|
+
sys.exit(2)
|
|
87
|
+
|
|
88
|
+
label = squad_source_label(root)
|
|
89
|
+
env_val = os.environ.get(ENV_VAR) or "(unset)"
|
|
90
|
+
|
|
91
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
92
|
+
table.add_column(style="bold")
|
|
93
|
+
table.add_column()
|
|
94
|
+
table.add_row("Resolved path:", str(root))
|
|
95
|
+
table.add_row("Source:", label)
|
|
96
|
+
table.add_row(f"${ENV_VAR}:", env_val)
|
|
97
|
+
table.add_row("Bundled vendor:", str(bundled_squad_dir()))
|
|
98
|
+
|
|
99
|
+
console.print()
|
|
100
|
+
console.print(
|
|
101
|
+
Panel(
|
|
102
|
+
table,
|
|
103
|
+
title="[bold gold1]🔍 Squad resolution[/bold gold1]",
|
|
104
|
+
border_style="gold1",
|
|
105
|
+
padding=(1, 2),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
console.print()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ignore_excluded(squad_src: Path):
|
|
112
|
+
"""shutil.copytree ignore-callback that skips EXCLUDE_RELATIVE dirs."""
|
|
113
|
+
|
|
114
|
+
def _ignore(_dir: str, names: list[str]) -> list[str]:
|
|
115
|
+
base = Path(_dir).relative_to(squad_src)
|
|
116
|
+
return [
|
|
117
|
+
n for n in names
|
|
118
|
+
if str((base / n).as_posix()) in EXCLUDE_RELATIVE
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
return _ignore
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _locate_target_default() -> Path | None:
|
|
125
|
+
"""Try to auto-detect the `lovarch_cli/squad/` dir in the dev install.
|
|
126
|
+
|
|
127
|
+
Works when this file is imported from an editable install (i.e. the
|
|
128
|
+
user's clone of `lovarch-cli`). Returns None when imported from a
|
|
129
|
+
site-packages install (brew / pipx) — caller must require --target.
|
|
130
|
+
"""
|
|
131
|
+
bundled = bundled_squad_dir()
|
|
132
|
+
# Heuristic: if the bundled dir is writable AND lives inside a path
|
|
133
|
+
# that does NOT contain "site-packages" or "/Cellar/", treat it as
|
|
134
|
+
# the dev-install target.
|
|
135
|
+
try:
|
|
136
|
+
path_str = str(bundled)
|
|
137
|
+
if "site-packages" in path_str or "/Cellar/" in path_str:
|
|
138
|
+
return None
|
|
139
|
+
# Touch-test writability
|
|
140
|
+
if not os.access(bundled, os.W_OK):
|
|
141
|
+
return None
|
|
142
|
+
return bundled
|
|
143
|
+
except (OSError, PermissionError):
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dev_app.command("refresh-squad")
|
|
148
|
+
def refresh_squad_command(
|
|
149
|
+
source: Annotated[
|
|
150
|
+
Path | None,
|
|
151
|
+
typer.Option(
|
|
152
|
+
"--source",
|
|
153
|
+
"-s",
|
|
154
|
+
help=(
|
|
155
|
+
"Source squad-architettura-progetto path. Defaults to "
|
|
156
|
+
f"${ENV_VAR} or ~/Lovarch/squads/architettura-progetto."
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
] = None,
|
|
160
|
+
target: Annotated[
|
|
161
|
+
Path | None,
|
|
162
|
+
typer.Option(
|
|
163
|
+
"--target",
|
|
164
|
+
"-t",
|
|
165
|
+
help=(
|
|
166
|
+
"Destination lovarch_cli/squad/ path. Auto-detected if "
|
|
167
|
+
"running from a dev install (pip install -e), else required."
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
] = None,
|
|
171
|
+
dry_run: Annotated[
|
|
172
|
+
bool,
|
|
173
|
+
typer.Option(
|
|
174
|
+
"--dry-run",
|
|
175
|
+
help="Show what would change, no write.",
|
|
176
|
+
),
|
|
177
|
+
] = False,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Promote the dev squad source into the vendored snapshot."""
|
|
180
|
+
# ── Resolve SOURCE ──────────────────────────────────────────────────
|
|
181
|
+
if source is None:
|
|
182
|
+
env_src = os.environ.get(ENV_VAR)
|
|
183
|
+
if env_src:
|
|
184
|
+
source = Path(env_src).expanduser().resolve()
|
|
185
|
+
else:
|
|
186
|
+
source = DEFAULT_SOURCE
|
|
187
|
+
source = source.expanduser().resolve()
|
|
188
|
+
|
|
189
|
+
if not source.is_dir():
|
|
190
|
+
err_console.print(
|
|
191
|
+
f"\n[red]✗ Source not found: {source}[/red]\n"
|
|
192
|
+
f"[dim]Set ${ENV_VAR}, pass --source, or check that the path "
|
|
193
|
+
f"exists.[/dim]\n"
|
|
194
|
+
)
|
|
195
|
+
sys.exit(2)
|
|
196
|
+
if not (source / "scripts" / "pipeline_runner.py").exists():
|
|
197
|
+
err_console.print(
|
|
198
|
+
f"\n[red]✗ Source does not look like a squad payload: "
|
|
199
|
+
f"missing scripts/pipeline_runner.py at {source}[/red]\n"
|
|
200
|
+
)
|
|
201
|
+
sys.exit(2)
|
|
202
|
+
|
|
203
|
+
# ── Resolve TARGET ──────────────────────────────────────────────────
|
|
204
|
+
if target is None:
|
|
205
|
+
target = _locate_target_default()
|
|
206
|
+
if target is None:
|
|
207
|
+
err_console.print(
|
|
208
|
+
"\n[red]✗ Cannot auto-detect target — you're not running from "
|
|
209
|
+
"a dev install (pip install -e). Pass --target explicitly to "
|
|
210
|
+
"the lovarch_cli/squad/ dir of your lovarch-cli clone.[/red]\n"
|
|
211
|
+
)
|
|
212
|
+
sys.exit(2)
|
|
213
|
+
target = target.expanduser().resolve()
|
|
214
|
+
|
|
215
|
+
# ── Plan ────────────────────────────────────────────────────────────
|
|
216
|
+
summary = Table(show_header=False, box=None, padding=(0, 2))
|
|
217
|
+
summary.add_column(style="bold")
|
|
218
|
+
summary.add_column()
|
|
219
|
+
summary.add_row("Source:", str(source))
|
|
220
|
+
summary.add_row("Target:", str(target))
|
|
221
|
+
summary.add_row("Dry-run:", "yes" if dry_run else "no")
|
|
222
|
+
summary.add_row(
|
|
223
|
+
"Excluded:",
|
|
224
|
+
", ".join(sorted(EXCLUDE_RELATIVE)),
|
|
225
|
+
)
|
|
226
|
+
console.print()
|
|
227
|
+
console.print(
|
|
228
|
+
Panel(
|
|
229
|
+
summary,
|
|
230
|
+
title="[bold gold1]🔄 Refresh squad vendor[/bold gold1]",
|
|
231
|
+
border_style="gold1",
|
|
232
|
+
padding=(1, 2),
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if dry_run:
|
|
237
|
+
console.print(
|
|
238
|
+
"[dim]Dry-run: no files modified. Re-run without --dry-run "
|
|
239
|
+
"to apply.[/dim]\n"
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# ── Execute ─────────────────────────────────────────────────────────
|
|
244
|
+
# Wipe target preserving the dir itself (so file ownership / git
|
|
245
|
+
# status of the parent stays sane).
|
|
246
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
for child in target.iterdir():
|
|
248
|
+
if child.is_dir():
|
|
249
|
+
shutil.rmtree(child)
|
|
250
|
+
else:
|
|
251
|
+
child.unlink()
|
|
252
|
+
|
|
253
|
+
ignore = _ignore_excluded(source)
|
|
254
|
+
copied_dirs = 0
|
|
255
|
+
copied_files = 0
|
|
256
|
+
for dirname in COPY_DIRS:
|
|
257
|
+
src_dir = source / dirname
|
|
258
|
+
if src_dir.exists() and src_dir.is_dir():
|
|
259
|
+
shutil.copytree(src_dir, target / dirname, ignore=ignore)
|
|
260
|
+
copied_dirs += 1
|
|
261
|
+
for filename in COPY_FILES:
|
|
262
|
+
src_file = source / filename
|
|
263
|
+
if src_file.exists() and src_file.is_file():
|
|
264
|
+
shutil.copy2(src_file, target / filename)
|
|
265
|
+
copied_files += 1
|
|
266
|
+
|
|
267
|
+
total_files = sum(1 for _ in target.rglob("*") if _.is_file())
|
|
268
|
+
|
|
269
|
+
done = Text.from_markup(
|
|
270
|
+
f"[green]✓[/green] Refreshed [bold]{copied_dirs}[/bold] dirs + "
|
|
271
|
+
f"[bold]{copied_files}[/bold] top-level files\n"
|
|
272
|
+
f" [dim]{total_files} files total at {target}[/dim]\n\n"
|
|
273
|
+
f"Next steps:\n"
|
|
274
|
+
f" 1. [cyan]cd {target.parent.parent}[/cyan]\n"
|
|
275
|
+
f" 2. [cyan]git diff --stat lovarch_cli/squad/[/cyan] — review the diff\n"
|
|
276
|
+
f" 3. [cyan]git add lovarch_cli/squad/[/cyan] + commit\n"
|
|
277
|
+
f" 4. Cut a release with [cyan]git tag v0.1.X && git push[/cyan]"
|
|
278
|
+
)
|
|
279
|
+
console.print(
|
|
280
|
+
Panel(
|
|
281
|
+
done,
|
|
282
|
+
title="[bold green]✓ Refresh complete[/bold green]",
|
|
283
|
+
border_style="green",
|
|
284
|
+
padding=(1, 2),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
console.print()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""`lovarch do` — run platform workflows from the terminal (premium).
|
|
2
|
+
|
|
3
|
+
lovarch do render "soggiorno minimal, luce naturale" -o render.png
|
|
4
|
+
lovarch do render "attico" --mode plan_to_3d --ref pianta.png
|
|
5
|
+
lovarch do colors --style natural
|
|
6
|
+
lovarch do copy "consegna ristrutturazione attico Brera"
|
|
7
|
+
|
|
8
|
+
Credits are debited server-side by the platform (same as the web app); costs
|
|
9
|
+
are always expressed in the user's credits.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
err_console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
do_app = typer.Typer(
|
|
24
|
+
help="Esegui i workflow della piattaforma Lovarch (premium).",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
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
|
+
def _lang(language: str | None) -> str:
|
|
41
|
+
if language:
|
|
42
|
+
return language
|
|
43
|
+
from lovarch_cli.i18n import current_lang
|
|
44
|
+
|
|
45
|
+
return current_lang()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@do_app.command("render")
|
|
49
|
+
def render_command(
|
|
50
|
+
description: str = typer.Argument(..., help="Descrizione della scena da renderizzare."),
|
|
51
|
+
output: Path = typer.Option(Path("render.png"), "--output", "-o", help="File di destinazione."),
|
|
52
|
+
mode: str = typer.Option(None, "--mode", help="room_render | render_3d | plan_to_3d | lighting_only | closeup_detail | closeup_angle (vuoto = sketch/testo→render 2D)."),
|
|
53
|
+
style: str = typer.Option("moderno", "--style", "-s", help="Stile del render."),
|
|
54
|
+
aspect: str = typer.Option("16:9", "--aspect", help="Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4)."),
|
|
55
|
+
ref: Path = typer.Option(None, "--ref", help="Immagine di riferimento (sketch/foto/pianta)."),
|
|
56
|
+
language: str = typer.Option(None, "--language", help="Lingua dell'output (default: configurata)."),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Render fotorealistico via Render Studio (crediti addebitati dalla piattaforma)."""
|
|
59
|
+
from lovarch_cli.mcp.tools import tool_render
|
|
60
|
+
|
|
61
|
+
out = asyncio.run(tool_render(
|
|
62
|
+
_workflows(), description=description, output_path=str(output),
|
|
63
|
+
mode=mode or None, render_style=style, aspect_ratio=aspect,
|
|
64
|
+
reference_image_path=str(ref) if ref else None, language=_lang(language),
|
|
65
|
+
))
|
|
66
|
+
if not out.get("ok"):
|
|
67
|
+
err_console.print(f"[red]✗ {out.get('error')}[/red]")
|
|
68
|
+
raise typer.Exit(1)
|
|
69
|
+
if out.get("saved_to"):
|
|
70
|
+
console.print(f"[green]✓[/green] Render salvato: [bold]{out['saved_to']}[/bold]")
|
|
71
|
+
if out.get("image_url"):
|
|
72
|
+
console.print(f"[dim]Nel tuo account Lovarch: {out['image_url']}[/dim]")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@do_app.command("colors")
|
|
76
|
+
def colors_command(
|
|
77
|
+
style: str = typer.Option("modern", "--style", "-s", help="modern | vintage | natural | bold | custom."),
|
|
78
|
+
base: str = typer.Option(None, "--base", help="Colori base separati da virgola (es. '#A16207,#09090B')."),
|
|
79
|
+
image_url: str = typer.Option(None, "--from-image", help="URL immagine da cui estrarre la palette."),
|
|
80
|
+
language: str = typer.Option(None, "--language", help="Lingua dell'output."),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Palette colori brand via piattaforma."""
|
|
83
|
+
wf = _workflows()
|
|
84
|
+
from lovarch_cli.workflows import WorkflowError
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
out = asyncio.run(wf.colors(
|
|
88
|
+
style=style,
|
|
89
|
+
base_colors=[c.strip() for c in base.split(",")] if base else None,
|
|
90
|
+
image_url=image_url, language=_lang(language),
|
|
91
|
+
))
|
|
92
|
+
except WorkflowError as exc:
|
|
93
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
console.print_json(json.dumps(out, ensure_ascii=False))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@do_app.command("copy")
|
|
99
|
+
def copy_command(
|
|
100
|
+
brief: str = typer.Argument(..., help="Brief del contenuto (min 5 caratteri)."),
|
|
101
|
+
mode: str = typer.Option("post", "--mode", help="post | story | carousel."),
|
|
102
|
+
slides: int = typer.Option(5, "--slides", help="Numero slide (solo carousel)."),
|
|
103
|
+
language: str = typer.Option(None, "--language", help="Lingua dell'output."),
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Copy di marketing (caption + hashtags) via piattaforma."""
|
|
106
|
+
wf = _workflows()
|
|
107
|
+
from lovarch_cli.workflows import WorkflowError
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
out = asyncio.run(wf.copy(brief, mode=mode, slide_count=slides, language=_lang(language)))
|
|
111
|
+
except WorkflowError as exc:
|
|
112
|
+
err_console.print(f"[red]✗ {exc}[/red]")
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
if out.get("caption"):
|
|
115
|
+
console.print(f"\n[bold gold1]Caption[/bold gold1]\n{out['caption']}\n")
|
|
116
|
+
if out.get("hashtags"):
|
|
117
|
+
console.print("[bold gold1]Hashtags[/bold gold1]\n" + " ".join(out["hashtags"]) + "\n")
|
|
118
|
+
rest = {k: v for k, v in out.items() if k not in ("caption", "hashtags")}
|
|
119
|
+
if rest:
|
|
120
|
+
console.print_json(json.dumps(rest, ensure_ascii=False))
|