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