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,375 @@
|
|
|
1
|
+
"""lovarch run — Execute the squad pipeline against a project.
|
|
2
|
+
|
|
3
|
+
Pragmatic shell-out: invokes the existing `pipeline_runner.py` (1821 lines,
|
|
4
|
+
proven in production) as a subprocess instead of pulling its logic into the
|
|
5
|
+
CLI's modular phases architecture (that's Story 1.3 — deferred to Q3 2026).
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. Resolve project dir (~/.lovarch/projects/<name>/)
|
|
9
|
+
2. Verify audit was run and passed (last_audit.verdict in project.yaml)
|
|
10
|
+
3. Load credentials → detect mode (free/premium)
|
|
11
|
+
4. Premium: pre-flight credits.check_or_raise(required=3500)
|
|
12
|
+
Free: skip credits gate
|
|
13
|
+
5. Persistence: create_execution() → exec_id
|
|
14
|
+
6. Locate bundled pipeline_runner.py inside lovarch_cli/squad/
|
|
15
|
+
7. subprocess.Popen with --input-dir <project_input> + mode flag
|
|
16
|
+
Free → --dry-run (simulation, no API calls)
|
|
17
|
+
Premium → --real (production, hits Lovarch Supabase + AI providers)
|
|
18
|
+
8. Stream stdout/stderr to user in real time (preserve ANSI colors)
|
|
19
|
+
9. Wait for exit code → persistence.update_execution(status)
|
|
20
|
+
10. Show summary panel with `lovarch status` hint
|
|
21
|
+
|
|
22
|
+
Free mode dry-run is intentionally limited: the existing pipeline_runner
|
|
23
|
+
hardcodes LovarchClient() which connects to Supabase. A "true" Free run
|
|
24
|
+
that uses LocalSqliteClient + LocalFilesystemStorage requires the Story 1.3
|
|
25
|
+
refactor (replace LovarchClient calls with persistence ABC). For the curso
|
|
26
|
+
delivery, Free → dry-run is sufficient (shows the orchestration flow,
|
|
27
|
+
useful for teaching), Premium → real run delivers actual artifacts.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import os
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Annotated
|
|
38
|
+
|
|
39
|
+
import typer
|
|
40
|
+
import yaml
|
|
41
|
+
from rich.console import Console
|
|
42
|
+
from rich.panel import Panel
|
|
43
|
+
from rich.text import Text
|
|
44
|
+
|
|
45
|
+
from lovarch_cli.clients.persistence import ExecutionMode
|
|
46
|
+
from lovarch_cli.config import DEFAULT_HOME, load_credentials
|
|
47
|
+
from lovarch_cli.credits import (
|
|
48
|
+
InsufficientCreditsError,
|
|
49
|
+
get_credits_client,
|
|
50
|
+
)
|
|
51
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
52
|
+
|
|
53
|
+
console = Console()
|
|
54
|
+
err_console = Console(stderr=True)
|
|
55
|
+
|
|
56
|
+
# Typical workflow cost (mirrors what the squad bills for a full run).
|
|
57
|
+
# This is the threshold that gates premium runs at pre-flight.
|
|
58
|
+
DEFAULT_REQUIRED_CREDITS = 3_500
|
|
59
|
+
|
|
60
|
+
# Default workflow until we expose more squads via `lovarch run --workflow X`.
|
|
61
|
+
DEFAULT_WORKFLOW = "dal-brief-al-cantiere"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _pipeline_runner_path(squad_root: Path) -> Path:
|
|
65
|
+
"""Locate pipeline_runner.py inside the resolved squad root."""
|
|
66
|
+
return squad_root / "scripts" / "pipeline_runner.py"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_mode_from_creds() -> ExecutionMode:
|
|
70
|
+
"""Map persisted credentials.mode → ExecutionMode enum."""
|
|
71
|
+
creds = load_credentials()
|
|
72
|
+
if creds.mode == "premium":
|
|
73
|
+
return ExecutionMode.PREMIUM
|
|
74
|
+
# 'free' or 'none' → Free mode dry-run (no real calls)
|
|
75
|
+
return ExecutionMode.FREE
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_audit_verdict(project_dir: Path) -> str | None:
|
|
79
|
+
"""Return the last_audit.verdict from project.yaml, or None if absent."""
|
|
80
|
+
yaml_path = project_dir / "project.yaml"
|
|
81
|
+
if not yaml_path.exists():
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
meta = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
85
|
+
except yaml.YAMLError:
|
|
86
|
+
return None
|
|
87
|
+
last = meta.get("last_audit") or {}
|
|
88
|
+
return last.get("verdict")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Map pipeline_runner.py exit codes to a terminal run status. Kept in sync with
|
|
92
|
+
# the EXIT_* constants in squads/architettura-progetto/scripts/pipeline_runner.py.
|
|
93
|
+
_RUN_EXIT_STATUS = {
|
|
94
|
+
0: "completed",
|
|
95
|
+
3: "qa_rejected", # Tier 2 QA REJECT — NOT a crash, NOT a success
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _status_from_returncode(returncode: int) -> str:
|
|
100
|
+
"""Translate the runner's exit code into a terminal run status."""
|
|
101
|
+
return _RUN_EXIT_STATUS.get(returncode, "failed")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_last_run(project_dir: Path, status: str, returncode: int) -> None:
|
|
105
|
+
"""Persist the terminal run status into project.yaml `last_run`.
|
|
106
|
+
|
|
107
|
+
Best-effort: a write failure must never change the run's exit code.
|
|
108
|
+
"""
|
|
109
|
+
yaml_path = project_dir / "project.yaml"
|
|
110
|
+
try:
|
|
111
|
+
meta = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
112
|
+
except (OSError, yaml.YAMLError):
|
|
113
|
+
meta = {}
|
|
114
|
+
meta["last_run"] = {
|
|
115
|
+
"status": status,
|
|
116
|
+
"exit_code": returncode,
|
|
117
|
+
"at": datetime.now(timezone.utc).isoformat(),
|
|
118
|
+
}
|
|
119
|
+
try:
|
|
120
|
+
yaml_path.write_text(
|
|
121
|
+
yaml.safe_dump(meta, sort_keys=False, allow_unicode=True),
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
)
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_command(
|
|
129
|
+
project: Annotated[
|
|
130
|
+
str,
|
|
131
|
+
typer.Argument(help="Project name (created via `lovarch init`)."),
|
|
132
|
+
],
|
|
133
|
+
workflow: Annotated[
|
|
134
|
+
str,
|
|
135
|
+
typer.Option(
|
|
136
|
+
"--workflow",
|
|
137
|
+
"-w",
|
|
138
|
+
help="Workflow to execute.",
|
|
139
|
+
),
|
|
140
|
+
] = DEFAULT_WORKFLOW,
|
|
141
|
+
skip_audit: Annotated[
|
|
142
|
+
bool,
|
|
143
|
+
typer.Option(
|
|
144
|
+
"--skip-audit",
|
|
145
|
+
help="Skip the audit-verdict check (dangerous).",
|
|
146
|
+
),
|
|
147
|
+
] = False,
|
|
148
|
+
skip_credits: Annotated[
|
|
149
|
+
bool,
|
|
150
|
+
typer.Option(
|
|
151
|
+
"--skip-credits",
|
|
152
|
+
help="Skip the credits pre-flight (Premium only — for testing).",
|
|
153
|
+
),
|
|
154
|
+
] = False,
|
|
155
|
+
force_dry_run: Annotated[
|
|
156
|
+
bool,
|
|
157
|
+
typer.Option(
|
|
158
|
+
"--dry-run",
|
|
159
|
+
help="Force dry-run mode regardless of free/premium auth.",
|
|
160
|
+
),
|
|
161
|
+
] = False,
|
|
162
|
+
lang_flag: Annotated[
|
|
163
|
+
str | None,
|
|
164
|
+
typer.Option("--lang", "-l", help="Override language (it/pt/en/es)."),
|
|
165
|
+
] = None,
|
|
166
|
+
home_override: Annotated[
|
|
167
|
+
Path | None,
|
|
168
|
+
typer.Option(
|
|
169
|
+
"--home",
|
|
170
|
+
help="Override $HOME/.lovarch root (mainly for tests).",
|
|
171
|
+
hidden=True,
|
|
172
|
+
),
|
|
173
|
+
] = None,
|
|
174
|
+
squad_src: Annotated[
|
|
175
|
+
Path | None,
|
|
176
|
+
typer.Option(
|
|
177
|
+
"--squad-src",
|
|
178
|
+
help=(
|
|
179
|
+
"Path to a squad-architettura-progetto source dir to use "
|
|
180
|
+
"instead of the bundled payload. Also reads $LOVARCH_SQUAD_SRC. "
|
|
181
|
+
"Use for the developer's edit-and-test loop against the "
|
|
182
|
+
"monorepo Lovarch squad sources."
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
] = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Run the squad pipeline against a project."""
|
|
188
|
+
if lang_flag is not None:
|
|
189
|
+
set_current_lang(lang_flag)
|
|
190
|
+
lang = current_lang()
|
|
191
|
+
|
|
192
|
+
home = home_override or DEFAULT_HOME
|
|
193
|
+
proj_dir = home / "projects" / project
|
|
194
|
+
if not proj_dir.is_dir():
|
|
195
|
+
err_console.print(
|
|
196
|
+
f"\n[red]✗ {t('run.no_project', lang=lang, name=project)}[/red]\n"
|
|
197
|
+
)
|
|
198
|
+
sys.exit(2)
|
|
199
|
+
|
|
200
|
+
input_dir = proj_dir / "input"
|
|
201
|
+
if not input_dir.is_dir():
|
|
202
|
+
err_console.print(
|
|
203
|
+
f"\n[red]✗ {t('run.no_input', lang=lang, path=str(input_dir))}[/red]\n"
|
|
204
|
+
)
|
|
205
|
+
sys.exit(2)
|
|
206
|
+
|
|
207
|
+
# ─── Pre-flight: audit verdict gate ──────────────────────────────────
|
|
208
|
+
if not skip_audit:
|
|
209
|
+
verdict = _read_audit_verdict(proj_dir)
|
|
210
|
+
if verdict is None:
|
|
211
|
+
err_console.print(
|
|
212
|
+
f"\n[yellow]{t('run.no_audit', lang=lang, name=project)}[/yellow]\n"
|
|
213
|
+
)
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
if verdict == "FAIL":
|
|
216
|
+
err_console.print(
|
|
217
|
+
f"\n[red]{t('run.audit_failed', lang=lang, name=project)}[/red]\n"
|
|
218
|
+
)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
# ─── Mode resolution ─────────────────────────────────────────────────
|
|
222
|
+
mode = ExecutionMode.FREE if force_dry_run else _resolve_mode_from_creds()
|
|
223
|
+
is_dry_run = (mode == ExecutionMode.FREE) or force_dry_run
|
|
224
|
+
|
|
225
|
+
# ─── Pre-flight: credits check (Premium only) ────────────────────────
|
|
226
|
+
if mode == ExecutionMode.PREMIUM and not skip_credits:
|
|
227
|
+
try:
|
|
228
|
+
credits_client = get_credits_client(mode)
|
|
229
|
+
asyncio.run(
|
|
230
|
+
credits_client.check_or_raise(required=DEFAULT_REQUIRED_CREDITS)
|
|
231
|
+
)
|
|
232
|
+
except InsufficientCreditsError as exc:
|
|
233
|
+
bal = exc.balance
|
|
234
|
+
err_console.print(
|
|
235
|
+
"\n[red]"
|
|
236
|
+
+ t(
|
|
237
|
+
"errors.credits_insufficient",
|
|
238
|
+
lang=lang,
|
|
239
|
+
remaining=bal.credits_remaining,
|
|
240
|
+
required=bal.required,
|
|
241
|
+
deficit=bal.deficit,
|
|
242
|
+
)
|
|
243
|
+
+ "[/red]\n"
|
|
244
|
+
)
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
except RuntimeError as exc:
|
|
247
|
+
# Premium client unavailable (no keyring session). Programmer-facing.
|
|
248
|
+
err_console.print(
|
|
249
|
+
f"\n[red]{t('errors.credits_check_failed', lang=lang, message=str(exc))}[/red]\n"
|
|
250
|
+
)
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
# ─── Resolve squad payload + locate pipeline_runner.py ──────────────
|
|
254
|
+
from lovarch_cli.squad_loader import (
|
|
255
|
+
SquadNotFoundError,
|
|
256
|
+
resolve_squad_root,
|
|
257
|
+
squad_source_label,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
squad_root = resolve_squad_root(override=squad_src)
|
|
262
|
+
except SquadNotFoundError as exc:
|
|
263
|
+
err_console.print(f"\n[red]✗ {exc}[/red]\n")
|
|
264
|
+
sys.exit(2)
|
|
265
|
+
|
|
266
|
+
runner = _pipeline_runner_path(squad_root)
|
|
267
|
+
if not runner.exists():
|
|
268
|
+
err_console.print(
|
|
269
|
+
f"\n[red]✗ {t('run.no_runner', lang=lang, path=str(runner))}[/red]\n"
|
|
270
|
+
)
|
|
271
|
+
sys.exit(2)
|
|
272
|
+
|
|
273
|
+
# Show which squad source is in use (helpful during dev loop).
|
|
274
|
+
src_label = squad_source_label(squad_root)
|
|
275
|
+
if src_label != "bundled":
|
|
276
|
+
console.print(
|
|
277
|
+
f"[dim cyan]↳ squad: {squad_root} ({src_label})[/dim cyan]"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# ─── Header panel ────────────────────────────────────────────────────
|
|
281
|
+
mode_label = t(
|
|
282
|
+
"run.mode_free" if is_dry_run else "run.mode_premium",
|
|
283
|
+
lang=lang,
|
|
284
|
+
)
|
|
285
|
+
body_lines = [
|
|
286
|
+
t("run.starting", lang=lang, project=project, workflow=workflow),
|
|
287
|
+
t("run.mode_label", lang=lang, mode=mode_label),
|
|
288
|
+
]
|
|
289
|
+
if is_dry_run:
|
|
290
|
+
body_lines.append(t("run.dry_run_note", lang=lang))
|
|
291
|
+
console.print()
|
|
292
|
+
console.print(
|
|
293
|
+
Panel(
|
|
294
|
+
Text.from_markup("\n".join(body_lines)),
|
|
295
|
+
title=f"[bold gold1]🚀 {t('run.title', lang=lang)}[/bold gold1]",
|
|
296
|
+
border_style="gold1",
|
|
297
|
+
padding=(1, 2),
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
console.print()
|
|
301
|
+
|
|
302
|
+
# ─── Build subprocess command ────────────────────────────────────────
|
|
303
|
+
cmd = [
|
|
304
|
+
sys.executable,
|
|
305
|
+
str(runner),
|
|
306
|
+
"--input-dir",
|
|
307
|
+
str(input_dir),
|
|
308
|
+
]
|
|
309
|
+
cmd.append("--dry-run" if is_dry_run else "--real")
|
|
310
|
+
|
|
311
|
+
# Env vars passed through. The pipeline_runner reads OPENAI_API_KEY,
|
|
312
|
+
# MAPBOX_TOKEN, etc. directly from os.environ — so any keys the user
|
|
313
|
+
# has exported in their shell are inherited.
|
|
314
|
+
env = dict(os.environ)
|
|
315
|
+
env.setdefault("LOVARCH_PROJECT_NAME", project)
|
|
316
|
+
env.setdefault("LOVARCH_WORKFLOW", workflow)
|
|
317
|
+
|
|
318
|
+
# PREMIUM: hand the user's Supabase session to the runner so ALL paid AI
|
|
319
|
+
# goes through cli-ai-generate (debiting the user's credits, 1000cr=$1) and
|
|
320
|
+
# persistence writes as the user (RLS) — the runner never touches the
|
|
321
|
+
# student's OPENAI_API_KEY nor a service_role key. Also pass --user-id so the
|
|
322
|
+
# execution rows are owned by the token's user (RLS requires it).
|
|
323
|
+
if mode == ExecutionMode.PREMIUM and not is_dry_run:
|
|
324
|
+
from lovarch_cli.auth.session import LovarchSession
|
|
325
|
+
from lovarch_cli.config import DEFAULT_API_ANON_KEY, DEFAULT_API_URL
|
|
326
|
+
|
|
327
|
+
session = LovarchSession.load()
|
|
328
|
+
if session is not None:
|
|
329
|
+
env["LOVARCH_ACCESS_TOKEN"] = session.access_token
|
|
330
|
+
env["LOVARCH_ANON_KEY"] = DEFAULT_API_ANON_KEY
|
|
331
|
+
env["LOVARCH_API_URL"] = DEFAULT_API_URL
|
|
332
|
+
env["LOVARCH_SUPABASE_URL"] = DEFAULT_API_URL
|
|
333
|
+
cmd += ["--user-id", session.user_id]
|
|
334
|
+
|
|
335
|
+
# ─── Run subprocess + stream output ──────────────────────────────────
|
|
336
|
+
try:
|
|
337
|
+
result = subprocess.run(cmd, env=env, check=False)
|
|
338
|
+
except FileNotFoundError as exc:
|
|
339
|
+
err_console.print(
|
|
340
|
+
f"\n[red]✗ {t('run.subprocess_failed', lang=lang, message=str(exc))}[/red]\n"
|
|
341
|
+
)
|
|
342
|
+
sys.exit(2)
|
|
343
|
+
except KeyboardInterrupt:
|
|
344
|
+
err_console.print(
|
|
345
|
+
f"\n[yellow]⚠ {t('run.interrupted', lang=lang)}[/yellow]\n"
|
|
346
|
+
)
|
|
347
|
+
sys.exit(130)
|
|
348
|
+
|
|
349
|
+
# ─── Result panel ────────────────────────────────────────────────────
|
|
350
|
+
# Terminal status is derived from the runner's exit code:
|
|
351
|
+
# 0 → completed · 3 → qa_rejected (Tier 2 QA REJECT) · other → failed
|
|
352
|
+
run_status = _status_from_returncode(result.returncode)
|
|
353
|
+
_write_last_run(proj_dir, run_status, result.returncode)
|
|
354
|
+
|
|
355
|
+
if run_status == "completed":
|
|
356
|
+
summary_key, border = "run.completed", "green"
|
|
357
|
+
elif run_status == "qa_rejected":
|
|
358
|
+
summary_key, border = "run.qa_rejected", "yellow"
|
|
359
|
+
else:
|
|
360
|
+
summary_key, border = "run.failed", "red"
|
|
361
|
+
|
|
362
|
+
console.print()
|
|
363
|
+
console.print(
|
|
364
|
+
Panel(
|
|
365
|
+
Text.from_markup(
|
|
366
|
+
f"{t(summary_key, lang=lang, project=project)}\n\n"
|
|
367
|
+
f"{t('run.next_steps', lang=lang, project=project)}"
|
|
368
|
+
),
|
|
369
|
+
title=f"[bold {border}]{t('run.summary_title', lang=lang)}[/bold {border}]",
|
|
370
|
+
border_style=border,
|
|
371
|
+
padding=(1, 2),
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
console.print()
|
|
375
|
+
sys.exit(result.returncode)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""arch signup — Free mode interactive registration with lead capture.
|
|
2
|
+
|
|
3
|
+
Flow:
|
|
4
|
+
1. Welcome banner (4-language)
|
|
5
|
+
2. Interactive prompts: full_name, email, phone, country, language
|
|
6
|
+
3. GDPR consent (mandatory — Italian/EU compliance)
|
|
7
|
+
4. POST → cli-signup EF (validates server-side, creates shadow user, lead)
|
|
8
|
+
5. Save returned token to ~/.lovarch/credentials.json (chmod 0600)
|
|
9
|
+
6. Success message + next-steps (arch init, arch run)
|
|
10
|
+
|
|
11
|
+
Localization: i18n keys live in lovarch_cli/i18n/translations/{it,pt,en,es}.json.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Annotated
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.prompt import Confirm, Prompt
|
|
25
|
+
from rich.text import Text
|
|
26
|
+
|
|
27
|
+
from lovarch_cli.api import ApiClient, LovarchApiError
|
|
28
|
+
from lovarch_cli.config import Credentials, save_credentials
|
|
29
|
+
from lovarch_cli.i18n import current_lang, set_current_lang, t
|
|
30
|
+
from lovarch_cli.i18n.loader import VALID_LANGUAGES
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
err_console = Console(stderr=True)
|
|
34
|
+
|
|
35
|
+
EMAIL_RX = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
|
36
|
+
PHONE_RX = re.compile(r"^\+?[1-9]\d{6,14}$")
|
|
37
|
+
COUNTRY_RX = re.compile(r"^[A-Z]{2}$")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def signup_command(
|
|
41
|
+
yes: Annotated[
|
|
42
|
+
bool,
|
|
43
|
+
typer.Option(
|
|
44
|
+
"--yes",
|
|
45
|
+
"-y",
|
|
46
|
+
help="Auto-accept TOS (skip GDPR consent prompt — for CI only).",
|
|
47
|
+
),
|
|
48
|
+
] = False,
|
|
49
|
+
lang_flag: Annotated[
|
|
50
|
+
str | None,
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--lang",
|
|
53
|
+
help="Force language (it/pt/en/es). Default: detect from $LANG.",
|
|
54
|
+
),
|
|
55
|
+
] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Cadastro Free interativo (interactive Free signup).
|
|
58
|
+
|
|
59
|
+
Use --yes to auto-accept TOS for non-interactive (CI/Docker) flows.
|
|
60
|
+
"""
|
|
61
|
+
# ─── Language detection ──────────────────────────────────────────────
|
|
62
|
+
if lang_flag is not None:
|
|
63
|
+
set_current_lang(lang_flag)
|
|
64
|
+
lang = current_lang()
|
|
65
|
+
|
|
66
|
+
# ─── Welcome banner ──────────────────────────────────────────────────
|
|
67
|
+
console.print()
|
|
68
|
+
console.print(
|
|
69
|
+
Panel(
|
|
70
|
+
Text.from_markup(t("signup.welcome_body", lang=lang)),
|
|
71
|
+
title=f"[bold gold1]🎓 {t('signup.welcome_title', lang=lang)}[/bold gold1]",
|
|
72
|
+
border_style="gold1",
|
|
73
|
+
padding=(1, 2),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
console.print()
|
|
77
|
+
|
|
78
|
+
# ─── Confirm/select language ─────────────────────────────────────────
|
|
79
|
+
lang = Prompt.ask(
|
|
80
|
+
f"[bold]{t('signup.prompt_language', lang=lang)}[/bold]",
|
|
81
|
+
choices=list(VALID_LANGUAGES),
|
|
82
|
+
default=lang,
|
|
83
|
+
)
|
|
84
|
+
set_current_lang(lang)
|
|
85
|
+
|
|
86
|
+
# ─── Collect inputs ──────────────────────────────────────────────────
|
|
87
|
+
while True:
|
|
88
|
+
full_name = Prompt.ask(
|
|
89
|
+
f"[bold]{t('signup.prompt_full_name', lang=lang)}[/bold]"
|
|
90
|
+
).strip()
|
|
91
|
+
if len(full_name) >= 3:
|
|
92
|
+
break
|
|
93
|
+
err_console.print(f"[red]{t('signup.name_too_short', lang=lang)}[/red]")
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
email = (
|
|
97
|
+
Prompt.ask(f"[bold]{t('signup.prompt_email', lang=lang)}[/bold]")
|
|
98
|
+
.strip()
|
|
99
|
+
.lower()
|
|
100
|
+
)
|
|
101
|
+
if EMAIL_RX.match(email):
|
|
102
|
+
break
|
|
103
|
+
err_console.print(f"[red]{t('signup.invalid_email', lang=lang)}[/red]")
|
|
104
|
+
|
|
105
|
+
while True:
|
|
106
|
+
phone = Prompt.ask(
|
|
107
|
+
f"[bold]{t('signup.prompt_phone', lang=lang)}[/bold]"
|
|
108
|
+
).strip()
|
|
109
|
+
if PHONE_RX.match(phone):
|
|
110
|
+
break
|
|
111
|
+
err_console.print(f"[red]{t('signup.invalid_phone', lang=lang)}[/red]")
|
|
112
|
+
|
|
113
|
+
while True:
|
|
114
|
+
country = (
|
|
115
|
+
Prompt.ask(f"[bold]{t('signup.prompt_country', lang=lang)}[/bold]")
|
|
116
|
+
.strip()
|
|
117
|
+
.upper()
|
|
118
|
+
)
|
|
119
|
+
if COUNTRY_RX.match(country):
|
|
120
|
+
break
|
|
121
|
+
err_console.print(f"[red]{t('signup.invalid_country', lang=lang)}[/red]")
|
|
122
|
+
|
|
123
|
+
# ─── GDPR consent (mandatory) ────────────────────────────────────────
|
|
124
|
+
if not yes:
|
|
125
|
+
console.print(
|
|
126
|
+
f"\n[dim]{t('signup.tos_url_label', lang=lang)} "
|
|
127
|
+
f"https://lovarch.com/legal/cli-tos[/dim]"
|
|
128
|
+
)
|
|
129
|
+
accept = Confirm.ask(
|
|
130
|
+
f"[bold yellow]{t('signup.prompt_consent', lang=lang)}[/bold yellow]",
|
|
131
|
+
default=False,
|
|
132
|
+
)
|
|
133
|
+
if not accept:
|
|
134
|
+
err_console.print(
|
|
135
|
+
f"\n[red]✗ {t('signup.consent_required', lang=lang)}[/red]"
|
|
136
|
+
)
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
|
|
139
|
+
# ─── Submit to EF ────────────────────────────────────────────────────
|
|
140
|
+
console.print(f"\n[dim]{t('signup.submitting', lang=lang)}[/dim]")
|
|
141
|
+
api = ApiClient()
|
|
142
|
+
payload = {
|
|
143
|
+
"full_name": full_name,
|
|
144
|
+
"email": email,
|
|
145
|
+
"phone": phone,
|
|
146
|
+
"country": country,
|
|
147
|
+
"language": lang,
|
|
148
|
+
"source": "cli-free",
|
|
149
|
+
"accept_tos": True,
|
|
150
|
+
"cli_version": "0.1.0",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
response = asyncio.run(api.invoke_ef("cli-signup", payload))
|
|
155
|
+
except LovarchApiError as exc:
|
|
156
|
+
err_console.print(f"\n[red]✗ {exc}[/red]")
|
|
157
|
+
if exc.error_code:
|
|
158
|
+
err_console.print(f"[dim](error_code: {exc.error_code})[/dim]")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
# ─── Save credentials ────────────────────────────────────────────────
|
|
162
|
+
creds = Credentials(
|
|
163
|
+
mode="free",
|
|
164
|
+
lead_id=response["lead_id"],
|
|
165
|
+
user_id=response["user_id"],
|
|
166
|
+
free_token=response["free_token"],
|
|
167
|
+
email=email,
|
|
168
|
+
full_name=full_name,
|
|
169
|
+
country=country,
|
|
170
|
+
language=lang,
|
|
171
|
+
signed_up_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
172
|
+
upgrade_url=response.get("upgrade_url"),
|
|
173
|
+
)
|
|
174
|
+
creds_path = save_credentials(creds)
|
|
175
|
+
|
|
176
|
+
# ─── Success ─────────────────────────────────────────────────────────
|
|
177
|
+
console.print()
|
|
178
|
+
console.print(
|
|
179
|
+
Panel(
|
|
180
|
+
Text.from_markup(t("signup.next_steps", lang=lang)),
|
|
181
|
+
border_style="green",
|
|
182
|
+
padding=(1, 2),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
console.print(f"\n[dim]Credentials: {creds_path}[/dim]")
|