veris-cli 2.25.1__tar.gz → 2.25.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.
Files changed (28) hide show
  1. {veris_cli-2.25.1 → veris_cli-2.25.2}/PKG-INFO +2 -1
  2. {veris_cli-2.25.1 → veris_cli-2.25.2}/pyproject.toml +2 -1
  3. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/api.py +47 -0
  4. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/env.py +124 -81
  5. {veris_cli-2.25.1 → veris_cli-2.25.2}/.gitignore +0 -0
  6. {veris_cli-2.25.1 → veris_cli-2.25.2}/README.md +0 -0
  7. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/__init__.py +0 -0
  8. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/build_context.py +0 -0
  9. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/cli.py +0 -0
  10. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/__init__.py +0 -0
  11. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/_helpers.py +0 -0
  12. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/auth.py +0 -0
  13. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/evaluations.py +0 -0
  14. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/profile.py +0 -0
  15. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/reports.py +0 -0
  16. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/run.py +0 -0
  17. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/scenarios.py +0 -0
  18. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/commands/simulations.py +0 -0
  19. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/config.py +0 -0
  20. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/output.py +0 -0
  21. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/prompts.py +0 -0
  22. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/run_output.py +0 -0
  23. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/__init__.py +0 -0
  24. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_build.sh +0 -0
  25. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_push.sh +0 -0
  26. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/searchable_checkbox.py +0 -0
  27. {veris_cli-2.25.1 → veris_cli-2.25.2}/src/veris_cli/templates.py +0 -0
  28. {veris_cli-2.25.1 → veris_cli-2.25.2}/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.25.1
3
+ Version: 2.25.2
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
@@ -13,6 +13,7 @@ Requires-Dist: pydantic>=2.6
13
13
  Requires-Dist: pyyaml>=6.0.1
14
14
  Requires-Dist: questionary>=2.0.0
15
15
  Requires-Dist: rich>=13.7.0
16
+ Requires-Dist: tenacity>=8.2.0
16
17
  Provides-Extra: dev
17
18
  Requires-Dist: pre-commit>=4.3.0; extra == 'dev'
18
19
  Provides-Extra: test
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "veris-cli"
3
- version = "2.25.1"
3
+ version = "2.25.2"
4
4
  description = "CLI to connect local agents to the Veris backend"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "questionary>=2.0.0",
16
16
  "pyyaml>=6.0.1",
17
17
  "pydantic>=2.6",
18
+ "tenacity>=8.2.0",
18
19
  ]
19
20
 
20
21
  [project.scripts]
@@ -3,6 +3,13 @@
3
3
  from typing import Any, Optional
4
4
 
5
5
  import httpx
6
+ from tenacity import (
7
+ retry,
8
+ retry_if_exception,
9
+ retry_if_exception_type,
10
+ stop_after_attempt,
11
+ wait_exponential,
12
+ )
6
13
 
7
14
  from veris_cli.config import Config
8
15
 
@@ -29,6 +36,26 @@ def _raise_for_status(response: httpx.Response) -> None:
29
36
  raise APIError(response.status_code, detail, str(response.url))
30
37
 
31
38
 
39
+ _RETRYABLE_STATUSES = frozenset({502, 503, 504})
40
+
41
+
42
+ def _is_transient_api_error(exc: BaseException) -> bool:
43
+ return isinstance(exc, APIError) and exc.status_code in _RETRYABLE_STATUSES
44
+
45
+
46
+ # Retries transient failures (network errors, 502/503/504). Only applied to
47
+ # idempotent reads — mutating calls (POST/PUT/DELETE) are left alone to avoid
48
+ # duplicate side effects on retry.
49
+ _retry_transient = retry(
50
+ reraise=True,
51
+ stop=stop_after_attempt(5),
52
+ wait=wait_exponential(multiplier=1, min=1, max=10),
53
+ retry=(
54
+ retry_if_exception_type(httpx.TransportError) | retry_if_exception(_is_transient_api_error)
55
+ ),
56
+ )
57
+
58
+
32
59
  class VerisAPI:
33
60
  """Simple HTTP client for Veris backend API."""
34
61
 
@@ -66,6 +93,7 @@ class VerisAPI:
66
93
  _raise_for_status(response)
67
94
  return response.json()
68
95
 
96
+ @_retry_transient
69
97
  def list_environments(
70
98
  self, status: Optional[str] = None, limit: int = 20, offset: int = 0
71
99
  ) -> dict[str, Any]:
@@ -81,6 +109,7 @@ class VerisAPI:
81
109
  _raise_for_status(response)
82
110
  return response.json()
83
111
 
