@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.
Files changed (59) hide show
  1. package/package.json +31 -0
  2. package/src/capability/AGENTS.md +58 -0
  3. package/src/capability/commands.test.ts +414 -0
  4. package/src/capability/commands.ts +70 -0
  5. package/src/capability/docs.test.ts +199 -0
  6. package/src/capability/docs.ts +46 -0
  7. package/src/capability/index.ts +20 -0
  8. package/src/capability/loader.test.ts +815 -0
  9. package/src/capability/loader.ts +492 -0
  10. package/src/capability/registry.test.ts +473 -0
  11. package/src/capability/registry.ts +55 -0
  12. package/src/capability/rules.test.ts +145 -0
  13. package/src/capability/rules.ts +133 -0
  14. package/src/capability/skills.test.ts +316 -0
  15. package/src/capability/skills.ts +56 -0
  16. package/src/capability/sources.test.ts +338 -0
  17. package/src/capability/sources.ts +966 -0
  18. package/src/capability/subagents.test.ts +478 -0
  19. package/src/capability/subagents.ts +103 -0
  20. package/src/capability/yaml-parser.ts +81 -0
  21. package/src/config/AGENTS.md +46 -0
  22. package/src/config/capabilities.ts +82 -0
  23. package/src/config/env.test.ts +286 -0
  24. package/src/config/env.ts +96 -0
  25. package/src/config/index.ts +6 -0
  26. package/src/config/loader.test.ts +282 -0
  27. package/src/config/loader.ts +137 -0
  28. package/src/config/parser.test.ts +281 -0
  29. package/src/config/parser.ts +55 -0
  30. package/src/config/profiles.test.ts +259 -0
  31. package/src/config/profiles.ts +75 -0
  32. package/src/config/provider.test.ts +79 -0
  33. package/src/config/provider.ts +55 -0
  34. package/src/debug.ts +20 -0
  35. package/src/gitignore/manager.test.ts +219 -0
  36. package/src/gitignore/manager.ts +167 -0
  37. package/src/index.test.ts +26 -0
  38. package/src/index.ts +39 -0
  39. package/src/mcp-json/index.ts +1 -0
  40. package/src/mcp-json/manager.test.ts +415 -0
  41. package/src/mcp-json/manager.ts +118 -0
  42. package/src/state/active-profile.test.ts +131 -0
  43. package/src/state/active-profile.ts +41 -0
  44. package/src/state/index.ts +2 -0
  45. package/src/state/manifest.test.ts +548 -0
  46. package/src/state/manifest.ts +164 -0
  47. package/src/sync.ts +213 -0
  48. package/src/templates/agents.test.ts +23 -0
  49. package/src/templates/agents.ts +14 -0
  50. package/src/templates/claude.test.ts +48 -0
  51. package/src/templates/claude.ts +122 -0
  52. package/src/test-utils/helpers.test.ts +196 -0
  53. package/src/test-utils/helpers.ts +187 -0
  54. package/src/test-utils/index.ts +30 -0
  55. package/src/test-utils/mocks.test.ts +83 -0
  56. package/src/test-utils/mocks.ts +101 -0
  57. package/src/types/capability-export.ts +234 -0
  58. package/src/types/index.test.ts +28 -0
  59. 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
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./capabilities";
2
+ export * from "./env";
3
+ export * from "./loader";
4
+ export * from "./parser";
5
+ export * from "./profiles";
6
+ export * from "./provider";