@langchain/langgraph-cli 1.5.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.
- package/dist/cli/build.mjs +2 -3
- package/dist/cli/deploy.mjs +8 -13
- package/dist/cli/dev.mjs +7 -7
- package/dist/cli/dev.python.mjs +1 -0
- package/dist/cli/up.mjs +2 -3
- package/dist/cli/utils/studio-url.mjs +16 -0
- package/dist/docker/compose.mjs +5 -3
- package/dist/docker/errors.mjs +22 -0
- package/package.json +2 -2
- package/dist/docker/shell.mjs +0 -62
package/dist/cli/build.mjs
CHANGED
|
@@ -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 =
|
|
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,
|
package/dist/cli/deploy.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 () => {
|
package/dist/cli/dev.python.mjs
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/dist/docker/compose.mjs
CHANGED
|
@@ -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 {
|
|
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 $
|
|
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 $
|
|
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.
|
|
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.
|
|
43
|
+
"@langchain/langgraph-api": "1.5.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/babel__code-frame": "^7.0.6",
|
package/dist/docker/shell.mjs
DELETED
|
@@ -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
|
-
}
|