veris-cli 2.25.0__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.0 → veris_cli-2.25.2}/PKG-INFO +2 -1
  2. {veris_cli-2.25.0 → veris_cli-2.25.2}/pyproject.toml +2 -1
  3. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/api.py +47 -0
  4. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/env.py +137 -63
  5. {veris_cli-2.25.0 → veris_cli-2.25.2}/.gitignore +0 -0
  6. {veris_cli-2.25.0 → veris_cli-2.25.2}/README.md +0 -0
  7. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/__init__.py +0 -0
  8. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/build_context.py +0 -0
  9. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/cli.py +0 -0
  10. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/__init__.py +0 -0
  11. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/_helpers.py +0 -0
  12. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/auth.py +0 -0
  13. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/evaluations.py +0 -0
  14. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/profile.py +0 -0
  15. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/reports.py +0 -0
  16. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/run.py +0 -0
  17. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/scenarios.py +0 -0
  18. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/commands/simulations.py +0 -0
  19. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/config.py +0 -0
  20. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/output.py +0 -0
  21. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/prompts.py +0 -0
  22. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/run_output.py +0 -0
  23. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/__init__.py +0 -0
  24. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_build.sh +0 -0
  25. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/scripts/docker_push.sh +0 -0
  26. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/searchable_checkbox.py +0 -0
  27. {veris_cli-2.25.0 → veris_cli-2.25.2}/src/veris_cli/templates.py +0 -0
  28. {veris_cli-2.25.0 → 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.0
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.0"
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))
@@ -730,9 +791,16 @@ def env_submit(ctx, env_id: str | None, target: str | None):
730
791
  @env.command(name="push")
731
792
  @click.option("--tag", default="latest", help="Image tag (default: latest)")
732
793
  @click.option("--env-id", default=None, help="Environment ID (overrides config)")
794
+ @click.option(
795
+ "--force",
796
+ "-f",
797
+ is_flag=True,
798
+ default=False,
799
+ help="Skip the sync guard for managed-setup envs (use in CI).",
800
+ )
733
801
  @target_option
734
802
  @click.pass_context
735
- def env_push(ctx, tag: str, env_id: str | None, target: str | None):
803
+ def env_push(ctx, tag: str, env_id: str | None, force: bool, target: str | None):
736
804
  """Build and push environment image to Veris.
737
805
 
738
806
  Resolves the target's block from veris.yaml (including an optional
@@ -784,50 +852,63 @@ def env_push(ctx, tag: str, env_id: str | None, target: str | None):
784
852
 
785
853
  api = VerisAPI(profile=profile)
786
854
 
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.
855
+ # Fetch env info early — needed for Dockerfile check and sync guard.
795
856
  try:
796
857
  env_info = api.get_environment(env_id)
797
858
  except Exception:
798
859
  env_info = {}
799
860
 
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)
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.
870
+ if sys.stdin.isatty():
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
821
878
  parsed = _load_veris_yaml(veris_yaml_path)
822
879
  target_obj = parsed.get_target(resolved_target)
823
880
  dockerfile_rel = target_obj.resolved_dockerfile()
824
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)
885
+ else:
886
+ output.print_error(
887
+ "Dockerfile not found. Run 'veris env config pull' to download it."
888
+ )
889
+ sys.exit(1)
825
890
  else:
826
891
  output.print_error(
827
- "Remote config was updated since your last sync. Run 'veris env config pull' first."
892
+ "Dockerfile not found. Create it locally, or run 'veris env submit' "
893
+ "for managed setup."
828
894
  )
829
895
  sys.exit(1)
830
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
911
+
831
912
  try:
832
913
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
833
914
  # Re-fetch to get the server-assigned config_updated_at
@@ -982,9 +1063,16 @@ def config():
982
1063
  help="Path to veris.yaml (default: .veris/veris.yaml)",
983
1064
  )
984
1065
  @click.option("--env-id", default=None, help="Environment ID (uses project config if omitted)")
1066
+ @click.option(
1067
+ "--force",
1068
+ "-f",
1069
+ is_flag=True,
1070
+ default=False,
1071
+ help="Skip the sync guard for managed-setup envs (use in CI).",
1072
+ )
985
1073
  @target_option
986
1074
  @click.pass_context
987
- def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
1075
+ def config_push(ctx, file_path: str | None, env_id: str, force: bool, target: str | None):
988
1076
  """Upload veris.yaml config to the backend without building an image.
989
1077
 
990
1078
  Reads .veris/veris.yaml by default, or a custom path via --file. With
@@ -1012,39 +1100,16 @@ def config_push(ctx, file_path: str | None, env_id: str, target: str | None):
1012
1100
  api = VerisAPI(profile=profile)
1013
1101
  pc = ProjectConfig(profile=profile, target=resolved_target)
1014
1102
 
1015
- # Config sync guard
1016
1103
  try:
1017
1104
  env_info = api.get_environment(env_id)
1018
1105
  except Exception:
1019
1106
  env_info = {}
1020
1107
 
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)
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
1048
1113
 
1049
1114
  try:
1050
1115
  api.upload_environment_config(env_id, target_obj.to_upload_dict())
@@ -1087,8 +1152,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1087
1152
  "Your environment is being set up. You'll receive an email when it's ready."
1088
1153
  )
1089
1154
  else:
1090
- output.print_info("No remote config available. Run 'veris env submit' first.")
1091
- return
1155
+ output.print_info(
1156
+ "No remote config available yet. "
1157
+ "If using managed setup, run 'veris env submit'."
1158
+ )
1159
+ return False
1092
1160
 
1093
1161
  veris_dir = Path.cwd() / ".veris"
1094
1162
  veris_dir.mkdir(parents=True, exist_ok=True)
@@ -1103,6 +1171,11 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1103
1171
  output.print_warning("Failed to download Dockerfile.sandbox")
1104
1172
 
1105
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
+ )
1106
1179
  if veris_yaml_url and resolved_target:
1107
1180
  resp = httpx.get(veris_yaml_url, timeout=60)
1108
1181
  if resp.status_code == 200:
@@ -1124,6 +1197,7 @@ def config_pull(ctx, env_id: str | None, target: str | None):
1124
1197
  env_detail = api.get_environment(env_id)
1125
1198
  config_ts = env_detail.get("config_updated_at") or datetime.now(UTC).isoformat()
1126
1199
  ProjectConfig(profile=profile, target=resolved_target).set_config_synced_at(config_ts)
1200
+ return True
1127
1201
 
1128
1202
  except Exception as e:
1129
1203
  output.print_error(f"Failed to pull config: {e}")
File without changes
File without changes