112
+ @_retry_transient
84
113
  def get_environment(self, environment_id: str) -> dict[str, Any]:
85
114
  """Get environment details."""
86
115
  with httpx.Client(
@@ -124,6 +153,7 @@ class VerisAPI:
124
153
  _raise_for_status(response)
125
154
  return response.json()
126
155
 
156
+ @_retry_transient
127
157
  def get_build_status(self, environment_id: str, build_id: str) -> dict[str, Any]:
128
158
  """Poll build status."""
129
159
  with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=30) as client:
@@ -133,6 +163,7 @@ class VerisAPI:
133
163
  _raise_for_status(response)
134
164
  return response.json()
135
165
 
166
+ @_retry_transient
136
167
  def get_build_logs(
137
168
  self, environment_id: str, build_id: str, cursor: str | None = None
138
169
  ) -> dict[str, Any]:
@@ -169,6 +200,7 @@ class VerisAPI:
169
200
  _raise_for_status(response)
170
201
 
171
202
  # Scenario Sets
203
+ @_retry_transient
172
204
  def list_scenario_sets(
173
205
  self, environment_id: Optional[str] = None, limit: int = 100, skip: int = 0
174
206
  ) -> list[dict[str, Any]]:
@@ -189,6 +221,7 @@ class VerisAPI:
189
221
  else data
190
222
  )
191
223
 
224
+ @_retry_transient
192
225
  def get_scenario_set(self, set_id: str) -> dict[str, Any]:
193
226
  """Get scenario set details."""
