@langchain/langgraph-cli 1.5.1-rc.0 → 1.5.1

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.
@@ -1,6 +1,5 @@
1
1
  import { getDockerCapabilities } from "../docker/compose.mjs";
2
2
  import { assembleLocalDeps, configToDocker, getBaseImage, } from "../docker/docker.mjs";
3
- import { getExecaOptions } from "../docker/shell.mjs";
4
3
  import { getConfig } from "../utils/config.mjs";
5
4
  import { builder } from "./utils/builder.mjs";
6
5
  import { getProjectPath } from "./utils/project.mjs";
@@ -33,11 +32,11 @@ builder
33
32
  await getDockerCapabilities();
34
33
  const projectDir = path.dirname(configPath);
35
34
  const config = getConfig(await fs.readFile(configPath, "utf-8"));
36
- const opts = await getExecaOptions({
35
+ const opts = {
37
36
  cwd: projectDir,
38
37
  stderr: "inherit",
39
38
  stdout: "inherit",
40
- });
39
+ };
41
40
  const localDeps = await assembleLocalDeps(configPath, config);
42
41
  const input = await configToDocker(configPath, config, localDeps, {
43
42
  watch: false,
@@ -18,7 +18,7 @@ import { withAnalytics } from "./utils/analytics.mjs";
18
18
  import { getProjectPath } from "./utils/project.mjs";
19
19
  import { getConfig } from "../utils/config.mjs";
20
20
  import { assembleLocalDeps, configToDocker, getBaseImage, } from "../docker/docker.mjs";
21
- import { getExecaOptions } from "../docker/shell.mjs";
21
+ import { DOCKER_NOT_INSTALLED, DOCKER_NOT_RUNNING, isBinaryNotFound, } from "../docker/errors.mjs";
22
22
  import { HostBackendClient, HostBackendError, } from "./utils/host-backend.mjs";
23
23
  import { Emitter, Spinner } from "./utils/deploy-output.mjs";
24
24
  import { createArchive, BYTES_PER_MIB } from "./utils/archive.mjs";
@@ -276,23 +276,18 @@ async function callWithOptionalTenant(client, operation) {
276
276
  // ---------------------------------------------------------------------------
277
277
  // Build mode resolution
278
278
  // ---------------------------------------------------------------------------
279
- const DOCKER_NOT_INSTALLED = "Docker is required but not installed.\n" +
280
- "Install Docker Desktop: https://docs.docker.com/get-docker/";
281
- const DOCKER_NOT_RUNNING = "Docker is installed but not running.\nStart Docker and try again.";
282
279
  async function canBuildLocally() {
283
- let opts;
284
- try {
285
- opts = await getExecaOptions({ reject: false });
286
- }
287
- catch {
288
- return [false, DOCKER_NOT_INSTALLED];
289
- }
280
+ const opts = { reject: false };
290
281
  try {
291
282
  const info = await $(opts) `docker info`;
283
+ if (isBinaryNotFound(info))
284
+ return [false, DOCKER_NOT_INSTALLED];
292
285
  if (info.exitCode !== 0)
293
286
  return [false, DOCKER_NOT_RUNNING];
294
287
  }
295
- catch {
288
+ catch (error) {
289
+ if (isBinaryNotFound(error))
290
+ return [false, DOCKER_NOT_INSTALLED];
296
291
  return [false, DOCKER_NOT_RUNNING];
297
292
  }
298
293
  if (os.arch() !== "x64") {
@@ -583,7 +578,7 @@ async function runLocalBuild(args) {
583
578
  const dockerfile = generatedDockerfile.replace(/^FROM .*$/m, `FROM ${baseImageRef}`);
584
579
  const needsBuildx = os.arch() !== "x64";
585
580
  const localTag = `langgraph-deploy-tmp:${Math.floor(Date.now() / 1000)}`;
586
- const baseOpts = await getExecaOptions({ cwd: projectDir });
581
+ const baseOpts = { cwd: projectDir };
587
582
  const stdio = args.verbose
588
583
  ? { stdout: "inherit", stderr: "inherit" }
589
584
  : { stdout: "ignore", stderr: "ignore" };
package/dist/cli/dev.mjs CHANGED
@@ -7,6 +7,7 @@ import open from "open";
7
7
  import { startCloudflareTunnel } from "./cloudflare.mjs";
8
8
  import { createIpcServer } from "./utils/ipc/server.mjs";
9
9
  import { getProjectPath } from "./utils/project.mjs";
10
+ import { DEFAULT_STUDIO_URL, resolveStudioUrl } from "./utils/studio-url.mjs";
10
11
  import { getConfig } from "../utils/config.mjs";
11
12
  import { builder } from "./utils/builder.mjs";
12
13
  import { logError, logger } from "../utils/logging.mjs";
@@ -22,6 +23,7 @@ builder
22
23
  .option("-n, --n-jobs-per-worker <number>", "number of workers to run", "10")
23
24
  .option("-c, --config <path>", "path to configuration file", process.cwd())
24
25
  .option("--tunnel", "use Cloudflare Tunnel to expose the server to the internet")
26
+ .option("--studio-url <url>", `URL of the LangGraph Studio instance to connect to (defaults to ${DEFAULT_STUDIO_URL})`)
25
27
  .allowExcessArguments()
26
28
  .allowUnknownOption()
27
29
  .exitOverride((error) => gracefulExit(error.exitCode))
@@ -31,6 +33,7 @@ builder
31
33
  host: command.opts().host !== "localhost",
32
34
  n_jobs_per_worker: command.opts().nJobsPerWorker !== "10",
33
35
  tunnel: Boolean(command.opts().tunnel),
36
+ studio_url: Boolean(command.opts().studioUrl),
34
37
  })))
35
38
  .action(async (options, { args }) => {
36
39
  try {
@@ -46,7 +49,7 @@ builder
46
49
  let hasOpenedFlag = false;
47
50
  let child = undefined;
48
51
  let tunnel = undefined;
49
- let hostUrl = "https://smith.langchain.com";
52
+ let hostUrl = DEFAULT_STUDIO_URL;
50
53
  let envNoBrowser = process.env.BROWSER === "none";
51
54
  server.on("data", async (data) => {
52
55
  const response = z.object({ queryParams: z.string() }).parse(data);
@@ -103,16 +106,13 @@ builder
103
106
  const removedTarget = oldWatch.filter((target) => !newWatch.includes(target));
104
107
  watcher.unwatch(removedTarget).add(addedTarget);
105
108
  }
106
- try {
109
+ hostUrl = await resolveStudioUrl(options.studioUrl, async () => {
107
110
  const { Client } = await import("langsmith");
108
111
  const apiUrl = env?.["LANGSMITH_ENDPOINT"] ||
109
112
  env?.["LANGCHAIN_ENDPOINT"] ||
110
113
  undefined;
111
- hostUrl = new Client({ apiUrl }).getHostUrl() || hostUrl;
112
- }
113
- catch {
114
- // pass
115
- }
114
+ return new Client({ apiUrl }).getHostUrl();
115
+ });
116
116
  return { config, env, hostUrl };
117
117
  };
118
118
  const launchServer = async () => {
@@ -121,6 +121,7 @@ export async function spawnPythonServer(args, context, options) {
121
121
  context.configPath,
122
122
  ...(args.browser ? [] : ["--no-browser"]),
123
123
  ...(args.reload ? [] : ["--no-reload"]),
124
+ ...(args.studioUrl ? ["--studio-url", args.studioUrl] : []),
124
125
  ...args.rest,
125
126
  ], {
126
127
  stdio: ["inherit", "inherit", "inherit"],
package/dist/cli/up.mjs CHANGED
@@ -6,7 +6,6 @@ import { getProjectPath } from "./utils/project.mjs";
6
6
  import { logger } from "../utils/logging.mjs";
7
7
  import { createCompose, getDockerCapabilities } from "../docker/compose.mjs";
8
8
  import { configToCompose, getBaseImage } from "../docker/docker.mjs";
9
- import { getExecaOptions } from "../docker/shell.mjs";
10
9
  import { $ } from "execa";
11
10
  import { createHash } from "node:crypto";
12
11
  import dedent from "dedent";
@@ -71,11 +70,11 @@ builder
71
70
  watch: capabilities.watchAvailable,
72
71
  });
73
72
  const name = getProjectName(configPath);
74
- const execOpts = await getExecaOptions({
73
+ const execOpts = {
75
74
  cwd,
76
75
  stdout: "inherit",
77
76
  stderr: "inherit",
78
- });
77
+ };
79
78
  const exec = $(execOpts);
80
79
  if (!config._INTERNAL_docker_tag && params.pull) {
81
80
  // pull the image
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_STUDIO_URL = "https://smith.langchain.com";
2
+ /**
3
+ * Resolves the Studio host the same way as the Python CLI: an explicit
4
+ * `--studio-url` wins, then the host derived from the LangSmith endpoint,
5
+ * then the public LangSmith host. `deriveUrl` only runs without a flag.
6
+ */
7
+ export async function resolveStudioUrl(studioUrl, deriveUrl) {
8
+ if (studioUrl)
9
+ return studioUrl;
10
+ try {
11
+ return (await deriveUrl()) || DEFAULT_STUDIO_URL;
12
+ }
13
+ catch {
14
+ return DEFAULT_STUDIO_URL;
15
+ }
16
+ }
@@ -1,7 +1,7 @@
1
1
  import { $ } from "execa";
2
2
  import * as yaml from "yaml";
3
3
  import { z } from "zod/v3";
4
- import { getExecaOptions } from "./shell.mjs";
4
+ import { DOCKER_NOT_INSTALLED, isBinaryNotFound } from "./errors.mjs";
5
5
  export const DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable";
6
6
  const REDIS = {
7
7
  image: "redis:6",
@@ -53,10 +53,12 @@ function compareVersion(a, b) {
53
53
  export async function getDockerCapabilities() {
54
54
  let rawInfo = null;
55
55
  try {
56
- const { stdout } = await $(await getExecaOptions()) `docker info -f json`;
56
+ const { stdout } = await $ `docker info -f json`;
57
57
  rawInfo = JSON.parse(stdout);
58
58
  }
59
59
  catch (error) {
60
+ if (isBinaryNotFound(error))
61
+ throw new Error(DOCKER_NOT_INSTALLED);
60
62
  throw new Error("Docker not installed or not running: " + error);
61
63
  }
62
64
  const info = z
@@ -84,7 +86,7 @@ export async function getDockerCapabilities() {
84
86
  }
85
87
  else {
86
88
  try {
87
- const standalone = await $(await getExecaOptions()) `docker-compose --version --short`;
89
+ const standalone = await $ `docker-compose --version --short`;
88
90
  composeRes = {
89
91
  composeType: "standalone",
90
92
  versionCompose: parseVersion(standalone.stdout),
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared diagnostics for the Docker CLI invocations made by `build`, `up` and
3
+ * `deploy`.
4
+ *
5
+ * The `docker` binary is resolved from the inherited environment, exactly as
6
+ * any other command a user runs in their shell. These helpers turn a failed
7
+ * spawn into the same two-way distinction the Python CLI makes: the binary is
8
+ * missing, or the daemon is not answering.
9
+ */
10
+ export const DOCKER_NOT_INSTALLED = "Docker is required but not installed.\n" +
11
+ "Install Docker Desktop: https://docs.docker.com/get-docker/";
12
+ export const DOCKER_NOT_RUNNING = "Docker is installed but not running.\nStart Docker and try again.";
13
+ /**
14
+ * Whether a rejected (or `reject: false`) execa call failed because the binary
15
+ * could not be found on `PATH`, as opposed to running and exiting non-zero.
16
+ */
17
+ export function isBinaryNotFound(error) {
18
+ return (typeof error === "object" &&
19
+ error != null &&
20
+ "code" in error &&
21
+ error.code === "ENOENT");
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-cli",
3
- "version": "1.5.1-rc.0",
3
+ "version": "1.5.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^18.19.0 || >=20.16.0"
@@ -40,7 +40,7 @@
40
40
  "yaml": "^2.8.3",
41
41
  "zod": "^3.25.76 || ^4",
42
42
  "create-langgraph": "1.1.5",
43
- "@langchain/langgraph-api": "1.5.1-rc.0"
43
+ "@langchain/langgraph-api": "1.5.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/babel__code-frame": "^7.0.6",
@@ -1,62 +0,0 @@
1
- import { $ } from "execa";
2
- import { homedir } from "node:os";
3
- let PATH = undefined;
4
- // TODO: macOS related only
5
- async function getUserShell() {
6
- const dscl = await $({
7
- shell: true,
8
- }) `dscl . -read ~/ UserShell | sed 's/UserShell: //'`;
9
- return dscl.stdout.trim();
10
- }
11
- async function verifyDockerPath(PATH) {
12
- await $({ env: { PATH } }) `which docker`;
13
- return PATH;
14
- }
15
- // TODO: macOS related only
16
- async function extractPathFromShell() {
17
- const pathToShell = await getUserShell().catch(() => "/bin/zsh");
18
- const args = pathToShell.includes("csh")
19
- ? ["-c", "echo $PATH"]
20
- : ["-lc", "echo $PATH"];
21
- const shell = await $(pathToShell, args);
22
- return shell.stdout.trim();
23
- }
24
- // TODO: macOS related only
25
- async function guessUserPath() {
26
- return [
27
- "/bin",
28
- "/usr/bin",
29
- "/sbin",
30
- "/usr/sbin",
31
- "/opt/homebrew/bin",
32
- "/opt/homebrew/sbin",
33
- `${homedir()}/.local/bin`,
34
- "/Applications/Docker.app/Contents/Resources/bin",
35
- `${homedir()}/.docker/bin`,
36
- // support for Rancher Desktop
37
- // https://github.com/langchain-ai/langgraph-studio/issues/24#issuecomment-2274046328
38
- // https://github.com/langchain-ai/langgraph-studio/issues/122
39
- `${homedir()}/.rd/bin`,
40
- `/Applications/Rancher Desktop.app/Contents/Resources/resources/darwin/bin`,
41
- ].join(":");
42
- }
43
- async function getLoginPath() {
44
- if (PATH)
45
- return { PATH };
46
- const [fromShell, fromBackup] = await Promise.allSettled([extractPathFromShell(), guessUserPath()].map((promise) => promise.then(verifyDockerPath)));
47
- if (fromShell.status === "fulfilled") {
48
- PATH = fromShell.value;
49
- }
50
- else if (fromBackup.status === "fulfilled") {
51
- PATH = fromBackup.value;
52
- }
53
- else {
54
- console.error("Failed to get PATH from shell or backup", fromShell.reason, fromBackup.reason);
55
- throw fromShell.reason || fromBackup.reason;
56
- }
57
- return { PATH };
58
- }
59
- export async function getExecaOptions(options) {
60
- const env = await getLoginPath();
61
- return { ...options, env };
62
- }