1111from pathlib import Path
1212from typing import Any , Callable , Dict , Iterator , Optional , Sequence
1313
14- from . import AuthenticationError , ExecutionTimeout , Subfork , SubforkError , __version__
14+ from . import APIError , AuthenticationError , ExecutionTimeout , Subfork , SubforkError , __version__
15+
16+
17+ def print_api_error (status_code : int , message : str ) -> None :
18+ """Write a concise API diagnostic, coloring only the status code on terminals."""
19+ code = str (status_code )
20+ if sys .stderr .isatty () and os .environ .get ("TERM" ) != "dumb" and not os .environ .get ("NO_COLOR" ):
21+ code = "\033 [33m" + code + "\033 [0m"
22+ print ("{}: {}" .format (code , message ), file = sys .stderr )
1523
1624
1725@contextmanager
@@ -161,6 +169,10 @@ def build_parser() -> argparse.ArgumentParser:
161169 for command in ("get" , "versions" , "interface" , "export" , "publish" , "execute" ):
162170 child = commands .add_parser (command )
163171 child .add_argument ("graph_id" )
172+ if command in {"export" , "execute" }:
173+ child .add_argument (
174+ "-f" , "--force" , action = "store_true" , help = "Overwrite an existing output file"
175+ )
164176 if command == "export" :
165177 child .add_argument (
166178 "--output" , default = "-" , help = "Definition JSON path, or - for stdout"
@@ -171,9 +183,7 @@ def build_parser() -> argparse.ArgumentParser:
171183 child .add_argument ("--comment" , default = "" )
172184 elif command == "execute" :
173185 child .add_argument ("--version" , default = "draft" )
174- child .add_argument (
175- "-o" , "--out" , help = "Write result JSON to a new file instead of stdout"
176- )
186+ child .add_argument ("-o" , "--out" , help = "Write result JSON to a file instead of stdout" )
177187 child .add_argument (
178188 "--no-wait" , action = "store_true" , help = "Return submission status immediately"
179189 )
@@ -297,8 +307,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
297307 args = build_parser ().parse_args (argv )
298308 try :
299309 result_file = getattr (args , "out" , None )
300- if result_file is not None and Path (result_file ).exists ():
301- raise ValueError ("Output file already exists; choose a new path." )
310+ force = getattr (args , "force" , False )
311+ export_file = getattr (args , "output" , "-" )
312+ destination = (
313+ result_file
314+ if result_file is not None
315+ else (export_file if export_file != "-" else None )
316+ )
317+ if destination is not None and Path (destination ).exists () and not force :
318+ raise ValueError ("Output file already exists; use --force to overwrite." )
302319 with Subfork (base_url = args .base_url , timeout = args .timeout ) as client :
303320 result = graph_command (client , args )
304321 failed = args .command == "execute" and result .get ("status" ) in {
@@ -321,9 +338,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
321338 if result_file is None and output == "-" :
322339 sys .stdout .write (rendered )
323340 else :
324- # Exclusive creation avoids silently overwriting an existing file .
341+ # Open only after the request and serialization succeed .
325342 with Path (result_file if result_file is not None else output ).open (
326- "x" , encoding = "utf-8" , newline = "\n "
343+ "w" if force else " x" , encoding = "utf-8" , newline = "\n "
327344 ) as stream :
328345 stream .write (rendered )
329346 if failed :
@@ -344,14 +361,21 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
344361 )
345362 return 1
346363 except AuthenticationError :
347- print (
348- "subfork: authentication failed (HTTP 401). Check that SUBFORK_API_KEY "
364+ print_api_error (
365+ 401 ,
366+ "Authentication failed. Check that SUBFORK_API_KEY "
349367 "is an active key issued by the service selected with --base-url or "
350368 "SUBFORK_BASE_URL (default: https://subfork.com). "
351369 "The CLI reads exported environment variables; it does not load .env files." ,
352- file = sys .stderr ,
353370 )
354371 return 1
372+ except APIError as exc :
373+ message = str (exc )
374+ prefix = "Subfork API returned HTTP {}." .format (exc .status_code )
375+ if message .startswith (prefix ):
376+ message = message [len (prefix ) :].strip () or "API request failed."
377+ print_api_error (exc .status_code , message )
378+ return 1
355379 except (SubforkError , ValueError , OSError ) as exc :
356380 print ("subfork: {}" .format (exc ), file = sys .stderr )
357381 return 1
0 commit comments