11import argparse
2+ import io
23import os
4+ import sys
35import time
4- from contextlib import suppress
6+ from contextlib import redirect_stderr , suppress
57from pathlib import Path
68
79from argcomplete import FilesCompleter # type: ignore[attr-defined]
1416 PresetListOutput ,
1517)
1618from dstack ._internal .cli .services .completion import ProjectNameCompleter
19+ from dstack ._internal .cli .services .configurators import APPLY_STDIN_NAME
1720from dstack ._internal .cli .services .presets .apply import apply_preset
1821from dstack ._internal .cli .services .presets .create import (
1922 CreationStopped ,
2831)
2932from dstack ._internal .cli .services .presets .output import get_presets_table , print_presets
3033from dstack ._internal .cli .services .presets .session import (
31- list_agent_sessions ,
32- load_resumable_agent_session ,
34+ list_preset_sessions ,
35+ load_resumable_session ,
3336 resolve_session_ref ,
3437)
3538from dstack ._internal .cli .services .presets .store import (
3639 PresetStore ,
3740 load_preset_configuration ,
41+ parse_preset_configuration ,
3842 resolve_preset_prompt ,
3943)
4044from dstack ._internal .cli .services .profile import (
@@ -251,9 +255,13 @@ def _list(self, args: argparse.Namespace) -> None:
251255 limit = args .limit ,
252256 )
253257 return
258+ # The store warns about unreadable presets on stderr once per read;
259+ # inside Live that would tear the render on every refresh. The first
260+ # read happens before Live starts so warnings print once, above the
261+ # table; refreshes read with stderr suppressed.
262+ presets , sessions = self ._list_presets_and_sessions (base = base , repo = repo )
254263 with Live (console = console , refresh_per_second = LIVE_TABLE_REFRESH_RATE_PER_SEC ) as live :
255264 while True :
256- presets , sessions = self ._list_presets_and_sessions (base = base , repo = repo )
257265 live .update (
258266 get_presets_table (
259267 presets ,
@@ -264,13 +272,15 @@ def _list(self, args: argparse.Namespace) -> None:
264272 )
265273 )
266274 time .sleep (LIVE_TABLE_PROVISION_INTERVAL_SECS )
275+ with redirect_stderr (io .StringIO ()):
276+ presets , sessions = self ._list_presets_and_sessions (base = base , repo = repo )
267277
268278 def _list_presets_and_sessions (
269279 self , * , base : str | None , repo : str | None
270280 ) -> tuple [list [Preset ], list [dict ]]:
271281 self ._reconcile ()
272282 presets = PresetStore ().list ()
273- sessions = list_agent_sessions ()
283+ sessions = list_preset_sessions ()
274284 if base or repo :
275285 repo_to_base = {preset .model : preset .base for preset in presets }
276286 presets = _filter_presets (presets , base = base , repo = repo )
@@ -282,13 +292,14 @@ def _list_presets_and_sessions(
282292 return presets , sessions
283293
284294 def _create (self , args : argparse .Namespace ) -> None :
285- configuration_path , configuration = load_preset_configuration (args .configuration_file )
295+ _check_stdin_configuration_confirmable (args )
296+ _ , configuration = _read_configuration_arg (args .configuration_file )
286297 configuration = _get_effective_configuration (configuration , args , require_name = False )
287- user_prompt = resolve_preset_prompt (configuration , configuration_path )
298+ user_prompt = resolve_preset_prompt (configuration , _prompt_base ( args . configuration_file ) )
288299 store = PresetStore ()
289300 resume_session = None
290301 if getattr (args , "resume" , None ):
291- resume_session = load_resumable_agent_session (args .resume )
302+ resume_session = load_resumable_session (args .resume )
292303 if getattr (args , "trials" , None ) is not None :
293304 console .print (
294305 "[warning]--trials is ignored when resuming: "
@@ -368,7 +379,7 @@ def _get(self, args: argparse.Namespace) -> None:
368379
369380 def _apply (self , args : argparse .Namespace ) -> None :
370381 self ._reconcile ()
371- configuration_path , configuration = load_preset_configuration (args .configuration_file )
382+ configuration_path , configuration = _read_configuration_arg (args .configuration_file )
372383 configuration = _get_effective_configuration (configuration , args )
373384 apply_preset (
374385 api = Client .from_config (project_name = args .project ),
@@ -532,6 +543,28 @@ def _confirm_preset_creation(store: PresetStore, name: str | None, *, assume_yes
532543 return True
533544
534545
546+ def _read_configuration_arg (configuration_file : str ) -> tuple [str , PresetConfiguration ]:
547+ """`-f <path>`, or `-f -` for stdin — the same convention as `dstack apply`."""
548+ if configuration_file == APPLY_STDIN_NAME :
549+ return APPLY_STDIN_NAME , parse_preset_configuration (sys .stdin )
550+ path = Path (configuration_file )
551+ return str (path .resolve ()), load_preset_configuration (path )
552+
553+
554+ def _prompt_base (configuration_file : str ) -> Path :
555+ """Prompt files resolve relative to the configuration file; cwd for stdin."""
556+ if configuration_file == APPLY_STDIN_NAME :
557+ return Path .cwd ()
558+ return Path (configuration_file ).resolve ().parent
559+
560+
561+ def _check_stdin_configuration_confirmable (args : argparse .Namespace ) -> None :
562+ # Same rule as `dstack apply`: the confirmation prompt cannot read from a
563+ # stdin that is the configuration itself.
564+ if not args .yes and args .configuration_file == APPLY_STDIN_NAME :
565+ raise CLIError ("Cannot read configuration from stdin if -y/--yes is not specified" )
566+
567+
535568def _get_effective_configuration (
536569 configuration : PresetConfiguration ,
537570 args : argparse .Namespace ,
0 commit comments