@glubean/runner 0.2.3 → 0.2.5

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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @module bootstrap
3
+ *
4
+ * Project-level plugin bootstrap. Any SDK consumer that depends on plugin
5
+ * registration state (matchers, protocol adapters) must call `bootstrap()`
6
+ * at the start of its process — before scanning, running tests, handling
7
+ * MCP requests, or emitting metadata.
8
+ *
9
+ * The bootstrap contract is the single point where "which plugins does this
10
+ * project use" gets resolved. It locates a `glubean.setup.(ts|js|mjs)` file
11
+ * via walk-up from the given start directory and dynamically imports it.
12
+ * That file is expected to call `installPlugin(...)` at module top level.
13
+ *
14
+ * Idempotent by design — calling `bootstrap()` multiple times (across
15
+ * entry points, across sub-scans within one process) is safe and cheap.
16
+ *
17
+ * **TypeScript setup files**: Loading a `.ts` setup file requires the
18
+ * calling process to have a TypeScript module resolver active (tsx,
19
+ * ts-node, etc.). All first-party Glubean entry points (runner, scanner,
20
+ * CLI, MCP server, VSCode extension) run under tsx already, so this is
21
+ * transparent. Third-party embeds that cannot load `.ts` should ship a
22
+ * `glubean.setup.js` or `glubean.setup.mjs` instead.
23
+ *
24
+ * @see {@link installPlugin} in `./install-plugin.js`
25
+ */
26
+ /**
27
+ * Walk up from `startDir` searching for a Glubean setup file. Returns the
28
+ * absolute path of the first match found, or `undefined` if no setup file
29
+ * exists anywhere between `startDir` and the filesystem root (or `stopDir`).
30
+ *
31
+ * Setup files checked per directory, in priority order:
32
+ * `glubean.setup.ts` → `glubean.setup.js` → `glubean.setup.mjs`.
33
+ *
34
+ * @param startDir Directory to begin searching from (absolute or relative to cwd).
35
+ * @param stopDir Optional upper bound for the walk (defaults to filesystem root).
36
+ */
37
+ export declare function discoverSetupFile(startDir: string, stopDir?: string): string | undefined;
38
+ /**
39
+ * Locate and import the project's `glubean.setup` file, triggering the
40
+ * `installPlugin(...)` calls inside it.
41
+ *
42
+ * This function is the **bootstrap contract** for SDK consumers: every
43
+ * entry point that observes plugin-registered state (scanner for
44
+ * `.contract.ts` extraction, runner for test execution, MCP server for
45
+ * metadata, VSCode for scan) **must** await `bootstrap()` before doing
46
+ * its own work. Failing to do so causes a silent split where scan-time
47
+ * sees an empty adapter registry while runtime sees the full one.
48
+ *
49
+ * **Behavior:**
50
+ * - No setup file found → no-op (projects without plugins are fine, silent).
51
+ * - Setup file found and not yet imported this process → import it and
52
+ * await the returned promise (the setup file may call `await installPlugin(...)`).
53
+ * - Setup file found and already imported successfully → no-op (idempotent).
54
+ * - Setup file found but `import()` throws → the error is recorded **and**
55
+ * re-thrown. Every subsequent `bootstrap()` call that resolves to the same
56
+ * file re-throws the remembered error rather than silently succeeding.
57
+ * This is consistent with `installPlugin`'s "setup failure is
58
+ * process-unrecoverable" contract: later scanner / runner / MCP callers
59
+ * MUST see the failure, not a false success.
60
+ *
61
+ * @param startDir Starting directory for the walk-up search. Typically the
62
+ * project root or the process cwd. Caller decides — the SDK
63
+ * does not assume `process.cwd()`.
64
+ * @param stopDir Optional upper bound for the walk (defaults to filesystem root).
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // runner startup
69
+ * import { bootstrap } from "@glubean/sdk";
70
+ * await bootstrap(projectRoot);
71
+ * // ... now safe to import test files / .contract.ts files
72
+ * ```
73
+ */
74
+ export declare function bootstrap(startDir: string, stopDir?: string): Promise<void>;
75
+ /**
76
+ * Test-only: clear the "already bootstrapped" cache so a subsequent
77
+ * `bootstrap()` call will re-import the setup file. Does **not** reset any
78
+ * plugin state on the globals — combine with
79
+ * `__resetInstalledPluginsForTesting()` if you need a full reset.
80
+ *
81
+ * @internal
82
+ */
83
+ export declare function __resetBootstrapForTesting(): void;
84
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AA8CH;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAiCpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @module bootstrap
3
+ *
4
+ * Project-level plugin bootstrap. Any SDK consumer that depends on plugin
5
+ * registration state (matchers, protocol adapters) must call `bootstrap()`
6
+ * at the start of its process — before scanning, running tests, handling
7
+ * MCP requests, or emitting metadata.
8
+ *
9
+ * The bootstrap contract is the single point where "which plugins does this
10
+ * project use" gets resolved. It locates a `glubean.setup.(ts|js|mjs)` file
11
+ * via walk-up from the given start directory and dynamically imports it.
12
+ * That file is expected to call `installPlugin(...)` at module top level.
13
+ *
14
+ * Idempotent by design — calling `bootstrap()` multiple times (across
15
+ * entry points, across sub-scans within one process) is safe and cheap.
16
+ *
17
+ * **TypeScript setup files**: Loading a `.ts` setup file requires the
18
+ * calling process to have a TypeScript module resolver active (tsx,
19
+ * ts-node, etc.). All first-party Glubean entry points (runner, scanner,
20
+ * CLI, MCP server, VSCode extension) run under tsx already, so this is
21
+ * transparent. Third-party embeds that cannot load `.ts` should ship a
22
+ * `glubean.setup.js` or `glubean.setup.mjs` instead.
23
+ *
24
+ * @see {@link installPlugin} in `./install-plugin.js`
25
+ */
26
+ import { dirname, isAbsolute, parse, relative, resolve } from "node:path";
27
+ import { existsSync } from "node:fs";
28
+ import { pathToFileURL } from "node:url";
29
+ /**
30
+ * Setup file names checked in priority order during walk-up. A directory
31
+ * containing any of these files is the project root for plugin purposes.
32
+ * The first hit in each directory wins.
33
+ */
34
+ const SETUP_FILE_NAMES = [
35
+ "glubean.setup.ts",
36
+ "glubean.setup.js",
37
+ "glubean.setup.mjs",
38
+ ];
39
+ const loadState = new Map();
40
+ /**
41
+ * Return true iff `descendant` is the same as or nested inside `ancestor`.
42
+ * Both inputs must be absolute, normalized paths.
43
+ * @internal
44
+ */
45
+ function isAncestorOrSame(ancestor, descendant) {
46
+ const rel = relative(ancestor, descendant);
47
+ // Empty string → equal. Relative path that is not ".." prefixed and not
48
+ // absolute → descendant is inside ancestor. Anything else → outside.
49
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
50
+ }
51
+ /**
52
+ * Walk up from `startDir` searching for a Glubean setup file. Returns the
53
+ * absolute path of the first match found, or `undefined` if no setup file
54
+ * exists anywhere between `startDir` and the filesystem root (or `stopDir`).
55
+ *
56
+ * Setup files checked per directory, in priority order:
57
+ * `glubean.setup.ts` → `glubean.setup.js` → `glubean.setup.mjs`.
58
+ *
59
+ * @param startDir Directory to begin searching from (absolute or relative to cwd).
60
+ * @param stopDir Optional upper bound for the walk (defaults to filesystem root).
61
+ */
62
+ export function discoverSetupFile(startDir, stopDir) {
63
+ const startAbs = resolve(startDir);
64
+ let root;
65
+ if (stopDir !== undefined) {
66
+ const stopAbs = resolve(stopDir);
67
+ // Reject a stopDir that is not an ancestor of (or equal to) startDir.
68
+ // Silently walking past a non-ancestor stopDir could pick up an unrelated
69
+ // setup file above it — reviewer-flagged bug. Fail loud instead.
70
+ if (!isAncestorOrSame(stopAbs, startAbs)) {
71
+ throw new Error(`discoverSetupFile: stopDir "${stopAbs}" is not an ancestor of startDir "${startAbs}". ` +
72
+ "stopDir must be equal to or above startDir in the directory tree.");
73
+ }
74
+ root = stopAbs;
75
+ }
76
+ else {
77
+ root = parse(startAbs).root;
78
+ }
79
+ let dir = startAbs;
80
+ while (true) {
81
+ for (const name of SETUP_FILE_NAMES) {
82
+ const candidate = resolve(dir, name);
83
+ if (existsSync(candidate)) {
84
+ return candidate;
85
+ }
86
+ }
87
+ if (dir === root)
88
+ break;
89
+ const parent = dirname(dir);
90
+ if (parent === dir)
91
+ break;
92
+ dir = parent;
93
+ }
94
+ return undefined;
95
+ }
96
+ /**
97
+ * Locate and import the project's `glubean.setup` file, triggering the
98
+ * `installPlugin(...)` calls inside it.
99
+ *
100
+ * This function is the **bootstrap contract** for SDK consumers: every
101
+ * entry point that observes plugin-registered state (scanner for
102
+ * `.contract.ts` extraction, runner for test execution, MCP server for
103
+ * metadata, VSCode for scan) **must** await `bootstrap()` before doing
104
+ * its own work. Failing to do so causes a silent split where scan-time
105
+ * sees an empty adapter registry while runtime sees the full one.
106
+ *
107
+ * **Behavior:**
108
+ * - No setup file found → no-op (projects without plugins are fine, silent).
109
+ * - Setup file found and not yet imported this process → import it and
110
+ * await the returned promise (the setup file may call `await installPlugin(...)`).
111
+ * - Setup file found and already imported successfully → no-op (idempotent).
112
+ * - Setup file found but `import()` throws → the error is recorded **and**
113
+ * re-thrown. Every subsequent `bootstrap()` call that resolves to the same
114
+ * file re-throws the remembered error rather than silently succeeding.
115
+ * This is consistent with `installPlugin`'s "setup failure is
116
+ * process-unrecoverable" contract: later scanner / runner / MCP callers
117
+ * MUST see the failure, not a false success.
118
+ *
119
+ * @param startDir Starting directory for the walk-up search. Typically the
120
+ * project root or the process cwd. Caller decides — the SDK
121
+ * does not assume `process.cwd()`.
122
+ * @param stopDir Optional upper bound for the walk (defaults to filesystem root).
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * // runner startup
127
+ * import { bootstrap } from "@glubean/sdk";
128
+ * await bootstrap(projectRoot);
129
+ * // ... now safe to import test files / .contract.ts files
130
+ * ```
131
+ */
132
+ export async function bootstrap(startDir, stopDir) {
133
+ const setupFile = discoverSetupFile(startDir, stopDir);
134
+ if (!setupFile)
135
+ return;
136
+ const state = loadState.get(setupFile);
137
+ if (state?.status === "ok") {
138
+ // Fast path: already loaded successfully.
139
+ return;
140
+ }
141
+ if (state?.status === "failed") {
142
+ // Previous attempt threw. Re-throw the remembered error so downstream
143
+ // callers (scanner / runner / MCP) don't silently proceed with a
144
+ // half-initialized plugin registry.
145
+ throw state.error;
146
+ }
147
+ const url = pathToFileURL(setupFile).href;
148
+ try {
149
+ await import(url);
150
+ loadState.set(setupFile, { status: "ok" });
151
+ }
152
+ catch (err) {
153
+ const error = err instanceof Error ? err : new Error(String(err));
154
+ loadState.set(setupFile, { status: "failed", error });
155
+ throw error;
156
+ }
157
+ }
158
+ /**
159
+ * Test-only: clear the "already bootstrapped" cache so a subsequent
160
+ * `bootstrap()` call will re-import the setup file. Does **not** reset any
161
+ * plugin state on the globals — combine with
162
+ * `__resetInstalledPluginsForTesting()` if you need a full reset.
163
+ *
164
+ * @internal
165
+ */
166
+ export function __resetBootstrapForTesting() {
167
+ loadState.clear();
168
+ }
169
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @module env
3
+ *
4
+ * Canonical project-env loader for all Glubean entry points (CLI `run`,
5
+ * MCP tool handlers, VSCode extension, any future consumer).
6
+ *
7
+ * Historical background: CLI and MCP used to each have their own env loader
8
+ * (CLI via `loadEnvFile` without expansion, MCP via a handwritten
9
+ * `parseEnvContent`). Both dropped `${NAME}` expansion from the production
10
+ * path despite `expandVars` being implemented in CLI's shared lib — a silent
11
+ * regression from the original design. This module is the single place all
12
+ * entry points load env files from now on, with full expansion semantics.
13
+ *
14
+ * This lives in `@glubean/runner` because it's tool-level runtime
15
+ * infrastructure (same category as `bootstrap()`), not design-time SDK API.
16
+ */
17
+ /**
18
+ * Load a single `.env`-style file and return its parsed key-value pairs.
19
+ *
20
+ * - File missing (ENOENT) → returns `{}` silently (consumers decide whether
21
+ * missing is an error)
22
+ * - File unreadable (other IO error) → returns `{}` with a warning to stderr
23
+ * - Content is parsed with the standard `dotenv` package (no expansion)
24
+ *
25
+ * This is the low-level primitive. Most callers want
26
+ * {@link loadProjectEnv} instead.
27
+ */
28
+ export declare function loadEnvFile(envPath: string): Promise<Record<string, string>>;
29
+ /**
30
+ * Expand `${NAME}` references in env values.
31
+ *
32
+ * Lookup order per reference:
33
+ * 1. Already-resolved values from this same expansion pass (supports
34
+ * forward references — keys defined earlier in insertion order).
35
+ * 2. `process.env[NAME]` (host environment variables).
36
+ * 3. Empty string fallback.
37
+ *
38
+ * Iteration is insertion order. This means:
39
+ * - Within a single file, a later key can reference earlier keys.
40
+ * - When called on a merged `{ ...vars, ...secrets }` object, secrets-only
41
+ * keys can reference any key from `vars` (because vars keys are inserted
42
+ * first). Vars keys referencing secrets-only keys **will not resolve**
43
+ * in a single pass — they'd need a multi-pass resolver.
44
+ *
45
+ * The multi-pass limitation is accepted — callers who need full
46
+ * topological expansion should use the SDK's `{{NAME}}` template at
47
+ * runtime via `resolveTemplate`, which resolves lazily in test execution
48
+ * context and can pull from vars / secrets / session dynamically.
49
+ */
50
+ export declare function expandVars(vars: Record<string, string>): Record<string, string>;
51
+ /**
52
+ * Result of loading a project's env + secrets, with `${NAME}` references
53
+ * fully expanded.
54
+ *
55
+ * `vars` and `secrets` are kept as separate objects (never merged) so
56
+ * downstream layers can apply redaction / masking to secrets without
57
+ * affecting vars.
58
+ *
59
+ * On key collision between `.env` and `.env.secrets`, the secret wins and
60
+ * the key appears **only** in `secrets`, not in `vars` (no duplication).
61
+ */
62
+ export interface ProjectEnv {
63
+ vars: Record<string, string>;
64
+ secrets: Record<string, string>;
65
+ }
66
+ /**
67
+ * Load a project's `.env` + `.env.secrets` with full `${NAME}` expansion.
68
+ *
69
+ * This is the canonical entry point for loading project env in any Glubean
70
+ * tool. CLI, MCP, and future consumers should all go through this function.
71
+ *
72
+ * ### Behavior
73
+ *
74
+ * 1. Reads `<rootDir>/<envFileName>` (default `.env`) and
75
+ * `<rootDir>/<envFileName>.secrets` (`.env.secrets` by default). Missing
76
+ * files are silently treated as empty.
77
+ * 2. Merges them temporarily (secrets override vars on key collision) and
78
+ * runs {@link expandVars} over the merged set so `${NAME}` references
79
+ * can cross between vars and secrets in either direction (subject to
80
+ * insertion-order single-pass limitation — see `expandVars` docs).
81
+ * 3. Splits the expanded merged map back into `vars` and `secrets`,
82
+ * preserving the invariant: a key on collision appears only in
83
+ * `secrets`, never duplicated into `vars`.
84
+ *
85
+ * ### Naming convention
86
+ *
87
+ * `.env` → `.env.secrets`
88
+ * `.env.staging` → `.env.staging.secrets`
89
+ * `.env.ci` → `.env.ci.secrets`
90
+ *
91
+ * The secrets path is always `<envFileName>.secrets` in the same directory.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { loadProjectEnv } from "@glubean/runner";
96
+ *
97
+ * const { vars, secrets } = await loadProjectEnv(projectRoot);
98
+ * const { vars: stagingVars, secrets: stagingSecrets } =
99
+ * await loadProjectEnv(projectRoot, ".env.staging");
100
+ * ```
101
+ */
102
+ export declare function loadProjectEnv(rootDir: string, envFileName?: string): Promise<ProjectEnv>;
103
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,WAAW,SAAS,GACnB,OAAO,CAAC,UAAU,CAAC,CA2BrB"}
package/dist/env.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @module env
3
+ *
4
+ * Canonical project-env loader for all Glubean entry points (CLI `run`,
5
+ * MCP tool handlers, VSCode extension, any future consumer).
6
+ *
7
+ * Historical background: CLI and MCP used to each have their own env loader
8
+ * (CLI via `loadEnvFile` without expansion, MCP via a handwritten
9
+ * `parseEnvContent`). Both dropped `${NAME}` expansion from the production
10
+ * path despite `expandVars` being implemented in CLI's shared lib — a silent
11
+ * regression from the original design. This module is the single place all
12
+ * entry points load env files from now on, with full expansion semantics.
13
+ *
14
+ * This lives in `@glubean/runner` because it's tool-level runtime
15
+ * infrastructure (same category as `bootstrap()`), not design-time SDK API.
16
+ */
17
+ import { readFile } from "node:fs/promises";
18
+ import { resolve } from "node:path";
19
+ import { parse as parseDotenv } from "dotenv";
20
+ /**
21
+ * Load a single `.env`-style file and return its parsed key-value pairs.
22
+ *
23
+ * - File missing (ENOENT) → returns `{}` silently (consumers decide whether
24
+ * missing is an error)
25
+ * - File unreadable (other IO error) → returns `{}` with a warning to stderr
26
+ * - Content is parsed with the standard `dotenv` package (no expansion)
27
+ *
28
+ * This is the low-level primitive. Most callers want
29
+ * {@link loadProjectEnv} instead.
30
+ */
31
+ export async function loadEnvFile(envPath) {
32
+ try {
33
+ const content = await readFile(envPath, "utf-8");
34
+ return parseDotenv(content);
35
+ }
36
+ catch (error) {
37
+ if (error.code === "ENOENT") {
38
+ return {};
39
+ }
40
+ console.warn(`Warning: Could not read env file ${envPath}: ${error.message}`);
41
+ return {};
42
+ }
43
+ }
44
+ /**
45
+ * Expand `${NAME}` references in env values.
46
+ *
47
+ * Lookup order per reference:
48
+ * 1. Already-resolved values from this same expansion pass (supports
49
+ * forward references — keys defined earlier in insertion order).
50
+ * 2. `process.env[NAME]` (host environment variables).
51
+ * 3. Empty string fallback.
52
+ *
53
+ * Iteration is insertion order. This means:
54
+ * - Within a single file, a later key can reference earlier keys.
55
+ * - When called on a merged `{ ...vars, ...secrets }` object, secrets-only
56
+ * keys can reference any key from `vars` (because vars keys are inserted
57
+ * first). Vars keys referencing secrets-only keys **will not resolve**
58
+ * in a single pass — they'd need a multi-pass resolver.
59
+ *
60
+ * The multi-pass limitation is accepted — callers who need full
61
+ * topological expansion should use the SDK's `{{NAME}}` template at
62
+ * runtime via `resolveTemplate`, which resolves lazily in test execution
63
+ * context and can pull from vars / secrets / session dynamically.
64
+ */
65
+ export function expandVars(vars) {
66
+ const result = {};
67
+ for (const [key, value] of Object.entries(vars)) {
68
+ result[key] = value.replace(/\$\{(\w+)\}/g, (_, name) => {
69
+ return result[name] ?? process.env[name] ?? "";
70
+ });
71
+ }
72
+ return result;
73
+ }
74
+ /**
75
+ * Load a project's `.env` + `.env.secrets` with full `${NAME}` expansion.
76
+ *
77
+ * This is the canonical entry point for loading project env in any Glubean
78
+ * tool. CLI, MCP, and future consumers should all go through this function.
79
+ *
80
+ * ### Behavior
81
+ *
82
+ * 1. Reads `<rootDir>/<envFileName>` (default `.env`) and
83
+ * `<rootDir>/<envFileName>.secrets` (`.env.secrets` by default). Missing
84
+ * files are silently treated as empty.
85
+ * 2. Merges them temporarily (secrets override vars on key collision) and
86
+ * runs {@link expandVars} over the merged set so `${NAME}` references
87
+ * can cross between vars and secrets in either direction (subject to
88
+ * insertion-order single-pass limitation — see `expandVars` docs).
89
+ * 3. Splits the expanded merged map back into `vars` and `secrets`,
90
+ * preserving the invariant: a key on collision appears only in
91
+ * `secrets`, never duplicated into `vars`.
92
+ *
93
+ * ### Naming convention
94
+ *
95
+ * `.env` → `.env.secrets`
96
+ * `.env.staging` → `.env.staging.secrets`
97
+ * `.env.ci` → `.env.ci.secrets`
98
+ *
99
+ * The secrets path is always `<envFileName>.secrets` in the same directory.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * import { loadProjectEnv } from "@glubean/runner";
104
+ *
105
+ * const { vars, secrets } = await loadProjectEnv(projectRoot);
106
+ * const { vars: stagingVars, secrets: stagingSecrets } =
107
+ * await loadProjectEnv(projectRoot, ".env.staging");
108
+ * ```
109
+ */
110
+ export async function loadProjectEnv(rootDir, envFileName = ".env") {
111
+ const envPath = resolve(rootDir, envFileName);
112
+ const secretsPath = resolve(rootDir, `${envFileName}.secrets`);
113
+ const rawVars = await loadEnvFile(envPath);
114
+ const rawSecrets = await loadEnvFile(secretsPath);
115
+ // Merge for expansion so `${NAME}` can cross-reference both files.
116
+ // `{ ...rawVars, ...rawSecrets }` keeps insertion order — vars keys first,
117
+ // then secrets-only keys — so secrets can reference vars in a single pass.
118
+ const merged = { ...rawVars, ...rawSecrets };
119
+ const expanded = expandVars(merged);
120
+ // Split back. Keys that exist in both files → secret wins, appears only
121
+ // in `secrets` (not duplicated into `vars`).
122
+ const vars = {};
123
+ for (const key of Object.keys(rawVars)) {
124
+ if (!(key in rawSecrets)) {
125
+ vars[key] = expanded[key];
126
+ }
127
+ }
128
+ const secrets = {};
129
+ for (const key of Object.keys(rawSecrets)) {
130
+ secrets[key] = expanded[key];
131
+ }
132
+ return { vars, secrets };
133
+ }
134
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAe;IAEf,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACjD,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,oCAAoC,OAAO,KAAM,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;QACzF,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,UAAU,CACxB,IAA4B;IAE5B,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE;YAC9D,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAAe,EACf,WAAW,GAAG,MAAM;IAEpB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,WAAW,UAAU,CAAC,CAAC;IAE/D,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAC;IAElD,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,UAAU,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAEpC,wEAAwE;IACxE,6CAA6C;IAC7C,MAAM,IAAI,GAA2B,EAAE,CAAC;IACxC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC"}
package/dist/harness.js CHANGED
@@ -8,7 +8,9 @@
8
8
  import { parseArgs } from "node:util";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
  import { inferJsonSchema, truncateDeep } from "./schema_inference.js";
