arga-cli 0.1.4__tar.gz → 0.1.5__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.4 → arga_cli-0.1.5}/PKG-INFO +51 -10
- {arga_cli-0.1.4 → arga_cli-0.1.5}/README.md +48 -9
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/main.py +247 -61
- arga_cli-0.1.5/arga_cli/wizard/__init__.py +90 -0
- arga_cli-0.1.5/arga_cli/wizard/constants.py +316 -0
- arga_cli-0.1.5/arga_cli/wizard/env.py +292 -0
- arga_cli-0.1.5/arga_cli/wizard/output.py +48 -0
- arga_cli-0.1.5/arga_cli/wizard/prompts.py +140 -0
- arga_cli-0.1.5/arga_cli/wizard/provision.py +162 -0
- arga_cli-0.1.5/arga_cli/wizard/session.py +28 -0
- arga_cli-0.1.5/arga_cli/wizard/summary.py +60 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/PKG-INFO +51 -10
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/SOURCES.txt +8 -0
- arga_cli-0.1.5/arga_cli.egg-info/requires.txt +3 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/pyproject.toml +3 -1
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_validate_pr.py +1 -27
- arga_cli-0.1.4/arga_cli.egg-info/requires.txt +0 -1
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/__init__.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/mcp.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/dependency_links.txt +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/entry_points.txt +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/top_level.txt +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/setup.cfg +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_git.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_mcp.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_runs.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_scan.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_test_url.py +0 -0
- {arga_cli-0.1.4 → arga_cli-0.1.5}/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.5
|
|
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
|
|
@@ -17,6 +17,8 @@ Classifier: Topic :: Software Development
|
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: questionary>=2.0.0
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
20
22
|
|
|
21
23
|
# Arga CLI
|
|
22
24
|
|
|
@@ -87,6 +89,14 @@ Start a pull request validation run:
|
|
|
87
89
|
arga validate pr --repo arga-labs/validation-server --pr 182
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
Any of these commands accept `--json` for machine-parseable output:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
arga test url --url https://demo-app.com --prompt "test login" --json
|
|
96
|
+
arga runs list --json
|
|
97
|
+
arga runs status <run_id> --json
|
|
98
|
+
```
|
|
99
|
+
|
|
90
100
|
Create a commit that skips Arga validation:
|
|
91
101
|
|
|
92
102
|
```bash
|
|
@@ -119,12 +129,6 @@ arga runs logs <run_id>
|
|
|
119
129
|
arga runs cancel <run_id>
|
|
120
130
|
```
|
|
121
131
|
|
|
122
|
-
`arga validate url` is also available and currently behaves the same as `arga test url`:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
arga validate url --url https://demo-app.com --prompt "test checkout"
|
|
126
|
-
```
|
|
127
|
-
|
|
128
132
|
## Command Reference
|
|
129
133
|
|
|
130
134
|
### Authentication
|
|
@@ -143,7 +147,6 @@ arga logout
|
|
|
143
147
|
|
|
144
148
|
```bash
|
|
145
149
|
arga test url --url https://demo-app.com --prompt "test login flow"
|
|
146
|
-
arga validate url --url https://demo-app.com --prompt "test checkout"
|
|
147
150
|
arga validate pr --repo arga-labs/validation-server --pr 182
|
|
148
151
|
arga validate install arga-labs/validation-server
|
|
149
152
|
arga validate config arga-labs/validation-server
|
|
@@ -151,8 +154,9 @@ arga validate config set arga-labs/validation-server --trigger branch --branch m
|
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
- `arga test url` starts a one-off validation against a deployed URL.
|
|
154
|
-
- `arga validate url` is an equivalent URL-validation entry point under the `validate` namespace.
|
|
155
157
|
- `arga validate pr` starts GitHub-backed PR validation for a repository and pull request number.
|
|
158
|
+
|
|
159
|
+
Both accept `--json` to output `{"run_id": "...", "status": "..."}` instead of human-readable text.
|
|
156
160
|
- `arga validate install <repo>` installs the GitHub webhook for automatic validation on a repository.
|
|
157
161
|
- `arga validate config <repo>` shows the current automatic validation settings, including install state, trigger mode, selected branch, and PR comment behavior.
|
|
158
162
|
- `arga validate config set <repo>` updates the automatic validation settings. Any omitted options keep their current value.
|
|
@@ -201,6 +205,8 @@ arga runs cancel <run_id>
|
|
|
201
205
|
- Add `--errors-only` to keep only failed worker logs plus warning/error runtime entries.
|
|
202
206
|
- `arga runs cancel <run_id>` cancels the run through the validation API.
|
|
203
207
|
|
|
208
|
+
Both `runs list` and `runs status` accept `--json` for structured output.
|
|
209
|
+
|
|
204
210
|
### Git Wrappers
|
|
205
211
|
|
|
206
212
|
```bash
|
|
@@ -260,6 +266,42 @@ If you need to add the server manually, the generated config looks like:
|
|
|
260
266
|
}
|
|
261
267
|
```
|
|
262
268
|
|
|
269
|
+
## JSON Output
|
|
270
|
+
|
|
271
|
+
Key commands support `--json` for use in CI pipelines, shell scripts, and agent automation:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Capture the run ID from a validation
|
|
275
|
+
RUN_ID=$(arga test url --url https://app.example.com --prompt "test login" --json | jq -r .run_id)
|
|
276
|
+
|
|
277
|
+
# Poll run status as JSON
|
|
278
|
+
arga runs status "$RUN_ID" --json | jq .status
|
|
279
|
+
|
|
280
|
+
# List runs as a JSON array
|
|
281
|
+
arga runs list --repo arga-labs/validation-server --json | jq '.[].run_id'
|
|
282
|
+
|
|
283
|
+
# Start PR validation and capture result
|
|
284
|
+
arga validate pr --repo arga-labs/validation-server --pr 182 --json
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Commands that support `--json`:
|
|
288
|
+
|
|
289
|
+
| Command | JSON shape |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `arga test url` | `{"run_id": "...", "status": "..."}` |
|
|
292
|
+
| `arga validate pr` | `{"run_id": "...", "status": "..."}` |
|
|
293
|
+
| `arga runs status <id>` | Full run object |
|
|
294
|
+
| `arga runs list` | Array of run summaries |
|
|
295
|
+
|
|
296
|
+
## Example Project
|
|
297
|
+
|
|
298
|
+
See [ArgaLabs/example-app](https://github.com/ArgaLabs/example-app) for a complete working example showing how to integrate Arga into a Next.js project with:
|
|
299
|
+
|
|
300
|
+
- GitHub Actions CI validation on every PR
|
|
301
|
+
- MCP config for Cursor and Claude Code
|
|
302
|
+
- A shell script for manual validation
|
|
303
|
+
- End-to-end walkthrough in the README
|
|
304
|
+
|
|
263
305
|
## Using A Custom API URL
|
|
264
306
|
|
|
265
307
|
By default, the CLI targets `https://api.argalabs.com`.
|
|
@@ -270,7 +312,6 @@ To point it at another environment, pass `--api-url` or set `ARGA_API_URL`:
|
|
|
270
312
|
arga login --api-url http://localhost:8000
|
|
271
313
|
arga mcp install --api-url http://localhost:8000
|
|
272
314
|
arga test url --api-url http://localhost:8000 --url https://demo-app.com --prompt "test checkout"
|
|
273
|
-
arga validate url --api-url http://localhost:8000 --url https://demo-app.com --prompt "test checkout"
|
|
274
315
|
arga validate pr --api-url http://localhost:8000 --repo arga-labs/validation-server --pr 182
|
|
275
316
|
```
|
|
276
317
|
|
|
@@ -67,6 +67,14 @@ Start a pull request validation run:
|
|
|
67
67
|
arga validate pr --repo arga-labs/validation-server --pr 182
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Any of these commands accept `--json` for machine-parseable output:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
arga test url --url https://demo-app.com --prompt "test login" --json
|
|
74
|
+
arga runs list --json
|
|
75
|
+
arga runs status <run_id> --json
|
|
76
|
+
```
|
|
77
|
+
|
|
70
78
|
Create a commit that skips Arga validation:
|
|
71
79
|
|
|
72
80
|
```bash
|
|
@@ -99,12 +107,6 @@ arga runs logs <run_id>
|
|
|
99
107
|
arga runs cancel <run_id>
|
|
100
108
|
```
|
|
101
109
|
|
|
102
|
-
`arga validate url` is also available and currently behaves the same as `arga test url`:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
arga validate url --url https://demo-app.com --prompt "test checkout"
|
|
106
|
-
```
|
|
107
|
-
|
|
108
110
|
## Command Reference
|
|
109
111
|
|
|
110
112
|
### Authentication
|
|
@@ -123,7 +125,6 @@ arga logout
|
|
|
123
125
|
|
|
124
126
|
```bash
|
|
125
127
|
arga test url --url https://demo-app.com --prompt "test login flow"
|
|
126
|
-
arga validate url --url https://demo-app.com --prompt "test checkout"
|
|
127
128
|
arga validate pr --repo arga-labs/validation-server --pr 182
|
|
128
129
|
arga validate install arga-labs/validation-server
|
|
129
130
|
arga validate config arga-labs/validation-server
|
|
@@ -131,8 +132,9 @@ arga validate config set arga-labs/validation-server --trigger branch --branch m
|
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
- `arga test url` starts a one-off validation against a deployed URL.
|
|
134
|
-
- `arga validate url` is an equivalent URL-validation entry point under the `validate` namespace.
|
|
135
135
|
- `arga validate pr` starts GitHub-backed PR validation for a repository and pull request number.
|
|
136
|
+
|
|
137
|
+
Both accept `--json` to output `{"run_id": "...", "status": "..."}` instead of human-readable text.
|
|
136
138
|
- `arga validate install <repo>` installs the GitHub webhook for automatic validation on a repository.
|
|
137
139
|
- `arga validate config <repo>` shows the current automatic validation settings, including install state, trigger mode, selected branch, and PR comment behavior.
|
|
138
140
|
- `arga validate config set <repo>` updates the automatic validation settings. Any omitted options keep their current value.
|
|
@@ -181,6 +183,8 @@ arga runs cancel <run_id>
|
|
|
181
183
|
- Add `--errors-only` to keep only failed worker logs plus warning/error runtime entries.
|
|
182
184
|
- `arga runs cancel <run_id>` cancels the run through the validation API.
|
|
183
185
|
|
|
186
|
+
Both `runs list` and `runs status` accept `--json` for structured output.
|
|
187
|
+
|
|
184
188
|
### Git Wrappers
|
|
185
189
|
|
|
186
190
|
```bash
|
|
@@ -240,6 +244,42 @@ If you need to add the server manually, the generated config looks like:
|
|
|
240
244
|
}
|
|
241
245
|
```
|
|
242
246
|
|
|
247
|
+
## JSON Output
|
|
248
|
+
|
|
249
|
+
Key commands support `--json` for use in CI pipelines, shell scripts, and agent automation:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Capture the run ID from a validation
|
|
253
|
+
RUN_ID=$(arga test url --url https://app.example.com --prompt "test login" --json | jq -r .run_id)
|
|
254
|
+
|
|
255
|
+
# Poll run status as JSON
|
|
256
|
+
arga runs status "$RUN_ID" --json | jq .status
|
|
257
|
+
|
|
258
|
+
# List runs as a JSON array
|
|
259
|
+
arga runs list --repo arga-labs/validation-server --json | jq '.[].run_id'
|
|
260
|
+
|
|
261
|
+
# Start PR validation and capture result
|
|
262
|
+
arga validate pr --repo arga-labs/validation-server --pr 182 --json
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Commands that support `--json`:
|
|
266
|
+
|
|
267
|
+
| Command | JSON shape |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `arga test url` | `{"run_id": "...", "status": "..."}` |
|
|
270
|
+
| `arga validate pr` | `{"run_id": "...", "status": "..."}` |
|
|
271
|
+
| `arga runs status <id>` | Full run object |
|
|
272
|
+
| `arga runs list` | Array of run summaries |
|
|
273
|
+
|
|
274
|
+
## Example Project
|
|
275
|
+
|
|
276
|
+
See [ArgaLabs/example-app](https://github.com/ArgaLabs/example-app) for a complete working example showing how to integrate Arga into a Next.js project with:
|
|
277
|
+
|
|
278
|
+
- GitHub Actions CI validation on every PR
|
|
279
|
+
- MCP config for Cursor and Claude Code
|
|
280
|
+
- A shell script for manual validation
|
|
281
|
+
- End-to-end walkthrough in the README
|
|
282
|
+
|
|
243
283
|
## Using A Custom API URL
|
|
244
284
|
|
|
245
285
|
By default, the CLI targets `https://api.argalabs.com`.
|
|
@@ -250,7 +290,6 @@ To point it at another environment, pass `--api-url` or set `ARGA_API_URL`:
|
|
|
250
290
|
arga login --api-url http://localhost:8000
|
|
251
291
|
arga mcp install --api-url http://localhost:8000
|
|
252
292
|
arga test url --api-url http://localhost:8000 --url https://demo-app.com --prompt "test checkout"
|
|
253
|
-
arga validate url --api-url http://localhost:8000 --url https://demo-app.com --prompt "test checkout"
|
|
254
293
|
arga validate pr --api-url http://localhost:8000 --repo arga-labs/validation-server --pr 182
|
|
255
294
|
```
|
|
256
295
|
|
|
@@ -85,6 +85,7 @@ class ApiClient:
|
|
|
85
85
|
prompt: str,
|
|
86
86
|
email: str | None = None,
|
|
87
87
|
password: str | None = None,
|
|
88
|
+
ttl_minutes: int | None = None,
|
|
88
89
|
) -> dict[str, str]:
|
|
89
90
|
payload: dict[str, object] = {
|
|
90
91
|
"url": url,
|
|
@@ -95,6 +96,8 @@ class ApiClient:
|
|
|
95
96
|
"email": email or "",
|
|
96
97
|
"password": password or "",
|
|
97
98
|
}
|
|
99
|
+
if ttl_minutes is not None:
|
|
100
|
+
payload["ttl_minutes"] = ttl_minutes
|
|
98
101
|
response = self._client.post(
|
|
99
102
|
f"{self._api_url}/validate/url",
|
|
100
103
|
json=payload,
|
|
@@ -398,9 +401,34 @@ def run_whoami(args: argparse.Namespace) -> int:
|
|
|
398
401
|
elif billing_plan in ("team", "paid"):
|
|
399
402
|
print("Twins per run: unlimited")
|
|
400
403
|
|
|
404
|
+
max_ttl = plan_limits.get("max_ttl_minutes")
|
|
405
|
+
if max_ttl is not None:
|
|
406
|
+
print(f"Max run TTL: {max_ttl} minutes")
|
|
407
|
+
elif billing_plan in ("team", "paid"):
|
|
408
|
+
print("Max run TTL: 480 minutes")
|
|
409
|
+
|
|
401
410
|
return 0
|
|
402
411
|
|
|
403
412
|
|
|
413
|
+
def _resolve_ttl(client: ApiClient, requested_ttl: int | None) -> int | None:
|
|
414
|
+
"""Resolve TTL based on user plan. Free users are capped at 10 minutes."""
|
|
415
|
+
FREE_TTL = 10
|
|
416
|
+
|
|
417
|
+
me = client.get_me()
|
|
418
|
+
billing_plan = me.get("billing_plan", "free")
|
|
419
|
+
|
|
420
|
+
if billing_plan in ("team", "paid"):
|
|
421
|
+
return requested_ttl # None means server default (30 min)
|
|
422
|
+
|
|
423
|
+
# Free tier — locked to 10 minutes
|
|
424
|
+
if requested_ttl is not None and requested_ttl != FREE_TTL:
|
|
425
|
+
raise CliError(
|
|
426
|
+
f"Free plan runs are limited to {FREE_TTL} minutes. "
|
|
427
|
+
f"Upgrade to Team for custom TTL (up to 480 minutes)."
|
|
428
|
+
)
|
|
429
|
+
return FREE_TTL
|
|
430
|
+
|
|
431
|
+
|
|
404
432
|
def run_test_url(args: argparse.Namespace) -> int:
|
|
405
433
|
if bool(args.email) != bool(args.password):
|
|
406
434
|
raise CliError("Both --email and --password must be provided together.")
|
|
@@ -408,11 +436,13 @@ def run_test_url(args: argparse.Namespace) -> int:
|
|
|
408
436
|
api_key = load_api_key()
|
|
409
437
|
client = ApiClient(args.api_url, api_key=api_key)
|
|
410
438
|
try:
|
|
439
|
+
ttl_minutes = _resolve_ttl(client, getattr(args, "ttl", None))
|
|
411
440
|
payload = client.start_url_validation(
|
|
412
441
|
url=args.url,
|
|
413
442
|
prompt=args.prompt,
|
|
414
443
|
email=args.email,
|
|
415
444
|
password=args.password,
|
|
445
|
+
ttl_minutes=ttl_minutes,
|
|
416
446
|
)
|
|
417
447
|
finally:
|
|
418
448
|
client.close()
|
|
@@ -423,7 +453,8 @@ def run_test_url(args: argparse.Namespace) -> int:
|
|
|
423
453
|
|
|
424
454
|
print("Starting validation...\n")
|
|
425
455
|
print(f"URL: {args.url}")
|
|
426
|
-
print(f"Prompt: {args.prompt}
|
|
456
|
+
print(f"Prompt: {args.prompt}")
|
|
457
|
+
print(f"TTL: {ttl_minutes} minutes\n")
|
|
427
458
|
print(f"Run ID: {payload.get('run_id', 'unknown')}")
|
|
428
459
|
print(f"Status: {payload.get('status', 'unknown')}")
|
|
429
460
|
return 0
|
|
@@ -453,7 +484,6 @@ def run_validate_pr(args: argparse.Namespace) -> int:
|
|
|
453
484
|
def _validate_help_text() -> str:
|
|
454
485
|
return (
|
|
455
486
|
"usage: arga validate pr --repo <owner/repo> --pr <number>\n"
|
|
456
|
-
" arga validate url --url <url> --prompt <prompt>\n"
|
|
457
487
|
" arga validate install <repo>\n"
|
|
458
488
|
" arga validate config <repo>\n"
|
|
459
489
|
" arga validate config set <repo> [--trigger pr|branch] [--branch <name>] [--comments on|off]\n\n"
|
|
@@ -473,20 +503,6 @@ def _build_validate_pr_parser() -> argparse.ArgumentParser:
|
|
|
473
503
|
return parser
|
|
474
504
|
|
|
475
505
|
|
|
476
|
-
def _build_validate_url_parser() -> argparse.ArgumentParser:
|
|
477
|
-
parser = argparse.ArgumentParser(
|
|
478
|
-
prog="arga validate url",
|
|
479
|
-
description="Run a browser validation against a deployed URL.",
|
|
480
|
-
allow_abbrev=False,
|
|
481
|
-
)
|
|
482
|
-
parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
483
|
-
parser.add_argument("--url", required=True, help="Deployed application URL")
|
|
484
|
-
parser.add_argument("--prompt", required=True, help="Natural language instructions for the agent")
|
|
485
|
-
parser.add_argument("--email", help="Optional login email")
|
|
486
|
-
parser.add_argument("--password", help="Optional login password")
|
|
487
|
-
return parser
|
|
488
|
-
|
|
489
|
-
|
|
490
506
|
def _build_validate_install_parser() -> argparse.ArgumentParser:
|
|
491
507
|
parser = argparse.ArgumentParser(
|
|
492
508
|
prog="arga validate install",
|
|
@@ -575,11 +591,7 @@ def run_validate_config_set(args: argparse.Namespace) -> int:
|
|
|
575
591
|
try:
|
|
576
592
|
current = client.get_github_validation_config(repo=args.repo)
|
|
577
593
|
trigger_mode = args.trigger or str(current.get("trigger_mode") or "pr")
|
|
578
|
-
comment_on_pr = (
|
|
579
|
-
current.get("comment_on_pr", True)
|
|
580
|
-
if args.comments is None
|
|
581
|
-
else args.comments == "on"
|
|
582
|
-
)
|
|
594
|
+
comment_on_pr = current.get("comment_on_pr", True) if args.comments is None else args.comments == "on"
|
|
583
595
|
branch: str | None = None
|
|
584
596
|
if trigger_mode == "branch":
|
|
585
597
|
branch = args.branch or str(current.get("branch") or current.get("default_branch") or "").strip() or None
|
|
@@ -677,16 +689,12 @@ def _wait_for_scan_approval(client: ApiClient, run_id: str) -> dict[str, Any]:
|
|
|
677
689
|
|
|
678
690
|
time.sleep(POLL_INTERVAL_SECONDS)
|
|
679
691
|
|
|
680
|
-
raise CliError(
|
|
681
|
-
f"Timed out waiting for the scan plan to be ready for run {last_run.get('id', run_id)}."
|
|
682
|
-
)
|
|
692
|
+
raise CliError(f"Timed out waiting for the scan plan to be ready for run {last_run.get('id', run_id)}.")
|
|
683
693
|
|
|
684
694
|
|
|
685
695
|
def _print_scan_summary(run_id: str, run: dict[str, Any]) -> None:
|
|
686
696
|
report = run.get("redteam_report_json")
|
|
687
|
-
anomaly_count = (
|
|
688
|
-
len(report.get("anomalies") or []) if isinstance(report, dict) else 0
|
|
689
|
-
)
|
|
697
|
+
anomaly_count = len(report.get("anomalies") or []) if isinstance(report, dict) else 0
|
|
690
698
|
print(f"Run ID: {run_id}")
|
|
691
699
|
print(f"Status: {_status_from_run(run)}")
|
|
692
700
|
print(f"URL: {run.get('frontend_url') or run.get('pr_url') or 'unknown'}")
|
|
@@ -833,8 +841,7 @@ def _print_runs_table(runs: list[dict[str, Any]]) -> None:
|
|
|
833
841
|
for run in runs
|
|
834
842
|
]
|
|
835
843
|
widths = [
|
|
836
|
-
max(len(headers[index]), max((len(row[index]) for row in rows), default=0))
|
|
837
|
-
for index in range(len(headers))
|
|
844
|
+
max(len(headers[index]), max((len(row[index]) for row in rows), default=0)) for index in range(len(headers))
|
|
838
845
|
]
|
|
839
846
|
|
|
840
847
|
def format_row(values: list[str]) -> str:
|
|
@@ -1068,8 +1075,6 @@ def run_validate_cli(argv: list[str]) -> int:
|
|
|
1068
1075
|
|
|
1069
1076
|
if argv[0] == "pr":
|
|
1070
1077
|
return run_validate_pr(_build_validate_pr_parser().parse_args(argv[1:]))
|
|
1071
|
-
if argv[0] == "url":
|
|
1072
|
-
return run_test_url(_build_validate_url_parser().parse_args(argv[1:]))
|
|
1073
1078
|
if argv[0] == "install":
|
|
1074
1079
|
return run_validate_install(_build_validate_install_parser().parse_args(argv[1:]))
|
|
1075
1080
|
if argv[0] == "config":
|
|
@@ -1152,9 +1157,7 @@ def _build_skip_commit_args(git_args: list[str]) -> tuple[list[str], str | None,
|
|
|
1152
1157
|
|
|
1153
1158
|
file_path = _extract_commit_file_path(git_args)
|
|
1154
1159
|
if file_path is None:
|
|
1155
|
-
raise CliError(
|
|
1156
|
-
"Error: `arga commit --skip` requires a commit message via `-m/--message` or `-F/--file`."
|
|
1157
|
-
)
|
|
1160
|
+
raise CliError("Error: `arga commit --skip` requires a commit message via `-m/--message` or `-F/--file`.")
|
|
1158
1161
|
|
|
1159
1162
|
if file_path == "-":
|
|
1160
1163
|
stdin_message = sys.stdin.read()
|
|
@@ -1224,26 +1227,215 @@ def run_push_cli(argv: list[str]) -> int:
|
|
|
1224
1227
|
return _run_git_command(["push", *git_args])
|
|
1225
1228
|
|
|
1226
1229
|
|
|
1227
|
-
def
|
|
1228
|
-
|
|
1230
|
+
def _wizard_help_text() -> str:
|
|
1231
|
+
return (
|
|
1232
|
+
"usage: arga wizard [command] [options]\n\n"
|
|
1233
|
+
"Commands:\n"
|
|
1234
|
+
" init Run the quickstart wizard (default)\n"
|
|
1235
|
+
" status Check twin session health\n"
|
|
1236
|
+
" reset Reset all twins to seed state\n"
|
|
1237
|
+
" extend Extend session by 10 minutes\n"
|
|
1238
|
+
" teardown Destroy session and clean up\n"
|
|
1239
|
+
" env Re-run .env rewriting step\n\n"
|
|
1240
|
+
"Options:\n"
|
|
1241
|
+
" --api-url API base URL\n"
|
|
1242
|
+
" --no-shape-detect Disable heuristic detection of API keys by value pattern\n"
|
|
1243
|
+
" -h, --help Show this help"
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def _build_wizard_init_parser() -> argparse.ArgumentParser:
|
|
1248
|
+
parser = argparse.ArgumentParser(prog="arga wizard", allow_abbrev=False)
|
|
1249
|
+
parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1250
|
+
parser.add_argument("--no-shape-detect", action="store_true", default=False)
|
|
1251
|
+
return parser
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def _build_wizard_session_parser(prog: str) -> argparse.ArgumentParser:
|
|
1255
|
+
parser = argparse.ArgumentParser(prog=prog, allow_abbrev=False)
|
|
1256
|
+
parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1257
|
+
return parser
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def run_wizard_init(args: argparse.Namespace) -> int:
|
|
1261
|
+
"""Run the full quickstart wizard natively."""
|
|
1262
|
+
from arga_cli.wizard import run_wizard
|
|
1263
|
+
|
|
1229
1264
|
try:
|
|
1230
1265
|
api_key = load_api_key()
|
|
1231
1266
|
except (NotAuthenticatedError, CliError):
|
|
1232
|
-
|
|
1267
|
+
api_key = None
|
|
1268
|
+
|
|
1269
|
+
return run_wizard(
|
|
1270
|
+
api_url=args.api_url,
|
|
1271
|
+
api_key=api_key,
|
|
1272
|
+
cwd=os.getcwd(),
|
|
1273
|
+
shape_detect=not getattr(args, "no_shape_detect", False),
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def run_wizard_status(_args: argparse.Namespace) -> int:
|
|
1278
|
+
from arga_cli.wizard.constants import TWIN_CATALOG
|
|
1279
|
+
from arga_cli.wizard.output import print_summary_box
|
|
1280
|
+
from arga_cli.wizard.provision import with_proxy_token
|
|
1281
|
+
from arga_cli.wizard.session import load_session
|
|
1282
|
+
|
|
1283
|
+
session = load_session(os.getcwd())
|
|
1284
|
+
client = ApiClient(session["api_url"], api_key=session["api_key"])
|
|
1285
|
+
try:
|
|
1286
|
+
response = client._client.get(
|
|
1287
|
+
f"{client._api_url}/validate/twins/provision/{session['run_id']}/status",
|
|
1288
|
+
headers=client._auth_headers(),
|
|
1289
|
+
)
|
|
1290
|
+
status = client._parse_json(response, "Failed to get status")
|
|
1291
|
+
finally:
|
|
1292
|
+
client.close()
|
|
1293
|
+
|
|
1294
|
+
lines = [
|
|
1295
|
+
"[bold]Twin Session Status[/bold]",
|
|
1296
|
+
"",
|
|
1297
|
+
f"Run ID: {status['run_id']}",
|
|
1298
|
+
f"Status: {'[green]' + status['status'] + '[/green]' if status['status'] == 'ready' else '[yellow]' + status['status'] + '[/yellow]'}",
|
|
1299
|
+
"",
|
|
1300
|
+
]
|
|
1301
|
+
for name, info in status.get("twins", {}).items():
|
|
1302
|
+
label = TWIN_CATALOG.get(name, {}).get("label", name).ljust(16)
|
|
1303
|
+
url = with_proxy_token(info.get("base_url", ""), status.get("proxy_token"))
|
|
1304
|
+
lines.append(f"{label} [underline]{url}[/underline]")
|
|
1305
|
+
if status.get("expires_at"):
|
|
1306
|
+
lines.append("")
|
|
1307
|
+
lines.append(f"Expires: {status['expires_at']}")
|
|
1308
|
+
print_summary_box(lines)
|
|
1309
|
+
return 0
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def run_wizard_reset(_args: argparse.Namespace) -> int:
|
|
1313
|
+
from arga_cli.wizard.constants import TWIN_CATALOG
|
|
1314
|
+
from arga_cli.wizard.output import console, green, header
|
|
1315
|
+
from arga_cli.wizard.provision import with_proxy_token
|
|
1316
|
+
from arga_cli.wizard.session import load_session
|
|
1317
|
+
|
|
1318
|
+
session = load_session(os.getcwd())
|
|
1319
|
+
client = ApiClient(session["api_url"], api_key=session["api_key"])
|
|
1320
|
+
header("Resetting all twins...")
|
|
1321
|
+
|
|
1322
|
+
twins = session.get("twins", {})
|
|
1323
|
+
proxy_token = session.get("proxy_token")
|
|
1324
|
+
|
|
1325
|
+
# Refresh from API if possible
|
|
1326
|
+
try:
|
|
1327
|
+
response = client._client.get(
|
|
1328
|
+
f"{client._api_url}/validate/twins/provision/{session['run_id']}/status",
|
|
1329
|
+
headers=client._auth_headers(),
|
|
1330
|
+
)
|
|
1331
|
+
status = client._parse_json(response, "Failed to get status")
|
|
1332
|
+
if status.get("status") == "ready":
|
|
1333
|
+
twins = {
|
|
1334
|
+
name: {"base_url": info.get("base_url", ""), "admin_url": info.get("admin_url", "")}
|
|
1335
|
+
for name, info in status.get("twins", {}).items()
|
|
1336
|
+
}
|
|
1337
|
+
proxy_token = status.get("proxy_token", proxy_token)
|
|
1338
|
+
except Exception:
|
|
1339
|
+
pass
|
|
1340
|
+
|
|
1341
|
+
for name, twin in twins.items():
|
|
1342
|
+
label = TWIN_CATALOG.get(name, {}).get("label", name)
|
|
1343
|
+
try:
|
|
1344
|
+
reset_url = with_proxy_token(f"{twin['admin_url']}/admin/reset", proxy_token)
|
|
1345
|
+
client._client.post(reset_url, json={}, headers={"Content-Type": "application/json"})
|
|
1346
|
+
console.print(f" {label}: [green]reset[/green]")
|
|
1347
|
+
except Exception as exc:
|
|
1348
|
+
console.print(f" {label}: [red]failed \u2014 {exc}[/red]")
|
|
1349
|
+
|
|
1350
|
+
client.close()
|
|
1351
|
+
green("\nDone.")
|
|
1352
|
+
return 0
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def run_wizard_extend(_args: argparse.Namespace) -> int:
|
|
1356
|
+
from arga_cli.wizard.output import error, green
|
|
1357
|
+
from arga_cli.wizard.session import load_session
|
|
1358
|
+
|
|
1359
|
+
session = load_session(os.getcwd())
|
|
1360
|
+
client = ApiClient(session["api_url"], api_key=session["api_key"])
|
|
1361
|
+
try:
|
|
1362
|
+
response = client._client.post(
|
|
1363
|
+
f"{client._api_url}/validate/twins/provision/{session['run_id']}/extend",
|
|
1364
|
+
json={"ttl_minutes": 10},
|
|
1365
|
+
headers=client._auth_headers(),
|
|
1366
|
+
)
|
|
1367
|
+
client._parse_json(response, "Failed to extend session")
|
|
1368
|
+
green("\nSession extended by 10 minutes.")
|
|
1369
|
+
except Exception as exc:
|
|
1370
|
+
error(f"Failed to extend: {exc}")
|
|
1233
1371
|
return 1
|
|
1372
|
+
finally:
|
|
1373
|
+
client.close()
|
|
1374
|
+
return 0
|
|
1375
|
+
|
|
1234
1376
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
cmd.extend(["--api-url", args.api_url])
|
|
1377
|
+
def run_wizard_teardown(_args: argparse.Namespace) -> int:
|
|
1378
|
+
from arga_cli.wizard.output import error, green
|
|
1379
|
+
from arga_cli.wizard.session import delete_session, load_session
|
|
1239
1380
|
|
|
1381
|
+
session = load_session(os.getcwd())
|
|
1382
|
+
client = ApiClient(session["api_url"], api_key=session["api_key"])
|
|
1240
1383
|
try:
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1384
|
+
response = client._client.post(
|
|
1385
|
+
f"{client._api_url}/validate/{session['run_id']}/cancel",
|
|
1386
|
+
headers=client._auth_headers(),
|
|
1387
|
+
)
|
|
1388
|
+
client._parse_json(response, "Failed to cancel run")
|
|
1389
|
+
delete_session(os.getcwd())
|
|
1390
|
+
green("\nSession destroyed. .arga-session.json removed.")
|
|
1391
|
+
except Exception as exc:
|
|
1392
|
+
error(f"Failed to teardown: {exc}")
|
|
1246
1393
|
return 1
|
|
1394
|
+
finally:
|
|
1395
|
+
client.close()
|
|
1396
|
+
return 0
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
def run_wizard_env(args: argparse.Namespace) -> int:
|
|
1400
|
+
from arga_cli.wizard.env import rewrite_env_files
|
|
1401
|
+
from arga_cli.wizard.prompts import select_twins
|
|
1402
|
+
|
|
1403
|
+
selected = select_twins()
|
|
1404
|
+
if not selected:
|
|
1405
|
+
return 1
|
|
1406
|
+
rewrite_env_files(
|
|
1407
|
+
os.getcwd(),
|
|
1408
|
+
selected,
|
|
1409
|
+
shape_detect=not getattr(args, "no_shape_detect", False),
|
|
1410
|
+
)
|
|
1411
|
+
return 0
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def run_wizard_cli(argv: list[str]) -> int:
|
|
1415
|
+
if not argv or argv[0] in {"-h", "--help"}:
|
|
1416
|
+
print(_wizard_help_text())
|
|
1417
|
+
return 0
|
|
1418
|
+
|
|
1419
|
+
command = argv[0]
|
|
1420
|
+
|
|
1421
|
+
if command == "init":
|
|
1422
|
+
return run_wizard_init(_build_wizard_init_parser().parse_args(argv[1:]))
|
|
1423
|
+
if command == "status":
|
|
1424
|
+
return run_wizard_status(_build_wizard_session_parser("arga wizard status").parse_args(argv[1:]))
|
|
1425
|
+
if command == "reset":
|
|
1426
|
+
return run_wizard_reset(_build_wizard_session_parser("arga wizard reset").parse_args(argv[1:]))
|
|
1427
|
+
if command == "extend":
|
|
1428
|
+
return run_wizard_extend(_build_wizard_session_parser("arga wizard extend").parse_args(argv[1:]))
|
|
1429
|
+
if command == "teardown":
|
|
1430
|
+
return run_wizard_teardown(_build_wizard_session_parser("arga wizard teardown").parse_args(argv[1:]))
|
|
1431
|
+
if command == "env":
|
|
1432
|
+
return run_wizard_env(_build_wizard_init_parser().parse_args(argv[1:]))
|
|
1433
|
+
|
|
1434
|
+
# No recognized subcommand — treat everything as flags for `init`
|
|
1435
|
+
if command.startswith("-"):
|
|
1436
|
+
return run_wizard_init(_build_wizard_init_parser().parse_args(argv))
|
|
1437
|
+
|
|
1438
|
+
raise CliError(f"Unknown wizard subcommand: {command}")
|
|
1247
1439
|
|
|
1248
1440
|
|
|
1249
1441
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -1276,6 +1468,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1276
1468
|
test_url_parser.add_argument("--prompt", required=True, help="Natural language instructions for the agent")
|
|
1277
1469
|
test_url_parser.add_argument("--email", help="Optional login email")
|
|
1278
1470
|
test_url_parser.add_argument("--password", help="Optional login password")
|
|
1471
|
+
test_url_parser.add_argument(
|
|
1472
|
+
"--ttl",
|
|
1473
|
+
type=int,
|
|
1474
|
+
default=None,
|
|
1475
|
+
help="Run duration in minutes (Team/Paid: 1-480, default 30; Free: fixed at 10)",
|
|
1476
|
+
)
|
|
1279
1477
|
test_url_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1280
1478
|
test_url_parser.set_defaults(func=run_test_url)
|
|
1281
1479
|
|
|
@@ -1289,18 +1487,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1289
1487
|
validate_pr_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1290
1488
|
validate_pr_parser.set_defaults(func=run_validate_pr)
|
|
1291
1489
|
|
|
1292
|
-
validate_url_parser = validate_subparsers.add_parser(
|
|
1293
|
-
"url",
|
|
1294
|
-
help="Run a browser validation against a deployed URL",
|
|
1295
|
-
)
|
|
1296
|
-
validate_url_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1297
|
-
validate_url_parser.add_argument("--url", required=True, help="Deployed application URL")
|
|
1298
|
-
validate_url_parser.add_argument("--prompt", required=True, help="Natural language instructions for the agent")
|
|
1299
|
-
validate_url_parser.add_argument("--email", help="Optional login email")
|
|
1300
|
-
validate_url_parser.add_argument("--password", help="Optional login password")
|
|
1301
|
-
validate_url_parser.add_argument("--json", action="store_true", default=False, help="Output result as JSON")
|
|
1302
|
-
validate_url_parser.set_defaults(func=run_test_url)
|
|
1303
|
-
|
|
1304
1490
|
mcp_parser = subparsers.add_parser("mcp", help="Manage MCP integrations")
|
|
1305
1491
|
mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True)
|
|
1306
1492
|
|
|
@@ -1352,9 +1538,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1352
1538
|
runs_cancel_parser.add_argument("run_id", help="Validation run ID")
|
|
1353
1539
|
runs_cancel_parser.set_defaults(func=run_runs_cancel)
|
|
1354
1540
|
|
|
1355
|
-
|
|
1356
|
-
wizard_parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Arga API base URL")
|
|
1357
|
-
wizard_parser.set_defaults(func=run_wizard)
|
|
1541
|
+
subparsers.add_parser("wizard", help="Twins quickstart wizard (run `arga wizard --help` for subcommands)")
|
|
1358
1542
|
|
|
1359
1543
|
subparsers.add_parser("commit", help="Wrap git commit and optionally mark it to skip Arga validation")
|
|
1360
1544
|
subparsers.add_parser("push", help="Wrap git push and verify skip state when requested")
|
|
@@ -1372,6 +1556,8 @@ def main() -> None:
|
|
|
1372
1556
|
exit_code = run_validate_cli(sys.argv[2:])
|
|
1373
1557
|
elif len(sys.argv) > 1 and sys.argv[1] == "scan":
|
|
1374
1558
|
exit_code = run_scan_cli(sys.argv[2:])
|
|
1559
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "wizard":
|
|
1560
|
+
exit_code = run_wizard_cli(sys.argv[2:])
|
|
1375
1561
|
else:
|
|
1376
1562
|
parser = build_parser()
|
|
1377
1563
|
args = parser.parse_args()
|