Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/core/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ import type { TraceOptions } from "./trace.js";
import type { CancellationOptions } from "./cancellation.js";
import { genaiscriptDebug } from "./debug.js";
import { YAMLTryParse } from "./yaml.js";
import { JSON5TryParse } from "./json5.js";
import type { PromptArgs, PromptScript } from "./types.js";
const dbg = genaiscriptDebug("config:env");

/**
Expand Down Expand Up @@ -122,6 +124,50 @@ export function findEnvVar(
return undefined;
}

/**
* Parses default script metadata from GENAISCRIPT_DEFAULT_SCRIPT_META environment variable.
* The environment variable should contain a JSON payload of PromptScript metadata.
* This metadata gets merged last into the main script metadata object.
*
* @param env - The environment variables as key-value pairs.
* @returns A PromptArgs object containing the parsed metadata, or undefined if no valid metadata found.
*/
export function parseDefaultMetaFromEnv(env: Record<string, string>): Partial<PromptArgs> | undefined {
const envValue = env.GENAISCRIPT_DEFAULT_SCRIPT_META;
if (!envValue) {
dbg("GENAISCRIPT_DEFAULT_SCRIPT_META not found in environment variables");
return undefined;
}

dbg(`found GENAISCRIPT_DEFAULT_SCRIPT_META: ${envValue}`);

try {
const parsed = JSON5TryParse(envValue);
if (!parsed || typeof parsed !== "object") {
dbg("GENAISCRIPT_DEFAULT_SCRIPT_META could not be parsed as valid JSON object");
return undefined;
}

dbg(`parsed GENAISCRIPT_DEFAULT_SCRIPT_META: %O`, parsed);

// Filter to only include valid PromptArgs fields (exclude text, id, jsSource, defTools, resolvedSystem)
const excludedFields = new Set(['text', 'id', 'jsSource', 'defTools', 'resolvedSystem']);
const filtered: Partial<PromptArgs> = {};

for (const [key, value] of Object.entries(parsed)) {
if (!excludedFields.has(key)) {
(filtered as any)[key] = value;
}
}

dbg(`filtered GENAISCRIPT_DEFAULT_SCRIPT_META: %O`, filtered);
return filtered;
} catch (error) {
dbg(`failed to parse GENAISCRIPT_DEFAULT_SCRIPT_META: ${error}`);
return undefined;
}
}