194
227
  with httpx.Client(
@@ -232,6 +265,7 @@ class VerisAPI:
232
265
  _raise_for_status(response)
233
266
  return response.json()
234
267
 
268
+ @_retry_transient
235
269
  def list_runs(
236
270
  self,
237
271
  status: Optional[str] = None,
@@ -253,6 +287,7 @@ class VerisAPI:
253
287
  _raise_for_status(response)
254
288
  return response.json()
255
289
 
290
+ @_retry_transient
256
291
  def get_run(self, run_id: str) -> dict[str, Any]:
257
292
  """Get run details."""
258
293
  with httpx.Client(
@@ -270,6 +305,7 @@ class VerisAPI:
270
305
  response = client.delete(f"/v1/runs/{run_id}")
271
306
  _raise_for_status(response)
272
307
 
308
+ @_retry_transient
273
309
  def get_run_events(self, run_id: str, limit: int = 100, offset: int = 0) -> dict[str, Any]:
274
310
  """Get run events/logs."""
275
311
  params = {"limit": limit, "offset": offset}
@@ -280,6 +316,7 @@ class VerisAPI:
280
316
  _raise_for_status(response)
281
317
  return response.json()
282
318
 
319
+ @_retry_transient
283
320
  def list_run_simulations(self, run_id: str) -> dict[str, Any]:
284
321
  """List simulations for a run."""
285
322
  with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=30) as client:
@@ -307,6 +344,7 @@ class VerisAPI:
307
344
  return response.json()
308
345
 
309
346
  # Graders
347
+ @_retry_transient
310
348
  def list_graders(
311
349
  self,
312
350
  environment_id: str,
@@ -340,6 +378,7 @@ class VerisAPI:
340
378
  _raise_for_status(response)
341
379
  return response.json()
342
380
 
381
+ @_retry_transient
343
382
  def list_evaluation_runs(self, run_id: str, limit: int = 20, offset: int = 0) -> dict[str, Any]:
344
383
  """List evaluation runs for a given run."""
345
384
  params = {"limit": limit, "offset": offset}
@@ -350,6 +389,7 @@ class VerisAPI:
350
389
  _raise_for_status(response)
351
390
  return response.json()
352
391
 
392
+ @_retry_transient
353
393
  def get_evaluation_run(self, run_id: str, eval_run_id: str) -> dict[str, Any]:
354
394
  """Get evaluation run details including per-simulation results."""
355
395
  with httpx.Client(
@@ -360,6 +400,7 @@ class VerisAPI:
360
400
  return response.json()
361
401
 
362
402
  # Environment Variables
403
+ @_retry_transient
363
404
  def list_environment_variables(self, environment_id: str) -> dict[str, Any]:
364
405
  """List environment variables for an environment."""
365
406
  with httpx.Client(
@@ -404,6 +445,7 @@ class VerisAPI:
404
445
  _raise_for_status(response)
405
446
  return response.json()
406
447
 
448
+ @_retry_transient
407
449
  def list_reports(self, environment_id: str, limit: int = 20, offset: int = 0) -> dict[str, Any]:
408
450
  """List reports for an environment."""
409
451
  params: dict[str, Any] = {
@@ -418,6 +460,7 @@ class VerisAPI:
418
460
  _raise_for_status(response)
419
461
  return response.json()
420
462
 
463
+ @_retry_transient
421
464
  def get_report(self, report_id: str) -> dict[str, Any]:
422
465
  """Get report details including html_content."""
423
466
  with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=60) as client:
@@ -425,6 +468,7 @@ class VerisAPI:
425
468
  _raise_for_status(response)
426
469
  return response.json()
427
470
 
471
+ @_retry_transient
428
472
  def download_report(self, report_id: str, format: str) -> bytes:
429
473
  """Download report in the specified format (pdf, md)."""
430
474
  with httpx.Client(base_url=self.base_url, headers=self._headers(), timeout=120) as client:
@@ -449,6 +493,7 @@ class VerisAPI:
449
493
  _raise_for_status(response)
450
494
  return response.json()
451
495
 
496
+ @_retry_transient
452
497
  def get_managed_config(self, environment_id: str) -> dict[str, Any]:
453
498
  """Get managed config files (signed download URLs) for an environment."""
454
499
  with httpx.Client(
@@ -459,6 +504,7 @@ class VerisAPI:
459
504
  return response.json()
460
505
 
461
506
  # Sandbox metadata
507
+ @_retry_transient
462
508
  def list_services(self) -> dict[str, Any]:
463
509
  """List available sandbox mock services."""
464
510
  with httpx.Client(
@@ -468,6 +514,7 @@ class VerisAPI:
468
514
  _raise_for_status(response)
469
515
  return response.json()
470
516
 
517
+ @_retry_transient
471
518
  def list_channels(self) -> dict[str, Any]:
472
519
  """List available channel types."""
473
520
  with httpx.Client(
@@ -22,6 +22,63 @@ from veris_cli.config import Config, ProjectConfig
22
22
  from veris_cli.veris_yaml import DEFAULT_DOCKERFILE, VerisYaml, slugify
23
23
 
24
24
 
25
+ def _check_sync_guard(
26
+ env_info: dict,
27
+ pc: ProjectConfig,
28
+ force: bool,
29
+ ) -> str | None:
30
+ """Check if remote managed config diverges from local and prompt user.
31
+
32
+ Returns:
33
+ "push" — user chose to overwrite remote with local config
34
+ "pull" — user chose to pull remote config
35
+ None — guard didn't fire (proceed normally)
36
+
37
+ Exits via sys.exit(1) in non-interactive mode when divergence is detected.
38
+ Raises SystemExit on cancel.
39
+ """
40
+ if env_info.get("status") != "ready" or force:
41
+ return None
42
+
43
+ remote_config_at = env_info.get("config_updated_at")
44
+ local_synced_at = pc.get_config_synced_at()
45
+ if not remote_config_at or (local_synced_at and remote_config_at <= local_synced_at):
46
+ return None
47
+
48
+ if not sys.stdin.isatty():
49
+ output.print_error(
50
+ "Remote config was updated since your last sync. "
51
+ "Run 'veris env config pull' first, or pass --force to overwrite."
52
+ )
53
+ sys.exit(1)
54
+
55
+ import questionary
56
+
57
+ if not local_synced_at:
58
+ sync_message = "Your managed setup is complete. New config is available on the server."
59
+ else:
60
+ sync_message = "Remote config was updated since your last sync."
61
+
62
+ action = questionary.select(
63
+ sync_message,
64
+ choices=[
65
+ questionary.Choice(title="Pull remote config (overwrites local)", value="pull"),
66
+ questionary.Choice(title="Push local config (overwrites remote)", value="push"),
67
+ questionary.Choice(title="Cancel", value="cancel"),
68
+ ],
69
+ ).ask()
70
+ if action == "cancel" or action is None:
71
+ sys.exit(0)
72
+ if action == "push":
73
+ confirm = questionary.confirm(
74
+ "This will overwrite the managed config generated by the Veris team. Continue?",
75
+ default=False,
76
+ ).ask()
77
+ if not confirm:
78
+ sys.exit(0)
79
+ return action
80
+
81
+
25
82
  def _env_push_local(
26
83
  env_id: str,
27
84
  tag: str,
@@ -439,6 +496,10 @@ def env_create(ctx, name: str | None, agent_name: str | None):
439
496
  if target_obj is not None:
440
497
  try:
441
498
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
499
+ # Record sync timestamp so `env push` sync guard stays in sync.
500
+ # We just uploaded from this machine — no need to fetch the
501
+ # server timestamp; "now" is accurate enough.
502
+ project_config.set_config_synced_at(datetime.now(UTC).isoformat())
442
503
  output.print_success("Config uploaded")
443
504
  except Exception as e:
444
505
  output.print_warning(f"Config upload failed (non-fatal): {e}")
@@ -714,7 +775,7 @@ def env_submit(ctx, env_id: str | None, target: str | None):
714
775
  api.notify_submit_uploaded(env_id)
715
776
  output.print_success("Submitted for managed setup!")
716
777
  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.")
778
+ output.print_info("Then run 'veris env config pull' followed by 'veris env push'.")
718
779
 
719
780
  except ValueError as e:
720
781
  output.print_error(str(e))
@@ -791,59 +852,62 @@ def env_push(ctx, tag: str, env_id: str | None, force: bool, target: str | None)
791
852
 
792
853
  api = VerisAPI(profile=profile)
793
854
 
794
- if not dockerfile_path.exists():
795
- output.print_error(
796
- f"{dockerfile_rel} not found. Run 'veris env create' or 'veris env config pull' first."
797
- )
798
- sys.exit(1)
799
-
800
- # Config sync guard: only managed-setup envs (status=ready) have config
801
- # that could be authored by someone other than this CLI user. For self-serve
802
- # envs (status=created), the user IS the author — no divergence is possible
803
- # and the guard would only cause spurious CI failures (local config_synced_at
804
- # is always missing on fresh runners). status=pending is already blocked by
805
- # the backend with its own message.
855
+ # Fetch env info early — needed for Dockerfile check and sync guard.
806
856
  try:
807
857
  env_info = api.get_environment(env_id)
808
858
  except Exception:
809
859
  env_info = {}
810
860
 
811
- pc = ProjectConfig(profile=profile, target=resolved_target)
812
-
813
- if env_info.get("status") == "ready" and not force:
814
- remote_config_at = env_info.get("config_updated_at")
815
- local_synced_at = pc.get_config_synced_at()
816
- if remote_config_at and (not local_synced_at or remote_config_at > local_synced_at):
861
+ if not dockerfile_path.exists():
862
+ env_status = env_info.get("status", "")
863
+ if env_status == "pending":
864
+ output.print_error(
865
+ "Your environment is still being set up. You'll receive an email when it's ready."
866
+ )
867
+ sys.exit(1)
868
+ elif env_status == "ready":
869
+ # Config is ready on the server — offer to pull it.
817
870
  if sys.stdin.isatty():
818
- import questionary
819
-
820
- action = questionary.select(
821
- "Remote config was updated since your last sync.",
822
- choices=[
823
- questionary.Choice(
824
- title="Pull remote config (overwrites local)", value="pull"
825
- ),
826
- questionary.Choice(
827
- title="Push local config (overwrites remote)", value="push"
828
- ),
829
- questionary.Choice(title="Cancel", value="cancel"),
830
- ],
831
- ).ask()
832
- if action == "cancel" or action is None:
833
- return
834
- if action == "pull":
835
- ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
836
- # Reload target config after pull (resolved_target stays the same)
837
- parsed = _load_veris_yaml(veris_yaml_path)
838
- target_obj = parsed.get_target(resolved_target)
839
- dockerfile_rel = target_obj.resolved_dockerfile()
840
- dockerfile_path = Path.cwd() / dockerfile_rel
871
+ output.print_info(
872
+ "Dockerfile not found locally, but config is ready on the server."
873
+ )
874
+ pulled = ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
875
+ if not pulled:
876
+ sys.exit(1)
877
+ # Reload after pull
878
+ parsed = _load_veris_yaml(veris_yaml_path)
879
+ target_obj = parsed.get_target(resolved_target)
880
+ dockerfile_rel = target_obj.resolved_dockerfile()
881
+ dockerfile_path = Path.cwd() / dockerfile_rel
882
+ if not dockerfile_path.exists():
883
+ output.print_error("Dockerfile still missing after config pull.")
884
+ sys.exit(1)
841
885
  else:
842
886
  output.print_error(
843
- "Remote config was updated since your last sync. "
844
- "Run 'veris env config pull' first, or pass --force to overwrite."
887
+ "Dockerfile not found. Run 'veris env config pull' to download it."
845
888
  )
846
889
  sys.exit(1)
890
+ else:
891
+ output.print_error(
892
+ "Dockerfile not found. Create it locally, or run 'veris env submit' "
893
+ "for managed setup."
894
+ )
895
+ sys.exit(1)
896
+
897
+ # Config sync guard: only managed-setup envs (status=ready) have config
898
+ # that could be authored by someone other than this CLI user.
899
+ pc = ProjectConfig(profile=profile, target=resolved_target)
900
+ guard_action = _check_sync_guard(env_info, pc, force)
901
+ if guard_action == "pull":
902
+ pulled = ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
903
+ if not pulled:
904
+ output.print_error("Config pull did not download files. Aborting push.")
905
+ sys.exit(1)
906
+ # Reload target config after pull (resolved_target stays the same)
907
+ parsed = _load_veris_yaml(veris_yaml_path)
908
+ target_obj = parsed.get_target(resolved_target)
909
+ dockerfile_rel = target_obj.resolved_dockerfile()
910
+ dockerfile_path = Path.cwd() / dockerfile_rel
847
911
 
848
912
  try:
849
913
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
@@ -1036,46 +1100,16 @@ def config_push(ctx, file_path: str | None, env_id: str, force: bool, target: st
1036
1100
  api = VerisAPI(profile=profile)
1037
1101
  pc = ProjectConfig(profile=profile, target=resolved_target)
1038
1102
 
1039
- # Config sync guard — only meaningful for managed-setup envs (status=ready)
1040
- # where someone else may have authored the remote config. See env_push for
1041
- # the full rationale.
1042
1103
  try:
1043
1104
  env_info = api.get_environment(env_id)
1044
1105
  except Exception:
1045
1106
  env_info = {}
1046
1107
 
1047
- if env_info.get("status") == "ready" and not force:
1048
- remote_config_at = env_info.get("config_updated_at")
1049
- local_synced_at = pc.get_config_synced_at()
1050
- if remote_config_at and (not local_synced_at or remote_config_at > local_synced_at):
1051
- if sys.stdin.isatty():
1052
- import questionary
1053
-
1054
- action = questionary.select(
1055
- "Remote config was updated since your last sync.",
1056
- choices=[
1057
- questionary.Choice(
1058
- title="Pull remote config (overwrites local)", value="pull"
1059
- ),
1060
- questionary.Choice(
1061
- title="Push local config (overwrites remote)", value="push"
1062
- ),
1063
- questionary.Choice(title="Cancel", value="cancel"),
1064
- ],
1065
- ).ask()
1066
- if action == "cancel" or action is None:
1067
- return
1068
- if action == "pull":
1069
- # Delegate to config pull logic
1070
- output.print_info("Pulling remote config...")
1071
- ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
1072
- return
1073
- else:
1074
- output.print_error(
1075
- "Remote config was updated since your last sync. "
1076
- "Run 'veris env config pull' first, or pass --force to overwrite."
1077
- )
1078
- sys.exit(1)
1108
+ guard_action = _check_sync_guard(env_info, pc, force)
1109
+ if guard_action == "pull":
1110
+ output.print_info("Pulling remote config...")
1111
+ ctx.invoke(config_pull, env_id=env_id, target=resolved_target)
1112
+ return
1079
1113
 
1080
1114
  try:
1081
1115
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
@@ -1118,8 +1152,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1118
1152
  "Your environment is being set up. You'll receive an email when it's ready."
1119
1153
  )
1120
1154
  else:
1121
- output.print_info("No remote config available. Run 'veris env submit' first.")
1122
- return
1155
+ output.print_info(
1156
+ "No remote config available yet. "
1157
+ "If using managed setup, run 'veris env submit'."
1158
+ )
1159
+ return False
1123
1160
 
1124
1161
  veris_dir = Path.cwd() / ".veris"
1125
1162
  veris_dir.mkdir(parents=True, exist_ok=True)
@@ -1134,6 +1171,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1134
1171
  output.print_warning("Failed to download Dockerfile.sandbox")
1135
1172
 
1136
1173
  veris_yaml_url = result.get("veris_yaml_url")
1174
+ if veris_yaml_url and not resolved_target:
1175
+ output.print_warning(
1176
+ "Skipped veris.yaml download — no target resolved. "
1177
+ "Use --target or set an active target with 'veris env targets set <name>'."
1178
+ )
1137
1179
  if veris_yaml_url and resolved_target:
1138
1180
  resp = httpx.get(veris_yaml_url, timeout=60)
1139
1181
  if resp.status_code == 200:
@@ -1155,6 +1197,7 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1155
1197
  env_detail = api.get_environment(env_id)
1156
1198
  config_ts = env_detail.get("config_updated_at") or datetime.now(UTC).isoformat()
1157
1199
  ProjectConfig(profile=profile, target=resolved_target).set_config_synced_at(config_ts)
1200
+ return True
1158
1201
 
1159
1202
  except Exception as e:
1160
1203
  output.print_error(f"Failed to pull config: {e}")
File without changes
File without changes