veris-cli 2.23.0__tar.gz → 2.25.0__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 (28) hide show
  1. {veris_cli-2.23.0 → veris_cli-2.25.0}/PKG-INFO +1 -1
  2. {veris_cli-2.23.0 → veris_cli-2.25.0}/pyproject.toml +1 -1
  3. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/api.py +28 -1
  4. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/build_context.py +17 -12
  5. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/env.py +260 -11
  6. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/simulations.py +7 -0
  7. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/config.py +10 -0
  8. {veris_cli-2.23.0 → veris_cli-2.25.0}/.gitignore +0 -0
  9. {veris_cli-2.23.0 → veris_cli-2.25.0}/README.md +0 -0
  10. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/__init__.py +0 -0
  11. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/cli.py +0 -0
  12. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/__init__.py +0 -0
  13. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/_helpers.py +0 -0
  14. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/auth.py +0 -0
  15. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/evaluations.py +0 -0
  16. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/profile.py +0 -0
  17. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/reports.py +0 -0
  18. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/run.py +0 -0
  19. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/commands/scenarios.py +0 -0
  20. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/output.py +0 -0
  21. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/prompts.py +0 -0
  22. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/run_output.py +0 -0
  23. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/scripts/__init__.py +0 -0
  24. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/scripts/docker_build.sh +0 -0
  25. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/scripts/docker_push.sh +0 -0
  26. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/searchable_checkbox.py +0 -0
  27. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/templates.py +0 -0
  28. {veris_cli-2.23.0 → veris_cli-2.25.0}/src/veris_cli/veris_yaml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-cli
3
- Version: 2.23.0
3
+ Version: 2.25.0
4
4
  Summary: CLI to connect local agents to the Veris backend
5
5
  Project-URL: Homepage, https://github.com/veris-ai/veris-cli
6
6
  Project-URL: Bug Tracker, https://github.com/veris-ai/veris-cli/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "veris-cli"
3
- version = "2.23.0"
3
+ version = "2.25.0"
4
4
  description = "CLI to connect local agents to the Veris backend"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -212,12 +212,13 @@ class VerisAPI:
212
212
  scenario_set_id: str,
213
213
  environment_id: str,
214
214
  config: Optional[dict] = None,
215
+ auto_evaluate: bool = False,
215
216
  ) -> dict[str, Any]:
216
217
  """Create a new run."""
217
218
  payload: dict[str, Any] = {
218
219
  "scenario_set_id": scenario_set_id,
219
220
  "environment_id": environment_id,
220
- "auto_evaluate": False,
221
+ "auto_evaluate": auto_evaluate,
221
222
  }
222
223
  if config:
223
224
  payload["config"] = config
@@ -431,6 +432,32 @@ class VerisAPI:
431
432
  _raise_for_status(response)
432
433
  return response.content
433
434
 
435
+ # Managed Setup
436
+ def create_submit(self, environment_id: str) -> dict[str, Any]:
437
+ """Get a signed upload URL for submitting agent source code."""
438
+ with httpx.Client(
439
+ base_url=self.base_url, headers=self._headers(), timeout=self.DEFAULT_TIMEOUT
440
+ ) as client:
441
+ response = client.post(f"/v1/environments/{environment_id}/submit")
442
+ _raise_for_status(response)
443
+ return response.json()
444
+
445
+ def notify_submit_uploaded(self, environment_id: str) -> dict[str, Any]:
446
+ """Confirm that the submit tarball has been uploaded."""
447
+ with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=30) as client:
448
+ response = client.post(f"/v1/environments/{environment_id}/submit/uploaded")
449
+ _raise_for_status(response)
450
+ return response.json()
451
+
452
+ def get_managed_config(self, environment_id: str) -> dict[str, Any]:
453
+ """Get managed config files (signed download URLs) for an environment."""
454
+ with httpx.Client(
455
+ base_url=self.base_url, headers=self._headers(), timeout=self.DEFAULT_TIMEOUT
456
+ ) as client:
457
+ response = client.get(f"/v1/environments/{environment_id}/managed-config")
458
+ _raise_for_status(response)
459
+ return response.json()
460
+
434
461
  # Sandbox metadata
