This commit is contained in:
William FH
2026-02-17 18:05:05 +00:00
parent a8328b74ce
commit c768e4768a
11 changed files with 308 additions and 346 deletions
-72
View File
@@ -948,7 +948,6 @@ dependencies = [
"serde_json",
"tempfile",
"thiserror 2.0.18",
"tokio",
"which",
"zip",
]
@@ -977,15 +976,6 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@@ -1096,29 +1086,6 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
@@ -1337,15 +1304,6 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -1495,12 +1453,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
@@ -1585,16 +1537,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
@@ -1784,25 +1726,11 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
-1
View File
@@ -13,7 +13,6 @@ path = "src/main.rs"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["rustls-tls", "blocking"], default-features = false }
zip = "2"
indexmap = { version = "2", features = ["serde"] }
+1 -5
View File
@@ -1,7 +1,3 @@
fn main() {
// Make version available at compile time
println!(
"cargo:rustc-env=CARGO_PKG_VERSION={}",
env!("CARGO_PKG_VERSION")
);
// Cargo already sets CARGO_PKG_VERSION, nothing to do
}
+111 -133
View File
@@ -7,11 +7,25 @@ use console::style;
use crate::analytics;
use crate::config::validate_config_file;
/// Find the Python interpreter, preferring python3 over python.
fn find_python() -> Result<String, String> {
for candidate in &["python3", "python"] {
if which::which(candidate).is_ok() {
return Ok(candidate.to_string());
}
}
Err(
"Python not found. The `langgraph dev` command requires Python >= 3.11 with \
langgraph-cli[inmem] installed.\n\
Install with: pip install -U \"langgraph-cli[inmem]\""
.to_string(),
)
}
/// Run the LangGraph API server in development mode (in-memory, via Python subprocess).
///
/// This spawns `python -c "from langgraph_api.cli import run_server; ..."` as a subprocess,
/// passing the parsed config as arguments. For JS graphs, an error is returned since the
/// in-memory server doesn't support them in this CLI.
/// This passes the config as a JSON object via stdin to a Python bootstrap script,
/// avoiding any string interpolation into Python source code.
#[allow(clippy::too_many_arguments)]
pub fn run(
host: &str,
@@ -48,162 +62,126 @@ pub fn run(
);
}
// Build the graphs JSON
let graphs_json = serde_json::to_string(&config_json.graphs)
.map_err(|e| format!("Failed to serialize graphs: {e}"))?;
let python = find_python()?;
// Build env JSON (optional)
let env_json = config_json
.env
.as_ref()
.map(|e| serde_json::to_string(e).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Pre-check that langgraph_api is importable
let check = Command::new(&python)
.args(["-c", "from langgraph_api.cli import run_server"])
.output();
// Build store JSON (optional)
let store_json = config_json
.store
.as_ref()
.map(|s| serde_json::to_string(s).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
match check {
Ok(output) if !output.status.success() => {
return Err(
"Required package 'langgraph-api' is not installed.\n\
Please install it with:\n\n\
pip install -U \"langgraph-cli[inmem]\"\n\n\
Note: The in-mem server requires Python 3.11 or higher."
.to_string(),
);
}
Err(_) => {
return Err(format!(
"Failed to run {python}. The `langgraph dev` command requires Python >= 3.11 with \
langgraph-cli[inmem] installed.\n\
Install with: pip install -U \"langgraph-cli[inmem]\""
));
}
_ => {}
}
// Build auth JSON (optional)
let auth_json = config_json
.auth
.as_ref()
.map(|a| serde_json::to_string(a).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Build a JSON config object to pass via stdin.
// This avoids interpolating user data into Python source code.
let dev_config = serde_json::json!({
"host": host,
"port": port,
"reload": !no_reload,
"open_browser": !no_browser,
"wait_for_client": wait_for_client,
"allow_blocking": allow_blocking,
"tunnel": tunnel,
"server_log_level": server_log_level,
"dependencies": config_json.dependencies,
"graphs": config_json.graphs,
"n_jobs_per_worker": n_jobs_per_worker,
"debug_port": debug_port,
"studio_url": studio_url,
"env": config_json.env,
"store": config_json.store,
"auth": config_json.auth,
"http": config_json.http,
"ui": config_json.ui,
"ui_config": config_json.ui_config,
"webhooks": config_json.webhooks,
});
// Build http JSON (optional)
let http_json = config_json
.http
.as_ref()
.map(|h| serde_json::to_string(h).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Build ui JSON (optional)
let ui_json = config_json
.ui
.as_ref()
.map(|u| serde_json::to_string(u).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Build ui_config JSON (optional)
let ui_config_json = config_json
.ui_config
.as_ref()
.map(|u| serde_json::to_string(u).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Build webhooks JSON (optional)
let webhooks_json = config_json
.webhooks
.as_ref()
.map(|w| serde_json::to_string(w).unwrap_or_else(|_| "null".to_string()))
.unwrap_or_else(|| "None".to_string());
// Build n_jobs_per_worker
let n_jobs_str = n_jobs_per_worker
.map(|n| n.to_string())
.unwrap_or_else(|| "None".to_string());
// Build debug_port
let debug_port_str = debug_port
.map(|p| p.to_string())
.unwrap_or_else(|| "None".to_string());
// Build studio_url
let studio_url_str = studio_url
.map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
.unwrap_or_else(|| "None".to_string());
// Construct the Python code to execute
let python_code = format!(
r#"
import sys, os, json
// Python bootstrap: reads JSON from stdin, calls run_server
let python_code = r#"
import sys, os, json, pathlib
config = json.loads(sys.stdin.read())
cwd = os.getcwd()
sys.path.append(cwd)
deps = json.loads('{deps_json}')
for dep in deps:
import pathlib
for dep in config.get('dependencies', []):
dep_path = pathlib.Path(cwd) / dep
if dep_path.is_dir() and dep_path.exists():
sys.path.append(str(dep_path))
from langgraph_api.cli import run_server
graphs = json.loads('{graphs}')
run_server(
"{host}",
{port},
{reload},
graphs,
n_jobs_per_worker={n_jobs},
open_browser={open_browser},
debug_port={debug_port},
env={env},
store={store},
wait_for_client={wait_for_client},
auth={auth},
http={http},
ui={ui},
ui_config={ui_config},
webhooks={webhooks},
studio_url={studio_url},
allow_blocking={allow_blocking},
tunnel={tunnel},
server_level="{server_log_level}",
config['host'],
config['port'],
config['reload'],
config['graphs'],
n_jobs_per_worker=config.get('n_jobs_per_worker'),
open_browser=config['open_browser'],
debug_port=config.get('debug_port'),
env=config.get('env'),
store=config.get('store'),
wait_for_client=config['wait_for_client'],
auth=config.get('auth'),
http=config.get('http'),
ui=config.get('ui'),
ui_config=config.get('ui_config'),
webhooks=config.get('webhooks'),
studio_url=config.get('studio_url'),
allow_blocking=config['allow_blocking'],
tunnel=config['tunnel'],
server_level=config['server_log_level'],
)
"#,
deps_json = serde_json::to_string(&config_json.dependencies)
.unwrap_or_else(|_| "[]".to_string())
.replace('\'', "\\'"),
graphs = graphs_json.replace('\'', "\\'"),
host = host,
port = port,
reload = if no_reload { "False" } else { "True" },
n_jobs = n_jobs_str,
open_browser = if no_browser { "False" } else { "True" },
debug_port = debug_port_str,
env = env_json,
store = store_json,
wait_for_client = if wait_for_client { "True" } else { "False" },
auth = auth_json,
http = http_json,
ui = ui_json,
ui_config = ui_config_json,
webhooks = webhooks_json,
studio_url = studio_url_str,
allow_blocking = if allow_blocking { "True" } else { "False" },
tunnel = if tunnel { "True" } else { "False" },
server_log_level = server_log_level,
);
"#;
eprintln!(
"{}",
style("Starting LangGraph API server in development mode...").green()
);
// Spawn Python subprocess
let status = Command::new("python")
// Spawn Python subprocess with config on stdin
let mut child = Command::new(&python)
.arg("-c")
.arg(&python_code)
.arg(python_code)
.current_dir(
config_path
.parent()
.unwrap_or_else(|| Path::new(".")),
)
.stdin(std::process::Stdio::inherit())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
"Python not found. The `langgraph dev` command requires Python >= 3.11 with \
langgraph-cli[inmem] installed.\n\
Install with: pip install -U \"langgraph-cli[inmem]\""
.to_string()
} else {
format!("Failed to start Python: {e}")
}
})?;
.spawn()
.map_err(|e| format!("Failed to start Python: {e}"))?;
// Write JSON config to stdin
if let Some(ref mut stdin) = child.stdin {
use std::io::Write;
let json_bytes = dev_config.to_string();
stdin
.write_all(json_bytes.as_bytes())
.map_err(|e| format!("Failed to write config to Python stdin: {e}"))?;
}
// Drop stdin to signal EOF
drop(child.stdin.take());
let status = child
.wait()
.map_err(|e| format!("Failed to wait for Python: {e}"))?;
if !status.success() {
let code = status.code().unwrap_or(1);
+40 -4
View File
@@ -9,7 +9,7 @@ use crate::config::validate_config_file;
use crate::docker::capabilities::{check_capabilities, ComposeType};
use crate::docker::compose::compose;
use crate::docker::dockerfile::config_to_compose;
use crate::exec::{run_command, run_command_streaming};
use crate::exec::{run_command, run_command_streaming_with_callback};
use crate::progress::Progress;
use crate::util::warn_non_wolfi_distro;
@@ -153,10 +153,46 @@ pub fn run(
cmd_args.push(a.as_str());
}
progress.finish();
// Run docker compose with streaming output, intercepting stdout
// to detect startup and show Ready! URLs
let mut ready_printed = false;
let debugger_base_url_query = debugger_base_url
.map(|s| s.to_string())
.unwrap_or_else(|| format!("http://127.0.0.1:{port}"));
// Run docker compose with streaming output
run_command_streaming(compose_cmd[0], &cmd_args, Some(&compose_stdin), verbose)?;
run_command_streaming_with_callback(
compose_cmd[0],
&cmd_args,
Some(&compose_stdin),
verbose,
|line| {
if !ready_printed {
if line.contains("unpacking to docker.io") {
progress.set_message("Starting...");
} else if line.contains("Application startup complete") {
progress.finish();
ready_printed = true;
let debugger_origin = if let Some(dp) = debugger_port {
format!("http://localhost:{dp}")
} else {
"https://smith.langchain.com".to_string()
};
println!(
"Ready!\n\
- API: http://localhost:{port}\n\
- Docs: http://localhost:{port}/docs\n\
- LangGraph Studio: {debugger_origin}/studio/?baseUrl={debugger_base_url_query}"
);
}
}
},
)?;
if !ready_printed {
progress.finish();
}
Ok(())
}
-7
View File
@@ -78,13 +78,6 @@ pub enum GraphSpec {
}
impl GraphSpec {
pub fn get_path(&self) -> Option<&str> {
match self {
GraphSpec::Path(s) => Some(s),
GraphSpec::Dict(m) => m.get("path").and_then(|v| v.as_str()),
}
}
pub fn set_path(&mut self, new_path: String) {
match self {
GraphSpec::Path(s) => *s = new_path,
-3
View File
@@ -1,6 +1,3 @@
pub const DEFAULT_CONFIG: &str = "langgraph.json";
pub const DEFAULT_PORT: u16 = 8123;
pub const MIN_NODE_VERSION: u32 = 20;
pub const DEFAULT_NODE_VERSION: u32 = 20;
+2
View File
@@ -28,7 +28,9 @@ pub enum ComposeType {
/// Docker capabilities detected on the system.
#[derive(Debug, Clone)]
pub struct DockerCapabilities {
#[allow(dead_code)]
pub version_docker: Version,
#[allow(dead_code)]
pub version_compose: Version,
pub healthcheck_start_interval: bool,
pub compose_type: ComposeType,
+67 -109
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::path::Path;
use std::sync::LazyLock;
use indexmap::IndexMap;
@@ -13,13 +14,15 @@ use crate::constants::{BUILD_TOOLS, DEFAULT_NODE_VERSION};
use regex::Regex;
static IMAGE_VERSION_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r":(\d+(?:\.\d+)?(?:\.\d+)?)(?:-|$)").unwrap());
/// Check if a base image supports uv.
fn image_supports_uv(base_image: &str) -> bool {
if base_image == "langchain/langgraph-trial" {
return false;
}
let re = Regex::new(r":(\d+(?:\.\d+)?(?:\.\d+)?)(?:-|$)").unwrap();
match re.find(base_image) {
match IMAGE_VERSION_RE.find(base_image) {
None => true, // Default image supports it
Some(m) => {
let raw = &base_image[m.start() + 1..m.end()];
@@ -109,6 +112,66 @@ fn get_pip_cleanup_lines(
commands.join("\n")
}
/// Build ENV lines for all config-driven environment variables (shared between Python and Node).
fn build_config_env_vars(config: &Config) -> Vec<String> {
let mut env_vars = Vec::new();
if let Some(ref store) = config.store {
env_vars.push(format!(
"ENV LANGGRAPH_STORE='{}'",
serde_json::to_string(store).unwrap()
));
}
if let Some(ref auth) = config.auth {
env_vars.push(format!(
"ENV LANGGRAPH_AUTH='{}'",
serde_json::to_string(auth).unwrap()
));
}
if let Some(ref encryption) = config.encryption {
env_vars.push(format!(
"ENV LANGGRAPH_ENCRYPTION='{}'",
serde_json::to_string(encryption).unwrap()
));
}
if let Some(ref http) = config.http {
env_vars.push(format!(
"ENV LANGGRAPH_HTTP='{}'",
serde_json::to_string(http).unwrap()
));
}
if let Some(ref webhooks) = config.webhooks {
env_vars.push(format!(
"ENV LANGGRAPH_WEBHOOKS='{}'",
serde_json::to_string(webhooks).unwrap()
));
}
if let Some(ref checkpointer) = config.checkpointer {
env_vars.push(format!(
"ENV LANGGRAPH_CHECKPOINTER='{}'",
serde_json::to_string(checkpointer).unwrap()
));
}
if let Some(ref ui) = config.ui {
env_vars.push(format!(
"ENV LANGGRAPH_UI='{}'",
serde_json::to_string(ui).unwrap()
));
}
if let Some(ref ui_config) = config.ui_config {
env_vars.push(format!(
"ENV LANGGRAPH_UI_CONFIG='{}'",
serde_json::to_string(ui_config).unwrap()
));
}
env_vars.push(format!(
"ENV LANGSERVE_GRAPHS='{}'",
serde_json::to_string(&config.graphs).unwrap()
));
env_vars
}
/// Detect the Node.js package manager install command.
fn get_node_pm_install_cmd(config_path: &Path, _config: &Config) -> String {
let parent = config_path.parent().unwrap();
@@ -275,60 +338,7 @@ pub fn python_config_to_docker(
let installs = installs.join("\n\n");
// Environment variables
let mut env_vars = Vec::new();
if let Some(ref store) = config.store {
env_vars.push(format!(
"ENV LANGGRAPH_STORE='{}'",
serde_json::to_string(store).unwrap()
));
}
if let Some(ref auth) = config.auth {
env_vars.push(format!(
"ENV LANGGRAPH_AUTH='{}'",
serde_json::to_string(auth).unwrap()
));
}
if let Some(ref encryption) = config.encryption {
env_vars.push(format!(
"ENV LANGGRAPH_ENCRYPTION='{}'",
serde_json::to_string(encryption).unwrap()
));
}
if let Some(ref http) = config.http {
env_vars.push(format!(
"ENV LANGGRAPH_HTTP='{}'",
serde_json::to_string(http).unwrap()
));
}
if let Some(ref webhooks) = config.webhooks {
env_vars.push(format!(
"ENV LANGGRAPH_WEBHOOKS='{}'",
serde_json::to_string(webhooks).unwrap()
));
}
if let Some(ref checkpointer) = config.checkpointer {
env_vars.push(format!(
"ENV LANGGRAPH_CHECKPOINTER='{}'",
serde_json::to_string(checkpointer).unwrap()
));
}
if let Some(ref ui) = config.ui {
env_vars.push(format!(
"ENV LANGGRAPH_UI='{}'",
serde_json::to_string(ui).unwrap()
));
}
if let Some(ref ui_config) = config.ui_config {
env_vars.push(format!(
"ENV LANGGRAPH_UI_CONFIG='{}'",
serde_json::to_string(ui_config).unwrap()
));
}
env_vars.push(format!(
"ENV LANGSERVE_GRAPHS='{}'",
serde_json::to_string(&config.graphs).unwrap()
));
let env_vars = build_config_env_vars(config);
// JS install
let js_inst_str =
@@ -451,59 +461,7 @@ pub fn node_config_to_docker(
let image_str = docker_tag(config, Some(base_image), api_version);
// Environment variables
let mut env_vars = Vec::new();
if let Some(ref store) = config.store {
env_vars.push(format!(
"ENV LANGGRAPH_STORE='{}'",
serde_json::to_string(store).unwrap()
));
}
if let Some(ref auth) = config.auth {
env_vars.push(format!(
"ENV LANGGRAPH_AUTH='{}'",
serde_json::to_string(auth).unwrap()
));
}
if let Some(ref encryption) = config.encryption {
env_vars.push(format!(
"ENV LANGGRAPH_ENCRYPTION='{}'",
serde_json::to_string(encryption).unwrap()
));
}
if let Some(ref http) = config.http {
env_vars.push(format!(
"ENV LANGGRAPH_HTTP='{}'",
serde_json::to_string(http).unwrap()
));
}
if let Some(ref webhooks) = config.webhooks {
env_vars.push(format!(
"ENV LANGGRAPH_WEBHOOKS='{}'",
serde_json::to_string(webhooks).unwrap()
));
}
if let Some(ref checkpointer) = config.checkpointer {
env_vars.push(format!(
"ENV LANGGRAPH_CHECKPOINTER='{}'",
serde_json::to_string(checkpointer).unwrap()
));
}
if let Some(ref ui) = config.ui {
env_vars.push(format!(
"ENV LANGGRAPH_UI='{}'",
serde_json::to_string(ui).unwrap()
));
}
if let Some(ref ui_config) = config.ui_config {
env_vars.push(format!(
"ENV LANGGRAPH_UI_CONFIG='{}'",
serde_json::to_string(ui_config).unwrap()
));
}
env_vars.push(format!(
"ENV LANGSERVE_GRAPHS='{}'",
serde_json::to_string(&config.graphs).unwrap()
));
let env_vars = build_config_env_vars(config);
let (install_step, build_step) = if let Some(_bc) = build_context {
let cr = container_root.as_ref().unwrap();
+76 -1
View File
@@ -1,4 +1,4 @@
use std::io::Write;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
/// Run a command synchronously, optionally piping stdin, and capturing stdout/stderr.
@@ -134,3 +134,78 @@ pub fn run_command_streaming(
Ok(())
}
/// Run a command, streaming stderr to the parent process, while intercepting
/// stdout line-by-line through a callback. Each stdout line is forwarded to
/// the parent's stdout after the callback processes it.
pub fn run_command_streaming_with_callback<F>(
cmd: &str,
args: &[&str],
input: Option<&str>,
verbose: bool,
mut on_stdout: F,
) -> Result<(), String>
where
F: FnMut(&str),
{
if verbose {
let cmd_str = format!("+ {} {}", cmd, args.join(" "));
if let Some(inp) = input {
let filtered: Vec<&str> = inp.lines().filter(|l| !l.is_empty()).collect();
println!("{} <\n{}", cmd_str, filtered.join("\n"));
} else {
println!("{cmd_str}");
}
}
let stdin_cfg = if input.is_some() {
Stdio::piped()
} else {
Stdio::null()
};
let mut child = Command::new(cmd)
.args(args)
.stdin(stdin_cfg)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| format!("Failed to execute `{cmd}`: {e}"))?;
if let Some(input_data) = input {
if let Some(ref mut stdin_handle) = child.stdin {
stdin_handle
.write_all(input_data.as_bytes())
.map_err(|e| format!("Failed to write to stdin of `{cmd}`: {e}"))?;
}
drop(child.stdin.take());
}
// Read stdout line by line, forward to our stdout, and call callback
if let Some(stdout) = child.stdout.take() {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(line) => {
println!("{line}");
on_stdout(&line);
}
Err(_) => break,
}
}
}
let status = child
.wait()
.map_err(|e| format!("Failed to wait for `{cmd}`: {e}"))?;
if !status.success() {
let code = status.code().unwrap_or(-1);
if code == 130 {
return Ok(());
}
return Err(format!("Command `{cmd}` exited with code {code}"));
}
Ok(())
}
+11 -11
View File
@@ -47,9 +47,9 @@ enum Commands {
#[arg(long)]
recreate: bool,
/// Pull latest images before running
#[arg(long, default_value_t = true)]
pull: bool,
/// Skip pulling latest images before running
#[arg(long)]
no_pull: bool,
/// Wait for services to be healthy before returning
#[arg(long)]
@@ -90,9 +90,9 @@ enum Commands {
#[arg(short, long)]
tag: String,
/// Pull latest images before building
#[arg(long, default_value_t = true)]
pull: bool,
/// Skip pulling latest images before building
#[arg(long)]
no_pull: bool,
/// Base image for the LangGraph API server
#[arg(long)]
@@ -152,7 +152,7 @@ enum Commands {
no_reload: bool,
/// Path to configuration file
#[arg(long, default_value = "langgraph.json")]
#[arg(short, long, default_value = "langgraph.json")]
config: String,
/// Max concurrent jobs per worker
@@ -210,7 +210,7 @@ fn main() {
verbose,
watch,
recreate,
pull,
no_pull,
wait,
debugger_port,
debugger_base_url,
@@ -225,7 +225,7 @@ fn main() {
verbose,
watch,
recreate,
pull,
!no_pull,
wait,
debugger_port,
debugger_base_url.as_deref(),
@@ -237,7 +237,7 @@ fn main() {
Commands::Build {
config,
tag,
pull,
no_pull,
base_image,
api_version,
install_command,
@@ -246,7 +246,7 @@ fn main() {
} => commands::build_cmd::run(
&config,
&tag,
pull,
!no_pull,
base_image.as_deref(),
api_version.as_deref(),
install_command.as_deref(),