@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,282 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { loadConfig } from "./loader";
4
+
5
+ const TEST_DIR = "/tmp/omnidev-test-loader";
6
+ const CONFIG_PATH = "omni.toml";
7
+ const LOCAL_CONFIG = "omni.local.toml";
8
+
9
+ // Save and restore the current working directory
10
+ let originalCwd: string;
11
+
12
+ beforeEach(() => {
13
+ // Save original cwd
14
+ originalCwd = process.cwd();
15
+
16
+ // Clean up test directory
17
+ if (existsSync(TEST_DIR)) {
18
+ rmSync(TEST_DIR, { recursive: true });
19
+ }
20
+ mkdirSync(TEST_DIR, { recursive: true });
21
+ process.chdir(TEST_DIR);
22
+ });
23
+
24
+ afterEach(() => {
25
+ // Restore original cwd
26
+ process.chdir(originalCwd);
27
+
28
+ // Clean up test directory
29
+ if (existsSync(TEST_DIR)) {
30
+ rmSync(TEST_DIR, { recursive: true });
31
+ }
32
+ });
33
+
34
+ describe("loadConfig", () => {
35
+ test("returns empty config when no files exist", async () => {
36
+ const config = await loadConfig();
37
+ expect(config).toEqual({
38
+ env: {},
39
+ profiles: {},
40
+ });
41
+ });
42
+
43
+ test("loads config when only main config exists", async () => {
44
+ mkdirSync(".omni", { recursive: true });
45
+ writeFileSync(
46
+ CONFIG_PATH,
47
+ `
48
+ project = "my-project"
49
+ active_profile = "dev"
50
+
51
+ [profiles.dev]
52
+ capabilities = ["tasks", "git"]
53
+ `,
54
+ );
55
+
56
+ const config = await loadConfig();
57
+ expect(config.project).toBe("my-project");
58
+ expect(config.active_profile).toBe("dev");
59
+ expect(config.profiles?.dev?.capabilities).toEqual(["tasks", "git"]);
60
+ });
61
+
62
+ test("loads local config when only local config exists", async () => {
63
+ mkdirSync(".omni", { recursive: true });
64
+ writeFileSync(
65
+ LOCAL_CONFIG,
66
+ `
67
+ project = "local-project"
68
+
69
+ [profiles.default]
70
+ capabilities = ["local-only"]
71
+ `,
72
+ );
73
+
74
+ const config = await loadConfig();
75
+ expect(config.project).toBe("local-project");
76
+ expect(config.profiles?.default?.capabilities).toEqual(["local-only"]);
77
+ });
78
+
79
+ test("merges main and local configs with local taking precedence", async () => {
80
+ mkdirSync(".omni", { recursive: true });
81
+ mkdirSync(".omni", { recursive: true });
82
+
83
+ writeFileSync(
84
+ CONFIG_PATH,
85
+ `
86
+ project = "main-project"
87
+ active_profile = "production"
88
+
89
+ [profiles.default]
90
+ capabilities = ["tasks"]
91
+
92
+ [env]
93
+ API_URL = "https://main-api.com"
94
+ `,
95
+ );
96
+
97
+ writeFileSync(
98
+ LOCAL_CONFIG,
99
+ `
100
+ project = "local-override"
101
+
102
+ [profiles.default]
103
+ capabilities = ["git"]
104
+
105
+ [env]
106
+ API_URL = "http://localhost:3000"
107
+ DEBUG = "true"
108
+ `,
109
+ );
110
+
111
+ const config = await loadConfig();
112
+
113
+ // Local overrides should take precedence
114
+ expect(config.project).toBe("local-override");
115
+
116
+ // Profile capabilities from local should override main
117
+ expect(config.profiles?.default?.capabilities).toEqual(["git"]);
118
+
119
+ // Env should be merged with local taking precedence
120
+ expect(config.env?.API_URL).toBe("http://localhost:3000");
121
+ expect(config.env?.DEBUG).toBe("true");
122
+ });
123
+
124
+ test("merges profiles with local taking precedence", async () => {
125
+ mkdirSync(".omni", { recursive: true });
126
+ mkdirSync(".omni", { recursive: true });
127
+
128
+ writeFileSync(
129
+ CONFIG_PATH,
130
+ `
131
+ [profiles.dev]
132
+ capabilities = ["tasks"]
133
+
134
+ [profiles.prod]
135
+ capabilities = ["git"]
136
+ `,
137
+ );
138
+
139
+ writeFileSync(
140
+ LOCAL_CONFIG,
141
+ `
142
+ [profiles.dev]
143
+ capabilities = ["local-tasks"]
144
+ `,
145
+ );
146
+
147
+ const config = await loadConfig();
148
+ expect(config.profiles?.dev?.capabilities).toEqual(["local-tasks"]);
149
+ expect(config.profiles?.prod?.capabilities).toEqual(["git"]);
150
+ });
151
+
152
+ test("handles empty profiles sections gracefully", async () => {
153
+ mkdirSync(".omni", { recursive: true });
154
+ writeFileSync(
155
+ CONFIG_PATH,
156
+ `
157
+ project = "test"
158
+ `,
159
+ );
160
+
161
+ const config = await loadConfig();
162
+ expect(config.profiles).toEqual({});
163
+ });
164
+
165
+ test("handles invalid TOML in main config", async () => {
166
+ mkdirSync(".omni", { recursive: true });
167
+ writeFileSync(CONFIG_PATH, "invalid toml [[[");
168
+
169
+ await expect(loadConfig()).rejects.toThrow("Invalid TOML in config");
170
+ });
171
+
172
+ test("handles invalid TOML in local config", async () => {
173
+ mkdirSync(".omni", { recursive: true });
174
+ writeFileSync(LOCAL_CONFIG, "invalid toml [[[");
175
+
176
+ await expect(loadConfig()).rejects.toThrow("Invalid TOML in config");
177
+ });
178
+
179
+ test("merges env objects correctly", async () => {
180
+ mkdirSync(".omni", { recursive: true });
181
+ mkdirSync(".omni", { recursive: true });
182
+
183
+ writeFileSync(
184
+ CONFIG_PATH,
185
+ `
186
+ [env]
187
+ VAR1 = "team1"
188
+ VAR2 = "team2"
189
+ `,
190
+ );
191
+
192
+ writeFileSync(
193
+ LOCAL_CONFIG,
194
+ `
195
+ [env]
196
+ VAR2 = "local2"
197
+ VAR3 = "local3"
198
+ `,
199
+ );
200
+
201
+ const config = await loadConfig();
202
+ expect(config.env?.VAR1).toBe("team1");
203
+ expect(config.env?.VAR2).toBe("local2");
204
+ expect(config.env?.VAR3).toBe("local3");
205
+ });
206
+
207
+ test("reads active_profile from config.toml for backwards compatibility", async () => {
208
+ mkdirSync(".omni", { recursive: true });
209
+
210
+ writeFileSync(
211
+ CONFIG_PATH,
212
+ `
213
+ active_profile = "production"
214
+ `,
215
+ );
216
+
217
+ const config = await loadConfig();
218
+ // active_profile is still readable from config.toml for backwards compatibility
219
+ // but new writes go to state file via setActiveProfile()
220
+ expect(config.active_profile).toBe("production");
221
+ });
222
+
223
+ test("loads sandbox_enabled = true from config", async () => {
224
+ mkdirSync(".omni", { recursive: true });
225
+ writeFileSync(
226
+ CONFIG_PATH,
227
+ `
228
+ sandbox_enabled = true
229
+ `,
230
+ );
231
+
232
+ const config = await loadConfig();
233
+ expect(config.sandbox_enabled).toBe(true);
234
+ });
235
+
236
+ test("loads sandbox_enabled = false from config", async () => {
237
+ mkdirSync(".omni", { recursive: true });
238
+ writeFileSync(
239
+ CONFIG_PATH,
240
+ `
241
+ sandbox_enabled = false
242
+ `,
243
+ );
244
+
245
+ const config = await loadConfig();
246
+ expect(config.sandbox_enabled).toBe(false);
247
+ });
248
+
249
+ test("sandbox_enabled is undefined when not specified", async () => {
250
+ mkdirSync(".omni", { recursive: true });
251
+ writeFileSync(
252
+ CONFIG_PATH,
253
+ `
254
+ project = "test"
255
+ `,
256
+ );
257
+
258
+ const config = await loadConfig();
259
+ expect(config.sandbox_enabled).toBeUndefined();
260
+ });
261
+
262
+ test("local config can override sandbox_enabled", async () => {
263
+ mkdirSync(".omni", { recursive: true });
264
+
265
+ writeFileSync(
266
+ CONFIG_PATH,
267
+ `
268
+ sandbox_enabled = true
269
+ `,
270
+ );
271
+
272
+ writeFileSync(
273
+ LOCAL_CONFIG,
274
+ `
275
+ sandbox_enabled = false
276
+ `,
277
+ );
278
+
279
+ const config = await loadConfig();
280
+ expect(config.sandbox_enabled).toBe(false);
281
+ });
282
+ });
@@ -0,0 +1,137 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { OmniConfig } from "../types";
3
+ import { parseOmniConfig } from "./parser";
4
+
5
+ const CONFIG_PATH = "omni.toml";
6
+ const LOCAL_CONFIG = "omni.local.toml";
7
+
8
+ /**
9
+ * Deep merge two config objects, with override taking precedence
10
+ * @param base - The base config object
11
+ * @param override - The override config object
12
+ * @returns Merged config with override values taking precedence
13
+ */
14
+ function mergeConfigs(base: OmniConfig, override: OmniConfig): OmniConfig {
15
+ const merged: OmniConfig = { ...base, ...override };
16
+
17
+ // Deep merge env
18
+ merged.env = { ...base.env, ...override.env };
19
+
20
+ // Deep merge profiles
21
+ merged.profiles = { ...base.profiles };
22
+ for (const [name, profile] of Object.entries(override.profiles || {})) {
23
+ merged.profiles[name] = {
24
+ ...(base.profiles?.[name] || {}),
25
+ ...profile,
26
+ };
27
+ }
28
+
29
+ return merged;
30
+ }
31
+
32
+ /**
33
+ * Load and merge config and local configuration files
34
+ * @returns Merged OmniConfig object
35
+ *
36
+ * Reads omni.toml (main config) and omni.local.toml (local overrides).
37
+ * Local config takes precedence over main config. Missing files are treated as empty configs.
38
+ */
39
+ export async function loadConfig(): Promise<OmniConfig> {
40
+ let baseConfig: OmniConfig = {};
41
+ let localConfig: OmniConfig = {};
42
+
43
+ if (existsSync(CONFIG_PATH)) {
44
+ const content = await Bun.file(CONFIG_PATH).text();
45
+ baseConfig = parseOmniConfig(content);
46
+ }
47
+
48
+ if (existsSync(LOCAL_CONFIG)) {
49
+ const content = await Bun.file(LOCAL_CONFIG).text();
50
+ localConfig = parseOmniConfig(content);
51
+ }
52
+
53
+ return mergeConfigs(baseConfig, localConfig);
54
+ }
55
+
56
+ /**
57
+ * Write config to omni.toml at project root
58
+ * @param config - The config object to write
59
+ */
60
+ export async function writeConfig(config: OmniConfig): Promise<void> {
61
+ const content = generateConfigToml(config);
62
+ await Bun.write(CONFIG_PATH, content);
63
+ }
64
+
65
+ /**
66
+ * Generate TOML content for OmniConfig
67
+ * @param config - The config object
68
+ * @returns TOML string
69
+ */
70
+ function generateConfigToml(config: OmniConfig): string {
71
+ const lines: string[] = [];
72
+
73
+ lines.push("# OmniDev Configuration");
74
+ lines.push("# Main configuration for your OmniDev project");
75
+ lines.push("");
76
+
77
+ // Project name
78
+ if (config.project) {
79
+ lines.push(`project = "${config.project}"`);
80
+ }
81
+
82
+ // Note: active_profile is stored in .omni/state/active-profile, not in config.toml
83
+ // We still read it from config.toml for backwards compatibility, but don't write it here
84
+
85
+ // Sandbox mode
86
+ if (config.sandbox_enabled !== undefined) {
87
+ lines.push(`sandbox_enabled = ${config.sandbox_enabled}`);
88
+ }
89
+
90
+ lines.push("");
91
+
92
+ // Providers
93
+ if (config.providers?.enabled && config.providers.enabled.length > 0) {
94
+ lines.push("[providers]");
95
+ lines.push(`enabled = [${config.providers.enabled.map((p) => `"${p}"`).join(", ")}]`);
96
+ lines.push("");
97
+ }
98
+
99
+ // Environment variables
100
+ if (config.env && Object.keys(config.env).length > 0) {
101
+ lines.push("[env]");
102
+ for (const [key, value] of Object.entries(config.env)) {
103
+ lines.push(`${key} = "${value}"`);
104
+ }
105
+ lines.push("");
106
+ }
107
+
108
+ // Capability sources (commented examples)
109
+ lines.push("# =============================================================================");
110
+ lines.push("# Capability Sources");
111
+ lines.push("# =============================================================================");
112
+ lines.push("# Fetch capabilities from Git repositories. On sync, these are cloned/updated");
113
+ lines.push("# and wrapped into capabilities automatically.");
114
+ lines.push("#");
115
+ lines.push("# [capabilities.sources]");
116
+ lines.push("# # Simple GitHub reference (auto-wrapped if no capability.toml)");
117
+ lines.push('# obsidian = "github:kepano/obsidian-skills"');
118
+ lines.push("#");
119
+ lines.push("# # Full configuration with version pinning");
120
+ lines.push('# my-cap = { source = "github:user/repo", ref = "v1.0.0" }');
121
+ lines.push("#");
122
+ lines.push("# # Force wrap mode (generate capability.toml from discovered content)");
123
+ lines.push('# external = { source = "github:user/skills-repo", type = "wrap" }');
124
+ lines.push("");
125
+
126
+ // Profiles
127
+ if (config.profiles && Object.keys(config.profiles).length > 0) {
128
+ for (const [name, profile] of Object.entries(config.profiles)) {
129
+ lines.push(`[profiles.${name}]`);
130
+ const capabilities = profile.capabilities ?? [];
131
+ lines.push(`capabilities = [${capabilities.map((id) => `"${id}"`).join(", ")}]`);
132
+ lines.push("");
133
+ }
134
+ }
135
+
136
+ return lines.join("\n");
137
+ }
@@ -0,0 +1,281 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseCapabilityConfig, parseOmniConfig } from "./parser";
3
+
4
+ describe("parseOmniConfig", () => {
5
+ test("parses valid TOML with all fields", () => {
6
+ const toml = `
7
+ project = "my-project"
8
+ default_profile = "dev"
9
+
10
+ [capabilities]
11
+ enable = ["tasks", "git"]
12
+ disable = ["docker"]
13
+
14
+ [env]
15
+ API_URL = "https://api.example.com"
16
+
17
+ [profiles.dev]
18
+ enable = ["debug"]
19
+ disable = []
20
+
21
+ [profiles.prod]
22
+ enable = []
23
+ disable = ["debug"]
24
+ `;
25
+
26
+ const config = parseOmniConfig(toml);
27
+
28
+ expect(config.project).toBe("my-project");
29
+ expect(config.default_profile).toBe("dev");
30
+ expect(config.capabilities?.enable).toEqual(["tasks", "git"]);
31
+ expect(config.capabilities?.disable).toEqual(["docker"]);
32
+ expect(config.env?.API_URL).toBe("https://api.example.com");
33
+ expect(config.profiles?.dev?.enable).toEqual(["debug"]);
34
+ expect(config.profiles?.prod?.disable).toEqual(["debug"]);
35
+ });
36
+
37
+ test("parses minimal TOML", () => {
38
+ const toml = `
39
+ project = "minimal"
40
+ `;
41
+
42
+ const config = parseOmniConfig(toml);
43
+
44
+ expect(config.project).toBe("minimal");
45
+ expect(config.capabilities).toBeUndefined();
46
+ expect(config.profiles).toBeUndefined();
47
+ });
48
+
49
+ test("parses empty TOML", () => {
50
+ const config = parseOmniConfig("");
51
+
52
+ expect(config).toEqual({});
53
+ });
54
+
55
+ test("parses TOML with arrays", () => {
56
+ const toml = `
57
+ [capabilities]
58
+ enable = ["cap1", "cap2", "cap3"]
59
+ `;
60
+
61
+ const config = parseOmniConfig(toml);
62
+
63
+ expect(config.capabilities?.enable).toEqual(["cap1", "cap2", "cap3"]);
64
+ });
65
+
66
+ test("parses TOML with nested tables", () => {
67
+ const toml = `
68
+ [profiles.dev]
69
+ enable = ["debug"]
70
+
71
+ [profiles.prod]
72
+ disable = ["debug"]
73
+ `;
74
+
75
+ const config = parseOmniConfig(toml);
76
+
77
+ expect(config.profiles?.dev?.enable).toEqual(["debug"]);
78
+ expect(config.profiles?.prod?.disable).toEqual(["debug"]);
79
+ });
80
+
81
+ test("throws error for invalid TOML syntax", () => {
82
+ const toml = `
83
+ project = "test
84
+ [invalid
85
+ `;
86
+
87
+ expect(() => parseOmniConfig(toml)).toThrow(/Invalid TOML in config:/);
88
+ });
89
+
90
+ test("throws error for duplicate keys", () => {
91
+ const toml = `
92
+ project = "test"
93
+ project = "duplicate"
94
+ `;
95
+
96
+ expect(() => parseOmniConfig(toml)).toThrow(/Invalid TOML in config:/);
97
+ });
98
+
99
+ test("handles boolean values", () => {
100
+ const toml = `
101
+ [capabilities]
102
+ debug = true
103
+ production = false
104
+ `;
105
+
106
+ const config = parseOmniConfig(toml);
107
+
108
+ expect((config.capabilities as Record<string, unknown>)?.debug).toBe(true);
109
+ expect((config.capabilities as Record<string, unknown>)?.production).toBe(false);
110
+ });
111
+
112
+ test("handles numeric values", () => {
113
+ const toml = `
114
+ timeout = 30
115
+ max_retries = 5
116
+ `;
117
+
118
+ const config = parseOmniConfig(toml);
119
+
120
+ expect((config as Record<string, unknown>).timeout).toBe(30);
121
+ expect((config as Record<string, unknown>).max_retries).toBe(5);
122
+ });
123
+ });
124
+
125
+ describe("parseCapabilityConfig", () => {
126
+ test("parses valid capability.toml with all required fields", () => {
127
+ const toml = `
128
+ [capability]
129
+ id = "tasks"
130
+ name = "Task Management"
131
+ version = "1.0.0"
132
+ description = "Manage tasks and workflows"
133
+ `;
134
+
135
+ const config = parseCapabilityConfig(toml);
136
+
137
+ expect(config.capability.id).toBe("tasks");
138
+ expect(config.capability.name).toBe("Task Management");
139
+ expect(config.capability.version).toBe("1.0.0");
140
+ expect(config.capability.description).toBe("Manage tasks and workflows");
141
+ });
142
+
143
+ test("parses capability with exports", () => {
144
+ const toml = `
145
+ [capability]
146
+ id = "tasks"
147
+ name = "Task Management"
148
+ version = "1.0.0"
149
+ description = "Manage tasks"
150
+
151
+ [exports]
152
+ module = "index.ts"
153
+ `;
154
+
155
+ const config = parseCapabilityConfig(toml);
156
+
157
+ expect(config.exports?.module).toBe("index.ts");
158
+ });
159
+
160
+ test("parses capability with env declarations", () => {
161
+ const toml = `
162
+ [capability]
163
+ id = "api"
164
+ name = "API Client"
165
+ version = "1.0.0"
166
+ description = "API access"
167
+
168
+ [env.API_KEY]
169
+ required = true
170
+ secret = true
171
+
172
+ [env.API_URL]
173
+ required = false
174
+ default = "https://api.example.com"
175
+ `;
176
+
177
+ const config = parseCapabilityConfig(toml);
178
+
179
+ expect(config.env?.API_KEY).toEqual({ required: true, secret: true });
180
+ expect(config.env?.API_URL).toEqual({
181
+ required: false,
182
+ default: "https://api.example.com",
183
+ });
184
+ });
185
+
186
+ test("parses capability with MCP config", () => {
187
+ const toml = `
188
+ [capability]
189
+ id = "custom"
190
+ name = "Custom Capability"
191
+ version = "1.0.0"
192
+ description = "Custom MCP"
193
+
194
+ [mcp]
195
+ command = "node"
196
+ args = ["server.js"]
197
+ transport = "stdio"
198
+
199
+ [mcp.env]
200
+ PORT = "3000"
201
+ `;
202
+
203
+ const config = parseCapabilityConfig(toml);
204
+
205
+ expect(config.mcp?.command).toBe("node");
206
+ expect(config.mcp?.args).toEqual(["server.js"]);
207
+ expect(config.mcp?.transport).toBe("stdio");
208
+ expect(config.mcp?.env?.PORT).toBe("3000");
209
+ });
210
+
211
+ test("throws error when capability.id is missing", () => {
212
+ const toml = `
213
+ [capability]
214
+ name = "Test"
215
+ version = "1.0.0"
216
+ `;
217
+
218
+ expect(() => parseCapabilityConfig(toml)).toThrow(
219
+ /capability.id is required in capability.toml/,
220
+ );
221
+ });
222
+
223
+ test("throws error when capability.name is missing", () => {
224
+ const toml = `
225
+ [capability]
226
+ id = "test"
227
+ version = "1.0.0"
228
+ `;
229
+
230
+ expect(() => parseCapabilityConfig(toml)).toThrow(
231
+ /capability.name is required in capability.toml/,
232
+ );
233
+ });
234
+
235
+ test("throws error when capability.version is missing", () => {
236
+ const toml = `
237
+ [capability]
238
+ id = "test"
239
+ name = "Test"
240
+ `;
241
+
242
+ expect(() => parseCapabilityConfig(toml)).toThrow(
243
+ /capability.version is required in capability.toml/,
244
+ );
245
+ });
246
+
247
+ test("throws error when capability table is missing", () => {
248
+ const toml = `
249
+ project = "test"
250
+ `;
251
+
252
+ expect(() => parseCapabilityConfig(toml)).toThrow(
253
+ /capability.id is required in capability.toml/,
254
+ );
255
+ });
256
+
257
+ test("throws error for invalid TOML syntax", () => {
258
+ const toml = `
259
+ [capability
260
+ id = "test"
261
+ `;
262
+
263
+ expect(() => parseCapabilityConfig(toml)).toThrow(/Invalid capability.toml:/);
264
+ });
265
+
266
+ test("handles empty env declarations", () => {
267
+ const toml = `
268
+ [capability]
269
+ id = "test"
270
+ name = "Test"
271
+ version = "1.0.0"
272
+ description = "Test"
273
+
274
+ [env.SIMPLE_VAR]
275
+ `;
276
+
277
+ const config = parseCapabilityConfig(toml);
278
+
279
+ expect(config.env?.SIMPLE_VAR).toEqual({});
280
+ });
281
+ });