@langchain/langgraph-cli 1.5.1-rc.0 → 1.5.2-dev.0
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 +78 -30
- 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/host-backend.mjs +7 -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 +3 -3
- 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") {
|
|
@@ -331,15 +326,21 @@ async function resolveBuildMode(remoteBuildFlag) {
|
|
|
331
326
|
}
|
|
332
327
|
return [!supported, localBuildError];
|
|
333
328
|
}
|
|
334
|
-
async function resolveDeployment(client, step, deploymentId, name, notFoundMessage) {
|
|
329
|
+
async function resolveDeployment(client, step, deploymentId, name, notFoundMessage, agent) {
|
|
335
330
|
if (deploymentId) {
|
|
336
331
|
emitter.step(step, `Using deployment ${deploymentId}`);
|
|
337
332
|
await callWithOptionalTenant(client, (c) => c.getDeployment(deploymentId));
|
|
338
333
|
return { deploymentId, needsCreation: false, step: step + 1 };
|
|
339
334
|
}
|
|
340
|
-
emitter.step(step,
|
|
341
|
-
|
|
342
|
-
|
|
335
|
+
emitter.step(step, agent
|
|
336
|
+
? `Looking up agent '${agent.agent_id}' in ${agent.environment}`
|
|
337
|
+
: `Looking up deployment '${name}'`);
|
|
338
|
+
const response = await callWithOptionalTenant(client, (c) => c.listDeployments(name, agent?.agent_id, agent?.environment));
|
|
339
|
+
let foundId = findDeploymentIdByName(response, name);
|
|
340
|
+
if (agent && Array.isArray(response.resources)) {
|
|
341
|
+
const found = response.resources.find((dep) => rec(dep).id && !rec(dep).is_preview);
|
|
342
|
+
foundId = found ? String(rec(found).id) : null;
|
|
343
|
+
}
|
|
343
344
|
if (foundId) {
|
|
344
345
|
emitter.info(`Found existing deployment (ID: ${foundId})`);
|
|
345
346
|
return { deploymentId: foundId, needsCreation: false, step: step + 1 };
|
|
@@ -348,17 +349,28 @@ async function resolveDeployment(client, step, deploymentId, name, notFoundMessa
|
|
|
348
349
|
return { deploymentId: undefined, needsCreation: true, step: step + 1 };
|
|
349
350
|
}
|
|
350
351
|
async function createDeployment(client, step, args) {
|
|
351
|
-
emitter.step(step,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
352
|
+
emitter.step(step, args.agent
|
|
353
|
+
? `Creating deployment for agent '${args.agent.agent_id}' in ${args.agent.environment}`
|
|
354
|
+
: `Creating deployment '${args.name}'`);
|
|
355
|
+
let created;
|
|
356
|
+
try {
|
|
357
|
+
created = await client.createDeployment(args);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (args.agent &&
|
|
361
|
+
error instanceof HostBackendError &&
|
|
362
|
+
error.statusCode === 409) {
|
|
363
|
+
throw new HostBackendError("This agent already has a deployment in this environment.", 409);
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
358
367
|
const id = typeof created.id === "string" ? created.id : undefined;
|
|
359
368
|
if (!id) {
|
|
360
369
|
throw new HostBackendError("POST /v2/deployments succeeded but response missing a valid 'id'");
|
|
361
370
|
}
|
|
371
|
+
if (args.agent && created.name) {
|
|
372
|
+
emitter.info(`Deployment name: ${created.name}`);
|
|
373
|
+
}
|
|
362
374
|
emitter.info(`Deployment ID: ${id}`, { deployment_id: id });
|
|
363
375
|
return { id, step: step + 1 };
|
|
364
376
|
}
|
|
@@ -583,7 +595,7 @@ async function runLocalBuild(args) {
|
|
|
583
595
|
const dockerfile = generatedDockerfile.replace(/^FROM .*$/m, `FROM ${baseImageRef}`);
|
|
584
596
|
const needsBuildx = os.arch() !== "x64";
|
|
585
597
|
const localTag = `langgraph-deploy-tmp:${Math.floor(Date.now() / 1000)}`;
|
|
586
|
-
const baseOpts =
|
|
598
|
+
const baseOpts = { cwd: projectDir };
|
|
587
599
|
const stdio = args.verbose
|
|
588
600
|
? { stdout: "inherit", stderr: "inherit" }
|
|
589
601
|
: { stdout: "ignore", stderr: "ignore" };
|
|
@@ -807,12 +819,42 @@ async function runRemoteBuild(args) {
|
|
|
807
819
|
await archive.cleanup();
|
|
808
820
|
}
|
|
809
821
|
}
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
// Main deploy orchestration
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
function resolveAgentOptions(opts) {
|
|
826
|
+
const agentId = opts.agentId ?? (process.env.LANGSMITH_AGENT_ID || undefined);
|
|
827
|
+
const agentEnvironment = opts.agentEnvironment ??
|
|
828
|
+
(process.env.LANGSMITH_AGENT_ENVIRONMENT || undefined);
|
|
829
|
+
if (agentId !== undefined || agentEnvironment !== undefined) {
|
|
830
|
+
emitter.note("Note: --agent-id and --agent-environment flags are in private beta");
|
|
831
|
+
}
|
|
832
|
+
if (agentId !== undefined && !agentId.trim()) {
|
|
833
|
+
throw new Error("--agent-id must not be empty.");
|
|
834
|
+
}
|
|
835
|
+
if (agentEnvironment !== undefined &&
|
|
836
|
+
!["development", "staging", "production"].includes(agentEnvironment)) {
|
|
837
|
+
throw new Error("--agent-environment must be development, staging, or production.");
|
|
838
|
+
}
|
|
839
|
+
return { agentId, agentEnvironment };
|
|
840
|
+
}
|
|
810
841
|
async function runDeploy(dockerBuildArgs, opts, remoteBuildFlag) {
|
|
811
842
|
emitter = new Emitter(opts.json);
|
|
812
843
|
noInput = !opts.input;
|
|
813
844
|
emitter.note("Note: 'langgraphjs deploy' is in beta. Expect frequent updates and improvements.");
|
|
814
845
|
if (!opts.json)
|
|
815
846
|
process.stdout.write("\n");
|
|
847
|
+
const { agentId, agentEnvironment } = resolveAgentOptions(opts);
|
|
848
|
+
let agent;
|
|
849
|
+
if (agentId !== undefined || agentEnvironment !== undefined) {
|
|
850
|
+
if (!agentId || !agentEnvironment) {
|
|
851
|
+
throw new Error("--agent-id and --agent-environment are required together.");
|
|
852
|
+
}
|
|
853
|
+
if (opts.name !== undefined || opts.deploymentId !== undefined) {
|
|
854
|
+
throw new Error("--agent-id and --agent-environment cannot be combined with --name or --deployment-id.");
|
|
855
|
+
}
|
|
856
|
+
agent = { agent_id: agentId, environment: agentEnvironment };
|
|
857
|
+
}
|
|
816
858
|
if (!["dev", "prod"].includes(opts.deploymentType)) {
|
|
817
859
|
throw new Error(`Invalid --deployment-type '${opts.deploymentType}' (expected 'dev' or 'prod').`);
|
|
818
860
|
}
|
|
@@ -825,16 +867,16 @@ async function runDeploy(dockerBuildArgs, opts, remoteBuildFlag) {
|
|
|
825
867
|
const envVars = await parseEnvFromConfig(rawConfig, configPath);
|
|
826
868
|
let name = opts.name;
|
|
827
869
|
let deploymentId = opts.deploymentId;
|
|
828
|
-
if (!deploymentId && !name) {
|
|
870
|
+
if (!agent && !deploymentId && !name) {
|
|
829
871
|
name = envVars[DEPLOYMENT_NAME_ENV] ?? process.env[DEPLOYMENT_NAME_ENV];
|
|
830
872
|
}
|
|
831
|
-
if (!deploymentId && !name) {
|
|
873
|
+
if (!agent && !deploymentId && !name) {
|
|
832
874
|
const defaultName = normalizeName(path.basename(process.cwd()));
|
|
833
875
|
name = noInput
|
|
834
876
|
? defaultName
|
|
835
877
|
: await promptText("Deployment name", defaultName);
|
|
836
878
|
}
|
|
837
|
-
if (name && !deploymentId) {
|
|
879
|
+
if (!agent && name && !deploymentId) {
|
|
838
880
|
name = normalizeName(name);
|
|
839
881
|
if (!noInput) {
|
|
840
882
|
const envPath = await resolveEnvPath(rawConfig, configPath);
|
|
@@ -856,12 +898,13 @@ async function runDeploy(dockerBuildArgs, opts, remoteBuildFlag) {
|
|
|
856
898
|
let step = 1;
|
|
857
899
|
const resolved = await resolveDeployment(client, step, deploymentId, name, useRemoteBuild
|
|
858
900
|
? "No deployment found. Will create."
|
|
859
|
-
: "No deployment found. Will create after build.");
|
|
901
|
+
: "No deployment found. Will create after build.", agent);
|
|
860
902
|
deploymentId = resolved.deploymentId;
|
|
861
903
|
step = resolved.step;
|
|
862
904
|
if (resolved.needsCreation) {
|
|
863
905
|
const created = await createDeployment(client, step, {
|
|
864
|
-
name
|
|
906
|
+
name,
|
|
907
|
+
agent,
|
|
865
908
|
deploymentType: opts.deploymentType,
|
|
866
909
|
source: useRemoteBuild ? "internal_source" : "internal_docker",
|
|
867
910
|
secrets,
|
|
@@ -953,8 +996,9 @@ async function runDeploy(dockerBuildArgs, opts, remoteBuildFlag) {
|
|
|
953
996
|
async function runList(opts) {
|
|
954
997
|
emitter = new Emitter(false);
|
|
955
998
|
noInput = false;
|
|
999
|
+
const { agentId, agentEnvironment } = resolveAgentOptions(opts);
|
|
956
1000
|
const client = await createHostBackendClient(opts.hostUrl, opts.apiKey);
|
|
957
|
-
const response = await callWithOptionalTenant(client, (c) => c.listDeployments(opts.nameContains));
|
|
1001
|
+
const response = await callWithOptionalTenant(client, (c) => c.listDeployments(opts.nameContains, agentId, agentEnvironment));
|
|
958
1002
|
const resources = Array.isArray(rec(response).resources)
|
|
959
1003
|
? rec(response).resources
|
|
960
1004
|
: [];
|
|
@@ -1112,6 +1156,8 @@ const deploy = builder
|
|
|
1112
1156
|
.option("--api-key <key>", "API key (or LANGGRAPH_HOST_API_KEY / LANGSMITH_API_KEY / LANGCHAIN_API_KEY).")
|
|
1113
1157
|
.option("--name <name>", "Deployment name (or LANGSMITH_DEPLOYMENT_NAME). Defaults to the current directory name.")
|
|
1114
1158
|
.option("--deployment-id <id>", "ID of an existing deployment to update.")
|
|
1159
|
+
.option("--agent-id <id>", "Agent ID (or LANGSMITH_AGENT_ID).")
|
|
1160
|
+
.option("--agent-environment <environment>", "Agent environment: development, staging, or production (or LANGSMITH_AGENT_ENVIRONMENT).")
|
|
1115
1161
|
.option("--deployment-type <type>", "Deployment type when creating: 'dev' or 'prod'.", "dev")
|
|
1116
1162
|
.option("--no-wait", "Skip waiting for deployment status.")
|
|
1117
1163
|
.option("--verbose", "Show more output from the server logs.", false)
|
|
@@ -1146,6 +1192,8 @@ const deploy = builder
|
|
|
1146
1192
|
deploy
|
|
1147
1193
|
.command("list")
|
|
1148
1194
|
.description("[Beta] List LangSmith Deployments.")
|
|
1195
|
+
.option("--agent-id <id>", "Agent ID (or LANGSMITH_AGENT_ID).")
|
|
1196
|
+
.option("--agent-environment <environment>", "Agent environment: development, staging, or production (or LANGSMITH_AGENT_ENVIRONMENT).")
|
|
1149
1197
|
.option("--api-key <key>", "API key.")
|
|
1150
1198
|
.option("--host-url <url>", "Host backend URL.", DEFAULT_HOST_URL)
|
|
1151
1199
|
.option("--name-contains <value>", "Only show deployments whose names contain this value.", "")
|
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
|
|
@@ -160,7 +160,7 @@ export class HostBackendClient {
|
|
|
160
160
|
*/
|
|
161
161
|
createDeployment(args) {
|
|
162
162
|
const payload = {
|
|
163
|
-
name: args.name,
|
|
163
|
+
...(args.agent ? { agent: args.agent } : { name: args.name }),
|
|
164
164
|
source: args.source,
|
|
165
165
|
source_config: { deployment_type: args.deploymentType },
|
|
166
166
|
source_revision_config: {},
|
|
@@ -179,9 +179,13 @@ export class HostBackendClient {
|
|
|
179
179
|
* @param nameContains - Optional substring filter on deployment names.
|
|
180
180
|
* @returns A paginated response object with a `resources` array.
|
|
181
181
|
*/
|
|
182
|
-
listDeployments(nameContains = "") {
|
|
182
|
+
listDeployments(nameContains = "", agentId, agentEnvironment) {
|
|
183
183
|
return this.request("GET", "/v2/deployments", {
|
|
184
|
-
params: {
|
|
184
|
+
params: {
|
|
185
|
+
name_contains: nameContains,
|
|
186
|
+
agent_id: agentId,
|
|
187
|
+
agent_environment: agentEnvironment,
|
|
188
|
+
},
|
|
185
189
|
});
|
|
186
190
|
}
|
|
187
191
|
/**
|
|
@@ -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.2-dev.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": "^18.19.0 || >=20.16.0"
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"winston-console-format": "^1.0.8",
|
|
40
40
|
"yaml": "^2.8.3",
|
|
41
41
|
"zod": "^3.25.76 || ^4",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"@langchain/langgraph-api": "1.5.2-dev.0",
|
|
43
|
+
"create-langgraph": "1.1.5"
|
|
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
|
-
}
|