arga-cli 0.1.7__tar.gz → 0.1.9__tar.gz
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.
- {arga_cli-0.1.7 → arga_cli-0.1.9}/PKG-INFO +1 -1
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/main.py +57 -3
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/__init__.py +13 -1
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/provision.py +10 -1
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/PKG-INFO +1 -1
- {arga_cli-0.1.7 → arga_cli-0.1.9}/pyproject.toml +1 -1
- {arga_cli-0.1.7 → arga_cli-0.1.9}/README.md +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/__init__.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/mcp.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/constants.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/env.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/output.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/prompts.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/session.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli/wizard/summary.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/SOURCES.txt +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/dependency_links.txt +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/entry_points.txt +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/requires.txt +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/arga_cli.egg-info/top_level.txt +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/setup.cfg +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_git.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_mcp.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_runs.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_test_url.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_validate_config.py +0 -0
- {arga_cli-0.1.7 → arga_cli-0.1.9}/tests/test_cli_validate_pr.py +0 -0
|
@@ -36,6 +36,44 @@ def _cli_version() -> str:
|
|
|
36
36
|
return "unknown"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
VERSION_CHECK_PATH = Path.home() / ".config" / "arga" / "version_check.json"
|
|
40
|
+
VERSION_CHECK_TTL_SECONDS = 86400 # 24 hours
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _check_for_update() -> None:
|
|
44
|
+
"""Print a warning if a newer version is available on PyPI. Caches for 24h."""
|
|
45
|
+
try:
|
|
46
|
+
current = _cli_version()
|
|
47
|
+
if current == "unknown":
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
now = time.time()
|
|
51
|
+
cached_latest: str | None = None
|
|
52
|
+
if VERSION_CHECK_PATH.exists():
|
|
53
|
+
data = json.loads(VERSION_CHECK_PATH.read_text())
|
|
54
|
+
if now - data.get("checked_at", 0) < VERSION_CHECK_TTL_SECONDS:
|
|
55
|
+
cached_latest = data.get("latest")
|
|
56
|
+
|
|
57
|
+
if cached_latest is None:
|
|
58
|
+
resp = httpx.get("https://pypi.org/pypi/arga-cli/json", timeout=3.0)
|
|
59
|
+
resp.raise_for_status()
|
|
60
|
+
cached_latest = resp.json()["info"]["version"]
|
|
61
|
+
VERSION_CHECK_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
VERSION_CHECK_PATH.write_text(json.dumps({"latest": cached_latest, "checked_at": now}))
|
|
63
|
+
|
|
64
|
+
if cached_latest and cached_latest != current:
|
|
65
|
+
latest_parts = tuple(int(x) for x in cached_latest.split("."))
|
|
66
|
+
current_parts = tuple(int(x) for x in current.split("."))
|
|
67
|
+
if latest_parts > current_parts:
|
|
68
|
+
print(
|
|
69
|
+
f"\033[33mwarning: arga-cli {cached_latest} available (you have {current}). "
|
|
70
|
+
f"Update with: uv tool upgrade arga-cli\033[0m",
|
|
71
|
+
file=sys.stderr,
|
|
72
|
+
)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
39
77
|
class CliError(Exception):
|
|
40
78
|
"""Base CLI error."""
|
|
41
79
|
|
|
@@ -275,7 +313,8 @@ class ApiClient:
|
|
|
275
313
|
if response.status_code == 401:
|
|
276
314
|
raise NotAuthenticatedError("Error: Not authenticated. Run `arga login`.")
|
|
277
315
|
if response.status_code == 403:
|
|
278
|
-
|
|
316
|
+
if not detail:
|
|
317
|
+
message += " Your plan may not support this feature. Check with `arga whoami`."
|
|
279
318
|
elif response.status_code == 404:
|
|
280
319
|
message += " Check that your plan supports this feature with `arga whoami`."
|
|
281
320
|
elif response.status_code == 429:
|
|
@@ -482,11 +521,16 @@ def _print_twin_env_vars(status: dict) -> None:
|
|
|
482
521
|
from arga_cli.wizard.provision import with_proxy_token
|
|
483
522
|
|
|
484
523
|
proxy_token = status.get("proxy_token")
|
|
524
|
+
# Public `pub-` hosts don't do proxy auth, so the base_url is directly
|
|
525
|
+
# callable from any native SDK (Slack, Stripe, Discord, …). Appending
|
|
526
|
+
# `?token=…` would be harmless but misleading — it'd suggest the token
|
|
527
|
+
# is required, which is exactly the friction public twins eliminate.
|
|
528
|
+
is_public = bool(status.get("is_public"))
|
|
485
529
|
print("\nTwin environment variables — update your app's config to point at these:\n")
|
|
486
530
|
for name, info in status.get("twins", {}).items():
|
|
487
531
|
label = info.get("label", name)
|
|
488
532
|
base_url = info.get("base_url", "")
|
|
489
|
-
if proxy_token and base_url:
|
|
533
|
+
if not is_public and proxy_token and base_url:
|
|
490
534
|
base_url = with_proxy_token(base_url, proxy_token)
|
|
491
535
|
print(f" {label}:")
|
|
492
536
|
print(f" Base URL: {base_url}")
|
|
@@ -1337,6 +1381,7 @@ def _wizard_help_text() -> str:
|
|
|
1337
1381
|
" env Re-run .env rewriting step\n\n"
|
|
1338
1382
|
"Options:\n"
|
|
1339
1383
|
" --api-url API base URL\n"
|
|
1384
|
+
" --ttl MINUTES Session TTL in minutes (paid/team: 1-480, default 60; free: fixed 10)\n"
|
|
1340
1385
|
" --no-shape-detect Disable heuristic detection of API keys by value pattern\n"
|
|
1341
1386
|
" -h, --help Show this help"
|
|
1342
1387
|
)
|
|
@@ -1345,6 +1390,7 @@ def _wizard_help_text() -> str:
|
|
|
1345
1390
|
def _build_wizard_init_parser() -> argparse.ArgumentParser:
|
|
1346
1391
|
parser = argparse.ArgumentParser(prog="arga wizard", allow_abbrev=False)
|
|
1347
1392
|
parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1393
|
+
parser.add_argument("--ttl", type=int, default=None, help="Session TTL in minutes (paid/team: 1-480, default 60)")
|
|
1348
1394
|
parser.add_argument("--no-shape-detect", action="store_true", default=False)
|
|
1349
1395
|
return parser
|
|
1350
1396
|
|
|
@@ -1369,6 +1415,7 @@ def run_wizard_init(args: argparse.Namespace) -> int:
|
|
|
1369
1415
|
api_key=api_key,
|
|
1370
1416
|
cwd=os.getcwd(),
|
|
1371
1417
|
shape_detect=not getattr(args, "no_shape_detect", False),
|
|
1418
|
+
ttl_minutes=args.ttl,
|
|
1372
1419
|
)
|
|
1373
1420
|
|
|
1374
1421
|
|
|
@@ -1396,9 +1443,15 @@ def run_wizard_status(_args: argparse.Namespace) -> int:
|
|
|
1396
1443
|
f"Status: {'[green]' + status['status'] + '[/green]' if status['status'] == 'ready' else '[yellow]' + status['status'] + '[/yellow]'}",
|
|
1397
1444
|
"",
|
|
1398
1445
|
]
|
|
1446
|
+
# Public `pub-` hosts are drop-in callable without the proxy token; only
|
|
1447
|
+
# decorate private hosts so the printed URL accurately reflects what
|
|
1448
|
+
# the user needs to use.
|
|
1449
|
+
is_public = bool(status.get("is_public"))
|
|
1450
|
+
proxy_token = status.get("proxy_token")
|
|
1399
1451
|
for name, info in status.get("twins", {}).items():
|
|
1400
1452
|
label = TWIN_CATALOG.get(name, {}).get("label", name).ljust(16)
|
|
1401
|
-
|
|
1453
|
+
base_url = info.get("base_url", "")
|
|
1454
|
+
url = base_url if is_public else with_proxy_token(base_url, proxy_token)
|
|
1402
1455
|
lines.append(f"{label} [underline]{url}[/underline]")
|
|
1403
1456
|
if status.get("expires_at"):
|
|
1404
1457
|
lines.append("")
|
|
@@ -1728,6 +1781,7 @@ def main() -> None:
|
|
|
1728
1781
|
except httpx.HTTPError as exc:
|
|
1729
1782
|
print(f"Network error: {exc}", file=sys.stderr)
|
|
1730
1783
|
raise SystemExit(1) from exc
|
|
1784
|
+
_check_for_update()
|
|
1731
1785
|
raise SystemExit(exit_code)
|
|
1732
1786
|
|
|
1733
1787
|
|
|
@@ -15,6 +15,7 @@ def run_wizard(
|
|
|
15
15
|
api_key: str | None = None,
|
|
16
16
|
cwd: str,
|
|
17
17
|
shape_detect: bool = True,
|
|
18
|
+
ttl_minutes: int | None = None,
|
|
18
19
|
) -> int:
|
|
19
20
|
"""Run the full quickstart wizard."""
|
|
20
21
|
from arga_cli.main import ApiClient
|
|
@@ -57,9 +58,20 @@ def run_wizard(
|
|
|
57
58
|
env_changes = rewrite_env_files(cwd, selected, shape_detect=shape_detect)
|
|
58
59
|
|
|
59
60
|
# Step 5: Provision
|
|
61
|
+
if ttl_minutes is not None:
|
|
62
|
+
resolved_ttl = ttl_minutes
|
|
63
|
+
elif billing_plan == "free":
|
|
64
|
+
resolved_ttl = 10
|
|
65
|
+
else:
|
|
66
|
+
resolved_ttl = 60
|
|
60
67
|
try:
|
|
61
|
-
status = provision_twins(client, selected, ttl_minutes=
|
|
68
|
+
status = provision_twins(client, selected, ttl_minutes=resolved_ttl, scenario_prompt=scenario_prompt)
|
|
62
69
|
except Exception as exc:
|
|
70
|
+
msg = str(exc)
|
|
71
|
+
if "provisions remaining" in msg.lower():
|
|
72
|
+
error("\n You've used all 5 quickstart provisions.")
|
|
73
|
+
yellow(" Run `arga login` to authenticate with your full account for unlimited access.\n")
|
|
74
|
+
return 1
|
|
63
75
|
error(f"\n Provisioning failed: {exc}")
|
|
64
76
|
if env_changes:
|
|
65
77
|
yellow(" Your .env has been updated. You can re-run the wizard to retry provisioning.")
|
|
@@ -114,7 +114,7 @@ def _format_seed_summary(seed_info: dict) -> list[str]:
|
|
|
114
114
|
counts = [
|
|
115
115
|
f"{key.replace('_', ' ')}: {value}"
|
|
116
116
|
for key, value in seed_info.items()
|
|
117
|
-
if key not in {"status", "twin"} and isinstance(value, (int, str))
|
|
117
|
+
if key not in {"status", "twin", "guild_id", "channel_ids"} and isinstance(value, (int, str))
|
|
118
118
|
]
|
|
119
119
|
return [f"Seeded — {', '.join(counts)}"] if counts else ["Seeded."]
|
|
120
120
|
if status_value == "skipped":
|
|
@@ -167,6 +167,15 @@ def seed_and_report(client: Any, status: dict) -> None:
|
|
|
167
167
|
env_vars = info.get("env_vars", {})
|
|
168
168
|
for key, val in env_vars.items():
|
|
169
169
|
console.print(f" [dim]{key}[/dim]: {val}")
|
|
170
|
+
|
|
171
|
+
# Print IDs from seed results (guild_id, channel_ids, etc.)
|
|
172
|
+
if seed_info is not None:
|
|
173
|
+
if seed_info.get("guild_id"):
|
|
174
|
+
console.print(f" [dim]GUILD_ID[/dim]: {seed_info['guild_id']}")
|
|
175
|
+
channel_ids = seed_info.get("channel_ids")
|
|
176
|
+
if channel_ids:
|
|
177
|
+
console.print(f" [dim]CHANNEL_IDS[/dim]: {', '.join(channel_ids)}")
|
|
178
|
+
|
|
170
179
|
console.print()
|
|
171
180
|
|
|
172
181
|
# Report backend-only twins
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "arga-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Command-line interface for Arga authentication, MCP installation, and browser validation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|