@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,815 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { discoverCapabilities, loadCapability, loadCapabilityConfig } from "./loader";
|
|
6
|
+
|
|
7
|
+
describe("discoverCapabilities", () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let capabilitiesDir: string;
|
|
10
|
+
let originalCwd: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Save current working directory
|
|
14
|
+
originalCwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
// Create test directory in os temp dir
|
|
17
|
+
testDir = mkdtempSync(join(tmpdir(), "test-capabilities-"));
|
|
18
|
+
capabilitiesDir = join(testDir, "omni", "capabilities");
|
|
19
|
+
mkdirSync(capabilitiesDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
// Change to test directory
|
|
22
|
+
process.chdir(testDir);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Restore working directory
|
|
27
|
+
process.chdir(originalCwd);
|
|
28
|
+
|
|
29
|
+
// Cleanup
|
|
30
|
+
if (existsSync(testDir)) {
|
|
31
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns empty array when capabilities directory does not exist", async () => {
|
|
36
|
+
// Remove the capabilities directory
|
|
37
|
+
rmSync(".omni/capabilities", { recursive: true, force: true });
|
|
38
|
+
|
|
39
|
+
const capabilities = await discoverCapabilities();
|
|
40
|
+
|
|
41
|
+
expect(capabilities).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns empty array when capabilities directory is empty", async () => {
|
|
45
|
+
const capabilities = await discoverCapabilities();
|
|
46
|
+
|
|
47
|
+
expect(capabilities).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("discovers a single capability with capability.toml", async () => {
|
|
51
|
+
// Create a capability directory with capability.toml
|
|
52
|
+
const capPath = join(".omni", "capabilities", "test-cap");
|
|
53
|
+
mkdirSync(capPath, { recursive: true });
|
|
54
|
+
writeFileSync(join(capPath, "capability.toml"), '[capability]\nid = "test-cap"');
|
|
55
|
+
|
|
56
|
+
const capabilities = await discoverCapabilities();
|
|
57
|
+
|
|
58
|
+
expect(capabilities).toEqual([".omni/capabilities/test-cap"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("discovers multiple capabilities with capability.toml", async () => {
|
|
62
|
+
// Create multiple capability directories
|
|
63
|
+
const cap1Path = join(".omni", "capabilities", "capability-1");
|
|
64
|
+
const cap2Path = join(".omni", "capabilities", "capability-2");
|
|
65
|
+
const cap3Path = join(".omni", "capabilities", "capability-3");
|
|
66
|
+
|
|
67
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
68
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
69
|
+
mkdirSync(cap3Path, { recursive: true });
|
|
70
|
+
|
|
71
|
+
writeFileSync(join(cap1Path, "capability.toml"), '[capability]\nid = "capability-1"');
|
|
72
|
+
writeFileSync(join(cap2Path, "capability.toml"), '[capability]\nid = "capability-2"');
|
|
73
|
+
writeFileSync(join(cap3Path, "capability.toml"), '[capability]\nid = "capability-3"');
|
|
74
|
+
|
|
75
|
+
const capabilities = await discoverCapabilities();
|
|
76
|
+
|
|
77
|
+
expect(capabilities).toHaveLength(3);
|
|
78
|
+
expect(capabilities).toContain(".omni/capabilities/capability-1");
|
|
79
|
+
expect(capabilities).toContain(".omni/capabilities/capability-2");
|
|
80
|
+
expect(capabilities).toContain(".omni/capabilities/capability-3");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("ignores directories without capability.toml", async () => {
|
|
84
|
+
// Create directory without capability.toml
|
|
85
|
+
const notACapPath = join(".omni", "capabilities", "not-a-capability");
|
|
86
|
+
mkdirSync(notACapPath, { recursive: true });
|
|
87
|
+
writeFileSync(join(notACapPath, "README.md"), "# Not a capability");
|
|
88
|
+
|
|
89
|
+
// Create a valid capability
|
|
90
|
+
const validCapPath = join(".omni", "capabilities", "valid-cap");
|
|
91
|
+
mkdirSync(validCapPath, { recursive: true });
|
|
92
|
+
writeFileSync(join(validCapPath, "capability.toml"), '[capability]\nid = "valid-cap"');
|
|
93
|
+
|
|
94
|
+
const capabilities = await discoverCapabilities();
|
|
95
|
+
|
|
96
|
+
expect(capabilities).toEqual([".omni/capabilities/valid-cap"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("ignores files in capabilities directory", async () => {
|
|
100
|
+
// Create the capabilities directory first
|
|
101
|
+
mkdirSync(join(".omni", "capabilities"), { recursive: true });
|
|
102
|
+
|
|
103
|
+
// Create a file in the capabilities directory (not a subdirectory)
|
|
104
|
+
writeFileSync(join(".omni", "capabilities", "README.md"), "# Capabilities");
|
|
105
|
+
|
|
106
|
+
// Create a valid capability
|
|
107
|
+
const validCapPath = join(".omni", "capabilities", "valid-cap");
|
|
108
|
+
mkdirSync(validCapPath, { recursive: true });
|
|
109
|
+
writeFileSync(join(validCapPath, "capability.toml"), '[capability]\nid = "valid-cap"');
|
|
110
|
+
|
|
111
|
+
const capabilities = await discoverCapabilities();
|
|
112
|
+
|
|
113
|
+
expect(capabilities).toEqual([".omni/capabilities/valid-cap"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("handles nested directories correctly (does not recurse)", async () => {
|
|
117
|
+
// Create a nested structure - should only discover top-level capabilities
|
|
118
|
+
const cap1Path = join(".omni", "capabilities", "capability-1");
|
|
119
|
+
const nestedCapPath = join(cap1Path, "nested-capability");
|
|
120
|
+
|
|
121
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
122
|
+
mkdirSync(nestedCapPath, { recursive: true });
|
|
123
|
+
|
|
124
|
+
writeFileSync(join(cap1Path, "capability.toml"), '[capability]\nid = "capability-1"');
|
|
125
|
+
writeFileSync(join(nestedCapPath, "capability.toml"), '[capability]\nid = "nested"');
|
|
126
|
+
|
|
127
|
+
const capabilities = await discoverCapabilities();
|
|
128
|
+
|
|
129
|
+
// Should only find the top-level capability, not the nested one
|
|
130
|
+
expect(capabilities).toEqual([".omni/capabilities/capability-1"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("returns paths in consistent format", async () => {
|
|
134
|
+
const capPath = join(".omni", "capabilities", "test-cap");
|
|
135
|
+
mkdirSync(capPath, { recursive: true });
|
|
136
|
+
writeFileSync(join(capPath, "capability.toml"), '[capability]\nid = "test-cap"');
|
|
137
|
+
|
|
138
|
+
const capabilities = await discoverCapabilities();
|
|
139
|
+
|
|
140
|
+
// Path should use forward slashes or be normalized
|
|
141
|
+
expect(capabilities[0]).toMatch(/^\.omni\/capabilities\/test-cap$/);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("loadCapabilityConfig", () => {
|
|
146
|
+
let testDir: string;
|
|
147
|
+
let capabilitiesDir: string;
|
|
148
|
+
let originalCwd: string;
|
|
149
|
+
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
// Save current working directory
|
|
152
|
+
originalCwd = process.cwd();
|
|
153
|
+
|
|
154
|
+
// Create test directory in os temp dir
|
|
155
|
+
testDir = mkdtempSync(join(tmpdir(), "test-capability-config-"));
|
|
156
|
+
capabilitiesDir = join(testDir, ".omni", "capabilities");
|
|
157
|
+
mkdirSync(capabilitiesDir, { recursive: true });
|
|
158
|
+
|
|
159
|
+
// Change to test directory
|
|
160
|
+
process.chdir(testDir);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
// Restore working directory
|
|
165
|
+
process.chdir(originalCwd);
|
|
166
|
+
|
|
167
|
+
// Cleanup
|
|
168
|
+
if (existsSync(testDir)) {
|
|
169
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("loads valid capability config with all required fields", async () => {
|
|
174
|
+
const capPath = join(".omni", "capabilities", "test-cap");
|
|
175
|
+
mkdirSync(capPath, { recursive: true });
|
|
176
|
+
writeFileSync(
|
|
177
|
+
join(capPath, "capability.toml"),
|
|
178
|
+
`[capability]
|
|
179
|
+
id = "test-cap"
|
|
180
|
+
name = "Test Capability"
|
|
181
|
+
version = "1.0.0"
|
|
182
|
+
description = "A test capability"`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const config = await loadCapabilityConfig(capPath);
|
|
186
|
+
|
|
187
|
+
expect(config.capability.id).toBe("test-cap");
|
|
188
|
+
expect(config.capability.name).toBe("Test Capability");
|
|
189
|
+
expect(config.capability.version).toBe("1.0.0");
|
|
190
|
+
expect(config.capability.description).toBe("A test capability");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("loads capability config with optional exports field", async () => {
|
|
194
|
+
const capPath = join(".omni", "capabilities", "with-exports");
|
|
195
|
+
mkdirSync(capPath, { recursive: true });
|
|
196
|
+
writeFileSync(
|
|
197
|
+
join(capPath, "capability.toml"),
|
|
198
|
+
`[capability]
|
|
199
|
+
id = "with-exports"
|
|
200
|
+
name = "With Exports"
|
|
201
|
+
version = "1.0.0"
|
|
202
|
+
description = "Has exports"
|
|
203
|
+
|
|
204
|
+
[exports]
|
|
205
|
+
functions = ["create", "list", "get"]`,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const config = await loadCapabilityConfig(capPath);
|
|
209
|
+
|
|
210
|
+
expect(config.capability.id).toBe("with-exports");
|
|
211
|
+
expect(config.exports?.functions).toEqual(["create", "list", "get"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("loads capability config with optional env field", async () => {
|
|
215
|
+
const capPath = join(".omni", "capabilities", "with-env");
|
|
216
|
+
mkdirSync(capPath, { recursive: true });
|
|
217
|
+
writeFileSync(
|
|
218
|
+
join(capPath, "capability.toml"),
|
|
219
|
+
`[capability]
|
|
220
|
+
id = "with-env"
|
|
221
|
+
name = "With Env"
|
|
222
|
+
version = "1.0.0"
|
|
223
|
+
description = "Has env vars"
|
|
224
|
+
|
|
225
|
+
[[env]]
|
|
226
|
+
key = "API_KEY"
|
|
227
|
+
description = "API key"
|
|
228
|
+
required = true
|
|
229
|
+
secret = true`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const config = await loadCapabilityConfig(capPath);
|
|
233
|
+
|
|
234
|
+
expect(config.capability.id).toBe("with-env");
|
|
235
|
+
expect(config.env).toBeDefined();
|
|
236
|
+
expect(config.env?.[0]?.key).toBe("API_KEY");
|
|
237
|
+
expect(config.env?.[0]?.required).toBe(true);
|
|
238
|
+
expect(config.env?.[0]?.secret).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("loads capability config with optional mcp field", async () => {
|
|
242
|
+
const capPath = join(".omni", "capabilities", "with-mcp");
|
|
243
|
+
mkdirSync(capPath, { recursive: true });
|
|
244
|
+
writeFileSync(
|
|
245
|
+
join(capPath, "capability.toml"),
|
|
246
|
+
`[capability]
|
|
247
|
+
id = "with-mcp"
|
|
248
|
+
name = "With MCP"
|
|
249
|
+
version = "1.0.0"
|
|
250
|
+
description = "Has MCP tools"
|
|
251
|
+
|
|
252
|
+
[mcp]
|
|
253
|
+
tools = ["test_tool"]`,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const config = await loadCapabilityConfig(capPath);
|
|
257
|
+
|
|
258
|
+
expect(config.capability.id).toBe("with-mcp");
|
|
259
|
+
expect(config.mcp?.tools).toEqual(["test_tool"]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("throws error for reserved capability name (fs)", async () => {
|
|
263
|
+
const capPath = join(".omni", "capabilities", "fs");
|
|
264
|
+
mkdirSync(capPath, { recursive: true });
|
|
265
|
+
writeFileSync(
|
|
266
|
+
join(capPath, "capability.toml"),
|
|
267
|
+
`[capability]
|
|
268
|
+
id = "fs"
|
|
269
|
+
name = "File System"
|
|
270
|
+
version = "1.0.0"
|
|
271
|
+
description = "Reserved name"`,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
275
|
+
'Capability name "fs" is reserved. Choose a different name.',
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("throws error for reserved capability name (react)", async () => {
|
|
280
|
+
const capPath = join(".omni", "capabilities", "react-cap");
|
|
281
|
+
mkdirSync(capPath, { recursive: true });
|
|
282
|
+
writeFileSync(
|
|
283
|
+
join(capPath, "capability.toml"),
|
|
284
|
+
`[capability]
|
|
285
|
+
id = "react"
|
|
286
|
+
name = "React"
|
|
287
|
+
version = "1.0.0"
|
|
288
|
+
description = "Reserved name"`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
292
|
+
'Capability name "react" is reserved. Choose a different name.',
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("throws error for reserved capability name (typescript)", async () => {
|
|
297
|
+
const capPath = join(".omni", "capabilities", "ts-cap");
|
|
298
|
+
mkdirSync(capPath, { recursive: true });
|
|
299
|
+
writeFileSync(
|
|
300
|
+
join(capPath, "capability.toml"),
|
|
301
|
+
`[capability]
|
|
302
|
+
id = "typescript"
|
|
303
|
+
name = "TypeScript"
|
|
304
|
+
version = "1.0.0"
|
|
305
|
+
description = "Reserved name"`,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow(
|
|
309
|
+
'Capability name "typescript" is reserved. Choose a different name.',
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("throws error when capability.toml is missing", async () => {
|
|
314
|
+
const capPath = join(".omni", "capabilities", "missing-config");
|
|
315
|
+
mkdirSync(capPath, { recursive: true });
|
|
316
|
+
|
|
317
|
+
// No capability.toml file created
|
|
318
|
+
|
|
319
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("throws error when capability.toml has missing required fields", async () => {
|
|
323
|
+
const capPath = join(".omni", "capabilities", "invalid");
|
|
324
|
+
mkdirSync(capPath, { recursive: true });
|
|
325
|
+
writeFileSync(
|
|
326
|
+
join(capPath, "capability.toml"),
|
|
327
|
+
`[capability]
|
|
328
|
+
id = "invalid"
|
|
329
|
+
# Missing name, version, description`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("throws error when capability.toml has invalid TOML syntax", async () => {
|
|
336
|
+
const capPath = join(".omni", "capabilities", "bad-toml");
|
|
337
|
+
mkdirSync(capPath, { recursive: true });
|
|
338
|
+
writeFileSync(
|
|
339
|
+
join(capPath, "capability.toml"),
|
|
340
|
+
`[capability
|
|
341
|
+
id = "bad-toml"
|
|
342
|
+
# Missing closing bracket`,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(async () => await loadCapabilityConfig(capPath)).toThrow();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("allows non-reserved capability names", async () => {
|
|
349
|
+
const capPath = join(".omni", "capabilities", "my-custom-capability");
|
|
350
|
+
mkdirSync(capPath, { recursive: true });
|
|
351
|
+
writeFileSync(
|
|
352
|
+
join(capPath, "capability.toml"),
|
|
353
|
+
`[capability]
|
|
354
|
+
id = "my-custom-capability"
|
|
355
|
+
name = "My Custom Capability"
|
|
356
|
+
version = "2.1.0"
|
|
357
|
+
description = "A custom capability"`,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const config = await loadCapabilityConfig(capPath);
|
|
361
|
+
|
|
362
|
+
expect(config.capability.id).toBe("my-custom-capability");
|
|
363
|
+
expect(config.capability.name).toBe("My Custom Capability");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("handles capability config with all optional fields defined", async () => {
|
|
367
|
+
const capPath = join(".omni", "capabilities", "complete-cap");
|
|
368
|
+
mkdirSync(capPath, { recursive: true });
|
|
369
|
+
writeFileSync(
|
|
370
|
+
join(capPath, "capability.toml"),
|
|
371
|
+
`[capability]
|
|
372
|
+
id = "complete-cap"
|
|
373
|
+
name = "Complete Capability"
|
|
374
|
+
version = "1.0.0"
|
|
375
|
+
description = "Has all fields"
|
|
376
|
+
|
|
377
|
+
[exports]
|
|
378
|
+
functions = ["fn1", "fn2"]
|
|
379
|
+
|
|
380
|
+
[[env]]
|
|
381
|
+
key = "VAR1"
|
|
382
|
+
description = "Variable 1"
|
|
383
|
+
required = true
|
|
384
|
+
secret = false
|
|
385
|
+
|
|
386
|
+
[[env]]
|
|
387
|
+
key = "VAR2"
|
|
388
|
+
description = "Variable 2"
|
|
389
|
+
required = false
|
|
390
|
+
secret = true
|
|
391
|
+
|
|
392
|
+
[mcp]
|
|
393
|
+
tools = ["tool1", "tool2"]`,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const config = await loadCapabilityConfig(capPath);
|
|
397
|
+
|
|
398
|
+
expect(config.capability.id).toBe("complete-cap");
|
|
399
|
+
expect(config.exports?.functions).toEqual(["fn1", "fn2"]);
|
|
400
|
+
expect(config.env).toHaveLength(2);
|
|
401
|
+
expect(config.mcp?.tools).toEqual(["tool1", "tool2"]);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("loadCapability", () => {
|
|
406
|
+
let testDir: string;
|
|
407
|
+
let capabilitiesDir: string;
|
|
408
|
+
let originalCwd: string;
|
|
409
|
+
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
// Save current working directory
|
|
412
|
+
originalCwd = process.cwd();
|
|
413
|
+
|
|
414
|
+
// Create test directory in os temp dir
|
|
415
|
+
testDir = mkdtempSync(join(tmpdir(), "test-load-capability-"));
|
|
416
|
+
capabilitiesDir = join(testDir, "omni", "capabilities");
|
|
417
|
+
mkdirSync(capabilitiesDir, { recursive: true });
|
|
418
|
+
|
|
419
|
+
// Change to test directory
|
|
420
|
+
process.chdir(testDir);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
afterEach(() => {
|
|
424
|
+
// Restore working directory
|
|
425
|
+
process.chdir(originalCwd);
|
|
426
|
+
|
|
427
|
+
// Cleanup
|
|
428
|
+
if (existsSync(testDir)) {
|
|
429
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("loads capability with minimal config (no optional fields)", async () => {
|
|
434
|
+
const capPath = join(".omni", "capabilities", "minimal-cap");
|
|
435
|
+
mkdirSync(capPath, { recursive: true });
|
|
436
|
+
writeFileSync(
|
|
437
|
+
join(capPath, "capability.toml"),
|
|
438
|
+
`[capability]
|
|
439
|
+
id = "minimal-cap"
|
|
440
|
+
name = "Minimal Capability"
|
|
441
|
+
version = "1.0.0"
|
|
442
|
+
description = "A minimal capability"`,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const capability = await loadCapability(capPath, {});
|
|
446
|
+
|
|
447
|
+
expect(capability.id).toBe("minimal-cap");
|
|
448
|
+
expect(capability.path).toBe(capPath);
|
|
449
|
+
expect(capability.config.capability.name).toBe("Minimal Capability");
|
|
450
|
+
expect(capability.skills).toEqual([]);
|
|
451
|
+
expect(capability.rules).toEqual([]);
|
|
452
|
+
expect(capability.docs).toEqual([]);
|
|
453
|
+
expect(capability.typeDefinitions).toBeUndefined();
|
|
454
|
+
expect(capability.exports).toEqual({});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("loads capability with skills from filesystem", async () => {
|
|
458
|
+
const capPath = join(".omni", "capabilities", "with-skills");
|
|
459
|
+
mkdirSync(capPath, { recursive: true });
|
|
460
|
+
writeFileSync(
|
|
461
|
+
join(capPath, "capability.toml"),
|
|
462
|
+
`[capability]
|
|
463
|
+
id = "with-skills"
|
|
464
|
+
name = "With Skills"
|
|
465
|
+
version = "1.0.0"
|
|
466
|
+
description = "Has skills"`,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Create a skill
|
|
470
|
+
const skillPath = join(capPath, "skills", "test-skill");
|
|
471
|
+
mkdirSync(skillPath, { recursive: true });
|
|
472
|
+
writeFileSync(
|
|
473
|
+
join(skillPath, "SKILL.md"),
|
|
474
|
+
`---
|
|
475
|
+
name: test-skill
|
|
476
|
+
description: A test skill
|
|
477
|
+
---
|
|
478
|
+
Do something useful`,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const capability = await loadCapability(capPath, {});
|
|
482
|
+
|
|
483
|
+
expect(capability.skills).toHaveLength(1);
|
|
484
|
+
expect(capability.skills[0]?.name).toBe("test-skill");
|
|
485
|
+
expect(capability.skills[0]?.description).toBe("A test skill");
|
|
486
|
+
expect(capability.skills[0]?.instructions).toBe("Do something useful");
|
|
487
|
+
expect(capability.skills[0]?.capabilityId).toBe("with-skills");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("loads capability with rules from filesystem", async () => {
|
|
491
|
+
const capPath = join(".omni", "capabilities", "with-rules");
|
|
492
|
+
mkdirSync(capPath, { recursive: true });
|
|
493
|
+
writeFileSync(
|
|
494
|
+
join(capPath, "capability.toml"),
|
|
495
|
+
`[capability]
|
|
496
|
+
id = "with-rules"
|
|
497
|
+
name = "With Rules"
|
|
498
|
+
version = "1.0.0"
|
|
499
|
+
description = "Has rules"`,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// Create rules
|
|
503
|
+
const rulesPath = join(capPath, "rules");
|
|
504
|
+
mkdirSync(rulesPath, { recursive: true });
|
|
505
|
+
writeFileSync(join(rulesPath, "rule-1.md"), "Rule 1 content");
|
|
506
|
+
writeFileSync(join(rulesPath, "rule-2.md"), "Rule 2 content");
|
|
507
|
+
|
|
508
|
+
const capability = await loadCapability(capPath, {});
|
|
509
|
+
|
|
510
|
+
expect(capability.rules).toHaveLength(2);
|
|
511
|
+
expect(capability.rules.find((r) => r.name === "rule-1")).toBeDefined();
|
|
512
|
+
expect(capability.rules.find((r) => r.name === "rule-2")).toBeDefined();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("loads capability with docs from filesystem", async () => {
|
|
516
|
+
const capPath = join(".omni", "capabilities", "with-docs");
|
|
517
|
+
mkdirSync(capPath, { recursive: true });
|
|
518
|
+
writeFileSync(
|
|
519
|
+
join(capPath, "capability.toml"),
|
|
520
|
+
`[capability]
|
|
521
|
+
id = "with-docs"
|
|
522
|
+
name = "With Docs"
|
|
523
|
+
version = "1.0.0"
|
|
524
|
+
description = "Has docs"`,
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Create definition.md
|
|
528
|
+
writeFileSync(join(capPath, "definition.md"), "# Definition");
|
|
529
|
+
|
|
530
|
+
// Create docs
|
|
531
|
+
const docsPath = join(capPath, "docs");
|
|
532
|
+
mkdirSync(docsPath, { recursive: true });
|
|
533
|
+
writeFileSync(join(docsPath, "guide.md"), "# Guide");
|
|
534
|
+
|
|
535
|
+
const capability = await loadCapability(capPath, {});
|
|
536
|
+
|
|
537
|
+
expect(capability.docs).toHaveLength(2);
|
|
538
|
+
expect(capability.docs.find((d) => d.name === "definition")).toBeDefined();
|
|
539
|
+
expect(capability.docs.find((d) => d.name === "guide")).toBeDefined();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("loads capability with type definitions from filesystem", async () => {
|
|
543
|
+
const capPath = join(".omni", "capabilities", "with-types");
|
|
544
|
+
mkdirSync(capPath, { recursive: true });
|
|
545
|
+
writeFileSync(
|
|
546
|
+
join(capPath, "capability.toml"),
|
|
547
|
+
`[capability]
|
|
548
|
+
id = "with-types"
|
|
549
|
+
name = "With Types"
|
|
550
|
+
version = "1.0.0"
|
|
551
|
+
description = "Has type definitions"`,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// Create types.d.ts
|
|
555
|
+
writeFileSync(join(capPath, "types.d.ts"), "export function doSomething(): void;");
|
|
556
|
+
|
|
557
|
+
const capability = await loadCapability(capPath, {});
|
|
558
|
+
|
|
559
|
+
expect(capability.typeDefinitions).toBe("export function doSomething(): void;");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("loads capability with exports from index.ts", async () => {
|
|
563
|
+
const capPath = join(".omni", "capabilities", "with-exports");
|
|
564
|
+
mkdirSync(capPath, { recursive: true });
|
|
565
|
+
writeFileSync(
|
|
566
|
+
join(capPath, "capability.toml"),
|
|
567
|
+
`[capability]
|
|
568
|
+
id = "with-exports"
|
|
569
|
+
name = "With Exports"
|
|
570
|
+
version = "1.0.0"
|
|
571
|
+
description = "Has exports"`,
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// Create index.ts with exports
|
|
575
|
+
writeFileSync(join(capPath, "index.ts"), 'export function myFunction() { return "hello"; }');
|
|
576
|
+
|
|
577
|
+
const capability = await loadCapability(capPath, {});
|
|
578
|
+
|
|
579
|
+
expect(capability.exports).toBeDefined();
|
|
580
|
+
expect(typeof capability.exports.myFunction).toBe("function");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("validates environment variables when config has env requirements", async () => {
|
|
584
|
+
const capPath = join(".omni", "capabilities", "needs-env");
|
|
585
|
+
mkdirSync(capPath, { recursive: true });
|
|
586
|
+
writeFileSync(
|
|
587
|
+
join(capPath, "capability.toml"),
|
|
588
|
+
`[capability]
|
|
589
|
+
id = "needs-env"
|
|
590
|
+
name = "Needs Env"
|
|
591
|
+
version = "1.0.0"
|
|
592
|
+
description = "Requires env vars"
|
|
593
|
+
|
|
594
|
+
[env.API_KEY]
|
|
595
|
+
required = true`,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Should throw when required env var is missing
|
|
599
|
+
expect(async () => await loadCapability(capPath, {})).toThrow();
|
|
600
|
+
|
|
601
|
+
// Should succeed when env var is provided
|
|
602
|
+
const capability = await loadCapability(capPath, { API_KEY: "test-key" });
|
|
603
|
+
expect(capability.id).toBe("needs-env");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("programmatic skills take precedence over filesystem", async () => {
|
|
607
|
+
const capPath = join(".omni", "capabilities", "programmatic-skills");
|
|
608
|
+
mkdirSync(capPath, { recursive: true });
|
|
609
|
+
writeFileSync(
|
|
610
|
+
join(capPath, "capability.toml"),
|
|
611
|
+
`[capability]
|
|
612
|
+
id = "programmatic-skills"
|
|
613
|
+
name = "Programmatic Skills"
|
|
614
|
+
version = "1.0.0"
|
|
615
|
+
description = "Has programmatic skills"`,
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
// Create filesystem skill
|
|
619
|
+
const skillPath = join(capPath, "skills", "fs-skill");
|
|
620
|
+
mkdirSync(skillPath, { recursive: true });
|
|
621
|
+
writeFileSync(
|
|
622
|
+
join(skillPath, "SKILL.md"),
|
|
623
|
+
`---
|
|
624
|
+
name: fs-skill
|
|
625
|
+
description: Filesystem skill
|
|
626
|
+
---
|
|
627
|
+
From filesystem`,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Create index.ts with programmatic skills
|
|
631
|
+
writeFileSync(
|
|
632
|
+
join(capPath, "index.ts"),
|
|
633
|
+
`export const skills = [
|
|
634
|
+
{
|
|
635
|
+
skillMd: \`---
|
|
636
|
+
name: programmatic-skill
|
|
637
|
+
description: Programmatic skill
|
|
638
|
+
---
|
|
639
|
+
From code\`
|
|
640
|
+
}
|
|
641
|
+
];`,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
const capability = await loadCapability(capPath, {});
|
|
645
|
+
|
|
646
|
+
// Should have programmatic skills, not filesystem ones
|
|
647
|
+
expect(capability.skills).toHaveLength(1);
|
|
648
|
+
expect(capability.skills[0]?.name).toBe("programmatic-skill");
|
|
649
|
+
expect(capability.skills[0]?.instructions).toBe("From code");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("programmatic rules take precedence over filesystem", async () => {
|
|
653
|
+
const capPath = join(".omni", "capabilities", "programmatic-rules");
|
|
654
|
+
mkdirSync(capPath, { recursive: true });
|
|
655
|
+
writeFileSync(
|
|
656
|
+
join(capPath, "capability.toml"),
|
|
657
|
+
`[capability]
|
|
658
|
+
id = "programmatic-rules"
|
|
659
|
+
name = "Programmatic Rules"
|
|
660
|
+
version = "1.0.0"
|
|
661
|
+
description = "Has programmatic rules"`,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Create filesystem rule
|
|
665
|
+
const rulesPath = join(capPath, "rules");
|
|
666
|
+
mkdirSync(rulesPath, { recursive: true });
|
|
667
|
+
writeFileSync(join(rulesPath, "fs-rule.md"), "From filesystem");
|
|
668
|
+
|
|
669
|
+
// Create index.ts with programmatic rules (new format)
|
|
670
|
+
writeFileSync(
|
|
671
|
+
join(capPath, "index.ts"),
|
|
672
|
+
`export const rules = [
|
|
673
|
+
'From code'
|
|
674
|
+
];`,
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
const capability = await loadCapability(capPath, {});
|
|
678
|
+
|
|
679
|
+
// Should have programmatic rules, not filesystem ones
|
|
680
|
+
expect(capability.rules).toHaveLength(1);
|
|
681
|
+
expect(capability.rules[0]?.name).toBe("rule-1");
|
|
682
|
+
expect(capability.rules[0]?.content).toBe("From code");
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("programmatic docs take precedence over filesystem", async () => {
|
|
686
|
+
const capPath = join(".omni", "capabilities", "programmatic-docs");
|
|
687
|
+
mkdirSync(capPath, { recursive: true });
|
|
688
|
+
writeFileSync(
|
|
689
|
+
join(capPath, "capability.toml"),
|
|
690
|
+
`[capability]
|
|
691
|
+
id = "programmatic-docs"
|
|
692
|
+
name = "Programmatic Docs"
|
|
693
|
+
version = "1.0.0"
|
|
694
|
+
description = "Has programmatic docs"`,
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Create filesystem doc
|
|
698
|
+
writeFileSync(join(capPath, "definition.md"), "From filesystem");
|
|
699
|
+
|
|
700
|
+
// Create index.ts with programmatic docs (new format)
|
|
701
|
+
writeFileSync(
|
|
702
|
+
join(capPath, "index.ts"),
|
|
703
|
+
`export const docs = [
|
|
704
|
+
{
|
|
705
|
+
title: 'programmatic-doc',
|
|
706
|
+
content: 'From code'
|
|
707
|
+
}
|
|
708
|
+
];`,
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const capability = await loadCapability(capPath, {});
|
|
712
|
+
|
|
713
|
+
// Should have programmatic docs, not filesystem ones
|
|
714
|
+
expect(capability.docs).toHaveLength(1);
|
|
715
|
+
expect(capability.docs[0]?.name).toBe("programmatic-doc");
|
|
716
|
+
expect(capability.docs[0]?.content).toBe("From code");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("programmatic type definitions take precedence over filesystem", async () => {
|
|
720
|
+
const capPath = join(".omni", "capabilities", "programmatic-types");
|
|
721
|
+
mkdirSync(capPath, { recursive: true });
|
|
722
|
+
writeFileSync(
|
|
723
|
+
join(capPath, "capability.toml"),
|
|
724
|
+
`[capability]
|
|
725
|
+
id = "programmatic-types"
|
|
726
|
+
name = "Programmatic Types"
|
|
727
|
+
version = "1.0.0"
|
|
728
|
+
description = "Has programmatic type definitions"`,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// Create filesystem types
|
|
732
|
+
writeFileSync(join(capPath, "types.d.ts"), "export type Foo = string;");
|
|
733
|
+
|
|
734
|
+
// Create index.ts with programmatic type definitions
|
|
735
|
+
writeFileSync(
|
|
736
|
+
join(capPath, "index.ts"),
|
|
737
|
+
'export const typeDefinitions = "export type Bar = number;";',
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const capability = await loadCapability(capPath, {});
|
|
741
|
+
|
|
742
|
+
// Should have programmatic type definitions, not filesystem ones
|
|
743
|
+
expect(capability.typeDefinitions).toBe("export type Bar = number;");
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("throws error when index.ts has import errors", async () => {
|
|
747
|
+
const capPath = join(".omni", "capabilities", "bad-import");
|
|
748
|
+
mkdirSync(capPath, { recursive: true });
|
|
749
|
+
writeFileSync(
|
|
750
|
+
join(capPath, "capability.toml"),
|
|
751
|
+
`[capability]
|
|
752
|
+
id = "bad-import"
|
|
753
|
+
name = "Bad Import"
|
|
754
|
+
version = "1.0.0"
|
|
755
|
+
description = "Has import errors"`,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Create index.ts with syntax error
|
|
759
|
+
writeFileSync(join(capPath, "index.ts"), "export const foo = bar; // bar is undefined");
|
|
760
|
+
|
|
761
|
+
// Should throw when trying to import
|
|
762
|
+
expect(async () => await loadCapability(capPath, {})).toThrow();
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("loads complete capability with all features", async () => {
|
|
766
|
+
const capPath = join(".omni", "capabilities", "complete");
|
|
767
|
+
mkdirSync(capPath, { recursive: true });
|
|
768
|
+
writeFileSync(
|
|
769
|
+
join(capPath, "capability.toml"),
|
|
770
|
+
`[capability]
|
|
771
|
+
id = "complete"
|
|
772
|
+
name = "Complete Capability"
|
|
773
|
+
version = "2.0.0"
|
|
774
|
+
description = "Has everything"`,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Create skills
|
|
778
|
+
const skillPath = join(capPath, "skills", "skill1");
|
|
779
|
+
mkdirSync(skillPath, { recursive: true });
|
|
780
|
+
writeFileSync(
|
|
781
|
+
join(skillPath, "SKILL.md"),
|
|
782
|
+
`---
|
|
783
|
+
name: skill1
|
|
784
|
+
description: First skill
|
|
785
|
+
---
|
|
786
|
+
Skill instructions`,
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
// Create rules
|
|
790
|
+
const rulesPath = join(capPath, "rules");
|
|
791
|
+
mkdirSync(rulesPath, { recursive: true });
|
|
792
|
+
writeFileSync(join(rulesPath, "rule1.md"), "Rule content");
|
|
793
|
+
|
|
794
|
+
// Create docs
|
|
795
|
+
writeFileSync(join(capPath, "definition.md"), "# Definition");
|
|
796
|
+
const docsPath = join(capPath, "docs");
|
|
797
|
+
mkdirSync(docsPath, { recursive: true });
|
|
798
|
+
writeFileSync(join(docsPath, "guide.md"), "# Guide");
|
|
799
|
+
|
|
800
|
+
// Create types
|
|
801
|
+
writeFileSync(join(capPath, "types.d.ts"), "export type T = string;");
|
|
802
|
+
|
|
803
|
+
// Create exports
|
|
804
|
+
writeFileSync(join(capPath, "index.ts"), "export function helper() { return 42; }");
|
|
805
|
+
|
|
806
|
+
const capability = await loadCapability(capPath, {});
|
|
807
|
+
|
|
808
|
+
expect(capability.id).toBe("complete");
|
|
809
|
+
expect(capability.skills).toHaveLength(1);
|
|
810
|
+
expect(capability.rules).toHaveLength(1);
|
|
811
|
+
expect(capability.docs).toHaveLength(2);
|
|
812
|
+
expect(capability.typeDefinitions).toBe("export type T = string;");
|
|
813
|
+
expect(typeof capability.exports.helper).toBe("function");
|
|
814
|
+
});
|
|
815
|
+
});
|