@omnidev-ai/core 0.1.1 → 0.3.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 +1 -1
- package/src/capability/commands.test.ts +6 -10
- package/src/capability/docs.test.ts +39 -46
- package/src/capability/docs.ts +3 -1
- package/src/capability/loader.test.ts +10 -157
- package/src/capability/loader.ts +8 -69
- package/src/capability/registry.test.ts +9 -27
- package/src/capability/rules.test.ts +25 -35
- package/src/capability/rules.ts +3 -1
- package/src/capability/skills.test.ts +6 -10
- package/src/capability/sources.test.ts +142 -41
- package/src/capability/sources.ts +377 -345
- package/src/capability/subagents.test.ts +7 -11
- package/src/capability/subagents.ts +3 -1
- package/src/capability/wrapping-integration.test.ts +412 -0
- package/src/config/capabilities.ts +0 -28
- package/src/config/env.test.ts +4 -18
- package/src/config/loader.test.ts +4 -86
- package/src/config/loader.ts +88 -18
- package/src/config/parser.test.ts +0 -25
- package/src/config/profiles.test.ts +5 -39
- package/src/config/provider.test.ts +5 -19
- package/src/index.ts +1 -3
- package/src/mcp-json/manager.test.ts +77 -182
- package/src/mcp-json/manager.ts +22 -34
- package/src/state/active-profile.test.ts +4 -18
- package/src/state/index.ts +1 -0
- package/src/state/manifest.test.ts +25 -162
- package/src/state/manifest.ts +4 -31
- package/src/state/providers.test.ts +125 -0
- package/src/state/providers.ts +69 -0
- package/src/sync.ts +128 -53
- package/src/templates/claude.ts +9 -74
- package/src/test-utils/helpers.test.ts +18 -0
- package/src/test-utils/helpers.ts +87 -2
- package/src/test-utils/index.ts +3 -0
- package/src/types/capability-export.ts +0 -77
- package/src/types/index.ts +66 -22
- package/src/gitignore/manager.test.ts +0 -216
- package/src/gitignore/manager.ts +0 -167
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
4
5
|
import { loadSubagents } from "./subagents";
|
|
5
6
|
|
|
6
7
|
describe("loadSubagents", () => {
|
|
7
|
-
const testDir =
|
|
8
|
-
|
|
8
|
+
const testDir = setupTestDir("capability-subagents-test-");
|
|
9
|
+
let capabilityPath: string;
|
|
9
10
|
|
|
10
11
|
beforeEach(() => {
|
|
12
|
+
capabilityPath = join(testDir.path, "test-capability");
|
|
11
13
|
mkdirSync(capabilityPath, { recursive: true });
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
if (testDir) {
|
|
16
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
16
|
test("returns empty array when subagents directory does not exist", async () => {
|
|
21
17
|
const subagents = await loadSubagents(capabilityPath, "test-cap");
|
|
22
18
|
expect(subagents).toEqual([]);
|
|
@@ -167,7 +163,7 @@ tools: Read, Glob, Grep, Bash
|
|
|
167
163
|
disallowedTools: Write, Edit
|
|
168
164
|
model: sonnet
|
|
169
165
|
permissionMode: acceptEdits
|
|
170
|
-
skills: prd, ralph,
|
|
166
|
+
skills: prd, ralph, capability-builder
|
|
171
167
|
---
|
|
172
168
|
|
|
173
169
|
You are a fully configured subagent.`;
|
|
@@ -183,7 +179,7 @@ You are a fully configured subagent.`;
|
|
|
183
179
|
expect(subagents[0]?.disallowedTools).toEqual(["Write", "Edit"]);
|
|
184
180
|
expect(subagents[0]?.model).toBe("sonnet");
|
|
185
181
|
expect(subagents[0]?.permissionMode).toBe("acceptEdits");
|
|
186
|
-
expect(subagents[0]?.skills).toEqual(["prd", "ralph", "
|
|
182
|
+
expect(subagents[0]?.skills).toEqual(["prd", "ralph", "capability-builder"]);
|
|
187
183
|
expect(subagents[0]?.systemPrompt).toBe("You are a fully configured subagent.");
|
|
188
184
|
});
|
|
189
185
|
|
|
@@ -29,7 +29,9 @@ export async function loadSubagents(
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const subagents: Subagent[] = [];
|
|
32
|
-
const entries = readdirSync(subagentsDir, { withFileTypes: true })
|
|
32
|
+
const entries = readdirSync(subagentsDir, { withFileTypes: true }).sort((a, b) =>
|
|
33
|
+
a.name.localeCompare(b.name),
|
|
34
|
+
);
|
|
33
35
|
|
|
34
36
|
for (const entry of entries) {
|
|
35
37
|
if (entry.isDirectory()) {
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parse as parseToml } from "smol-toml";
|
|
6
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
7
|
+
import type { CapabilityConfig, OmniConfig } from "../types/index.js";
|
|
8
|
+
import { generateMcpCapabilities } from "./sources.js";
|
|
9
|
+
|
|
10
|
+
describe("wrapping integration - expo-like structure", () => {
|
|
11
|
+
const testDir = setupTestDir("test-expo-", { chdir: true });
|
|
12
|
+
|
|
13
|
+
test("wraps expo-like plugin with .claude-plugin/plugin.json", () => {
|
|
14
|
+
// Simulate Expo skills structure: plugins/expo-app-design/
|
|
15
|
+
const pluginDir = join(testDir.path, "plugins", "expo-app-design");
|
|
16
|
+
mkdirSync(join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
17
|
+
mkdirSync(join(pluginDir, "skills"));
|
|
18
|
+
|
|
19
|
+
// Create plugin.json (like Expo has)
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(pluginDir, ".claude-plugin", "plugin.json"),
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
name: "expo-app-design",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
description: "Build robust, productivity apps with Expo",
|
|
26
|
+
author: {
|
|
27
|
+
name: "Evan Bacon",
|
|
28
|
+
email: "bacon@expo.io",
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Create README
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(pluginDir, "README.md"),
|
|
36
|
+
`# Expo App Design
|
|
37
|
+
|
|
38
|
+
Design amazing Expo applications with best practices.
|
|
39
|
+
|
|
40
|
+
This plugin provides tools for building production-ready Expo apps.`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Create a skill
|
|
44
|
+
const skillDir = join(pluginDir, "skills");
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(skillDir, "example-skill.md"),
|
|
47
|
+
`---
|
|
48
|
+
name: example-skill
|
|
49
|
+
description: Example skill for testing
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# Example Skill
|
|
53
|
+
|
|
54
|
+
This is an example skill.`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Verify structure exists
|
|
58
|
+
expect(existsSync(join(pluginDir, ".claude-plugin", "plugin.json"))).toBe(true);
|
|
59
|
+
expect(existsSync(join(pluginDir, "README.md"))).toBe(true);
|
|
60
|
+
expect(existsSync(join(pluginDir, "skills", "example-skill.md"))).toBe(true);
|
|
61
|
+
expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
|
|
62
|
+
|
|
63
|
+
// This should be detected as needing wrapping
|
|
64
|
+
// When fetchGitCapabilitySource runs, it will:
|
|
65
|
+
// 1. Check for capability.toml (not found)
|
|
66
|
+
// 2. Call shouldWrapDirectory
|
|
67
|
+
// 3. Find .claude-plugin/plugin.json
|
|
68
|
+
// 4. Generate capability.toml with metadata from plugin.json
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("wraps plugin with only directory structure (no plugin.json)", () => {
|
|
72
|
+
const pluginDir = join(testDir.path, "plugins", "simple-plugin");
|
|
73
|
+
mkdirSync(join(pluginDir, "skills"), { recursive: true });
|
|
74
|
+
|
|
75
|
+
// Create README
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(pluginDir, "README.md"),
|
|
78
|
+
`# Simple Plugin
|
|
79
|
+
|
|
80
|
+
A simple plugin without plugin.json metadata.
|
|
81
|
+
|
|
82
|
+
This demonstrates wrapping based on directory structure alone.`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Create a skill
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(pluginDir, "skills", "test-skill.md"),
|
|
88
|
+
`---
|
|
89
|
+
name: test-skill
|
|
90
|
+
description: Test skill
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
# Test Skill
|
|
94
|
+
|
|
95
|
+
Content here.`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Verify structure exists
|
|
99
|
+
expect(existsSync(join(pluginDir, ".claude-plugin"))).toBe(false);
|
|
100
|
+
expect(existsSync(join(pluginDir, "skills"))).toBe(true);
|
|
101
|
+
expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
|
|
102
|
+
|
|
103
|
+
// Should still be detected for wrapping due to skills/ directory
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("does not wrap when capability.toml already exists", () => {
|
|
107
|
+
const pluginDir = join(testDir.path, "plugins", "proper-capability");
|
|
108
|
+
mkdirSync(join(pluginDir, "skills"), { recursive: true });
|
|
109
|
+
|
|
110
|
+
// Create proper capability.toml
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(pluginDir, "capability.toml"),
|
|
113
|
+
`[capability]
|
|
114
|
+
id = "proper-capability"
|
|
115
|
+
name = "Proper Capability"
|
|
116
|
+
version = "1.0.0"
|
|
117
|
+
description = "This is a proper capability with its own TOML"
|
|
118
|
+
`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Create a skill
|
|
122
|
+
writeFileSync(join(pluginDir, "skills", "skill.md"), "# Skill\n");
|
|
123
|
+
|
|
124
|
+
// Verify capability.toml exists
|
|
125
|
+
expect(existsSync(join(pluginDir, "capability.toml"))).toBe(true);
|
|
126
|
+
expect(existsSync(join(pluginDir, "skills"))).toBe(true);
|
|
127
|
+
|
|
128
|
+
// Should NOT be wrapped - already has capability.toml
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("multiple plugins in monorepo structure", () => {
|
|
132
|
+
// Simulate full Expo skills structure
|
|
133
|
+
const plugins = ["expo-app-design", "expo-deployment", "upgrading-expo"];
|
|
134
|
+
|
|
135
|
+
for (const pluginName of plugins) {
|
|
136
|
+
const pluginDir = join(testDir.path, "plugins", pluginName);
|
|
137
|
+
mkdirSync(join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
138
|
+
mkdirSync(join(pluginDir, "skills"), { recursive: true });
|
|
139
|
+
|
|
140
|
+
writeFileSync(
|
|
141
|
+
join(pluginDir, ".claude-plugin", "plugin.json"),
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
name: pluginName,
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
description: `${pluginName} plugin`,
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
writeFileSync(join(pluginDir, "README.md"), `# ${pluginName}\n\nDescription here.`);
|
|
150
|
+
writeFileSync(join(pluginDir, "skills", "example.md"), "# Skill\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Verify all three plugins have correct structure
|
|
154
|
+
for (const pluginName of plugins) {
|
|
155
|
+
const pluginDir = join(testDir.path, "plugins", pluginName);
|
|
156
|
+
expect(existsSync(join(pluginDir, ".claude-plugin", "plugin.json"))).toBe(true);
|
|
157
|
+
expect(existsSync(join(pluginDir, "skills"))).toBe(true);
|
|
158
|
+
expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Each plugin would be loaded separately with:
|
|
162
|
+
// expo-app-design = { source = "github:expo/skills", path = "plugins/expo-app-design" }
|
|
163
|
+
// expo-deployment = { source = "github:expo/skills", path = "plugins/expo-deployment" }
|
|
164
|
+
// upgrading-expo = { source = "github:expo/skills", path = "plugins/upgrading-expo" }
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("MCP capability generation", () => {
|
|
169
|
+
const _testDir = setupTestDir("mcp-wrapping-test-", { chdir: true });
|
|
170
|
+
|
|
171
|
+
test("generates capability from omni.toml [mcps] section", async () => {
|
|
172
|
+
const config: OmniConfig = {
|
|
173
|
+
mcps: {
|
|
174
|
+
context7: {
|
|
175
|
+
command: "npx",
|
|
176
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
177
|
+
transport: "stdio",
|
|
178
|
+
env: {
|
|
179
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
|
|
180
|
+
API_KEY: "${CONTEXT7_API_KEY}",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await generateMcpCapabilities(config);
|
|
187
|
+
|
|
188
|
+
const capabilityDir = join(".omni", "capabilities", "context7");
|
|
189
|
+
expect(existsSync(capabilityDir)).toBe(true);
|
|
190
|
+
|
|
191
|
+
const tomlPath = join(capabilityDir, "capability.toml");
|
|
192
|
+
expect(existsSync(tomlPath)).toBe(true);
|
|
193
|
+
|
|
194
|
+
const tomlContent = await readFile(tomlPath, "utf-8");
|
|
195
|
+
const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
|
|
196
|
+
|
|
197
|
+
expect(parsed.capability.id).toBe("context7");
|
|
198
|
+
expect(parsed.capability.name).toBe("context7 (MCP)");
|
|
199
|
+
expect(parsed.capability.version).toBe("1.0.0");
|
|
200
|
+
expect(parsed.capability.metadata?.wrapped).toBe(true);
|
|
201
|
+
expect(parsed.capability.metadata?.generated_from_omni_toml).toBe(true);
|
|
202
|
+
|
|
203
|
+
expect(parsed.mcp?.command).toBe("npx");
|
|
204
|
+
expect(parsed.mcp?.args).toEqual(["-y", "@upstash/context7-mcp"]);
|
|
205
|
+
expect(parsed.mcp?.transport).toBe("stdio");
|
|
206
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
|
|
207
|
+
expect(parsed.mcp?.env?.API_KEY).toBe("${CONTEXT7_API_KEY}");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("generates multiple MCP capabilities", async () => {
|
|
211
|
+
const config: OmniConfig = {
|
|
212
|
+
mcps: {
|
|
213
|
+
context7: {
|
|
214
|
+
command: "npx",
|
|
215
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
216
|
+
},
|
|
217
|
+
filesystem: {
|
|
218
|
+
command: "node",
|
|
219
|
+
args: ["server.js"],
|
|
220
|
+
cwd: "./mcp-servers/filesystem",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await generateMcpCapabilities(config);
|
|
226
|
+
|
|
227
|
+
const context7Dir = join(".omni", "capabilities", "context7");
|
|
228
|
+
const filesystemDir = join(".omni", "capabilities", "filesystem");
|
|
229
|
+
|
|
230
|
+
expect(existsSync(context7Dir)).toBe(true);
|
|
231
|
+
expect(existsSync(filesystemDir)).toBe(true);
|
|
232
|
+
|
|
233
|
+
const context7TomlPath = join(context7Dir, "capability.toml");
|
|
234
|
+
const filesystemTomlPath = join(filesystemDir, "capability.toml");
|
|
235
|
+
|
|
236
|
+
expect(existsSync(context7TomlPath)).toBe(true);
|
|
237
|
+
expect(existsSync(filesystemTomlPath)).toBe(true);
|
|
238
|
+
|
|
239
|
+
const filesystemToml = await readFile(filesystemTomlPath, "utf-8");
|
|
240
|
+
const parsed = parseToml(filesystemToml) as unknown as CapabilityConfig;
|
|
241
|
+
|
|
242
|
+
expect(parsed.capability.id).toBe("filesystem");
|
|
243
|
+
expect(parsed.mcp?.cwd).toBe("./mcp-servers/filesystem");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("cleans up stale MCP capabilities", async () => {
|
|
247
|
+
// First generation with two MCPs
|
|
248
|
+
const config1: OmniConfig = {
|
|
249
|
+
mcps: {
|
|
250
|
+
context7: {
|
|
251
|
+
command: "npx",
|
|
252
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
253
|
+
},
|
|
254
|
+
filesystem: {
|
|
255
|
+
command: "node",
|
|
256
|
+
args: ["server.js"],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
await generateMcpCapabilities(config1);
|
|
262
|
+
|
|
263
|
+
const context7Dir = join(".omni", "capabilities", "context7");
|
|
264
|
+
const filesystemDir = join(".omni", "capabilities", "filesystem");
|
|
265
|
+
|
|
266
|
+
expect(existsSync(context7Dir)).toBe(true);
|
|
267
|
+
expect(existsSync(filesystemDir)).toBe(true);
|
|
268
|
+
|
|
269
|
+
// Second generation with only one MCP
|
|
270
|
+
const config2: OmniConfig = {
|
|
271
|
+
mcps: {
|
|
272
|
+
context7: {
|
|
273
|
+
command: "npx",
|
|
274
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
await generateMcpCapabilities(config2);
|
|
280
|
+
|
|
281
|
+
// context7 should still exist
|
|
282
|
+
expect(existsSync(context7Dir)).toBe(true);
|
|
283
|
+
|
|
284
|
+
// filesystem should be removed
|
|
285
|
+
expect(existsSync(filesystemDir)).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("cleans up all MCP capabilities when mcps section is empty", async () => {
|
|
289
|
+
// First generation with MCPs
|
|
290
|
+
const config1: OmniConfig = {
|
|
291
|
+
mcps: {
|
|
292
|
+
context7: {
|
|
293
|
+
command: "npx",
|
|
294
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
await generateMcpCapabilities(config1);
|
|
300
|
+
|
|
301
|
+
const context7Dir = join(".omni", "capabilities", "context7");
|
|
302
|
+
expect(existsSync(context7Dir)).toBe(true);
|
|
303
|
+
|
|
304
|
+
// Second generation with no MCPs
|
|
305
|
+
const config2: OmniConfig = {
|
|
306
|
+
mcps: {},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
await generateMcpCapabilities(config2);
|
|
310
|
+
|
|
311
|
+
// All MCP capabilities should be removed
|
|
312
|
+
expect(existsSync(context7Dir)).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("cleans up all MCP capabilities when mcps is undefined", async () => {
|
|
316
|
+
// First generation with MCPs
|
|
317
|
+
const config1: OmniConfig = {
|
|
318
|
+
mcps: {
|
|
319
|
+
context7: {
|
|
320
|
+
command: "npx",
|
|
321
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
await generateMcpCapabilities(config1);
|
|
327
|
+
|
|
328
|
+
const context7Dir = join(".omni", "capabilities", "context7");
|
|
329
|
+
expect(existsSync(context7Dir)).toBe(true);
|
|
330
|
+
|
|
331
|
+
// Second generation with undefined mcps
|
|
332
|
+
const config2: OmniConfig = {};
|
|
333
|
+
|
|
334
|
+
await generateMcpCapabilities(config2);
|
|
335
|
+
|
|
336
|
+
// All MCP capabilities should be removed
|
|
337
|
+
expect(existsSync(context7Dir)).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("generates capability without optional fields", async () => {
|
|
341
|
+
const config: OmniConfig = {
|
|
342
|
+
mcps: {
|
|
343
|
+
simple: {
|
|
344
|
+
command: "simple-mcp",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
await generateMcpCapabilities(config);
|
|
350
|
+
|
|
351
|
+
const capabilityDir = join(".omni", "capabilities", "simple");
|
|
352
|
+
expect(existsSync(capabilityDir)).toBe(true);
|
|
353
|
+
|
|
354
|
+
const tomlPath = join(capabilityDir, "capability.toml");
|
|
355
|
+
const tomlContent = await readFile(tomlPath, "utf-8");
|
|
356
|
+
const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
|
|
357
|
+
|
|
358
|
+
expect(parsed.capability.id).toBe("simple");
|
|
359
|
+
expect(parsed.mcp?.command).toBe("simple-mcp");
|
|
360
|
+
expect(parsed.mcp?.args).toBeUndefined();
|
|
361
|
+
expect(parsed.mcp?.env).toBeUndefined();
|
|
362
|
+
expect(parsed.mcp?.cwd).toBeUndefined();
|
|
363
|
+
expect(parsed.mcp?.transport).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("merges MCP env variables correctly", async () => {
|
|
367
|
+
const config: OmniConfig = {
|
|
368
|
+
mcps: {
|
|
369
|
+
github: {
|
|
370
|
+
command: "npx",
|
|
371
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
372
|
+
env: {
|
|
373
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
|
|
374
|
+
GITHUB_TOKEN: "${GITHUB_TOKEN}",
|
|
375
|
+
GITHUB_API_URL: "https://api.github.com",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
await generateMcpCapabilities(config);
|
|
382
|
+
|
|
383
|
+
const tomlPath = join(".omni", "capabilities", "github", "capability.toml");
|
|
384
|
+
const tomlContent = await readFile(tomlPath, "utf-8");
|
|
385
|
+
const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
|
|
386
|
+
|
|
387
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
|
|
388
|
+
expect(parsed.mcp?.env?.GITHUB_TOKEN).toBe("${GITHUB_TOKEN}");
|
|
389
|
+
expect(parsed.mcp?.env?.GITHUB_API_URL).toBe("https://api.github.com");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("does not affect non-MCP capabilities", async () => {
|
|
393
|
+
// Create a non-MCP capability directory
|
|
394
|
+
const nonMcpDir = join(".omni", "capabilities", "my-capability");
|
|
395
|
+
mkdirSync(nonMcpDir, { recursive: true });
|
|
396
|
+
writeFileSync(join(nonMcpDir, "capability.toml"), "# test");
|
|
397
|
+
|
|
398
|
+
const config: OmniConfig = {
|
|
399
|
+
mcps: {
|
|
400
|
+
context7: {
|
|
401
|
+
command: "npx",
|
|
402
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
await generateMcpCapabilities(config);
|
|
408
|
+
|
|
409
|
+
// Non-MCP capability should still exist
|
|
410
|
+
expect(existsSync(nonMcpDir)).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { discoverCapabilities, loadCapability } from "../capability/loader.js";
|
|
2
|
-
import { addCapabilityPatterns, removeCapabilityPatterns } from "../gitignore/manager.js";
|
|
3
1
|
import { loadConfig, writeConfig } from "./loader.js";
|
|
4
2
|
import { getActiveProfile, resolveEnabledCapabilities } from "./profiles.js";
|
|
5
3
|
|
|
@@ -16,7 +14,6 @@ export async function getEnabledCapabilities(): Promise<string[]> {
|
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* 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
17
|
* @param capabilityId - The ID of the capability to enable
|
|
21
18
|
*/
|
|
22
19
|
export async function enableCapability(capabilityId: string): Promise<void> {
|
|
@@ -35,27 +32,10 @@ export async function enableCapability(capabilityId: string): Promise<void> {
|
|
|
35
32
|
config.profiles[activeProfile].capabilities = Array.from(capabilities);
|
|
36
33
|
|
|
37
34
|
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
35
|
}
|
|
55
36
|
|
|
56
37
|
/**
|
|
57
38
|
* 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
39
|
* @param capabilityId - The ID of the capability to disable
|
|
60
40
|
*/
|
|
61
41
|
export async function disableCapability(capabilityId: string): Promise<void> {
|
|
@@ -71,12 +51,4 @@ export async function disableCapability(capabilityId: string): Promise<void> {
|
|
|
71
51
|
config.profiles[activeProfile].capabilities = Array.from(capabilities);
|
|
72
52
|
|
|
73
53
|
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
54
|
}
|
package/src/config/env.test.ts
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
4
4
|
import type { EnvDeclaration } from "../types";
|
|
5
5
|
import { isSecretEnvVar, loadEnvironment, validateEnv } from "./env";
|
|
6
6
|
|
|
7
7
|
describe("loadEnvironment", () => {
|
|
8
|
-
|
|
9
|
-
let testDir: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
// Create test directory in /tmp
|
|
13
|
-
testDir = tmpdir("env-test-");
|
|
14
|
-
process.chdir(testDir);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
process.chdir(originalCwd);
|
|
19
|
-
if (existsSync(testDir)) {
|
|
20
|
-
rmSync(testDir, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
});
|
|
8
|
+
setupTestDir("env-test-", { chdir: true });
|
|
23
9
|
|
|
24
10
|
test("returns empty object when no .omni/.env file exists", async () => {
|
|
25
11
|
const env = await loadEnvironment();
|
|
@@ -1,35 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
4
4
|
import { loadConfig } from "./loader";
|
|
5
5
|
|
|
6
6
|
const CONFIG_PATH = "omni.toml";
|
|
7
7
|
const LOCAL_CONFIG = "omni.local.toml";
|
|
8
8
|
|
|
9
|
-
// Save and restore the current working directory
|
|
10
|
-
let originalCwd: string;
|
|
11
|
-
let TEST_DIR: string;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
// Save original cwd
|
|
15
|
-
originalCwd = process.cwd();
|
|
16
|
-
|
|
17
|
-
// Create test directory in /tmp
|
|
18
|
-
TEST_DIR = tmpdir("loader-test-");
|
|
19
|
-
process.chdir(TEST_DIR);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
// Restore original cwd
|
|
24
|
-
process.chdir(originalCwd);
|
|
25
|
-
|
|
26
|
-
// Clean up test directory
|
|
27
|
-
if (existsSync(TEST_DIR)) {
|
|
28
|
-
rmSync(TEST_DIR, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
9
|
describe("loadConfig", () => {
|
|
10
|
+
setupTestDir("loader-test-", { chdir: true });
|
|
33
11
|
test("returns empty config when no files exist", async () => {
|
|
34
12
|
const config = await loadConfig();
|
|
35
13
|
expect(config).toEqual({
|
|
@@ -217,64 +195,4 @@ active_profile = "production"
|
|
|
217
195
|
// but new writes go to state file via setActiveProfile()
|
|
218
196
|
expect(config.active_profile).toBe("production");
|
|
219
197
|
});
|
|
220
|
-
|
|
221
|
-
test("loads sandbox_enabled = true from config", async () => {
|
|
222
|
-
mkdirSync(".omni", { recursive: true });
|
|
223
|
-
writeFileSync(
|
|
224
|
-
CONFIG_PATH,
|
|
225
|
-
`
|
|
226
|
-
sandbox_enabled = true
|
|
227
|
-
`,
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
const config = await loadConfig();
|
|
231
|
-
expect(config.sandbox_enabled).toBe(true);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("loads sandbox_enabled = false from config", async () => {
|
|
235
|
-
mkdirSync(".omni", { recursive: true });
|
|
236
|
-
writeFileSync(
|
|
237
|
-
CONFIG_PATH,
|
|
238
|
-
`
|
|
239
|
-
sandbox_enabled = false
|
|
240
|
-
`,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const config = await loadConfig();
|
|
244
|
-
expect(config.sandbox_enabled).toBe(false);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("sandbox_enabled is undefined when not specified", async () => {
|
|
248
|
-
mkdirSync(".omni", { recursive: true });
|
|
249
|
-
writeFileSync(
|
|
250
|
-
CONFIG_PATH,
|
|
251
|
-
`
|
|
252
|
-
project = "test"
|
|
253
|
-
`,
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
const config = await loadConfig();
|
|
257
|
-
expect(config.sandbox_enabled).toBeUndefined();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("local config can override sandbox_enabled", async () => {
|
|
261
|
-
mkdirSync(".omni", { recursive: true });
|
|
262
|
-
|
|
263
|
-
writeFileSync(
|
|
264
|
-
CONFIG_PATH,
|
|
265
|
-
`
|
|
266
|
-
sandbox_enabled = true
|
|
267
|
-
`,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
writeFileSync(
|
|
271
|
-
LOCAL_CONFIG,
|
|
272
|
-
`
|
|
273
|
-
sandbox_enabled = false
|
|
274
|
-
`,
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const config = await loadConfig();
|
|
278
|
-
expect(config.sandbox_enabled).toBe(false);
|
|
279
|
-
});
|
|
280
198
|
});
|