11
- import { setRuntime } from "@glubean/sdk/internal";
11
+ import { bootstrap } from "./bootstrap.js";
12
+ import { loadProjectOverlays } from "@glubean/scanner";
13
+ import { setRuntime, setExplicitInput, setBootstrapInput, setForceStandalone, } from "@glubean/sdk/internal";
12
14
  import ky from "ky";
13
15
  import { Expectation } from "@glubean/sdk/expect";
14
16
  // Global error handlers for async errors that escape try/catch
@@ -1013,6 +1015,84 @@ setRuntime({
1013
1015
  log: ctx.log,
1014
1016
  });
1015
1017
  try {
1018
+ // Bootstrap project plugins before loading user code. Without this, any
1019
+ // test module that uses plugin-registered primitives (custom matchers,
1020
+ // `contract.graphql(...)`, `contract.grpc(...)`, ...) would either fail
1021
+ // on first access or silently fall through to the default behavior — see
1022
+ // plugin-manifest-proposal.md D2.
1023
+ await bootstrap(process.cwd());
1024
+ // Eagerly load `.bootstrap.{ts,js,mjs}` files (attachment-model §7.4).
1025
+ // Parent process also calls this for its own scanning purposes, but the
1026
+ // SDK bootstrap registry is process-local — every harness subprocess
1027
+ // has its own empty Map until it imports the overlay modules itself.
1028
+ // Without this, a filtered run like `glubean run project.contract.ts`
1029
+ // would reach the child's dispatcher with no overlays registered and
1030
+ // silently fall through to the raw execution path, defeating both the
1031
+ // overlay intent and `runnability.requireAttachment` guards.
1032
+ const overlayLoad = await loadProjectOverlays(process.cwd());
1033
+ for (const err of overlayLoad.errors) {
1034
+ console.log(JSON.stringify({
1035
+ type: "log",
1036
+ message: `Bootstrap overlay failed to load: ${err.file} — ${err.error}`,
1037
+ }));
1038
+ }
1039
+ // Spike 3 — runner input channels (attachment-model §8). The CLI / MCP
1040
+ // serializes runner-supplied case input + bootstrap params into env
1041
+ // vars before spawning this subprocess. Each map is JSON `{ testId:
1042
+ // value }`. Force-standalone is a JSON-encoded `string[]` of testIds.
1043
+ // We populate the SDK runner-input channel here so the dispatcher's
1044
+ // §5.1 algorithm sees them when test.fn runs.
1045
+ const explicitMapRaw = process.env["GLUBEAN_RUNNER_EXPLICIT_INPUT_MAP"];
1046
+ if (explicitMapRaw) {
1047
+ try {
1048
+ const parsed = JSON.parse(explicitMapRaw);
1049
+ for (const [testId, value] of Object.entries(parsed)) {
1050
+ setExplicitInput(testId, value);
1051
+ }
1052
+ }
1053
+ catch (err) {
1054
+ console.log(JSON.stringify({
1055
+ type: "log",
1056
+ message: `Invalid GLUBEAN_RUNNER_EXPLICIT_INPUT_MAP JSON: ` +
1057
+ (err instanceof Error ? err.message : String(err)),
1058
+ }));
1059
+ }
1060
+ }
1061
+ const bootstrapMapRaw = process.env["GLUBEAN_RUNNER_BOOTSTRAP_INPUT_MAP"];
1062
+ if (bootstrapMapRaw) {
1063
+ try {
1064
+ const parsed = JSON.parse(bootstrapMapRaw);
1065
+ for (const [testId, value] of Object.entries(parsed)) {
1066
+ setBootstrapInput(testId, value);
1067
+ }
1068
+ }
1069
+ catch (err) {
1070
+ console.log(JSON.stringify({
1071
+ type: "log",
1072
+ message: `Invalid GLUBEAN_RUNNER_BOOTSTRAP_INPUT_MAP JSON: ` +
1073
+ (err instanceof Error ? err.message : String(err)),
1074
+ }));
1075
+ }
1076
+ }
1077
+ const forceStandaloneRaw = process.env["GLUBEAN_RUNNER_FORCE_STANDALONE_IDS"];
1078
+ if (forceStandaloneRaw) {
1079
+ try {
1080
+ const parsed = JSON.parse(forceStandaloneRaw);
1081
+ if (Array.isArray(parsed)) {
1082
+ for (const testId of parsed) {
1083
+ if (typeof testId === "string")
1084
+ setForceStandalone(testId);
1085
+ }
1086
+ }
1087
+ }
1088
+ catch (err) {
1089
+ console.log(JSON.stringify({
1090
+ type: "log",
1091
+ message: `Invalid GLUBEAN_RUNNER_FORCE_STANDALONE_IDS JSON: ` +
1092
+ (err instanceof Error ? err.message : String(err)),
1093
+ }));
1094
+ }
1095
+ }
1016
1096
  // Dynamic import - LOAD phase
1017
1097
  console.log(JSON.stringify({
1018
1098
  type: "log",