/**
* Parses default configuration values from the provided environment variables.
*
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { deleteUndefinedValues } from "./cleaners.js";
import { markdownScriptParse } from "./markdownscript.js";
import { readJSON } from "./fs.js";
import { frontmatterTryParse } from "./frontmatter.js";
import { parseDefaultMetaFromEnv } from "./env.js";
import type {
PromptArgs,
PromptScript,
Expand Down Expand Up @@ -241,5 +242,15 @@ export async function parsePromptScript(filename: string, content: string) {
};
}

// Parse and merge default metadata from environment variables (last to take priority)
const envDefaults = parseDefaultMetaFromEnv(process.env);
if (envDefaults?.metadata) {
// Only merge metadata field from environment defaults
script.metadata = metadataValidate({
...(script.metadata || {}),
...(envDefaults.metadata || {}), // env metadata takes precedence
});
}

return script;
}
81 changes: 80 additions & 1 deletion packages/core/test/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, assert } from "vitest";
import { parseAllowedDomains } from "../src/env.js";
import { parseAllowedDomains, parseDefaultMetaFromEnv } from "../src/env.js";

describe("env", () => {
describe("parseAllowedDomains", () => {
Expand Down Expand Up @@ -53,4 +53,83 @@ describe("env", () => {
assert.deepStrictEqual(result, ["github.com"]);
});
});

describe("parseDefaultMetaFromEnv", () => {
test("returns undefined when GENAISCRIPT_DEFAULT_SCRIPT_META not set", () => {
const result = parseDefaultMetaFromEnv({});
assert.strictEqual(result, undefined);
});

test("parses valid JSON metadata", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: '{"temperature": 0.5, "model": "gpt-4", "title": "Default Title"}'
};
const result = parseDefaultMetaFromEnv(env);
assert.deepStrictEqual(result, {
temperature: 0.5,
model: "gpt-4",
title: "Default Title"
});
});

test("parses valid JSON5 metadata", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: '{temperature: 0.5, model: "gpt-4", unlisted: true}'
};
const result = parseDefaultMetaFromEnv(env);
assert.deepStrictEqual(result, {
temperature: 0.5,
model: "gpt-4",
unlisted: true
});
});

test("handles metadata with nested objects", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: '{"metadata": {"key1": "value1", "key2": "value2"}, "vars": {"var1": "val1"}}'
};
const result = parseDefaultMetaFromEnv(env);
assert.deepStrictEqual(result, {
metadata: {
key1: "value1",
key2: "value2"
},
vars: {
var1: "val1"
}
});
});

test("returns undefined for invalid JSON", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: 'invalid json {'
};
const result = parseDefaultMetaFromEnv(env);
assert.strictEqual(result, undefined);
});

test("returns undefined for non-object values", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: '"just a string"'
};
const result = parseDefaultMetaFromEnv(env);
assert.strictEqual(result, undefined);
});

test("returns undefined for null values", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: 'null'
};
const result = parseDefaultMetaFromEnv(env);
assert.strictEqual(result, undefined);
});

test("handles empty object", () => {
const env = {
GENAISCRIPT_DEFAULT_SCRIPT_META: '{}'
};
const result = parseDefaultMetaFromEnv(env);
assert.deepStrictEqual(result, {});
});
});
});
107 changes: 106 additions & 1 deletion packages/core/test/template.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, test, assert } from "vitest"
import { describe, test, assert, vi, beforeEach, afterEach } from "vitest"
import { parsePromptScript } from "../src/template.js"

describe("template.ts - frontmatter parameters", () => {
Expand Down Expand Up @@ -119,4 +119,109 @@ Configuration test with {{items}} and {{config}}.`
maximum: 1
})
})
})

describe("template.ts - environment variable default metadata", () => {
let originalEnv: any

beforeEach(() => {
originalEnv = { ...process.env }
})

afterEach(() => {
process.env = originalEnv
})

test("should merge environment default metadata into script metadata field", async () => {
process.env.GENAISCRIPT_DEFAULT_SCRIPT_META = '{"metadata": {"env_key": "env_value", "shared_key": "env_shared"}}'

const content = `script({
title: "Test Script",
description: "A test script",
metadata: {
script_key: "script_value",
shared_key: "script_shared"
}
})

Hello world!`

const script = await parsePromptScript("test.genai.mts", content)

assert.strictEqual(script.title, "Test Script")
assert.strictEqual(script.description, "A test script")
assert.ok(script.metadata)
assert.strictEqual(script.metadata.env_key, "env_value")
assert.strictEqual(script.metadata.script_key, "script_value")
// Environment metadata should take precedence for shared keys
assert.strictEqual(script.metadata.shared_key, "env_shared")
})

test("should handle environment metadata without existing script metadata", async () => {
process.env.GENAISCRIPT_DEFAULT_SCRIPT_META = '{"metadata": {"env_key": "env_value"}}'

const content = `script({
title: "Test Script"
})

Hello world!`

const script = await parsePromptScript("test.genai.mts", content)

assert.strictEqual(script.title, "Test Script")
assert.ok(script.metadata)
assert.strictEqual(script.metadata.env_key, "env_value")
})


test("should work without environment variable set", async () => {
delete process.env.GENAISCRIPT_DEFAULT_SCRIPT_META

const content = `script({
title: "Test Script",
metadata: {
original: "value"
}
})

Hello world!`

const script = await parsePromptScript("test.genai.mts", content)

assert.strictEqual(script.title, "Test Script")
assert.ok(script.metadata)
assert.strictEqual(script.metadata.original, "value")
})

test("should handle invalid JSON in environment variable gracefully", async () => {
process.env.GENAISCRIPT_DEFAULT_SCRIPT_META = 'invalid json {'

const content = `script({
title: "Test Script"
})

Hello world!`

const script = await parsePromptScript("test.genai.mts", content)

assert.strictEqual(script.title, "Test Script")
// Should not throw an error, just ignore the invalid env var
})

test("should handle environment metadata with nested objects", async () => {
process.env.GENAISCRIPT_DEFAULT_SCRIPT_META = '{"metadata": {"nested": {"key": "value"}, "simple": "data"}}'

const content = `script({
title: "Test Script"
})

Hello world!`

const script = await parsePromptScript("test.genai.mts", content)

assert.strictEqual(script.title, "Test Script")
assert.ok(script.metadata)
assert.deepEqual(script.metadata.nested, { key: "value" })
assert.strictEqual(script.metadata.simple, "data")
})
})
Loading