@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,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
|
+
});
|