diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 82105a77b..734350439 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -61,6 +61,7 @@ RESERVED_ENV_VARS = frozenset( "LANGCHAIN_CALLBACKS_BACKGROUND", "DD_TRACE_PSYCOPG_ENABLED", "DD_TRACE_REDIS_ENABLED", + "LANGSMITH_DEPLOYMENT_NAME", "LANGGRAPH_CLOUD_LICENSE_KEY", # ALLOWED_SELF_HOSTED_ENV_VARS (rejected for non-self-hosted) "LANGSMITH_API_KEY", @@ -83,6 +84,8 @@ _API_KEY_ENV_NAMES = ( "LANGCHAIN_API_KEY", ) +_DEPLOYMENT_NAME_ENV = "LANGSMITH_DEPLOYMENT_NAME" + def _parse_dotenv_file(path: pathlib.Path) -> dict[str, str]: """Parse a .env file into a dict, skipping comments and blank lines.""" @@ -452,6 +455,7 @@ def _build( extra_flags: Sequence[str] = (), verbose: bool = True, ): + # pull latest images if pull: runner.run( subp_exec( @@ -462,15 +466,19 @@ def _build( ) ) set("Building...") + # apply options args = [ "-f", "-", # stdin "-t", tag, ] + # determine build context: use current directory for JS projects, config parent for Python is_js_project = config_json.get("node_version") and not config_json.get( "python_version" ) + # build/install commands only apply to JS projects for now + # without install/build command, JS projects will follow the old behavior if is_js_project and (build_command or install_command): build_context = str(pathlib.Path.cwd()) else: @@ -488,6 +496,7 @@ def _build( build_command=build_command, build_context=build_context, ) + # add additional_contexts if additional_contexts: for k, v in additional_contexts.items(): args.extend(["--build-context", f"{k}={v}"]) @@ -588,12 +597,16 @@ def build( @click.option( "--api-key", envvar="LANGGRAPH_HOST_API_KEY", - help="API key. If omitted, resolved from .env or prompted.", + help=( + "API key. Can also be set via LANGGRAPH_HOST_API_KEY, " + "LANGSMITH_API_KEY, or LANGCHAIN_API_KEY in your .env file." + ), ) @click.option( "--name", help=( - "Deployment name. Defaults to current directory name " + "Deployment name. Can also be set via LANGSMITH_DEPLOYMENT_NAME " + "in your .env file. Defaults to current directory name " "if --deployment-id is not provided." ), ) @@ -686,6 +699,8 @@ def deploy( if not api_key: api_key = click.prompt("Host API key", hide_input=True) + if not deployment_id and not name: + name = env_vars.get(_DEPLOYMENT_NAME_ENV) if not deployment_id and not name: default_name = _normalize_image_name(pathlib.Path.cwd().name) name = click.prompt("Deployment name", default=default_name) @@ -698,11 +713,13 @@ def deploy( local_tag = f"langgraph-deploy-tmp:{int(time.time())}" with Runner() as runner: - langgraph_cli.docker.check_capabilities( - runner, - require_compose=False, - require_buildx=needs_buildx, - ) + if shutil.which("docker") is None: + raise click.UsageError("Docker not installed") + if needs_buildx: + try: + runner.run(subp_exec("docker", "buildx", "version", collect=True)) + except click.exceptions.Exit: + raise click.UsageError("Docker Buildx not installed") from None def log_step(message: str) -> None: click.secho(message, fg="cyan") @@ -896,6 +913,7 @@ def deploy( ) if status != last_status: last_status = status + # pause spinner so we can avoid conflict when writing status set_progress("") click.secho(f" Status: {status}", fg="cyan") if status in _TERMINAL_STATUSES: diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 8dd02e0c7..2f55f3484 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -24,9 +24,9 @@ DockerComposeType = Literal["plugin", "standalone"] class DockerCapabilities(NamedTuple): version_docker: Version - version_compose: Version | None + version_compose: Version healthcheck_start_interval: bool - compose_type: DockerComposeType | None = None + compose_type: DockerComposeType = "plugin" def _parse_version(version: str) -> Version: @@ -45,9 +45,7 @@ def _parse_version(version: str) -> Version: ) -def check_capabilities( - runner, *, require_compose: bool = True, require_buildx: bool = False -) -> DockerCapabilities: +def check_capabilities(runner) -> DockerCapabilities: # check docker available if shutil.which("docker") is None: raise click.UsageError("Docker not installed") from None @@ -63,33 +61,25 @@ def check_capabilities( if not info["ServerVersion"]: raise click.UsageError("Docker not running") from None - compose_type: DockerComposeType | None = None - compose_version: Version | None = None - if require_compose: - try: - compose = next( - p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose" - ) - compose_version_str = compose["Version"] - compose_type = "plugin" - except (KeyError, StopIteration): - if shutil.which("docker-compose") is None: - raise click.UsageError("Docker Compose not installed") from None + compose_type: DockerComposeType + try: + compose = next( + p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose" + ) + compose_version_str = compose["Version"] + compose_type = "plugin" + except (KeyError, StopIteration): + if shutil.which("docker-compose") is None: + raise click.UsageError("Docker Compose not installed") from None - compose_version_str, _ = runner.run( - subp_exec("docker-compose", "--version", "--short", collect=True) - ) - compose_type = "standalone" - compose_version = _parse_version(compose_version_str) - - if require_buildx: - try: - runner.run(subp_exec("docker", "buildx", "version", collect=True)) - except click.exceptions.Exit: - raise click.UsageError("Docker Buildx not installed") from None + compose_version_str, _ = runner.run( + subp_exec("docker-compose", "--version", "--short", collect=True) + ) + compose_type = "standalone" # parse versions docker_version = _parse_version(info["ServerVersion"]) + compose_version = _parse_version(compose_version_str) # check capabilities return DockerCapabilities( diff --git a/libs/cli/langgraph_cli/progress.py b/libs/cli/langgraph_cli/progress.py index 78edfb649..5eb104fd8 100644 --- a/libs/cli/langgraph_cli/progress.py +++ b/libs/cli/langgraph_cli/progress.py @@ -24,6 +24,7 @@ class Progress: sys.stdout.write(next(self.spinner_generator) + " " + message) sys.stdout.flush() time.sleep(self.delay) + # clear the spinner and message sys.stdout.write( "\b" * (len(message) + 2) + " " * (len(message) + 2)