@stigmer/cli 3.1.5 → 3.1.7
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/commands/apply.js +1 -1
- package/commands/apply.js.map +1 -1
- package/commands/connect.js +11 -0
- package/commands/connect.js.map +1 -1
- package/package.json +5 -5
- package/resources/apply/declarative.js +1 -1
- package/resources/apply/declarative.js.map +1 -1
- package/resources/apply/discovery.d.ts +9 -2
- package/resources/apply/discovery.d.ts.map +1 -1
- package/resources/apply/discovery.js +10 -2
- package/resources/apply/discovery.js.map +1 -1
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +77 -8
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/discover.d.ts.map +1 -1
- package/resources/connect/discover.js +12 -4
- package/resources/connect/discover.js.map +1 -1
- package/resources/mcp/placeholder-resolver.d.ts +19 -0
- package/resources/mcp/placeholder-resolver.d.ts.map +1 -0
- package/resources/mcp/placeholder-resolver.js +52 -0
- package/resources/mcp/placeholder-resolver.js.map +1 -0
- package/resources/mcp/runtime-env.d.ts +16 -0
- package/resources/mcp/runtime-env.d.ts.map +1 -1
- package/resources/mcp/runtime-env.js +29 -8
- package/resources/mcp/runtime-env.js.map +1 -1
- package/src/commands/apply.ts +1 -1
- package/src/commands/connect.test.ts +73 -0
- package/src/commands/connect.ts +14 -0
- package/src/resources/apply/declarative.ts +1 -1
- package/src/resources/apply/discovery.integration.test.ts +99 -0
- package/src/resources/apply/discovery.ts +10 -1
- package/src/resources/connect/__fixtures__/stdio-server.mjs +8 -3
- package/src/resources/connect/connect.integration.test.ts +103 -2
- package/src/resources/connect/connect.ts +87 -8
- package/src/resources/connect/discover.test.ts +40 -1
- package/src/resources/connect/discover.ts +16 -4
- package/src/resources/mcp/placeholder-resolver.test.ts +71 -0
- package/src/resources/mcp/placeholder-resolver.ts +63 -0
- package/src/resources/mcp/runtime-env.test.ts +35 -1
- package/src/resources/mcp/runtime-env.ts +35 -9
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
import { create } from "@bufbuild/protobuf";
|
|
9
9
|
import { McpServerSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
12
12
|
import { localDiscover } from "./discover.js";
|
|
13
|
+
import { PlaceholderResolutionError } from "../mcp/placeholder-resolver.js";
|
|
13
14
|
|
|
14
15
|
const FIXTURE = fileURLToPath(new URL("./__fixtures__/stdio-server.mjs", import.meta.url));
|
|
15
16
|
const CRASH_FIXTURE = fileURLToPath(new URL("./__fixtures__/stdio-crash.mjs", import.meta.url));
|
|
@@ -20,6 +21,15 @@ function stdioSpec(scriptPath: string) {
|
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// A spec whose args reference ${ALLOWED_DIR}, declared under spec.env — the shape
|
|
25
|
+
// the seedpack filesystem server uses (issue #141).
|
|
26
|
+
function stdioSpecWithPlaceholderArg(scriptPath: string) {
|
|
27
|
+
return create(McpServerSpecSchema, {
|
|
28
|
+
serverType: { case: "stdio", value: { command: process.execPath, args: [scriptPath, "${ALLOWED_DIR}"] } },
|
|
29
|
+
env: { ALLOWED_DIR: { isSecret: false, description: "Directory the server may access" } },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
describe("localDiscover (stdio subprocess)", () => {
|
|
24
34
|
it("discovers tools and resource templates and converts them to proto", async () => {
|
|
25
35
|
const caps = await localDiscover(stdioSpec(FIXTURE), [], 10_000);
|
|
@@ -43,3 +53,32 @@ describe("localDiscover (stdio subprocess)", () => {
|
|
|
43
53
|
await expect(localDiscover(stdioSpec(CRASH_FIXTURE), [], 10_000)).rejects.toThrow(/fixture boom: missing CONFIG/);
|
|
44
54
|
}, 15_000);
|
|
45
55
|
});
|
|
56
|
+
|
|
57
|
+
describe("localDiscover (${VAR} argument expansion — issue #141)", () => {
|
|
58
|
+
const original = process.env;
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
process.env = original;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("expands a declared ${VAR} arg from --env into the spawned subprocess", async () => {
|
|
64
|
+
const caps = await localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), ["ALLOWED_DIR=/tmp/allowed"], 10_000);
|
|
65
|
+
// The fixture echoes the argv it actually received; the resolved value must
|
|
66
|
+
// reach the subprocess, never the literal placeholder.
|
|
67
|
+
expect(caps.tools[0].description).toContain('args=["/tmp/allowed"]');
|
|
68
|
+
expect(caps.tools[0].description).not.toContain("${ALLOWED_DIR}");
|
|
69
|
+
}, 15_000);
|
|
70
|
+
|
|
71
|
+
it("expands a declared ${VAR} arg from the exported shell environment", async () => {
|
|
72
|
+
process.env = { ...original, ALLOWED_DIR: "/tmp/from-shell" };
|
|
73
|
+
const caps = await localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), [], 10_000);
|
|
74
|
+
expect(caps.tools[0].description).toContain('args=["/tmp/from-shell"]');
|
|
75
|
+
}, 15_000);
|
|
76
|
+
|
|
77
|
+
it("rejects with a placeholder error before spawning when the declared var is unset", async () => {
|
|
78
|
+
process.env = { ...original };
|
|
79
|
+
delete process.env.ALLOWED_DIR;
|
|
80
|
+
await expect(localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), [], 10_000)).rejects.toBeInstanceOf(
|
|
81
|
+
PlaceholderResolutionError,
|
|
82
|
+
);
|
|
83
|
+
}, 15_000);
|
|
84
|
+
});
|
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
import type { JsonObject } from "@bufbuild/protobuf";
|
|
22
22
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
23
23
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
24
|
-
import { mergeProcessEnv } from "../mcp/runtime-env.js";
|
|
24
|
+
import { mergeProcessEnv, resolveDeclaredEnvValues } from "../mcp/runtime-env.js";
|
|
25
|
+
import { resolveHeaders, resolvePlaceholders } from "../mcp/placeholder-resolver.js";
|
|
25
26
|
|
|
26
27
|
/** Discover an MCP server's capabilities locally without persisting to the backend. */
|
|
27
28
|
export async function localDiscover(
|
|
@@ -79,13 +80,21 @@ interface BuiltTransport {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
async function buildTransport(spec: McpServerSpec, envOverrides: readonly string[]): Promise<BuiltTransport> {
|
|
83
|
+
// ${VAR} placeholders in args/headers resolve against the exact same env the
|
|
84
|
+
// backend hands the runner for real connect (declared keys from the OS env +
|
|
85
|
+
// --env overrides) — so dry-run is a faithful preview. Resolution is strict:
|
|
86
|
+
// an unresolved placeholder throws before any subprocess is spawned, matching
|
|
87
|
+
// the proto contract (never pass a literal "${VAR}" to the server).
|
|
88
|
+
const resolutionEnv = resolveDeclaredEnvValues(spec.env ?? {}, envOverrides);
|
|
89
|
+
|
|
82
90
|
if (spec.serverType?.case === "stdio") {
|
|
83
91
|
const { command, args, workingDir } = spec.serverType.value;
|
|
84
92
|
if (command === "") throw new Error("stdio transport requires a command");
|
|
93
|
+
const resolvedArgs = args.map((arg, i) => resolvePlaceholders(arg, resolutionEnv, `stdio arg[${i}]`));
|
|
85
94
|
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
86
95
|
const transport = new StdioClientTransport({
|
|
87
96
|
command,
|
|
88
|
-
args:
|
|
97
|
+
args: resolvedArgs,
|
|
89
98
|
cwd: workingDir !== "" ? workingDir : undefined,
|
|
90
99
|
env: mergeProcessEnv([...envOverrides, ...goRunEnvOverrides(command, args)]),
|
|
91
100
|
// "pipe" exposes the child stderr as a PassThrough immediately, so we can
|
|
@@ -104,8 +113,11 @@ async function buildTransport(spec: McpServerSpec, envOverrides: readonly string
|
|
|
104
113
|
const { url, headers } = spec.serverType.value;
|
|
105
114
|
if (url === "") throw new Error("HTTP transport requires a URL");
|
|
106
115
|
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
107
|
-
const
|
|
108
|
-
const transport = new StreamableHTTPClientTransport(
|
|
116
|
+
const resolvedHeaders = Object.keys(headers).length > 0 ? resolveHeaders(headers, resolutionEnv) : undefined;
|
|
117
|
+
const transport = new StreamableHTTPClientTransport(
|
|
118
|
+
new URL(url),
|
|
119
|
+
resolvedHeaders ? { requestInit: { headers: resolvedHeaders } } : undefined,
|
|
120
|
+
);
|
|
109
121
|
return { transport, readStderr: () => "" };
|
|
110
122
|
}
|
|
111
123
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Unit tests for the strict ${VAR} placeholder resolver.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { PlaceholderResolutionError, resolveHeaders, resolvePlaceholders } from "./placeholder-resolver.js";
|
|
5
|
+
|
|
6
|
+
describe("resolvePlaceholders", () => {
|
|
7
|
+
it("returns a string with no placeholders unchanged", () => {
|
|
8
|
+
expect(resolvePlaceholders("/tmp/data.sqlite", {})).toBe("/tmp/data.sqlite");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("resolves a single placeholder from the env map", () => {
|
|
12
|
+
expect(resolvePlaceholders("${DIR}", { DIR: "/home/me" })).toBe("/home/me");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("resolves a placeholder embedded in surrounding text", () => {
|
|
16
|
+
expect(resolvePlaceholders("${DB_PATH}/data.sqlite", { DB_PATH: "/var/lib" })).toBe("/var/lib/data.sqlite");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves multiple and adjacent placeholders", () => {
|
|
20
|
+
expect(resolvePlaceholders("${A}${B}-${A}", { A: "x", B: "y" })).toBe("xy-x");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("resolves to an empty string when the value is empty", () => {
|
|
24
|
+
// An empty-but-present value is a resolved value, not a missing variable.
|
|
25
|
+
expect(resolvePlaceholders("[${X}]", { X: "" })).toBe("[]");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("ignores $VAR without braces (only ${VAR} is a placeholder)", () => {
|
|
29
|
+
expect(resolvePlaceholders("$HOME/bin", { HOME: "/root" })).toBe("$HOME/bin");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("throws PlaceholderResolutionError for an unresolved variable, carrying name + context", () => {
|
|
33
|
+
let caught: unknown;
|
|
34
|
+
try {
|
|
35
|
+
resolvePlaceholders("${MISSING}", { OTHER: "1" }, "stdio arg[2]");
|
|
36
|
+
} catch (err) {
|
|
37
|
+
caught = err;
|
|
38
|
+
}
|
|
39
|
+
expect(caught).toBeInstanceOf(PlaceholderResolutionError);
|
|
40
|
+
const e = caught as PlaceholderResolutionError;
|
|
41
|
+
expect(e.variableName).toBe("MISSING");
|
|
42
|
+
expect(e.context).toBe("stdio arg[2]");
|
|
43
|
+
expect(e.message).toContain("${MISSING}");
|
|
44
|
+
expect(e.message).toContain("stdio arg[2]");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("throws on the first unresolved variable even when others resolve", () => {
|
|
48
|
+
expect(() => resolvePlaceholders("${A}-${B}", { A: "ok" })).toThrow(PlaceholderResolutionError);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("resolveHeaders", () => {
|
|
53
|
+
it("resolves placeholders in every header value", () => {
|
|
54
|
+
const resolved = resolveHeaders(
|
|
55
|
+
{ Authorization: "Bearer ${TOKEN}", "X-Static": "v1" },
|
|
56
|
+
{ TOKEN: "abc123" },
|
|
57
|
+
);
|
|
58
|
+
expect(resolved).toEqual({ Authorization: "Bearer abc123", "X-Static": "v1" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("throws with the offending header name in the context", () => {
|
|
62
|
+
let caught: unknown;
|
|
63
|
+
try {
|
|
64
|
+
resolveHeaders({ Authorization: "Bearer ${API_KEY}" }, {});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
caught = err;
|
|
67
|
+
}
|
|
68
|
+
expect(caught).toBeInstanceOf(PlaceholderResolutionError);
|
|
69
|
+
expect((caught as PlaceholderResolutionError).message).toContain('header "Authorization"');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Strict placeholder resolver for ${VAR_NAME} syntax in MCP server configs.
|
|
2
|
+
//
|
|
3
|
+
// The MCP server spec (StdioServerConfig.args, HttpServerConfig.headers) allows
|
|
4
|
+
// values to reference declared env vars with ${VAR_NAME}. The proto contract is
|
|
5
|
+
// explicit that resolution is strict: a missing variable must produce a clear
|
|
6
|
+
// error rather than passing a literal "${VAR}" to the subprocess/server.
|
|
7
|
+
//
|
|
8
|
+
// This mirrors the runner's shared resolver (backend/services/runner/src/shared/
|
|
9
|
+
// placeholder-resolver.ts) so `connect --dry-run` (which discovers locally)
|
|
10
|
+
// behaves identically to real connect (which resolves on the runner). Kept
|
|
11
|
+
// intentionally dependency-free and semantically identical so the two can be
|
|
12
|
+
// collapsed into one shared package later without any behavior change.
|
|
13
|
+
|
|
14
|
+
const PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
15
|
+
|
|
16
|
+
export class PlaceholderResolutionError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
public readonly variableName: string,
|
|
19
|
+
public readonly context?: string,
|
|
20
|
+
) {
|
|
21
|
+
const where = context ? ` in ${context}` : "";
|
|
22
|
+
super(
|
|
23
|
+
`Unresolved placeholder \${${variableName}}${where}: ` +
|
|
24
|
+
`variable is not present in the execution environment`,
|
|
25
|
+
);
|
|
26
|
+
this.name = "PlaceholderResolutionError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve all ${VAR_NAME} placeholders in a template string.
|
|
32
|
+
*
|
|
33
|
+
* @throws PlaceholderResolutionError if any placeholder cannot be resolved.
|
|
34
|
+
*/
|
|
35
|
+
export function resolvePlaceholders(
|
|
36
|
+
template: string,
|
|
37
|
+
envVars: Record<string, string>,
|
|
38
|
+
context?: string,
|
|
39
|
+
): string {
|
|
40
|
+
return template.replace(PLACEHOLDER_RE, (_match, varName: string) => {
|
|
41
|
+
if (varName in envVars) {
|
|
42
|
+
return envVars[varName];
|
|
43
|
+
}
|
|
44
|
+
throw new PlaceholderResolutionError(varName, context);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve placeholders in every value of a headers map.
|
|
50
|
+
*
|
|
51
|
+
* @throws PlaceholderResolutionError if any header value contains an
|
|
52
|
+
* unresolvable placeholder.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveHeaders(
|
|
55
|
+
headers: Record<string, string>,
|
|
56
|
+
envVars: Record<string, string>,
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
const resolved: Record<string, string> = {};
|
|
59
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
60
|
+
resolved[key] = resolvePlaceholders(value, envVars, `header "${key}"`);
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
@@ -4,12 +4,18 @@ import { create } from "@bufbuild/protobuf";
|
|
|
4
4
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { UsageError } from "../../errors/index.js";
|
|
7
|
-
import { buildRuntimeEnv, mergeProcessEnv, parseEnvOverrides } from "./runtime-env.js";
|
|
7
|
+
import { buildRuntimeEnv, mergeProcessEnv, parseEnvOverrides, resolveDeclaredEnvValues } from "./runtime-env.js";
|
|
8
8
|
|
|
9
9
|
function serverWithEnv(env: Record<string, { isSecret: boolean }>) {
|
|
10
10
|
return create(McpServerSchema, { metadata: { id: "mcp_1" }, spec: { env } });
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// resolveDeclaredEnvValues takes the raw declarations map; the tests only rely on
|
|
14
|
+
// keys being present, so a minimal declaration object is sufficient.
|
|
15
|
+
function decls(...keys: string[]) {
|
|
16
|
+
return Object.fromEntries(keys.map((k) => [k, { isSecret: false }]));
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
describe("parseEnvOverrides", () => {
|
|
14
20
|
it("parses KEY=VALUE pairs, preserving '=' in the value", () => {
|
|
15
21
|
expect(parseEnvOverrides(["A=1", "B=x=y"])).toEqual({ A: "1", B: "x=y" });
|
|
@@ -66,3 +72,31 @@ describe("mergeProcessEnv", () => {
|
|
|
66
72
|
expect(merged.NEW).toBe("1");
|
|
67
73
|
});
|
|
68
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("resolveDeclaredEnvValues", () => {
|
|
77
|
+
const original = process.env;
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
process.env = { ...original, ALLOWED_DIR: "/from-os", EMPTY_VAR: "" };
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
process.env = original;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("pulls declared keys from the OS environment", () => {
|
|
86
|
+
expect(resolveDeclaredEnvValues(decls("ALLOWED_DIR"))).toEqual({ ALLOWED_DIR: "/from-os" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("skips declared keys that are unset or empty in the environment", () => {
|
|
90
|
+
expect(resolveDeclaredEnvValues(decls("EMPTY_VAR", "MISSING"))).toEqual({});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("lets --env overrides win over the OS environment", () => {
|
|
94
|
+
expect(resolveDeclaredEnvValues(decls("ALLOWED_DIR"), ["ALLOWED_DIR=/override"])).toEqual({
|
|
95
|
+
ALLOWED_DIR: "/override",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes an --env override even when the key is undeclared (matches buildRuntimeEnv)", () => {
|
|
100
|
+
expect(resolveDeclaredEnvValues(decls(), ["EXTRA=1"])).toEqual({ EXTRA: "1" });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -36,6 +36,39 @@ export function parseEnvOverrides(overrides: readonly string[]): Record<string,
|
|
|
36
36
|
return result;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the values behind a server's declared env vars: declared keys pulled
|
|
41
|
+
* from the OS environment (non-empty only), with `--env` overrides layered on
|
|
42
|
+
* top (override wins; an undeclared override is still included).
|
|
43
|
+
*
|
|
44
|
+
* This is the single source of truth for "which env value is in play". It backs
|
|
45
|
+
* both the runtime_env sent to the backend (buildRuntimeEnv) and the ${VAR}
|
|
46
|
+
* placeholder resolution done locally for `--dry-run` (discover.ts), so the two
|
|
47
|
+
* can never drift: dry-run resolves against the exact same map the backend hands
|
|
48
|
+
* the runner (minus platform-injected infra vars, which a local machine cannot
|
|
49
|
+
* know).
|
|
50
|
+
*
|
|
51
|
+
* `declarations` is the server's `spec.env` map; only its keys (the declared
|
|
52
|
+
* variable names) are read, so any map keyed by those names is accepted.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveDeclaredEnvValues(
|
|
55
|
+
declarations: Record<string, unknown>,
|
|
56
|
+
envOverrides: readonly string[] = [],
|
|
57
|
+
): Record<string, string> {
|
|
58
|
+
const values: Record<string, string> = {};
|
|
59
|
+
|
|
60
|
+
for (const key of Object.keys(declarations)) {
|
|
61
|
+
const value = process.env[key];
|
|
62
|
+
if (value !== undefined && value !== "") values[key] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [key, value] of Object.entries(parseEnvOverrides(envOverrides))) {
|
|
66
|
+
values[key] = value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return values;
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
/**
|
|
40
73
|
* Build the runtime_env map for the Connect RPC from the OS environment and the
|
|
41
74
|
* supplied --env overrides. Only keys with a non-empty value are emitted.
|
|
@@ -45,17 +78,10 @@ export function buildRuntimeEnv(
|
|
|
45
78
|
envOverrides: readonly string[] = [],
|
|
46
79
|
): Record<string, ExecutionValue> {
|
|
47
80
|
const declarations = server.spec?.env ?? {};
|
|
48
|
-
const
|
|
81
|
+
const values = resolveDeclaredEnvValues(declarations, envOverrides);
|
|
49
82
|
const runtime: Record<string, ExecutionValue> = {};
|
|
50
83
|
|
|
51
|
-
for (const [key,
|
|
52
|
-
const value = process.env[key];
|
|
53
|
-
if (value !== undefined && value !== "") {
|
|
54
|
-
runtime[key] = create(ExecutionValueSchema, { value, isSecret: decl.isSecret });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
84
|
+
for (const [key, value] of Object.entries(values)) {
|
|
59
85
|
runtime[key] = create(ExecutionValueSchema, { value, isSecret: declarations[key]?.isSecret ?? false });
|
|
60
86
|
}
|
|
61
87
|
|