435
462
  def list_services(self) -> dict[str, Any]:
436
463
  """List available sandbox mock services."""
@@ -31,6 +31,7 @@ def create_build_context(
31
31
  project_root: Path,
32
32
  output_path: Path | None = None,
33
33
  dockerfile: Path | str | None = None,
34
+ require_dockerfile: bool = True,
34
35
  ) -> tuple[Path, int]:
35
36
  """Create a tar.gz build context from the project root.
36
37
 
@@ -43,6 +44,8 @@ def create_build_context(
43
44
  output_path: Where to write the tarball. If None, uses a tempfile.
44
45
  dockerfile: Dockerfile path (absolute or relative to project_root). Defaults
45
46
  to `.veris/Dockerfile.sandbox` when None.
47
+ require_dockerfile: If False, skip Dockerfile validation and inclusion.
48
+ Used by ``env submit`` where no Dockerfile exists yet.
46
49
 
47
50
  Returns:
48
51
  Tuple of (tarball_path, size_bytes).
@@ -57,18 +60,20 @@ def create_build_context(
57
60
  os.close(fd)
58
61
  output_path = Path(tmp)
59
62
 
60
- if dockerfile is None:
61
- dockerfile_path = project_root / ".veris" / "Dockerfile.sandbox"
62
- dockerfile_label = ".veris/Dockerfile.sandbox"
63
- else:
64
- df = Path(dockerfile)
65
- dockerfile_path = df if df.is_absolute() else project_root / df
66
- dockerfile_label = str(dockerfile)
67
- if not dockerfile_path.exists():
68
- raise ValueError(f"{dockerfile_label} not found")
63
+ dockerfile_rel: str | None = None
64
+ if require_dockerfile:
65
+ if dockerfile is None:
66
+ dockerfile_path = project_root / ".veris" / "Dockerfile.sandbox"
67
+ dockerfile_label = ".veris/Dockerfile.sandbox"
68
+ else:
69
+ df = Path(dockerfile)
70
+ dockerfile_path = df if df.is_absolute() else project_root / df
71
+ dockerfile_label = str(dockerfile)
72
+ if not dockerfile_path.exists():
73
+ raise ValueError(f"{dockerfile_label} not found")
74
+ dockerfile_rel = str(dockerfile_path.relative_to(project_root))
69
75
 
70
76
  file_count = 0
71
- dockerfile_rel = str(dockerfile_path.relative_to(project_root))
72
77
  dockerfile_added = False
73
78
 
74
79
  with tarfile.open(output_path, "w:gz") as tar:
@@ -99,11 +104,11 @@ def create_build_context(
99
104
  full_path = Path(root) / f
100
105
  tar.add(str(full_path), arcname=rel_path)
101
106
  file_count += 1
102
- if rel_path == dockerfile_rel:
107
+ if dockerfile_rel and rel_path == dockerfile_rel:
103
108
  dockerfile_added = True
104
109
 
105
110
  # If the Dockerfile wasn't added (e.g. its dir was ignored), add it explicitly
106
- if not dockerfile_added:
111
+ if require_dockerfile and not dockerfile_added and dockerfile_rel:
107
112
  tar.add(str(dockerfile_path), arcname=dockerfile_rel)
108
113
  file_count += 1
109
114
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import sys
4
4
  import time
5
+ from datetime import UTC, datetime
5
6
  from pathlib import Path
6
7
 
7
8
  import click
@@ -364,8 +365,9 @@ def env_create(ctx, name: str | None, agent_name: str | None):
364
365
  if unconfigured:
365
366
  import questionary
366
367
 
368
+ _NEW_TARGET = "__new__"
367
369
  choices = [questionary.Choice(title=t, value=t) for t in unconfigured] + [
368
- questionary.Choice(title="Create a new target", value=None),
370
+ questionary.Choice(title="Create a new target", value=_NEW_TARGET),
369
371
  ]
370
372
  selected = questionary.select(
371
373
  "Existing targets found in veris.yaml. Set up environment for:",
@@ -373,7 +375,7 @@ def env_create(ctx, name: str | None, agent_name: str | None):
373
375
  ).ask()
374
376
  if selected is None:
375
377
  return
376
- if selected:
378
+ if selected and selected != _NEW_TARGET:
377
379
  name = selected
378
380
  # Pull the agent name from the target block
379
381
  t_obj = parsed.get_target(selected)
@@ -444,9 +446,12 @@ def env_create(ctx, name: str | None, agent_name: str | None):
444
446
  output.print_success(f"Environment created: {env_id}")
445
447
  output.print_info(f"Environment ID saved to .veris/config.yaml under target '{env_name}'")
446
448
  output.print_info("\nNext steps:")
447
- output.print_info(" 1. Edit .veris/Dockerfile.sandbox to configure your agent")
448
- output.print_info(" 2. Review .veris/veris.yaml settings")
449
- output.print_info(" 3. Run 'veris env push' to build and push your agent")
449
+ output.print_info(" Option A Self-serve:")
450
+ output.print_info(" 1. Edit .veris/Dockerfile.sandbox and .veris/veris.yaml")
451
+ output.print_info(" 2. Run 'veris env push' to build and push your agent")
452
+ output.print_info(" Option B — Let Veris set it up:")
453
+ output.print_info(" 1. Run 'veris env submit' to upload your code")
454
+ output.print_info(" 2. We'll email you when your environment is ready")
450
455
 
451
456
  except ValueError as e:
452
457
  output.print_error(str(e))
@@ -645,6 +650,83 @@ def _resolve_or_create_cross_profile(
645
650
  return None
646
651
 
647
652
 
653
+ @env.command(name="submit")
654
+ @click.option("--env-id", default=None, help="Environment ID (overrides config)")
655
+ @target_option
656
+ @click.pass_context
657
+ def env_submit(ctx, env_id: str | None, target: str | None):
658
+ """Submit your repo for managed environment setup.
659
+
660
+ Packages your project into a tarball and uploads it to Veris. The team
661
+ will generate your Dockerfile.sandbox and veris.yaml. You'll receive an
662
+ email when the setup is complete.
663
+
664
+ \b
665
+ After receiving the email:
666
+ veris env config pull # Pull generated config files
667
+ veris env push # Build and push your agent
668
+ """
669
+ profile = get_profile(ctx)
670
+ resolved_target = resolve_target(target, profile)
671
+
672
+ # Prompt for target selection if multiple targets and none resolved
673
+ if not resolved_target and not env_id and sys.stdin.isatty():
674
+ veris_yaml_path = Path.cwd() / ".veris" / "veris.yaml"
675
+ parsed = _try_load_veris_yaml(veris_yaml_path)
676
+ if parsed and len(parsed.target_names) > 1:
677
+ import questionary
678
+
679
+ resolved_target = questionary.select(
680
+ "Multiple targets found. Submit which one?",
681
+ choices=parsed.target_names,
682
+ ).ask()
683
+ if not resolved_target:
684
+ return
685
+
686
+ env_id = resolve_env_id(profile, env_id, resolved_target)
687
+
688
+ tarball_path = None
689
+ try:
690
+ from veris_cli.build_context import create_build_context
691
+
692
+ output.print_info("Packaging project...")
693
+ tarball_path, size = create_build_context(Path.cwd(), require_dockerfile=False)
694
+ size_mb = size / 1024 / 1024
695
+ output.print_success(f"Package size: {size_mb:.1f} MB")
696
+
697
+ api = VerisAPI(profile=profile)
698
+ submit_result = api.create_submit(env_id)
699
+ upload_url = submit_result["upload_url"]
700
+
701
+ output.print_info("Uploading...")
702
+ with open(tarball_path, "rb") as f:
703
+ upload_resp = httpx.put(
704
+ upload_url,
705
+ content=f,
706
+ headers={"Content-Type": "application/gzip"},
707
+ timeout=300,
708
+ )
709
+ if upload_resp.status_code not in (200, 201):
710
+ output.print_error(f"Upload failed: {upload_resp.status_code}")
711
+ sys.exit(1)
712
+ output.print_success("Upload complete")
713
+
714
+ api.notify_submit_uploaded(env_id)
715
+ output.print_success("Submitted for managed setup!")
716
+ output.print_info("You'll receive an email when your environment is ready.")
717
+ output.print_info("Then run 'veris env push' to build and deploy.")
718
+
719
+ except ValueError as e:
720
+ output.print_error(str(e))
721
+ sys.exit(1)
722
+ except Exception as e:
723
+ output.print_error(f"Submit failed: {e}")
724
+ sys.exit(1)
725
+ finally:
726
+ if tarball_path:
727
+ tarball_path.unlink(missing_ok=True)
728
+
729
+
648
730
  @env.command(name="push")
649
731
  @click.option("--tag", default="latest", help="Image tag (default: latest)")
650
732
  @click.option("--env-id", default=None, help="Environment ID (overrides config)")
@@ -674,6 +756,16 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
674
756
  parsed = _load_veris_yaml(veris_yaml_path)
675
757
  resolved_target = resolve_target(target, profile, parsed_veris_yaml=parsed)
676
758
 
759
+ if not resolved_target and not env_id and sys.stdin.isatty() and len(parsed.target_names) > 1:
760
+ import questionary
761
+
762
+ resolved_target = questionary.select(
763
+ "Multiple targets found. Push which one?",
764
+ choices=parsed.target_names,
765
+ ).ask()
766
+ if not resolved_target:
767
+ return
768
+
677
769
  try:
678
770
  target_obj = parsed.get_target(resolved_target)
679
771
  except KeyError as e:
@@ -682,9 +774,6 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
682
774
 
683
775
  dockerfile_rel = target_obj.resolved_dockerfile()
684
776
  dockerfile_path = Path.cwd() / dockerfile_rel
685
- if not dockerfile_path.exists():
686
- output.print_error(f"{dockerfile_rel} not found. Run 'veris env create' first.")
687
- sys.exit(1)
688
777
 
689
778
  if not env_id and not target:
690
779
  env_id = _migrate_legacy_config(profile, resolved_target, parsed)
@@ -693,9 +782,59 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
693
782
  if not env_id:
694
783
  env_id = resolve_env_id(profile, env_id, resolved_target)
695
784
 
785
+ api = VerisAPI(profile=profile)
786
+
787
+ if not dockerfile_path.exists():
788
+ output.print_error(
789
+ f"{dockerfile_rel} not found. Run 'veris env create' or 'veris env config pull' first."
790
+ )
791
+ sys.exit(1)
792
+
793
+ # Config sync guard: check if remote config was updated since last sync.
794
+ # Prevents accidentally overwriting team-generated config with local scaffold.
795
+ try:
796
+ env_info = api.get_environment(env_id)
797
+ except Exception:
798
+ env_info = {}
799
+
800
+ remote_config_at = env_info.get("config_updated_at")
801
+ pc = ProjectConfig(profile=profile, target=resolved_target)
802
+ local_synced_at = pc.get_config_synced_at()
803
+
804
+ if remote_config_at and (not local_synced_at or remote_config_at > local_synced_at):
805
+ if sys.stdin.isatty():
806
+ import questionary
807
+
808
+ action = questionary.select(
809
+ "Remote config was updated since your last sync.",
810
+ choices=[
811
+ questionary.Choice(title="Pull remote config (overwrites local)", value="pull"),
812
+ questionary.Choice(title="Push local config (overwrites remote)", value="push"),
813
+ questionary.Choice(title="Cancel", value="cancel"),
814
+ ],
815
+ ).ask()
816
+ if action == "cancel" or action is None:
817
+ return
818
+ if action == "pull":
819
+ ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
820
+ # Reload target config after pull (resolved_target stays the same)
821
+ parsed = _load_veris_yaml(veris_yaml_path)
822
+ target_obj = parsed.get_target(resolved_target)
823
+ dockerfile_rel = target_obj.resolved_dockerfile()
824
+ dockerfile_path = Path.cwd() / dockerfile_rel
825
+ else:
826
+ output.print_error(
827
+ "Remote config was updated since your last sync. Run 'veris env config pull' first."
828
+ )
829
+ sys.exit(1)
830
+
696
831
  try:
697
- api = VerisAPI(profile=profile)
698
832
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
833
+ # Re-fetch to get the server-assigned config_updated_at
834
+ updated_env = api.get_environment(env_id)
835
+ pc.set_config_synced_at(
836
+ updated_env.get("config_updated_at") or datetime.now(UTC).isoformat()
837
+ )
699
838
  output.print_success("Config validated and uploaded")
700
839
  except Exception as e:
701
840
  detail = getattr(e, "detail", str(e))
@@ -704,7 +843,6 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
704
843
 
705
844
  # On-prem clusters route through the customer's external registry instead of Cloud Build.
706
845
  try:
707
- env_info = api.get_environment(env_id)
708
846
  if env_info.get("image_registry_url"):
709
847
  tag_result = api.create_image_tag(env_id, tag)
710
848
  if tag_result.get("pull_credentials"):
@@ -871,9 +1009,48 @@ def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
871
1009
  output.print_error(str(e))
872
1010
  sys.exit(1)
873
1011
 
1012
+ api = VerisAPI(profile=profile)
1013
+ pc = ProjectConfig(profile=profile, target=resolved_target)
1014
+
1015
+ # Config sync guard
1016
+ try:
1017
+ env_info = api.get_environment(env_id)
1018
+ except Exception:
1019
+ env_info = {}
1020
+
1021
+ remote_config_at = env_info.get("config_updated_at")
1022
+ local_synced_at = pc.get_config_synced_at()
1023
+
1024
+ if remote_config_at and (not local_synced_at or remote_config_at > local_synced_at):
1025
+ if sys.stdin.isatty():
1026
+ import questionary
1027
+
1028
+ action = questionary.select(
1029
+ "Remote config was updated since your last sync.",
1030
+ choices=[
1031
+ questionary.Choice(title="Pull remote config (overwrites local)", value="pull"),
1032
+ questionary.Choice(title="Push local config (overwrites remote)", value="push"),
1033
+ questionary.Choice(title="Cancel", value="cancel"),
1034
+ ],
1035
+ ).ask()
1036
+ if action == "cancel" or action is None:
1037
+ return
1038
+ if action == "pull":
1039
+ # Delegate to config pull logic
1040
+ output.print_info("Pulling remote config...")
1041
+ ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
1042
+ return
1043
+ else:
1044
+ output.print_error(
1045
+ "Remote config was updated since your last sync. Run 'veris env config pull' first."
1046
+ )
1047
+ sys.exit(1)
1048
+
874
1049
  try:
875
- api = VerisAPI(profile=profile)
876
1050
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
1051
+ env_info = api.get_environment(env_id)
1052
+ config_ts = env_info.get("config_updated_at") or datetime.now(UTC).isoformat()
1053
+ pc.set_config_synced_at(config_ts)
877
1054
  output.print_success(f"Config uploaded for environment {env_id}")
878
1055
  except Exception as e:
879
1056
  detail = getattr(e, "detail", str(e))
@@ -881,6 +1058,78 @@ def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
881
1058
  sys.exit(1)
882
1059
 
883
1060
 
1061
+ @config.command(name="pull")
1062
+ @click.option("--env-id", default=None, help="Environment ID (uses project config if omitted)")
1063
+ @target_option
1064
+ @click.pass_context
1065
+ def config_pull(ctx, env_id: str | None, target: str | None):
1066
+ """Pull config files from Veris to local .veris/ directory.
1067
+
1068
+ Downloads the Dockerfile.sandbox and veris.yaml from the backend.
1069
+ Use after 'veris env submit' (managed setup) or to sync with
1070
+ remote config changes.
1071
+
1072
+ Example: veris env config pull
1073
+ """
1074
+ profile = get_profile(ctx)
1075
+ resolved_target = resolve_target(target, profile)
1076
+ env_id = resolve_env_id(profile, env_id, resolved_target)
1077
+
1078
+ try:
1079
+ api = VerisAPI(profile=profile)
1080
+ result = api.get_managed_config(env_id)
1081
+
1082
+ status = result.get("status", "created")
1083
+ if status != "ready":
1084
+ output.print_info(f"Environment status: {status}")
1085
+ if status == "pending":
1086
+ output.print_info(
1087
+ "Your environment is being set up. You'll receive an email when it's ready."
1088
+ )
1089
+ else:
1090
+ output.print_info("No remote config available. Run 'veris env submit' first.")
1091
+ return
1092
+
1093
+ veris_dir = Path.cwd() / ".veris"
1094
+ veris_dir.mkdir(parents=True, exist_ok=True)
1095
+
1096
+ dockerfile_url = result.get("dockerfile_url")
1097
+ if dockerfile_url:
1098
+ resp = httpx.get(dockerfile_url, timeout=60)
1099
+ if resp.status_code == 200:
1100
+ (veris_dir / "Dockerfile.sandbox").write_text(resp.text)
1101
+ output.print_success("Downloaded .veris/Dockerfile.sandbox")
1102
+ else:
1103
+ output.print_warning("Failed to download Dockerfile.sandbox")
1104
+
1105
+ veris_yaml_url = result.get("veris_yaml_url")
1106
+ if veris_yaml_url and resolved_target:
1107
+ resp = httpx.get(veris_yaml_url, timeout=60)
1108
+ if resp.status_code == 200:
1109
+ flat_config = yaml.safe_load(resp.text)
1110
+ # Merge into existing veris.yaml to preserve other targets
1111
+ yaml_path = veris_dir / "veris.yaml"
1112
+ if yaml_path.exists():
1113
+ existing = yaml.safe_load(yaml_path.read_text()) or {}
1114
+ else:
1115
+ existing = {"version": "1.0"}
1116
+ existing.setdefault("version", "1.0")
1117
+ existing[resolved_target] = flat_config
1118
+ yaml_path.write_text(yaml.dump(existing, default_flow_style=False, sort_keys=False))
1119
+ output.print_success("Downloaded .veris/veris.yaml")
1120
+ else:
1121
+ output.print_warning("Failed to download veris.yaml")
1122
+
1123
+ # Save sync timestamp
1124
+ env_detail = api.get_environment(env_id)
1125
+ config_ts = env_detail.get("config_updated_at") or datetime.now(UTC).isoformat()
1126
+ ProjectConfig(profile=profile, target=resolved_target).set_config_synced_at(config_ts)
1127
+
1128
+ except Exception as e:
1129
+ output.print_error(f"Failed to pull config: {e}")
1130
+ sys.exit(1)
1131
+
1132
+
884
1133
  @env.group()
885
1134
  def vars():
886
1135
  """Environment variable management."""
@@ -30,6 +30,11 @@ def simulations():
30
30
  help="Simulation timeout in seconds (60-3600)",
31
31
  )
32
32
  @click.option("--image-tag", default=None, help="Image tag to use (default: latest)")
33
+ @click.option(
34
+ "--auto-evaluate/--no-auto-evaluate",
35
+ default=True,
36
+ help="Auto-trigger evaluation on completion (default: enabled)",
37
+ )
33
38
  @click.pass_context
34
39
  def simulations_create(
35
40
  ctx,
@@ -37,6 +42,7 @@ def simulations_create(
37
42
  env_id: str,
38
43
  simulation_timeout: int,
39
44
  image_tag: str,
45
+ auto_evaluate: bool,
40
46
  ):
41
47
  """Create a new simulation run."""
42
48
  profile = get_profile(ctx)
@@ -71,6 +77,7 @@ def simulations_create(
71
77
  scenario_set_id=scenario_set_id,
72
78
  environment_id=env_id,
73
79
  config=config or None,
80
+ auto_evaluate=auto_evaluate,
74
81
  )
75
82
 
76
83
  run_id = result.get("id")
@@ -312,6 +312,16 @@ class ProjectConfig:
312
312
  raw.pop("active_target", None)
313
313
  self.save(raw)
314
314
 
315
+ def get_config_synced_at(self) -> Optional[str]:
316
+ """Get the last config sync timestamp (ISO format)."""
317
+ return self._load_scope().get("config_synced_at")
318
+
319
+ def set_config_synced_at(self, timestamp: str) -> None:
320
+ """Save the config sync timestamp (ISO format)."""
321
+ scope = self._load_scope()
322
+ scope["config_synced_at"] = timestamp
323
+ self._save_scope(scope)
324
+
315
325
  def get_ci_config(self) -> dict:
316
326
  """Get CI config block from project config (profile-level, not per-target)."""
317
327
  return self._load_profile().get("ci", {})
File without changes
File without changes