@omnidev-ai/core 0.1.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/package.json +31 -0
- package/src/capability/AGENTS.md +58 -0
- package/src/capability/commands.test.ts +414 -0
- package/src/capability/commands.ts +70 -0
- package/src/capability/docs.test.ts +199 -0
- package/src/capability/docs.ts +46 -0
- package/src/capability/index.ts +20 -0
- package/src/capability/loader.test.ts +815 -0
- package/src/capability/loader.ts +492 -0
- package/src/capability/registry.test.ts +473 -0
- package/src/capability/registry.ts +55 -0
- package/src/capability/rules.test.ts +145 -0
- package/src/capability/rules.ts +133 -0
- package/src/capability/skills.test.ts +316 -0
- package/src/capability/skills.ts +56 -0
- package/src/capability/sources.test.ts +338 -0
- package/src/capability/sources.ts +966 -0
- package/src/capability/subagents.test.ts +478 -0
- package/src/capability/subagents.ts +103 -0
- package/src/capability/yaml-parser.ts +81 -0
- package/src/config/AGENTS.md +46 -0
- package/src/config/capabilities.ts +82 -0
- package/src/config/env.test.ts +286 -0
- package/src/config/env.ts +96 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +282 -0
- package/src/config/loader.ts +137 -0
- package/src/config/parser.test.ts +281 -0
- package/src/config/parser.ts +55 -0
- package/src/config/profiles.test.ts +259 -0
- package/src/config/profiles.ts +75 -0
- package/src/config/provider.test.ts +79 -0
- package/src/config/provider.ts +55 -0
- package/src/debug.ts +20 -0
- package/src/gitignore/manager.test.ts +219 -0
- package/src/gitignore/manager.ts +167 -0
- package/src/index.test.ts +26 -0
- package/src/index.ts +39 -0
- package/src/mcp-json/index.ts +1 -0
- package/src/mcp-json/manager.test.ts +415 -0
- package/src/mcp-json/manager.ts +118 -0
- package/src/state/active-profile.test.ts +131 -0
- package/src/state/active-profile.ts +41 -0
- package/src/state/index.ts +2 -0
- package/src/state/manifest.test.ts +548 -0
- package/src/state/manifest.ts +164 -0
- package/src/sync.ts +213 -0
- package/src/templates/agents.test.ts +23 -0
- package/src/templates/agents.ts +14 -0
- package/src/templates/claude.test.ts +48 -0
- package/src/templates/claude.ts +122 -0
- package/src/test-utils/helpers.test.ts +196 -0
- package/src/test-utils/helpers.ts +187 -0
- package/src/test-utils/index.ts +30 -0
- package/src/test-utils/mocks.test.ts +83 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/types/capability-export.ts +234 -0
- package/src/types/index.test.ts +28 -0
- package/src/types/index.ts +270 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { discoverCapabilities, loadCapability } from "../capability/loader.js";
|
|
2
|
+
import { addCapabilityPatterns, removeCapabilityPatterns } from "../gitignore/manager.js";
|
|
3
|
+
import { loadConfig, writeConfig } from "./loader.js";
|
|
4
|
+
import { getActiveProfile, resolveEnabledCapabilities } from "./profiles.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get enabled capabilities for the active profile
|
|
8
|
+
* Includes both profile-specific and always-enabled capabilities
|
|
9
|
+
* @returns Array of enabled capability IDs
|
|
10
|
+
*/
|
|
11
|
+
export async function getEnabledCapabilities(): Promise<string[]> {
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
|
|
14
|
+
return resolveEnabledCapabilities(config, activeProfile);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Enable a capability by adding it to the active profile's capabilities list
|
|
19
|
+
* Also adds the capability's gitignore patterns to .omni/.gitignore if present
|
|
20
|
+
* @param capabilityId - The ID of the capability to enable
|
|
21
|
+
*/
|
|
22
|
+
export async function enableCapability(capabilityId: string): Promise<void> {
|
|
23
|
+
const config = await loadConfig();
|
|
24
|
+
const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
|
|
25
|
+
|
|
26
|
+
if (!config.profiles) {
|
|
27
|
+
config.profiles = {};
|
|
28
|
+
}
|
|
29
|
+
if (!config.profiles[activeProfile]) {
|
|
30
|
+
config.profiles[activeProfile] = { capabilities: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const capabilities = new Set(config.profiles[activeProfile].capabilities ?? []);
|
|
34
|
+
capabilities.add(capabilityId);
|
|
35
|
+
config.profiles[activeProfile].capabilities = Array.from(capabilities);
|
|
36
|
+
|
|
37
|
+
await writeConfig(config);
|
|
38
|
+
|
|
39
|
+
// Add gitignore patterns if the capability exports them
|
|
40
|
+
try {
|
|
41
|
+
const capabilityPaths = await discoverCapabilities();
|
|
42
|
+
for (const path of capabilityPaths) {
|
|
43
|
+
const capability = await loadCapability(path, process.env as Record<string, string>);
|
|
44
|
+
if (capability.id === capabilityId && capability.gitignore) {
|
|
45
|
+
await addCapabilityPatterns(capabilityId, capability.gitignore);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// If we can't load the capability or add patterns, log but don't fail
|
|
51
|
+
// This allows enabling capabilities even if gitignore management fails
|
|
52
|
+
console.warn(`Warning: Could not add gitignore patterns for ${capabilityId}:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Disable a capability by removing it from the active profile's capabilities list
|
|
58
|
+
* Also removes the capability's gitignore patterns from .omni/.gitignore
|
|
59
|
+
* @param capabilityId - The ID of the capability to disable
|
|
60
|
+
*/
|
|
61
|
+
export async function disableCapability(capabilityId: string): Promise<void> {
|
|
62
|
+
const config = await loadConfig();
|
|
63
|
+
const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
|
|
64
|
+
|
|
65
|
+
if (!config.profiles?.[activeProfile]) {
|
|
66
|
+
return; // Nothing to disable
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const capabilities = new Set(config.profiles[activeProfile].capabilities ?? []);
|
|
70
|
+
capabilities.delete(capabilityId);
|
|
71
|
+
config.profiles[activeProfile].capabilities = Array.from(capabilities);
|
|
72
|
+
|
|
73
|
+
await writeConfig(config);
|
|
74
|
+
|
|
75
|
+
// Remove gitignore patterns
|
|
76
|
+
try {
|
|
77
|
+
await removeCapabilityPatterns(capabilityId);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// If we can't remove patterns, log but don't fail
|
|
80
|
+
console.warn(`Warning: Could not remove gitignore patterns for ${capabilityId}:`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import type { EnvDeclaration } from "../types";
|
|
4
|
+
import { isSecretEnvVar, loadEnvironment, validateEnv } from "./env";
|
|
5
|
+
|
|
6
|
+
describe("loadEnvironment", () => {
|
|
7
|
+
const originalCwd = process.cwd();
|
|
8
|
+
const testDir = "/tmp/omnidev-test-env";
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clean up and create test directory
|
|
12
|
+
if (existsSync(testDir)) {
|
|
13
|
+
rmSync(testDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
mkdirSync(testDir, { recursive: true });
|
|
16
|
+
process.chdir(testDir);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.chdir(originalCwd);
|
|
21
|
+
if (existsSync(testDir)) {
|
|
22
|
+
rmSync(testDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns empty object when no .omni/.env file exists", async () => {
|
|
27
|
+
const env = await loadEnvironment();
|
|
28
|
+
// Should only contain process.env vars, no custom ones
|
|
29
|
+
expect(env).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("loads environment variables from .omni/.env", async () => {
|
|
33
|
+
mkdirSync(".omni", { recursive: true });
|
|
34
|
+
writeFileSync(".omni/.env", "TEST_VAR=test_value\nANOTHER_VAR=another_value");
|
|
35
|
+
|
|
36
|
+
const env = await loadEnvironment();
|
|
37
|
+
|
|
38
|
+
expect(env.TEST_VAR).toBe("test_value");
|
|
39
|
+
expect(env.ANOTHER_VAR).toBe("another_value");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("skips empty lines in .env file", async () => {
|
|
43
|
+
mkdirSync(".omni", { recursive: true });
|
|
44
|
+
writeFileSync(".omni/.env", "VAR1=value1\n\n\nVAR2=value2");
|
|
45
|
+
|
|
46
|
+
const env = await loadEnvironment();
|
|
47
|
+
|
|
48
|
+
expect(env.VAR1).toBe("value1");
|
|
49
|
+
expect(env.VAR2).toBe("value2");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("skips comment lines in .env file", async () => {
|
|
53
|
+
mkdirSync(".omni", { recursive: true });
|
|
54
|
+
writeFileSync(".omni/.env", "# This is a comment\nVAR1=value1\n# Another comment\nVAR2=value2");
|
|
55
|
+
|
|
56
|
+
const env = await loadEnvironment();
|
|
57
|
+
|
|
58
|
+
expect(env.VAR1).toBe("value1");
|
|
59
|
+
expect(env.VAR2).toBe("value2");
|
|
60
|
+
expect(env["# This is a comment"]).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("handles values with equals signs", async () => {
|
|
64
|
+
mkdirSync(".omni", { recursive: true });
|
|
65
|
+
writeFileSync(".omni/.env", "DATABASE_URL=postgres://user:pass@localhost:5432/db?param=value");
|
|
66
|
+
|
|
67
|
+
const env = await loadEnvironment();
|
|
68
|
+
|
|
69
|
+
expect(env.DATABASE_URL).toBe("postgres://user:pass@localhost:5432/db?param=value");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("handles quoted values", async () => {
|
|
73
|
+
mkdirSync(".omni", { recursive: true });
|
|
74
|
+
writeFileSync(
|
|
75
|
+
".omni/.env",
|
|
76
|
+
"SINGLE_QUOTED='single value'\nDOUBLE_QUOTED=\"double value\"\nUNQUOTED=unquoted value",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const env = await loadEnvironment();
|
|
80
|
+
|
|
81
|
+
expect(env.SINGLE_QUOTED).toBe("single value");
|
|
82
|
+
expect(env.DOUBLE_QUOTED).toBe("double value");
|
|
83
|
+
expect(env.UNQUOTED).toBe("unquoted value");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("trims whitespace around keys and values", async () => {
|
|
87
|
+
mkdirSync(".omni", { recursive: true });
|
|
88
|
+
writeFileSync(".omni/.env", " VAR1 = value1 \n\tVAR2\t=\tvalue2\t");
|
|
89
|
+
|
|
90
|
+
const env = await loadEnvironment();
|
|
91
|
+
|
|
92
|
+
expect(env.VAR1).toBe("value1");
|
|
93
|
+
expect(env.VAR2).toBe("value2");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("process.env takes precedence over .env file", async () => {
|
|
97
|
+
mkdirSync(".omni", { recursive: true });
|
|
98
|
+
writeFileSync(".omni/.env", "TEST_VAR=file_value");
|
|
99
|
+
|
|
100
|
+
// Set process env
|
|
101
|
+
process.env.TEST_VAR = "process_value";
|
|
102
|
+
|
|
103
|
+
const env = await loadEnvironment();
|
|
104
|
+
|
|
105
|
+
expect(env.TEST_VAR).toBe("process_value");
|
|
106
|
+
|
|
107
|
+
// Clean up
|
|
108
|
+
delete process.env.TEST_VAR;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("includes all process.env variables", async () => {
|
|
112
|
+
process.env.CUSTOM_VAR = "custom_value";
|
|
113
|
+
|
|
114
|
+
const env = await loadEnvironment();
|
|
115
|
+
|
|
116
|
+
expect(env.CUSTOM_VAR).toBe("custom_value");
|
|
117
|
+
|
|
118
|
+
// Clean up
|
|
119
|
+
delete process.env.CUSTOM_VAR;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("handles malformed lines gracefully", async () => {
|
|
123
|
+
mkdirSync(".omni", { recursive: true });
|
|
124
|
+
writeFileSync(
|
|
125
|
+
".omni/.env",
|
|
126
|
+
"VALID_VAR=value\nMALFORMED_LINE_NO_EQUALS\n=NO_KEY\nANOTHER_VAR=value2",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const env = await loadEnvironment();
|
|
130
|
+
|
|
131
|
+
expect(env.VALID_VAR).toBe("value");
|
|
132
|
+
expect(env.ANOTHER_VAR).toBe("value2");
|
|
133
|
+
expect(env.MALFORMED_LINE_NO_EQUALS).toBeUndefined();
|
|
134
|
+
expect(env[""]).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("validateEnv", () => {
|
|
139
|
+
test("passes validation when all required vars are present", () => {
|
|
140
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
141
|
+
API_KEY: { required: true },
|
|
142
|
+
DATABASE_URL: { required: true },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const env = {
|
|
146
|
+
API_KEY: "test-key",
|
|
147
|
+
DATABASE_URL: "postgres://localhost",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("passes validation when required var has default value", () => {
|
|
154
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
155
|
+
PORT: { required: true, default: "3000" },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const env = {};
|
|
159
|
+
|
|
160
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("throws error when required var is missing", () => {
|
|
164
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
165
|
+
API_KEY: { required: true },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const env = {};
|
|
169
|
+
|
|
170
|
+
expect(() => validateEnv(declarations, env, "test-capability")).toThrow(
|
|
171
|
+
'Missing required environment variable for capability "test-capability": API_KEY. Set it in .omni/.env or as environment variable.',
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("throws error with multiple missing vars", () => {
|
|
176
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
177
|
+
API_KEY: { required: true },
|
|
178
|
+
DATABASE_URL: { required: true },
|
|
179
|
+
SECRET_KEY: { required: true },
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const env = {};
|
|
183
|
+
|
|
184
|
+
expect(() => validateEnv(declarations, env, "test-capability")).toThrow(
|
|
185
|
+
'Missing required environment variables for capability "test-capability": API_KEY, DATABASE_URL, SECRET_KEY',
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("passes validation when optional vars are missing", () => {
|
|
190
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
191
|
+
API_KEY: { required: true },
|
|
192
|
+
OPTIONAL_VAR: { required: false },
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const env = {
|
|
196
|
+
API_KEY: "test-key",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("passes validation when no declarations are provided", () => {
|
|
203
|
+
const declarations: Record<string, EnvDeclaration> = {};
|
|
204
|
+
const env = {};
|
|
205
|
+
|
|
206
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("handles empty object declarations", () => {
|
|
210
|
+
const declarations: Record<string, EnvDeclaration | Record<string, never>> = {
|
|
211
|
+
VAR1: {},
|
|
212
|
+
VAR2: { required: true },
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const env = {
|
|
216
|
+
VAR2: "value",
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("uses default value when env var is missing", () => {
|
|
223
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
224
|
+
PORT: { required: true, default: "3000" },
|
|
225
|
+
HOST: { required: true, default: "localhost" },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const env = {
|
|
229
|
+
PORT: "8080", // Override default
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
expect(() => validateEnv(declarations, env, "test-capability")).not.toThrow();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("isSecretEnvVar", () => {
|
|
237
|
+
test("returns true when var is marked as secret", () => {
|
|
238
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
239
|
+
API_KEY: { secret: true },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect(isSecretEnvVar("API_KEY", declarations)).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("returns false when var is not marked as secret", () => {
|
|
246
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
247
|
+
PORT: { secret: false },
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
expect(isSecretEnvVar("PORT", declarations)).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("returns false when var is not declared", () => {
|
|
254
|
+
const declarations: Record<string, EnvDeclaration> = {};
|
|
255
|
+
|
|
256
|
+
expect(isSecretEnvVar("UNKNOWN_VAR", declarations)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("returns false when secret field is not set", () => {
|
|
260
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
261
|
+
DATABASE_URL: { required: true },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
expect(isSecretEnvVar("DATABASE_URL", declarations)).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("handles empty object declarations", () => {
|
|
268
|
+
const declarations: Record<string, EnvDeclaration | Record<string, never>> = {
|
|
269
|
+
VAR1: {},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
expect(isSecretEnvVar("VAR1", declarations)).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("returns true only when secret is explicitly true", () => {
|
|
276
|
+
const declarations: Record<string, EnvDeclaration> = {
|
|
277
|
+
SECRET_KEY: { secret: true, required: true },
|
|
278
|
+
API_KEY: { required: true },
|
|
279
|
+
PORT: { secret: false },
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
expect(isSecretEnvVar("SECRET_KEY", declarations)).toBe(true);
|
|
283
|
+
expect(isSecretEnvVar("API_KEY", declarations)).toBe(false);
|
|
284
|
+
expect(isSecretEnvVar("PORT", declarations)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import type { EnvDeclaration } from "../types";
|
|
3
|
+
|
|
4
|
+
const ENV_FILE = ".omni/.env";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Load environment variables from .omni/.env file and merge with process.env.
|
|
8
|
+
* Process environment variables take precedence over file values.
|
|
9
|
+
*
|
|
10
|
+
* @returns Merged environment variables
|
|
11
|
+
*/
|
|
12
|
+
export async function loadEnvironment(): Promise<Record<string, string>> {
|
|
13
|
+
const env: Record<string, string> = {};
|
|
14
|
+
|
|
15
|
+
// Load from .omni/.env
|
|
16
|
+
if (existsSync(ENV_FILE)) {
|
|
17
|
+
const content = await Bun.file(ENV_FILE).text();
|
|
18
|
+
for (const line of content.split("\n")) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
// Skip empty lines and comments
|
|
21
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
22
|
+
const eqIndex = trimmed.indexOf("=");
|
|
23
|
+
if (eqIndex > 0) {
|
|
24
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
25
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
26
|
+
// Remove quotes if present
|
|
27
|
+
const unquotedValue =
|
|
28
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
29
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
30
|
+
? value.slice(1, -1)
|
|
31
|
+
: value;
|
|
32
|
+
env[key] = unquotedValue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Process env takes precedence - filter out undefined values
|
|
39
|
+
const processEnv: Record<string, string> = {};
|
|
40
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
41
|
+
if (value !== undefined) {
|
|
42
|
+
processEnv[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { ...env, ...processEnv };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate that all required environment variables are present.
|
|
51
|
+
* Checks declarations from capability config and throws descriptive errors for missing vars.
|
|
52
|
+
*
|
|
53
|
+
* @param declarations - Environment variable declarations from capability.toml
|
|
54
|
+
* @param env - Loaded environment variables
|
|
55
|
+
* @param capabilityId - ID of the capability being validated
|
|
56
|
+
* @throws Error if required environment variables are missing
|
|
57
|
+
*/
|
|
58
|
+
export function validateEnv(
|
|
59
|
+
declarations: Record<string, EnvDeclaration | Record<string, never>>,
|
|
60
|
+
env: Record<string, string | undefined>,
|
|
61
|
+
capabilityId: string,
|
|
62
|
+
): void {
|
|
63
|
+
const missing: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (const [key, decl] of Object.entries(declarations)) {
|
|
66
|
+
const declaration = decl as EnvDeclaration;
|
|
67
|
+
const value = env[key] ?? declaration.default;
|
|
68
|
+
|
|
69
|
+
if (declaration.required && !value) {
|
|
70
|
+
missing.push(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (missing.length > 0) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Missing required environment variable${missing.length > 1 ? "s" : ""} for capability "${capabilityId}": ${missing.join(", ")}. ` +
|
|
77
|
+
`Set ${missing.length > 1 ? "them" : "it"} in .omni/.env or as environment variable${missing.length > 1 ? "s" : ""}.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if an environment variable should be treated as a secret.
|
|
84
|
+
* Secrets should be masked in logs and error messages.
|
|
85
|
+
*
|
|
86
|
+
* @param key - Environment variable name
|
|
87
|
+
* @param declarations - Environment variable declarations from capability.toml
|
|
88
|
+
* @returns true if the variable is marked as secret
|
|
89
|
+
*/
|
|
90
|
+
export function isSecretEnvVar(
|
|
91
|
+
key: string,
|
|
92
|
+
declarations: Record<string, EnvDeclaration | Record<string, never>>,
|
|
93
|
+
): boolean {
|
|
94
|
+
const decl = declarations[key] as EnvDeclaration | undefined;
|
|
95
|
+
return decl?.secret === true;
|
|
96
|
+
}
|