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.
Files changed (29) hide show
  1. {arga_cli-0.1.4 → arga_cli-0.1.5}/PKG-INFO +51 -10
  2. {arga_cli-0.1.4 → arga_cli-0.1.5}/README.md +48 -9
  3. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/main.py +247 -61
  4. arga_cli-0.1.5/arga_cli/wizard/__init__.py +90 -0
  5. arga_cli-0.1.5/arga_cli/wizard/constants.py +316 -0
  6. arga_cli-0.1.5/arga_cli/wizard/env.py +292 -0
  7. arga_cli-0.1.5/arga_cli/wizard/output.py +48 -0
  8. arga_cli-0.1.5/arga_cli/wizard/prompts.py +140 -0
  9. arga_cli-0.1.5/arga_cli/wizard/provision.py +162 -0
  10. arga_cli-0.1.5/arga_cli/wizard/session.py +28 -0
  11. arga_cli-0.1.5/arga_cli/wizard/summary.py +60 -0
  12. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/PKG-INFO +51 -10
  13. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/SOURCES.txt +8 -0
  14. arga_cli-0.1.5/arga_cli.egg-info/requires.txt +3 -0
  15. {arga_cli-0.1.4 → arga_cli-0.1.5}/pyproject.toml +3 -1
  16. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_validate_pr.py +1 -27
  17. arga_cli-0.1.4/arga_cli.egg-info/requires.txt +0 -1
  18. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/__init__.py +0 -0
  19. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli/mcp.py +0 -0
  20. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/dependency_links.txt +0 -0
  21. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/entry_points.txt +0 -0
  22. {arga_cli-0.1.4 → arga_cli-0.1.5}/arga_cli.egg-info/top_level.txt +0 -0
  23. {arga_cli-0.1.4 → arga_cli-0.1.5}/setup.cfg +0 -0
  24. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_git.py +0 -0
  25. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_mcp.py +0 -0
  26. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_runs.py +0 -0
  27. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_scan.py +0 -0
  28. {arga_cli-0.1.4 → arga_cli-0.1.5}/tests/test_cli_test_url.py +0 -0
  29. {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.4
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}\n")
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 run_wizard(args: argparse.Namespace) -> int:
1228
- """Launch the arga-wizard npm package, passing the stored API key."""
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
- print("Not logged in. Run `arga login` first, or use `npx arga-wizard` directly.")
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
- cmd: list[str] = ["npx", "arga-wizard"]
1236
- cmd.extend(["--api-key", api_key])
1237
- if args.api_url != DEFAULT_API_URL:
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
- result = subprocess.run(cmd, check=False)
1242
- return result.returncode
1243
- except FileNotFoundError:
1244
- print("Node.js is required for the wizard. Install it from https://nodejs.org")
1245
- print("Or run the wizard directly: npx arga-wizard")
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
- wizard_parser = subparsers.add_parser("wizard", help="Launch the twins quickstart wizard")
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()