arga-cli 0.1.0__tar.gz → 0.1.2__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.0 → arga_cli-0.1.2}/PKG-INFO +34 -1
- {arga_cli-0.1.0 → arga_cli-0.1.2}/README.md +33 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli/main.py +234 -1
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/PKG-INFO +34 -1
- {arga_cli-0.1.0 → arga_cli-0.1.2}/pyproject.toml +1 -1
- arga_cli-0.1.2/tests/test_cli_runs.py +337 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_test_url.py +22 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_validate_pr.py +22 -0
- arga_cli-0.1.0/tests/test_cli_runs.py +0 -139
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli/__init__.py +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli/mcp.py +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/SOURCES.txt +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/dependency_links.txt +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/entry_points.txt +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/requires.txt +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/arga_cli.egg-info/top_level.txt +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/setup.cfg +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_git.py +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_mcp.py +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_scan.py +0 -0
- {arga_cli-0.1.0 → arga_cli-0.1.2}/tests/test_cli_validate_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arga-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Command-line interface for Arga authentication, MCP installation, and browser validation
|
|
5
5
|
Author: Arga Labs
|
|
6
6
|
Project-URL: Homepage, https://github.com/ArgaLabs/arga-cli
|
|
@@ -115,6 +115,7 @@ List and inspect recent validation runs:
|
|
|
115
115
|
```bash
|
|
116
116
|
arga runs list --repo arga-labs/validation-server --limit 20
|
|
117
117
|
arga runs status <run_id>
|
|
118
|
+
arga runs logs <run_id>
|
|
118
119
|
arga runs cancel <run_id>
|
|
119
120
|
```
|
|
120
121
|
|
|
@@ -186,6 +187,7 @@ arga scan report <run_id>
|
|
|
186
187
|
```bash
|
|
187
188
|
arga runs list --repo arga-labs/validation-server --status running --limit 20
|
|
188
189
|
arga runs status <run_id>
|
|
190
|
+
arga runs logs <run_id>
|
|
189
191
|
arga runs cancel <run_id>
|
|
190
192
|
```
|
|
191
193
|
|
|
@@ -193,6 +195,9 @@ arga runs cancel <run_id>
|
|
|
193
195
|
- `--repo` narrows the list to a single repository.
|
|
194
196
|
- `--status` accepts `completed`, `failed`, or `running`. The `running` filter includes non-terminal states such as `queued`.
|
|
195
197
|
- `arga runs status <run_id>` prints a detailed summary for a specific run.
|
|
198
|
+
- `arga runs logs <run_id>` prints worker logs plus recent runtime logs for a run you own.
|
|
199
|
+
- When you omit `<run_id>`, `arga runs logs` falls back to `./.arga-session.json` when present, which makes wizard-created twin sessions easy to inspect from the same directory.
|
|
200
|
+
- Add `--json` to `arga runs logs` for a machine-readable response.
|
|
196
201
|
- `arga runs cancel <run_id>` cancels the run through the validation API.
|
|
197
202
|
|
|
198
203
|
### Git Wrappers
|
|
@@ -287,6 +292,34 @@ To install the current checkout as a shell command:
|
|
|
287
292
|
uv tool install -e .
|
|
288
293
|
```
|
|
289
294
|
|
|
295
|
+
## Releasing
|
|
296
|
+
|
|
297
|
+
This repo includes a GitHub Actions workflow at `.github/workflows/publish.yml` for trusted publishing.
|
|
298
|
+
|
|
299
|
+
One-time setup:
|
|
300
|
+
|
|
301
|
+
- Create GitHub Environments named `testpypi` and `pypi` in the repository settings.
|
|
302
|
+
- In TestPyPI, add a Trusted Publisher for this GitHub repository and workflow:
|
|
303
|
+
- owner: `ArgaLabs`
|
|
304
|
+
- repository: `arga-cli`
|
|
305
|
+
- workflow: `publish.yml`
|
|
306
|
+
- environment: `testpypi`
|
|
307
|
+
- In PyPI, add a Trusted Publisher with the same repository and workflow, but environment `pypi`.
|
|
308
|
+
|
|
309
|
+
Publishing flow:
|
|
310
|
+
|
|
311
|
+
- Manual TestPyPI publish: run the `Publish Package` workflow with `repository=testpypi`.
|
|
312
|
+
- Automatic PyPI publish: push a tag like `v0.1.0`.
|
|
313
|
+
- The workflow verifies that the tag version matches `project.version` in `pyproject.toml`, builds the package with `uv build`, and publishes with `uv publish`.
|
|
314
|
+
|
|
315
|
+
Typical release steps:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
uv run pytest
|
|
319
|
+
git tag v0.1.0
|
|
320
|
+
git push origin v0.1.0
|
|
321
|
+
```
|
|
322
|
+
|
|
290
323
|
## Config Storage
|
|
291
324
|
|
|
292
325
|
The CLI stores its local auth state in:
|
|
@@ -95,6 +95,7 @@ List and inspect recent validation runs:
|
|
|
95
95
|
```bash
|
|
96
96
|
arga runs list --repo arga-labs/validation-server --limit 20
|
|
97
97
|
arga runs status <run_id>
|
|
98
|
+
arga runs logs <run_id>
|
|
98
99
|
arga runs cancel <run_id>
|
|
99
100
|
```
|
|
100
101
|
|
|
@@ -166,6 +167,7 @@ arga scan report <run_id>
|
|
|
166
167
|
```bash
|
|
167
168
|
arga runs list --repo arga-labs/validation-server --status running --limit 20
|
|
168
169
|
arga runs status <run_id>
|
|
170
|
+
arga runs logs <run_id>
|
|
169
171
|
arga runs cancel <run_id>
|
|
170
172
|
```
|
|
171
173
|
|
|
@@ -173,6 +175,9 @@ arga runs cancel <run_id>
|
|
|
173
175
|
- `--repo` narrows the list to a single repository.
|
|
174
176
|
- `--status` accepts `completed`, `failed`, or `running`. The `running` filter includes non-terminal states such as `queued`.
|
|
175
177
|
- `arga runs status <run_id>` prints a detailed summary for a specific run.
|
|
178
|
+
- `arga runs logs <run_id>` prints worker logs plus recent runtime logs for a run you own.
|
|
179
|
+
- When you omit `<run_id>`, `arga runs logs` falls back to `./.arga-session.json` when present, which makes wizard-created twin sessions easy to inspect from the same directory.
|
|
180
|
+
- Add `--json` to `arga runs logs` for a machine-readable response.
|
|
176
181
|
- `arga runs cancel <run_id>` cancels the run through the validation API.
|
|
177
182
|
|
|
178
183
|
### Git Wrappers
|
|
@@ -267,6 +272,34 @@ To install the current checkout as a shell command:
|
|
|
267
272
|
uv tool install -e .
|
|
268
273
|
```
|
|
269
274
|
|
|
275
|
+
## Releasing
|
|
276
|
+
|
|
277
|
+
This repo includes a GitHub Actions workflow at `.github/workflows/publish.yml` for trusted publishing.
|
|
278
|
+
|
|
279
|
+
One-time setup:
|
|
280
|
+
|
|
281
|
+
- Create GitHub Environments named `testpypi` and `pypi` in the repository settings.
|
|
282
|
+
- In TestPyPI, add a Trusted Publisher for this GitHub repository and workflow:
|
|
283
|
+
- owner: `ArgaLabs`
|
|
284
|
+
- repository: `arga-cli`
|
|
285
|
+
- workflow: `publish.yml`
|
|
286
|
+
- environment: `testpypi`
|
|
287
|
+
- In PyPI, add a Trusted Publisher with the same repository and workflow, but environment `pypi`.
|
|
288
|
+
|
|
289
|
+
Publishing flow:
|
|
290
|
+
|
|
291
|
+
- Manual TestPyPI publish: run the `Publish Package` workflow with `repository=testpypi`.
|
|
292
|
+
- Automatic PyPI publish: push a tag like `v0.1.0`.
|
|
293
|
+
- The workflow verifies that the tag version matches `project.version` in `pyproject.toml`, builds the package with `uv build`, and publishes with `uv publish`.
|
|
294
|
+
|
|
295
|
+
Typical release steps:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
uv run pytest
|
|
299
|
+
git tag v0.1.0
|
|
300
|
+
git push origin v0.1.0
|
|
301
|
+
```
|
|
302
|
+
|
|
270
303
|
## Config Storage
|
|
271
304
|
|
|
272
305
|
The CLI stores its local auth state in:
|
|
@@ -20,6 +20,8 @@ from arga_cli.mcp import install_mcp_configuration
|
|
|
20
20
|
|
|
21
21
|
DEFAULT_API_URL = os.environ.get("ARGA_API_URL", "https://api.argalabs.com")
|
|
22
22
|
CONFIG_PATH = Path.home() / ".config" / "arga" / "config.json"
|
|
23
|
+
WIZARD_SESSION_FILE = ".arga-session.json"
|
|
24
|
+
WIZARD_SESSION_PATH = Path(WIZARD_SESSION_FILE)
|
|
23
25
|
POLL_INTERVAL_SECONDS = 2.0
|
|
24
26
|
POLL_TIMEOUT_SECONDS = 600.0
|
|
25
27
|
SKIP_TRAILER = "[skip arga]"
|
|
@@ -128,6 +130,13 @@ class ApiClient:
|
|
|
128
130
|
)
|
|
129
131
|
return self._parse_json(response, "Failed to load run details")
|
|
130
132
|
|
|
133
|
+
def get_run_logs(self, run_id: str) -> dict[str, Any]:
|
|
134
|
+
response = self._client.get(
|
|
135
|
+
f"{self._api_url}/runs/{run_id}/logs",
|
|
136
|
+
headers=self._auth_headers(),
|
|
137
|
+
)
|
|
138
|
+
return self._parse_json(response, "Failed to load run logs")
|
|
139
|
+
|
|
131
140
|
def get_redteam_report(self, run_id: str) -> dict[str, Any]:
|
|
132
141
|
response = self._client.get(
|
|
133
142
|
f"{self._api_url}/redteam/{run_id}/report",
|
|
@@ -251,6 +260,31 @@ def delete_api_key() -> bool:
|
|
|
251
260
|
return True
|
|
252
261
|
|
|
253
262
|
|
|
263
|
+
def load_wizard_session(path: Path = WIZARD_SESSION_PATH) -> dict[str, Any] | None:
|
|
264
|
+
try:
|
|
265
|
+
data = json.loads(path.read_text())
|
|
266
|
+
except FileNotFoundError:
|
|
267
|
+
return None
|
|
268
|
+
except json.JSONDecodeError as exc:
|
|
269
|
+
raise CliError(f"Invalid wizard session file: {path}") from exc
|
|
270
|
+
|
|
271
|
+
if not isinstance(data, dict):
|
|
272
|
+
raise CliError(f"Invalid wizard session file: {path}")
|
|
273
|
+
return data
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def resolve_logs_run_id(run_id: str | None, *, session_path: Path = WIZARD_SESSION_PATH) -> str:
|
|
277
|
+
if run_id:
|
|
278
|
+
return run_id
|
|
279
|
+
session = load_wizard_session(session_path)
|
|
280
|
+
session_run_id = str((session or {}).get("run_id") or "").strip()
|
|
281
|
+
if session_run_id:
|
|
282
|
+
return session_run_id
|
|
283
|
+
raise CliError(
|
|
284
|
+
f"Run ID is required. Pass one explicitly or run this command from a directory containing {WIZARD_SESSION_FILE}."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
254
288
|
def build_verification_url(start_payload: dict[str, str]) -> str:
|
|
255
289
|
verification_url = start_payload["verification_url"]
|
|
256
290
|
device_code = start_payload["device_code"]
|
|
@@ -330,6 +364,32 @@ def run_whoami(args: argparse.Namespace) -> int:
|
|
|
330
364
|
|
|
331
365
|
print(f"Logged in as: {payload.get('github_login', 'unknown')}")
|
|
332
366
|
print(f"Workspace: {payload.get('workspace', 'Unknown')}")
|
|
367
|
+
|
|
368
|
+
billing_plan = payload.get("billing_plan", "free")
|
|
369
|
+
print(f"Plan: {billing_plan}")
|
|
370
|
+
|
|
371
|
+
plan_limits = payload.get("plan_limits")
|
|
372
|
+
if plan_limits:
|
|
373
|
+
runs_remaining = plan_limits.get("validation_runs_remaining")
|
|
374
|
+
runs_limit = plan_limits.get("validation_runs_limit")
|
|
375
|
+
if runs_limit is not None:
|
|
376
|
+
print(f"Validation runs: {runs_remaining}/{runs_limit} remaining this month")
|
|
377
|
+
else:
|
|
378
|
+
print("Validation runs: unlimited")
|
|
379
|
+
|
|
380
|
+
ci_limit = plan_limits.get("ci_checks_limit")
|
|
381
|
+
ci_remaining = plan_limits.get("ci_checks_remaining")
|
|
382
|
+
if ci_limit is not None:
|
|
383
|
+
print(f"CI checks: {ci_remaining}/{ci_limit} remaining this month")
|
|
384
|
+
elif billing_plan in ("team", "paid"):
|
|
385
|
+
print("CI checks: unlimited")
|
|
386
|
+
|
|
387
|
+
max_twins = plan_limits.get("max_twins_per_run")
|
|
388
|
+
if max_twins is not None:
|
|
389
|
+
print(f"Twins per run: {max_twins}")
|
|
390
|
+
elif billing_plan in ("team", "paid"):
|
|
391
|
+
print("Twins per run: unlimited")
|
|
392
|
+
|
|
333
393
|
return 0
|
|
334
394
|
|
|
335
395
|
|
|
@@ -349,6 +409,10 @@ def run_test_url(args: argparse.Namespace) -> int:
|
|
|
349
409
|
finally:
|
|
350
410
|
client.close()
|
|
351
411
|
|
|
412
|
+
if getattr(args, "json", False):
|
|
413
|
+
print(json.dumps({"run_id": payload.get("run_id"), "status": payload.get("status")}))
|
|
414
|
+
return 0
|
|
415
|
+
|
|
352
416
|
print("Starting validation...\n")
|
|
353
417
|
print(f"URL: {args.url}")
|
|
354
418
|
print(f"Prompt: {args.prompt}\n")
|
|
@@ -365,6 +429,10 @@ def run_validate_pr(args: argparse.Namespace) -> int:
|
|
|
365
429
|
finally:
|
|
366
430
|
client.close()
|
|
367
431
|
|
|
432
|
+
if args.json:
|
|
433
|
+
print(json.dumps({"run_id": payload.get("run_id"), "status": payload.get("status")}))
|
|
434
|
+
return 0
|
|
435
|
+
|
|
368
436
|
print("Starting PR validation...\n")
|
|
369
437
|
print(f"Repository: {args.repo}")
|
|
370
438
|
print(f"PR: #{args.pr}\n")
|
|
@@ -770,6 +838,105 @@ def _print_runs_table(runs: list[dict[str, Any]]) -> None:
|
|
|
770
838
|
print(format_row(row))
|
|
771
839
|
|
|
772
840
|
|
|
841
|
+
def _print_worker_logs(worker_logs: list[dict[str, Any]]) -> None:
|
|
842
|
+
print("Worker Logs:")
|
|
843
|
+
if not worker_logs:
|
|
844
|
+
print("None")
|
|
845
|
+
return
|
|
846
|
+
|
|
847
|
+
for index, worker_log in enumerate(worker_logs):
|
|
848
|
+
job_id = str(worker_log.get("job_id") or "-")
|
|
849
|
+
metadata = [
|
|
850
|
+
str(worker_log.get("job_type") or "").strip(),
|
|
851
|
+
str(worker_log.get("target_role") or "").strip(),
|
|
852
|
+
str(worker_log.get("status") or "").strip(),
|
|
853
|
+
]
|
|
854
|
+
metadata_label = " / ".join(value for value in metadata if value)
|
|
855
|
+
print(f"{job_id}: {metadata_label or 'worker log'}")
|
|
856
|
+
|
|
857
|
+
error = str(worker_log.get("error") or "").strip()
|
|
858
|
+
content = str(worker_log.get("content") or "").rstrip()
|
|
859
|
+
if error:
|
|
860
|
+
print(f"Error: {error}")
|
|
861
|
+
elif content:
|
|
862
|
+
print(content)
|
|
863
|
+
else:
|
|
864
|
+
print("No log content available.")
|
|
865
|
+
|
|
866
|
+
if index < len(worker_logs) - 1:
|
|
867
|
+
print()
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _print_runtime_logs(runtime_logs: list[dict[str, Any]]) -> None:
|
|
871
|
+
print("Runtime Logs:")
|
|
872
|
+
if not runtime_logs:
|
|
873
|
+
print("None")
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
for index, runtime_log in enumerate(runtime_logs):
|
|
877
|
+
header_parts = [
|
|
878
|
+
_format_timestamp(runtime_log.get("timestamp")),
|
|
879
|
+
str(runtime_log.get("severity") or "").strip(),
|
|
880
|
+
str(runtime_log.get("service_name") or "").strip(),
|
|
881
|
+
str(runtime_log.get("event") or "").strip(),
|
|
882
|
+
str(runtime_log.get("code") or "").strip(),
|
|
883
|
+
]
|
|
884
|
+
header = " | ".join(part for part in header_parts if part and part != "-")
|
|
885
|
+
print(header or "Log entry")
|
|
886
|
+
|
|
887
|
+
message = str(runtime_log.get("message") or "").strip()
|
|
888
|
+
if message:
|
|
889
|
+
print(message)
|
|
890
|
+
|
|
891
|
+
metadata: list[str] = []
|
|
892
|
+
request_id = str(runtime_log.get("request_id") or "").strip()
|
|
893
|
+
if request_id:
|
|
894
|
+
metadata.append(f"request_id={request_id}")
|
|
895
|
+
job_id = str(runtime_log.get("job_id") or "").strip()
|
|
896
|
+
if job_id:
|
|
897
|
+
metadata.append(f"job_id={job_id}")
|
|
898
|
+
surface_name = str(runtime_log.get("surface_name") or "").strip()
|
|
899
|
+
if surface_name:
|
|
900
|
+
metadata.append(f"surface={surface_name}")
|
|
901
|
+
if metadata:
|
|
902
|
+
print(" ".join(metadata))
|
|
903
|
+
|
|
904
|
+
if index < len(runtime_logs) - 1:
|
|
905
|
+
print()
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _print_run_logs(payload: dict[str, Any], fallback_run_id: str) -> None:
|
|
909
|
+
run = payload.get("run")
|
|
910
|
+
run_data = run if isinstance(run, dict) else {}
|
|
911
|
+
worker_logs = payload.get("worker_logs")
|
|
912
|
+
runtime_logs = payload.get("runtime_logs")
|
|
913
|
+
warnings = payload.get("warnings")
|
|
914
|
+
timeline_events = run_data.get("event_log_json")
|
|
915
|
+
|
|
916
|
+
print(f"Run ID: {run_data.get('id', fallback_run_id)}")
|
|
917
|
+
print(f"Status: {run_data.get('status', 'unknown')}")
|
|
918
|
+
print(f"Type: {run_data.get('run_type', 'unknown')}")
|
|
919
|
+
print(f"Mode: {run_data.get('mode', 'unknown')}")
|
|
920
|
+
print(f"Repository: {run_data.get('repo_full_name') or '-'}")
|
|
921
|
+
print(f"PR/Branch: {_run_ref_label(run_data)}")
|
|
922
|
+
print(f"Commit: {run_data.get('commit_sha') or '-'}")
|
|
923
|
+
print(f"Created: {_format_timestamp(run_data.get('created_at'))}")
|
|
924
|
+
print(f"Environment URL: {run_data.get('environment_url') or '-'}")
|
|
925
|
+
if isinstance(timeline_events, list):
|
|
926
|
+
print(f"Timeline Events: {len(timeline_events)}")
|
|
927
|
+
|
|
928
|
+
print()
|
|
929
|
+
_print_worker_logs(worker_logs if isinstance(worker_logs, list) else [])
|
|
930
|
+
print()
|
|
931
|
+
_print_runtime_logs(runtime_logs if isinstance(runtime_logs, list) else [])
|
|
932
|
+
|
|
933
|
+
if isinstance(warnings, list) and warnings:
|
|
934
|
+
print()
|
|
935
|
+
print("Warnings:")
|
|
936
|
+
for warning in warnings:
|
|
937
|
+
print(f"- {warning}")
|
|
938
|
+
|
|
939
|
+
|
|
773
940
|
def run_runs_list(args: argparse.Namespace) -> int:
|
|
774
941
|
api_key = load_api_key()
|
|
775
942
|
client = ApiClient(args.api_url, api_key=api_key)
|
|
@@ -783,6 +950,10 @@ def run_runs_list(args: argparse.Namespace) -> int:
|
|
|
783
950
|
finally:
|
|
784
951
|
client.close()
|
|
785
952
|
|
|
953
|
+
if args.json:
|
|
954
|
+
print(json.dumps(runs))
|
|
955
|
+
return 0
|
|
956
|
+
|
|
786
957
|
if not runs:
|
|
787
958
|
print("No matching validation runs found.")
|
|
788
959
|
return 0
|
|
@@ -799,6 +970,10 @@ def run_runs_status(args: argparse.Namespace) -> int:
|
|
|
799
970
|
finally:
|
|
800
971
|
client.close()
|
|
801
972
|
|
|
973
|
+
if args.json:
|
|
974
|
+
print(json.dumps(run))
|
|
975
|
+
return 0
|
|
976
|
+
|
|
802
977
|
print(f"Run ID: {run.get('id', args.run_id)}")
|
|
803
978
|
print(f"Status: {run.get('status', 'unknown')}")
|
|
804
979
|
print(f"Type: {run.get('run_type', 'unknown')}")
|
|
@@ -812,6 +987,23 @@ def run_runs_status(args: argparse.Namespace) -> int:
|
|
|
812
987
|
return 0
|
|
813
988
|
|
|
814
989
|
|
|
990
|
+
def run_runs_logs(args: argparse.Namespace) -> int:
|
|
991
|
+
run_id = resolve_logs_run_id(args.run_id)
|
|
992
|
+
api_key = load_api_key()
|
|
993
|
+
client = ApiClient(args.api_url, api_key=api_key)
|
|
994
|
+
try:
|
|
995
|
+
payload = client.get_run_logs(run_id)
|
|
996
|
+
finally:
|
|
997
|
+
client.close()
|
|
998
|
+
|
|
999
|
+
if args.json:
|
|
1000
|
+
print(json.dumps(payload, indent=2))
|
|
1001
|
+
return 0
|
|
1002
|
+
|
|
1003
|
+
_print_run_logs(payload, run_id)
|
|
1004
|
+
return 0
|
|
1005
|
+
|
|
1006
|
+
|
|
815
1007
|
def run_runs_cancel(args: argparse.Namespace) -> int:
|
|
816
1008
|
api_key = load_api_key()
|
|
817
1009
|
client = ApiClient(args.api_url, api_key=api_key)
|
|
@@ -988,6 +1180,28 @@ def run_push_cli(argv: list[str]) -> int:
|
|
|
988
1180
|
return _run_git_command(["push", *git_args])
|
|
989
1181
|
|
|
990
1182
|
|
|
1183
|
+
def run_wizard(args: argparse.Namespace) -> int:
|
|
1184
|
+
"""Launch the arga-wizard npm package, passing the stored API key."""
|
|
1185
|
+
try:
|
|
1186
|
+
api_key = load_api_key()
|
|
1187
|
+
except (NotAuthenticatedError, CliError):
|
|
1188
|
+
print("Not logged in. Run `arga login` first, or use `npx arga-wizard` directly.")
|
|
1189
|
+
return 1
|
|
1190
|
+
|
|
1191
|
+
cmd: list[str] = ["npx", "arga-wizard"]
|
|
1192
|
+
cmd.extend(["--api-key", api_key])
|
|
1193
|
+
if args.api_url != DEFAULT_API_URL:
|
|
1194
|
+
cmd.extend(["--api-url", args.api_url])
|
|
1195
|
+
|
|
1196
|
+
try:
|
|
1197
|
+
result = subprocess.run(cmd, check=False)
|
|
1198
|
+
return result.returncode
|
|
1199
|
+
except FileNotFoundError:
|
|
1200
|
+
print("Node.js is required for the wizard. Install it from https://nodejs.org")
|
|
1201
|
+
print("Or run the wizard directly: npx arga-wizard")
|
|
1202
|
+
return 1
|
|
1203
|
+
|
|
1204
|
+
|
|
991
1205
|
def build_parser() -> argparse.ArgumentParser:
|
|
992
1206
|
parser = argparse.ArgumentParser(prog="arga")
|
|
993
1207
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
@@ -1013,6 +1227,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1013
1227
|
test_url_parser.add_argument("--prompt", required=True, help="Natural language instructions for the agent")
|
|
1014
1228
|
test_url_parser.add_argument("--email", help="Optional login email")
|
|
1015
1229
|
test_url_parser.add_argument("--password", help="Optional login password")
|
|
1230
|
+
test_url_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1016
1231
|
test_url_parser.set_defaults(func=run_test_url)
|
|
1017
1232
|
|
|
1018
1233
|
validate_parser = subparsers.add_parser("validate", help="Start PR or URL validation runs")
|
|
@@ -1022,6 +1237,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1022
1237
|
validate_pr_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1023
1238
|
validate_pr_parser.add_argument("--repo", required=True, help="Repository in owner/repo format")
|
|
1024
1239
|
validate_pr_parser.add_argument("--pr", required=True, type=int, help="Pull request number")
|
|
1240
|
+
validate_pr_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1025
1241
|
validate_pr_parser.set_defaults(func=run_validate_pr)
|
|
1026
1242
|
|
|
1027
1243
|
validate_url_parser = validate_subparsers.add_parser(
|
|
@@ -1033,6 +1249,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1033
1249
|
validate_url_parser.add_argument("--prompt", required=True, help="Natural language instructions for the agent")
|
|
1034
1250
|
validate_url_parser.add_argument("--email", help="Optional login email")
|
|
1035
1251
|
validate_url_parser.add_argument("--password", help="Optional login password")
|
|
1252
|
+
validate_url_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1036
1253
|
validate_url_parser.set_defaults(func=run_test_url)
|
|
1037
1254
|
|
|
1038
1255
|
mcp_parser = subparsers.add_parser("mcp", help="Manage MCP integrations")
|
|
@@ -1045,7 +1262,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1045
1262
|
mcp_install_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1046
1263
|
mcp_install_parser.set_defaults(func=run_mcp_install)
|
|
1047
1264
|
|
|
1048
|
-
runs_parser = subparsers.add_parser("runs", help="List, inspect, or
|
|
1265
|
+
runs_parser = subparsers.add_parser("runs", help="List, inspect, cancel, or read validation run logs")
|
|
1049
1266
|
runs_subparsers = runs_parser.add_subparsers(dest="runs_command", required=True)
|
|
1050
1267
|
|
|
1051
1268
|
runs_list_parser = runs_subparsers.add_parser("list", help="List recent validation runs")
|
|
@@ -1057,18 +1274,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1057
1274
|
help="Filter by validation status",
|
|
1058
1275
|
)
|
|
1059
1276
|
runs_list_parser.add_argument("--limit", type=int, default=20, help="Maximum number of runs to show")
|
|
1277
|
+
runs_list_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1060
1278
|
runs_list_parser.set_defaults(func=run_runs_list)
|
|
1061
1279
|
|
|
1062
1280
|
runs_status_parser = runs_subparsers.add_parser("status", help="Show detailed status for a validation run")
|
|
1063
1281
|
runs_status_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1064
1282
|
runs_status_parser.add_argument("run_id", help="Validation run ID")
|
|
1283
|
+
runs_status_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1065
1284
|
runs_status_parser.set_defaults(func=run_runs_status)
|
|
1066
1285
|
|
|
1286
|
+
runs_logs_parser = runs_subparsers.add_parser("logs", help="Show worker and runtime logs for a validation run")
|
|
1287
|
+
runs_logs_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1288
|
+
runs_logs_parser.add_argument(
|
|
1289
|
+
"run_id",
|
|
1290
|
+
nargs="?",
|
|
1291
|
+
help=f"Validation run ID. Defaults to {WIZARD_SESSION_FILE} in the current directory when available.",
|
|
1292
|
+
)
|
|
1293
|
+
runs_logs_parser.add_argument("--json", action="store_true", help="Print the raw JSON response")
|
|
1294
|
+
runs_logs_parser.set_defaults(func=run_runs_logs)
|
|
1295
|
+
|
|
1067
1296
|
runs_cancel_parser = runs_subparsers.add_parser("cancel", help="Cancel a validation run")
|
|
1068
1297
|
runs_cancel_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1069
1298
|
runs_cancel_parser.add_argument("run_id", help="Validation run ID")
|
|
1070
1299
|
runs_cancel_parser.set_defaults(func=run_runs_cancel)
|
|
1071
1300
|
|
|
1301
|
+
wizard_parser = subparsers.add_parser("wizard", help="Launch the twins quickstart wizard")
|
|
1302
|
+
wizard_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1303
|
+
wizard_parser.set_defaults(func=run_wizard)
|
|
1304
|
+
|
|
1072
1305
|
subparsers.add_parser("commit", help="Wrap git commit and optionally mark it to skip Arga validation")
|
|
1073
1306
|
subparsers.add_parser("push", help="Wrap git push and verify skip state when requested")
|
|
1074
1307
|
subparsers.add_parser("scan", help="Start an app scan or inspect a scan run")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arga-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Command-line interface for Arga authentication, MCP installation, and browser validation
|
|
5
5
|
Author: Arga Labs
|
|
6
6
|
Project-URL: Homepage, https://github.com/ArgaLabs/arga-cli
|
|
@@ -115,6 +115,7 @@ List and inspect recent validation runs:
|
|
|
115
115
|
```bash
|
|
116
116
|
arga runs list --repo arga-labs/validation-server --limit 20
|
|
117
117
|
arga runs status <run_id>
|
|
118
|
+
arga runs logs <run_id>
|
|
118
119
|
arga runs cancel <run_id>
|
|
119
120
|
```
|
|
120
121
|
|
|
@@ -186,6 +187,7 @@ arga scan report <run_id>
|
|
|
186
187
|
```bash
|
|
187
188
|
arga runs list --repo arga-labs/validation-server --status running --limit 20
|
|
188
189
|
arga runs status <run_id>
|
|
190
|
+
arga runs logs <run_id>
|
|
189
191
|
arga runs cancel <run_id>
|
|
190
192
|
```
|
|
191
193
|
|
|
@@ -193,6 +195,9 @@ arga runs cancel <run_id>
|
|
|
193
195
|
- `--repo` narrows the list to a single repository.
|
|
194
196
|
- `--status` accepts `completed`, `failed`, or `running`. The `running` filter includes non-terminal states such as `queued`.
|
|
195
197
|
- `arga runs status <run_id>` prints a detailed summary for a specific run.
|
|
198
|
+
- `arga runs logs <run_id>` prints worker logs plus recent runtime logs for a run you own.
|
|
199
|
+
- When you omit `<run_id>`, `arga runs logs` falls back to `./.arga-session.json` when present, which makes wizard-created twin sessions easy to inspect from the same directory.
|
|
200
|
+
- Add `--json` to `arga runs logs` for a machine-readable response.
|
|
196
201
|
- `arga runs cancel <run_id>` cancels the run through the validation API.
|
|
197
202
|
|
|
198
203
|
### Git Wrappers
|
|
@@ -287,6 +292,34 @@ To install the current checkout as a shell command:
|
|
|
287
292
|
uv tool install -e .
|
|
288
293
|
```
|
|
289
294
|
|
|
295
|
+
## Releasing
|
|
296
|
+
|
|
297
|
+
This repo includes a GitHub Actions workflow at `.github/workflows/publish.yml` for trusted publishing.
|
|
298
|
+
|
|
299
|
+
One-time setup:
|
|
300
|
+
|
|
301
|
+
- Create GitHub Environments named `testpypi` and `pypi` in the repository settings.
|
|
302
|
+
- In TestPyPI, add a Trusted Publisher for this GitHub repository and workflow:
|
|
303
|
+
- owner: `ArgaLabs`
|
|
304
|
+
- repository: `arga-cli`
|
|
305
|
+
- workflow: `publish.yml`
|
|
306
|
+
- environment: `testpypi`
|
|
307
|
+
- In PyPI, add a Trusted Publisher with the same repository and workflow, but environment `pypi`.
|
|
308
|
+
|
|
309
|
+
Publishing flow:
|
|
310
|
+
|
|
311
|
+
- Manual TestPyPI publish: run the `Publish Package` workflow with `repository=testpypi`.
|
|
312
|
+
- Automatic PyPI publish: push a tag like `v0.1.0`.
|
|
313
|
+
- The workflow verifies that the tag version matches `project.version` in `pyproject.toml`, builds the package with `uv build`, and publishes with `uv publish`.
|
|
314
|
+
|
|
315
|
+
Typical release steps:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
uv run pytest
|
|
319
|
+
git tag v0.1.0
|
|
320
|
+
git push origin v0.1.0
|
|
321
|
+
```
|
|
322
|
+
|
|
290
323
|
## Config Storage
|
|
291
324
|
|
|
292
325
|
The CLI stores its local auth state in:
|
|
@@ -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.2"
|
|
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"
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from arga_cli import main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_runs_list_prints_filtered_table(monkeypatch, capsys) -> None:
|
|
9
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
10
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
11
|
+
|
|
12
|
+
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
13
|
+
assert repo == "arga-labs/validation-server"
|
|
14
|
+
if offset == 0:
|
|
15
|
+
return {
|
|
16
|
+
"items": [
|
|
17
|
+
{
|
|
18
|
+
"run_id": "run_completed",
|
|
19
|
+
"status": "completed",
|
|
20
|
+
"repo": "arga-labs/validation-server",
|
|
21
|
+
"pr_number": 182,
|
|
22
|
+
"created_at": "2026-03-25T12:30:00Z",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"run_id": "run_failed",
|
|
26
|
+
"status": "failed",
|
|
27
|
+
"repo": "arga-labs/validation-server",
|
|
28
|
+
"branch": "main",
|
|
29
|
+
"created_at": "2026-03-25T12:00:00Z",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
"limit": limit,
|
|
33
|
+
"offset": offset,
|
|
34
|
+
"has_more": False,
|
|
35
|
+
"total": 2,
|
|
36
|
+
}
|
|
37
|
+
raise AssertionError("Unexpected extra page request")
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
40
|
+
|
|
41
|
+
args = main.build_parser().parse_args(
|
|
42
|
+
["runs", "list", "--repo", "arga-labs/validation-server", "--status", "failed", "--limit", "20"]
|
|
43
|
+
)
|
|
44
|
+
exit_code = args.func(args)
|
|
45
|
+
output = capsys.readouterr().out
|
|
46
|
+
|
|
47
|
+
assert exit_code == 0
|
|
48
|
+
assert "RUN_ID" in output
|
|
49
|
+
assert "run_failed" in output
|
|
50
|
+
assert "main" in output
|
|
51
|
+
assert "run_completed" not in output
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_runs_list_running_filter_includes_non_terminal_statuses(monkeypatch, capsys) -> None:
|
|
55
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
56
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
57
|
+
|
|
58
|
+
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
59
|
+
return {
|
|
60
|
+
"items": [
|
|
61
|
+
{
|
|
62
|
+
"run_id": "run_queued",
|
|
63
|
+
"status": "queued",
|
|
64
|
+
"repo": "arga-labs/validation-server",
|
|
65
|
+
"branch": "feature/arga",
|
|
66
|
+
"created_at": "2026-03-25T13:00:00Z",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"run_id": "run_completed",
|
|
70
|
+
"status": "completed",
|
|
71
|
+
"repo": "arga-labs/validation-server",
|
|
72
|
+
"branch": "main",
|
|
73
|
+
"created_at": "2026-03-25T12:00:00Z",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
"limit": limit,
|
|
77
|
+
"offset": offset,
|
|
78
|
+
"has_more": False,
|
|
79
|
+
"total": 2,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
83
|
+
|
|
84
|
+
args = main.build_parser().parse_args(["runs", "list", "--status", "running"])
|
|
85
|
+
exit_code = args.func(args)
|
|
86
|
+
output = capsys.readouterr().out
|
|
87
|
+
|
|
88
|
+
assert exit_code == 0
|
|
89
|
+
assert "run_queued" in output
|
|
90
|
+
assert "run_completed" not in output
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_runs_status_prints_detail_summary(monkeypatch, capsys) -> None:
|
|
94
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
95
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
96
|
+
|
|
97
|
+
def fake_get_run(self, run_id: str):
|
|
98
|
+
assert run_id == "run_123"
|
|
99
|
+
return {
|
|
100
|
+
"id": run_id,
|
|
101
|
+
"status": "running",
|
|
102
|
+
"run_type": "pr",
|
|
103
|
+
"mode": "auto_visual",
|
|
104
|
+
"repo_full_name": "arga-labs/validation-server",
|
|
105
|
+
"github_pr_number": 182,
|
|
106
|
+
"commit_sha": "abc123",
|
|
107
|
+
"created_at": "2026-03-25T12:30:00Z",
|
|
108
|
+
"environment_url": "https://preview.example.com",
|
|
109
|
+
"session_id": "session_1",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
monkeypatch.setattr(main.ApiClient, "get_run", fake_get_run)
|
|
113
|
+
|
|
114
|
+
args = main.build_parser().parse_args(["runs", "status", "run_123"])
|
|
115
|
+
exit_code = args.func(args)
|
|
116
|
+
output = capsys.readouterr().out
|
|
117
|
+
|
|
118
|
+
assert exit_code == 0
|
|
119
|
+
assert "Run ID: run_123" in output
|
|
120
|
+
assert "Status: running" in output
|
|
121
|
+
assert "Repository: arga-labs/validation-server" in output
|
|
122
|
+
assert "PR/Branch: PR #182" in output
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_runs_list_json_flag(monkeypatch, capsys) -> None:
|
|
126
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
127
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
128
|
+
|
|
129
|
+
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
130
|
+
return {
|
|
131
|
+
"items": [
|
|
132
|
+
{
|
|
133
|
+
"run_id": "run_1",
|
|
134
|
+
"status": "completed",
|
|
135
|
+
"repo": "arga-labs/validation-server",
|
|
136
|
+
"pr_number": 182,
|
|
137
|
+
"created_at": "2026-03-25T12:30:00Z",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"run_id": "run_2",
|
|
141
|
+
"status": "failed",
|
|
142
|
+
"repo": "arga-labs/validation-server",
|
|
143
|
+
"branch": "main",
|
|
144
|
+
"created_at": "2026-03-25T12:00:00Z",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
"limit": limit,
|
|
148
|
+
"offset": offset,
|
|
149
|
+
"has_more": False,
|
|
150
|
+
"total": 2,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
154
|
+
|
|
155
|
+
args = main.build_parser().parse_args(["runs", "list", "--json"])
|
|
156
|
+
exit_code = args.func(args)
|
|
157
|
+
output = capsys.readouterr().out
|
|
158
|
+
|
|
159
|
+
assert exit_code == 0
|
|
160
|
+
parsed = json.loads(output)
|
|
161
|
+
assert isinstance(parsed, list)
|
|
162
|
+
assert len(parsed) == 2
|
|
163
|
+
assert parsed[0]["run_id"] == "run_1"
|
|
164
|
+
assert parsed[1]["run_id"] == "run_2"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_runs_list_json_flag_empty(monkeypatch, capsys) -> None:
|
|
168
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
169
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
170
|
+
|
|
171
|
+
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
172
|
+
return {"items": [], "limit": limit, "offset": offset, "has_more": False, "total": 0}
|
|
173
|
+
|
|
174
|
+
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
175
|
+
|
|
176
|
+
args = main.build_parser().parse_args(["runs", "list", "--json"])
|
|
177
|
+
exit_code = args.func(args)
|
|
178
|
+
output = capsys.readouterr().out
|
|
179
|
+
|
|
180
|
+
assert exit_code == 0
|
|
181
|
+
assert json.loads(output) == []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_runs_status_json_flag(monkeypatch, capsys) -> None:
|
|
185
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
186
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
187
|
+
|
|
188
|
+
run_data = {
|
|
189
|
+
"id": "run_123",
|
|
190
|
+
"status": "running",
|
|
191
|
+
"run_type": "pr",
|
|
192
|
+
"mode": "auto_visual",
|
|
193
|
+
"repo_full_name": "arga-labs/validation-server",
|
|
194
|
+
"github_pr_number": 182,
|
|
195
|
+
"commit_sha": "abc123",
|
|
196
|
+
"created_at": "2026-03-25T12:30:00Z",
|
|
197
|
+
"environment_url": "https://preview.example.com",
|
|
198
|
+
"session_id": "session_1",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def fake_get_run(self, run_id: str):
|
|
202
|
+
return run_data
|
|
203
|
+
|
|
204
|
+
monkeypatch.setattr(main.ApiClient, "get_run", fake_get_run)
|
|
205
|
+
|
|
206
|
+
args = main.build_parser().parse_args(["runs", "status", "run_123", "--json"])
|
|
207
|
+
exit_code = args.func(args)
|
|
208
|
+
output = capsys.readouterr().out
|
|
209
|
+
|
|
210
|
+
assert exit_code == 0
|
|
211
|
+
parsed = json.loads(output)
|
|
212
|
+
assert parsed == run_data
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_runs_logs_prints_worker_and_runtime_logs(monkeypatch, capsys) -> None:
|
|
216
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
217
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
218
|
+
|
|
219
|
+
def fake_get_logs(self, run_id: str):
|
|
220
|
+
assert run_id == "run_123"
|
|
221
|
+
return {
|
|
222
|
+
"run": {
|
|
223
|
+
"id": run_id,
|
|
224
|
+
"status": "ready",
|
|
225
|
+
"run_type": "twin_quickstart",
|
|
226
|
+
"mode": "staging",
|
|
227
|
+
"repo_full_name": None,
|
|
228
|
+
"commit_sha": None,
|
|
229
|
+
"created_at": "2026-03-25T12:30:00Z",
|
|
230
|
+
"environment_url": "https://preview.example.com",
|
|
231
|
+
"event_log_json": [{"type": "environment_ready"}],
|
|
232
|
+
},
|
|
233
|
+
"worker_logs": [
|
|
234
|
+
{
|
|
235
|
+
"job_id": "job_1",
|
|
236
|
+
"job_type": "deploy",
|
|
237
|
+
"target_role": "warm-vm",
|
|
238
|
+
"status": "succeeded",
|
|
239
|
+
"content": "deploy output",
|
|
240
|
+
"truncated": False,
|
|
241
|
+
"error": None,
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
"runtime_logs": [
|
|
245
|
+
{
|
|
246
|
+
"timestamp": "2026-03-25T12:31:00Z",
|
|
247
|
+
"service_name": "arga-api",
|
|
248
|
+
"severity": "INFO",
|
|
249
|
+
"event": "environment_ready",
|
|
250
|
+
"code": "ok",
|
|
251
|
+
"request_id": "req_1",
|
|
252
|
+
"job_id": "job_1",
|
|
253
|
+
"surface_name": "app",
|
|
254
|
+
"message": "Environment ready.",
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
"warnings": ["Cloud Logging query was partially truncated."],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
monkeypatch.setattr(main.ApiClient, "get_run_logs", fake_get_logs)
|
|
261
|
+
|
|
262
|
+
args = main.build_parser().parse_args(["runs", "logs", "run_123"])
|
|
263
|
+
exit_code = args.func(args)
|
|
264
|
+
output = capsys.readouterr().out
|
|
265
|
+
|
|
266
|
+
assert exit_code == 0
|
|
267
|
+
assert "Run ID: run_123" in output
|
|
268
|
+
assert "Worker Logs:" in output
|
|
269
|
+
assert "deploy output" in output
|
|
270
|
+
assert "Runtime Logs:" in output
|
|
271
|
+
assert "Environment ready." in output
|
|
272
|
+
assert "Warnings:" in output
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_runs_logs_prints_json(monkeypatch, capsys) -> None:
|
|
276
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
277
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
278
|
+
|
|
279
|
+
payload = {
|
|
280
|
+
"run": {"id": "run_123", "status": "ready"},
|
|
281
|
+
"worker_logs": [],
|
|
282
|
+
"runtime_logs": [],
|
|
283
|
+
"warnings": [],
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
monkeypatch.setattr(main.ApiClient, "get_run_logs", lambda self, run_id: payload)
|
|
287
|
+
|
|
288
|
+
args = main.build_parser().parse_args(["runs", "logs", "run_123", "--json"])
|
|
289
|
+
exit_code = args.func(args)
|
|
290
|
+
output = capsys.readouterr().out
|
|
291
|
+
|
|
292
|
+
assert exit_code == 0
|
|
293
|
+
assert json.loads(output) == payload
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_runs_logs_uses_wizard_session_file_when_run_id_missing(monkeypatch, capsys, tmp_path) -> None:
|
|
297
|
+
monkeypatch.chdir(tmp_path)
|
|
298
|
+
(tmp_path / main.WIZARD_SESSION_FILE).write_text(json.dumps({"run_id": "run_from_session"}) + "\n")
|
|
299
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
300
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
301
|
+
|
|
302
|
+
def fake_get_logs(self, run_id: str):
|
|
303
|
+
assert run_id == "run_from_session"
|
|
304
|
+
return {
|
|
305
|
+
"run": {"id": run_id, "status": "ready"},
|
|
306
|
+
"worker_logs": [],
|
|
307
|
+
"runtime_logs": [],
|
|
308
|
+
"warnings": [],
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
monkeypatch.setattr(main.ApiClient, "get_run_logs", fake_get_logs)
|
|
312
|
+
|
|
313
|
+
args = main.build_parser().parse_args(["runs", "logs"])
|
|
314
|
+
exit_code = args.func(args)
|
|
315
|
+
output = capsys.readouterr().out
|
|
316
|
+
|
|
317
|
+
assert exit_code == 0
|
|
318
|
+
assert "Run ID: run_from_session" in output
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_runs_cancel_prints_cancelled_status(monkeypatch, capsys) -> None:
|
|
322
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
323
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
324
|
+
|
|
325
|
+
def fake_cancel(self, run_id: str):
|
|
326
|
+
assert run_id == "run_123"
|
|
327
|
+
return {"status": "cancelled"}
|
|
328
|
+
|
|
329
|
+
monkeypatch.setattr(main.ApiClient, "cancel_validation_run", fake_cancel)
|
|
330
|
+
|
|
331
|
+
args = main.build_parser().parse_args(["runs", "cancel", "run_123"])
|
|
332
|
+
exit_code = args.func(args)
|
|
333
|
+
output = capsys.readouterr().out
|
|
334
|
+
|
|
335
|
+
assert exit_code == 0
|
|
336
|
+
assert "Run ID: run_123" in output
|
|
337
|
+
assert "Status: cancelled" in output
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from arga_cli import main
|
|
4
6
|
|
|
5
7
|
|
|
@@ -26,3 +28,23 @@ def test_test_url_command_prints_run_id(monkeypatch, capsys) -> None:
|
|
|
26
28
|
assert "Starting validation..." in output
|
|
27
29
|
assert "Run ID: run_3421" in output
|
|
28
30
|
assert "Status: queued" in output
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_test_url_json_flag(monkeypatch, capsys) -> None:
|
|
34
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
35
|
+
|
|
36
|
+
def fake_start(self, *, url: str, prompt: str, email: str | None = None, password: str | None = None):
|
|
37
|
+
return {"run_id": "run_3421", "status": "queued", "session_id": "session_1"}
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(main.ApiClient, "start_url_validation", fake_start)
|
|
40
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
41
|
+
|
|
42
|
+
args = main.build_parser().parse_args(
|
|
43
|
+
["test", "url", "--url", "https://demo-app.com", "--prompt", "test login flow", "--json"]
|
|
44
|
+
)
|
|
45
|
+
exit_code = args.func(args)
|
|
46
|
+
output = capsys.readouterr().out
|
|
47
|
+
|
|
48
|
+
assert exit_code == 0
|
|
49
|
+
parsed = json.loads(output)
|
|
50
|
+
assert parsed == {"run_id": "run_3421", "status": "queued"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from arga_cli import main
|
|
4
6
|
|
|
5
7
|
|
|
@@ -51,3 +53,23 @@ def test_validate_url_alias_reuses_url_validation(monkeypatch, capsys) -> None:
|
|
|
51
53
|
assert exit_code == 0
|
|
52
54
|
assert "Starting validation..." in output
|
|
53
55
|
assert "Run ID: run_123" in output
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_validate_pr_json_flag(monkeypatch, capsys) -> None:
|
|
59
|
+
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
60
|
+
|
|
61
|
+
def fake_start(self, *, repo: str, pr_number: int):
|
|
62
|
+
return {"run_id": "run_83921", "status": "queued"}
|
|
63
|
+
|
|
64
|
+
monkeypatch.setattr(main.ApiClient, "start_pr_validation", fake_start)
|
|
65
|
+
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
66
|
+
|
|
67
|
+
args = main.build_parser().parse_args(
|
|
68
|
+
["validate", "pr", "--repo", "arga-labs/validation-server", "--pr", "182", "--json"]
|
|
69
|
+
)
|
|
70
|
+
exit_code = args.func(args)
|
|
71
|
+
output = capsys.readouterr().out
|
|
72
|
+
|
|
73
|
+
assert exit_code == 0
|
|
74
|
+
parsed = json.loads(output)
|
|
75
|
+
assert parsed == {"run_id": "run_83921", "status": "queued"}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from arga_cli import main
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_runs_list_prints_filtered_table(monkeypatch, capsys) -> None:
|
|
7
|
-
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
8
|
-
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
9
|
-
|
|
10
|
-
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
11
|
-
assert repo == "arga-labs/validation-server"
|
|
12
|
-
if offset == 0:
|
|
13
|
-
return {
|
|
14
|
-
"items": [
|
|
15
|
-
{
|
|
16
|
-
"run_id": "run_completed",
|
|
17
|
-
"status": "completed",
|
|
18
|
-
"repo": "arga-labs/validation-server",
|
|
19
|
-
"pr_number": 182,
|
|
20
|
-
"created_at": "2026-03-25T12:30:00Z",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"run_id": "run_failed",
|
|
24
|
-
"status": "failed",
|
|
25
|
-
"repo": "arga-labs/validation-server",
|
|
26
|
-
"branch": "main",
|
|
27
|
-
"created_at": "2026-03-25T12:00:00Z",
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
"limit": limit,
|
|
31
|
-
"offset": offset,
|
|
32
|
-
"has_more": False,
|
|
33
|
-
"total": 2,
|
|
34
|
-
}
|
|
35
|
-
raise AssertionError("Unexpected extra page request")
|
|
36
|
-
|
|
37
|
-
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
38
|
-
|
|
39
|
-
args = main.build_parser().parse_args(
|
|
40
|
-
["runs", "list", "--repo", "arga-labs/validation-server", "--status", "failed", "--limit", "20"]
|
|
41
|
-
)
|
|
42
|
-
exit_code = args.func(args)
|
|
43
|
-
output = capsys.readouterr().out
|
|
44
|
-
|
|
45
|
-
assert exit_code == 0
|
|
46
|
-
assert "RUN_ID" in output
|
|
47
|
-
assert "run_failed" in output
|
|
48
|
-
assert "main" in output
|
|
49
|
-
assert "run_completed" not in output
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_runs_list_running_filter_includes_non_terminal_statuses(monkeypatch, capsys) -> None:
|
|
53
|
-
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
54
|
-
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
55
|
-
|
|
56
|
-
def fake_list(self, *, repo: str | None = None, limit: int = 20, offset: int = 0):
|
|
57
|
-
return {
|
|
58
|
-
"items": [
|
|
59
|
-
{
|
|
60
|
-
"run_id": "run_queued",
|
|
61
|
-
"status": "queued",
|
|
62
|
-
"repo": "arga-labs/validation-server",
|
|
63
|
-
"branch": "feature/arga",
|
|
64
|
-
"created_at": "2026-03-25T13:00:00Z",
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"run_id": "run_completed",
|
|
68
|
-
"status": "completed",
|
|
69
|
-
"repo": "arga-labs/validation-server",
|
|
70
|
-
"branch": "main",
|
|
71
|
-
"created_at": "2026-03-25T12:00:00Z",
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
"limit": limit,
|
|
75
|
-
"offset": offset,
|
|
76
|
-
"has_more": False,
|
|
77
|
-
"total": 2,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
monkeypatch.setattr(main.ApiClient, "list_pr_validation_runs", fake_list)
|
|
81
|
-
|
|
82
|
-
args = main.build_parser().parse_args(["runs", "list", "--status", "running"])
|
|
83
|
-
exit_code = args.func(args)
|
|
84
|
-
output = capsys.readouterr().out
|
|
85
|
-
|
|
86
|
-
assert exit_code == 0
|
|
87
|
-
assert "run_queued" in output
|
|
88
|
-
assert "run_completed" not in output
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_runs_status_prints_detail_summary(monkeypatch, capsys) -> None:
|
|
92
|
-
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
93
|
-
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
94
|
-
|
|
95
|
-
def fake_get_run(self, run_id: str):
|
|
96
|
-
assert run_id == "run_123"
|
|
97
|
-
return {
|
|
98
|
-
"id": run_id,
|
|
99
|
-
"status": "running",
|
|
100
|
-
"run_type": "pr",
|
|
101
|
-
"mode": "auto_visual",
|
|
102
|
-
"repo_full_name": "arga-labs/validation-server",
|
|
103
|
-
"github_pr_number": 182,
|
|
104
|
-
"commit_sha": "abc123",
|
|
105
|
-
"created_at": "2026-03-25T12:30:00Z",
|
|
106
|
-
"environment_url": "https://preview.example.com",
|
|
107
|
-
"session_id": "session_1",
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
monkeypatch.setattr(main.ApiClient, "get_run", fake_get_run)
|
|
111
|
-
|
|
112
|
-
args = main.build_parser().parse_args(["runs", "status", "run_123"])
|
|
113
|
-
exit_code = args.func(args)
|
|
114
|
-
output = capsys.readouterr().out
|
|
115
|
-
|
|
116
|
-
assert exit_code == 0
|
|
117
|
-
assert "Run ID: run_123" in output
|
|
118
|
-
assert "Status: running" in output
|
|
119
|
-
assert "Repository: arga-labs/validation-server" in output
|
|
120
|
-
assert "PR/Branch: PR #182" in output
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def test_runs_cancel_prints_cancelled_status(monkeypatch, capsys) -> None:
|
|
124
|
-
monkeypatch.setattr(main, "load_api_key", lambda: "arga_api_key")
|
|
125
|
-
monkeypatch.setattr(main.ApiClient, "close", lambda self: None)
|
|
126
|
-
|
|
127
|
-
def fake_cancel(self, run_id: str):
|
|
128
|
-
assert run_id == "run_123"
|
|
129
|
-
return {"status": "cancelled"}
|
|
130
|
-
|
|
131
|
-
monkeypatch.setattr(main.ApiClient, "cancel_validation_run", fake_cancel)
|
|
132
|
-
|
|
133
|
-
args = main.build_parser().parse_args(["runs", "cancel", "run_123"])
|
|
134
|
-
exit_code = args.func(args)
|
|
135
|
-
output = capsys.readouterr().out
|
|
136
|
-
|
|
137
|
-
assert exit_code == 0
|
|
138
|
-
assert "Run ID: run_123" in output
|
|
139
|
-
assert "Status: cancelled" in output
|
|
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
|