@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,473 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { buildCapabilityRegistry } from "./registry";
|
|
5
|
+
|
|
6
|
+
describe("buildCapabilityRegistry", () => {
|
|
7
|
+
const testDir = "test-capability-registry";
|
|
8
|
+
const omniDir = join(testDir, ".omni");
|
|
9
|
+
const capabilitiesDir = join(testDir, ".omni", "capabilities");
|
|
10
|
+
let originalCwd: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Save current working directory
|
|
14
|
+
originalCwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
// Create test directory structure
|
|
17
|
+
if (existsSync(testDir)) {
|
|
18
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
mkdirSync(capabilitiesDir, { recursive: true });
|
|
21
|
+
mkdirSync(omniDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Create default config with profiles
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(testDir, "omni.toml"),
|
|
26
|
+
`project = "test"
|
|
27
|
+
active_profile = "default"
|
|
28
|
+
|
|
29
|
+
[profiles.default]
|
|
30
|
+
capabilities = ["cap1", "cap2"]
|
|
31
|
+
`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Change to test directory
|
|
35
|
+
process.chdir(testDir);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
// Restore working directory
|
|
40
|
+
process.chdir(originalCwd);
|
|
41
|
+
|
|
42
|
+
// Cleanup
|
|
43
|
+
if (existsSync(testDir)) {
|
|
44
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("builds empty registry when no capabilities exist", async () => {
|
|
49
|
+
const registry = await buildCapabilityRegistry();
|
|
50
|
+
|
|
51
|
+
expect(registry.capabilities.size).toBe(0);
|
|
52
|
+
expect(registry.getAllCapabilities()).toEqual([]);
|
|
53
|
+
expect(registry.getAllSkills()).toEqual([]);
|
|
54
|
+
expect(registry.getAllRules()).toEqual([]);
|
|
55
|
+
expect(registry.getAllDocs()).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("builds registry with single enabled capability", async () => {
|
|
59
|
+
// Create capability
|
|
60
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
61
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
62
|
+
writeFileSync(
|
|
63
|
+
join(cap1Path, "capability.toml"),
|
|
64
|
+
`[capability]
|
|
65
|
+
id = "cap1"
|
|
66
|
+
name = "Capability 1"
|
|
67
|
+
version = "1.0.0"
|
|
68
|
+
description = "First capability"`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const registry = await buildCapabilityRegistry();
|
|
72
|
+
|
|
73
|
+
expect(registry.capabilities.size).toBe(1);
|
|
74
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
75
|
+
expect(registry.getCapability("cap1")?.id).toBe("cap1");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("builds registry with multiple enabled capabilities", async () => {
|
|
79
|
+
// Create cap1
|
|
80
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
81
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
82
|
+
writeFileSync(
|
|
83
|
+
join(cap1Path, "capability.toml"),
|
|
84
|
+
`[capability]
|
|
85
|
+
id = "cap1"
|
|
86
|
+
name = "Capability 1"
|
|
87
|
+
version = "1.0.0"
|
|
88
|
+
description = "First capability"`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Create cap2
|
|
92
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
93
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(cap2Path, "capability.toml"),
|
|
96
|
+
`[capability]
|
|
97
|
+
id = "cap2"
|
|
98
|
+
name = "Capability 2"
|
|
99
|
+
version = "1.0.0"
|
|
100
|
+
description = "Second capability"`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const registry = await buildCapabilityRegistry();
|
|
104
|
+
|
|
105
|
+
expect(registry.capabilities.size).toBe(2);
|
|
106
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
107
|
+
expect(registry.getCapability("cap2")).toBeDefined();
|
|
108
|
+
expect(registry.getAllCapabilities()).toHaveLength(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("filters out disabled capabilities", async () => {
|
|
112
|
+
// Update config to only enable cap1
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join("omni.toml"),
|
|
115
|
+
`project = "test"
|
|
116
|
+
active_profile = "default"
|
|
117
|
+
|
|
118
|
+
[profiles.default]
|
|
119
|
+
capabilities = ["cap1"]
|
|
120
|
+
`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Create cap1 (enabled)
|
|
124
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
125
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(cap1Path, "capability.toml"),
|
|
128
|
+
`[capability]
|
|
129
|
+
id = "cap1"
|
|
130
|
+
name = "Capability 1"
|
|
131
|
+
version = "1.0.0"
|
|
132
|
+
description = "Enabled capability"`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Create cap2 (not enabled)
|
|
136
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
137
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
138
|
+
writeFileSync(
|
|
139
|
+
join(cap2Path, "capability.toml"),
|
|
140
|
+
`[capability]
|
|
141
|
+
id = "cap2"
|
|
142
|
+
name = "Capability 2"
|
|
143
|
+
version = "1.0.0"
|
|
144
|
+
description = "Disabled capability"`,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const registry = await buildCapabilityRegistry();
|
|
148
|
+
|
|
149
|
+
expect(registry.capabilities.size).toBe(1);
|
|
150
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
151
|
+
expect(registry.getCapability("cap2")).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("respects active profile configuration", async () => {
|
|
155
|
+
// Create config with dev profile that enables both cap1 and cap2
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join("omni.toml"),
|
|
158
|
+
`project = "test"
|
|
159
|
+
active_profile = "dev"
|
|
160
|
+
|
|
161
|
+
[profiles.default]
|
|
162
|
+
capabilities = ["cap1"]
|
|
163
|
+
|
|
164
|
+
[profiles.dev]
|
|
165
|
+
capabilities = ["cap1", "cap2"]
|
|
166
|
+
`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Create cap1
|
|
170
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
171
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
172
|
+
writeFileSync(
|
|
173
|
+
join(cap1Path, "capability.toml"),
|
|
174
|
+
`[capability]
|
|
175
|
+
id = "cap1"
|
|
176
|
+
name = "Capability 1"
|
|
177
|
+
version = "1.0.0"
|
|
178
|
+
description = "Base capability"`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Create cap2
|
|
182
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
183
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(cap2Path, "capability.toml"),
|
|
186
|
+
`[capability]
|
|
187
|
+
id = "cap2"
|
|
188
|
+
name = "Capability 2"
|
|
189
|
+
version = "1.0.0"
|
|
190
|
+
description = "Profile capability"`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const registry = await buildCapabilityRegistry();
|
|
194
|
+
|
|
195
|
+
// Should have both cap1 and cap2 since dev profile is active
|
|
196
|
+
expect(registry.capabilities.size).toBe(2);
|
|
197
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
198
|
+
expect(registry.getCapability("cap2")).toBeDefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("getAllSkills returns skills from all capabilities", async () => {
|
|
202
|
+
// Create cap1 with skill
|
|
203
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
204
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
205
|
+
writeFileSync(
|
|
206
|
+
join(cap1Path, "capability.toml"),
|
|
207
|
+
`[capability]
|
|
208
|
+
id = "cap1"
|
|
209
|
+
name = "Capability 1"
|
|
210
|
+
version = "1.0.0"
|
|
211
|
+
description = "Has skill"`,
|
|
212
|
+
);
|
|
213
|
+
const skill1Path = join(cap1Path, "skills", "skill1");
|
|
214
|
+
mkdirSync(skill1Path, { recursive: true });
|
|
215
|
+
writeFileSync(
|
|
216
|
+
join(skill1Path, "SKILL.md"),
|
|
217
|
+
`---
|
|
218
|
+
name: skill1
|
|
219
|
+
description: Skill 1
|
|
220
|
+
---
|
|
221
|
+
Instructions 1`,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Create cap2 with skill
|
|
225
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
226
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
227
|
+
writeFileSync(
|
|
228
|
+
join(cap2Path, "capability.toml"),
|
|
229
|
+
`[capability]
|
|
230
|
+
id = "cap2"
|
|
231
|
+
name = "Capability 2"
|
|
232
|
+
version = "1.0.0"
|
|
233
|
+
description = "Has skill"`,
|
|
234
|
+
);
|
|
235
|
+
const skill2Path = join(cap2Path, "skills", "skill2");
|
|
236
|
+
mkdirSync(skill2Path, { recursive: true });
|
|
237
|
+
writeFileSync(
|
|
238
|
+
join(skill2Path, "SKILL.md"),
|
|
239
|
+
`---
|
|
240
|
+
name: skill2
|
|
241
|
+
description: Skill 2
|
|
242
|
+
---
|
|
243
|
+
Instructions 2`,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const registry = await buildCapabilityRegistry();
|
|
247
|
+
|
|
248
|
+
const skills = registry.getAllSkills();
|
|
249
|
+
expect(skills).toHaveLength(2);
|
|
250
|
+
expect(skills.find((s) => s.name === "skill1")).toBeDefined();
|
|
251
|
+
expect(skills.find((s) => s.name === "skill2")).toBeDefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("getAllRules returns rules from all capabilities", async () => {
|
|
255
|
+
// Create cap1 with rule
|
|
256
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
257
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
258
|
+
writeFileSync(
|
|
259
|
+
join(cap1Path, "capability.toml"),
|
|
260
|
+
`[capability]
|
|
261
|
+
id = "cap1"
|
|
262
|
+
name = "Capability 1"
|
|
263
|
+
version = "1.0.0"
|
|
264
|
+
description = "Has rule"`,
|
|
265
|
+
);
|
|
266
|
+
const rulesPath = join(cap1Path, "rules");
|
|
267
|
+
mkdirSync(rulesPath, { recursive: true });
|
|
268
|
+
writeFileSync(join(rulesPath, "rule1.md"), "Rule 1 content");
|
|
269
|
+
|
|
270
|
+
// Create cap2 with rule
|
|
271
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
272
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
273
|
+
writeFileSync(
|
|
274
|
+
join(cap2Path, "capability.toml"),
|
|
275
|
+
`[capability]
|
|
276
|
+
id = "cap2"
|
|
277
|
+
name = "Capability 2"
|
|
278
|
+
version = "1.0.0"
|
|
279
|
+
description = "Has rule"`,
|
|
280
|
+
);
|
|
281
|
+
const rules2Path = join(cap2Path, "rules");
|
|
282
|
+
mkdirSync(rules2Path, { recursive: true });
|
|
283
|
+
writeFileSync(join(rules2Path, "rule2.md"), "Rule 2 content");
|
|
284
|
+
|
|
285
|
+
const registry = await buildCapabilityRegistry();
|
|
286
|
+
|
|
287
|
+
const rules = registry.getAllRules();
|
|
288
|
+
expect(rules).toHaveLength(2);
|
|
289
|
+
expect(rules.find((r) => r.name === "rule1")).toBeDefined();
|
|
290
|
+
expect(rules.find((r) => r.name === "rule2")).toBeDefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("getAllDocs returns docs from all capabilities", async () => {
|
|
294
|
+
// Create cap1 with doc
|
|
295
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
296
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
297
|
+
writeFileSync(
|
|
298
|
+
join(cap1Path, "capability.toml"),
|
|
299
|
+
`[capability]
|
|
300
|
+
id = "cap1"
|
|
301
|
+
name = "Capability 1"
|
|
302
|
+
version = "1.0.0"
|
|
303
|
+
description = "Has doc"`,
|
|
304
|
+
);
|
|
305
|
+
writeFileSync(join(cap1Path, "definition.md"), "# Cap1 Definition");
|
|
306
|
+
|
|
307
|
+
// Create cap2 with doc
|
|
308
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
309
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
310
|
+
writeFileSync(
|
|
311
|
+
join(cap2Path, "capability.toml"),
|
|
312
|
+
`[capability]
|
|
313
|
+
id = "cap2"
|
|
314
|
+
name = "Capability 2"
|
|
315
|
+
version = "1.0.0"
|
|
316
|
+
description = "Has doc"`,
|
|
317
|
+
);
|
|
318
|
+
writeFileSync(join(cap2Path, "definition.md"), "# Cap2 Definition");
|
|
319
|
+
|
|
320
|
+
const registry = await buildCapabilityRegistry();
|
|
321
|
+
|
|
322
|
+
const docs = registry.getAllDocs();
|
|
323
|
+
expect(docs).toHaveLength(2);
|
|
324
|
+
expect(docs.every((d) => d.name === "definition")).toBe(true);
|
|
325
|
+
expect(docs.find((d) => d.capabilityId === "cap1")).toBeDefined();
|
|
326
|
+
expect(docs.find((d) => d.capabilityId === "cap2")).toBeDefined();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("handles capability loading errors gracefully", async () => {
|
|
330
|
+
// Create valid capability
|
|
331
|
+
const cap1Path = join(".omni", "capabilities", "cap1");
|
|
332
|
+
mkdirSync(cap1Path, { recursive: true });
|
|
333
|
+
writeFileSync(
|
|
334
|
+
join(cap1Path, "capability.toml"),
|
|
335
|
+
`[capability]
|
|
336
|
+
id = "cap1"
|
|
337
|
+
name = "Valid Capability"
|
|
338
|
+
version = "1.0.0"
|
|
339
|
+
description = "Valid"`,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Create invalid capability
|
|
343
|
+
const cap2Path = join(".omni", "capabilities", "cap2");
|
|
344
|
+
mkdirSync(cap2Path, { recursive: true });
|
|
345
|
+
writeFileSync(
|
|
346
|
+
join(cap2Path, "capability.toml"),
|
|
347
|
+
`[capability]
|
|
348
|
+
id = "cap2"
|
|
349
|
+
# Missing required fields`,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const registry = await buildCapabilityRegistry();
|
|
353
|
+
|
|
354
|
+
// Should only have the valid capability
|
|
355
|
+
expect(registry.capabilities.size).toBe(1);
|
|
356
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
357
|
+
expect(registry.getCapability("cap2")).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("validates environment requirements before loading", async () => {
|
|
361
|
+
// Create capability requiring env var
|
|
362
|
+
const capPath = join(".omni", "capabilities", "cap1");
|
|
363
|
+
mkdirSync(capPath, { recursive: true });
|
|
364
|
+
writeFileSync(
|
|
365
|
+
join(capPath, "capability.toml"),
|
|
366
|
+
`[capability]
|
|
367
|
+
id = "cap1"
|
|
368
|
+
name = "Needs Env"
|
|
369
|
+
version = "1.0.0"
|
|
370
|
+
description = "Requires env"
|
|
371
|
+
|
|
372
|
+
[env.REQUIRED_VAR]
|
|
373
|
+
required = true`,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const registry = await buildCapabilityRegistry();
|
|
377
|
+
|
|
378
|
+
// Should not load capability without required env var
|
|
379
|
+
expect(registry.capabilities.size).toBe(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("loads capabilities when environment is satisfied", async () => {
|
|
383
|
+
// Create .env file with required var
|
|
384
|
+
writeFileSync(join(".omni", ".env"), "REQUIRED_VAR=test-value");
|
|
385
|
+
|
|
386
|
+
// Create capability requiring env var
|
|
387
|
+
const capPath = join(".omni", "capabilities", "cap1");
|
|
388
|
+
mkdirSync(capPath, { recursive: true });
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(capPath, "capability.toml"),
|
|
391
|
+
`[capability]
|
|
392
|
+
id = "cap1"
|
|
393
|
+
name = "Needs Env"
|
|
394
|
+
version = "1.0.0"
|
|
395
|
+
description = "Requires env"
|
|
396
|
+
|
|
397
|
+
[env.REQUIRED_VAR]
|
|
398
|
+
required = true`,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const registry = await buildCapabilityRegistry();
|
|
402
|
+
|
|
403
|
+
// Should load capability when env var is provided
|
|
404
|
+
expect(registry.capabilities.size).toBe(1);
|
|
405
|
+
expect(registry.getCapability("cap1")).toBeDefined();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("getCapability returns undefined for non-existent capability", async () => {
|
|
409
|
+
const registry = await buildCapabilityRegistry();
|
|
410
|
+
|
|
411
|
+
expect(registry.getCapability("non-existent")).toBeUndefined();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("getAllCapabilities returns empty array when no capabilities", async () => {
|
|
415
|
+
const registry = await buildCapabilityRegistry();
|
|
416
|
+
|
|
417
|
+
expect(registry.getAllCapabilities()).toEqual([]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("getAllSkills returns empty array when capabilities have no skills", async () => {
|
|
421
|
+
// Create capability without skills
|
|
422
|
+
const capPath = join(".omni", "capabilities", "cap1");
|
|
423
|
+
mkdirSync(capPath, { recursive: true });
|
|
424
|
+
writeFileSync(
|
|
425
|
+
join(capPath, "capability.toml"),
|
|
426
|
+
`[capability]
|
|
427
|
+
id = "cap1"
|
|
428
|
+
name = "No Skills"
|
|
429
|
+
version = "1.0.0"
|
|
430
|
+
description = "Has no skills"`,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const registry = await buildCapabilityRegistry();
|
|
434
|
+
|
|
435
|
+
expect(registry.getAllSkills()).toEqual([]);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("getAllRules returns empty array when capabilities have no rules", async () => {
|
|
439
|
+
// Create capability without rules
|
|
440
|
+
const capPath = join(".omni", "capabilities", "cap1");
|
|
441
|
+
mkdirSync(capPath, { recursive: true });
|
|
442
|
+
writeFileSync(
|
|
443
|
+
join(capPath, "capability.toml"),
|
|
444
|
+
`[capability]
|
|
445
|
+
id = "cap1"
|
|
446
|
+
name = "No Rules"
|
|
447
|
+
version = "1.0.0"
|
|
448
|
+
description = "Has no rules"`,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const registry = await buildCapabilityRegistry();
|
|
452
|
+
|
|
453
|
+
expect(registry.getAllRules()).toEqual([]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("getAllDocs returns empty array when capabilities have no docs", async () => {
|
|
457
|
+
// Create capability without docs
|
|
458
|
+
const capPath = join(".omni", "capabilities", "cap1");
|
|
459
|
+
mkdirSync(capPath, { recursive: true });
|
|
460
|
+
writeFileSync(
|
|
461
|
+
join(capPath, "capability.toml"),
|
|
462
|
+
`[capability]
|
|
463
|
+
id = "cap1"
|
|
464
|
+
name = "No Docs"
|
|
465
|
+
version = "1.0.0"
|
|
466
|
+
description = "Has no docs"`,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const registry = await buildCapabilityRegistry();
|
|
470
|
+
|
|
471
|
+
expect(registry.getAllDocs()).toEqual([]);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getEnabledCapabilities } from "../config/capabilities";
|
|
2
|
+
import { loadEnvironment } from "../config/env";
|
|
3
|
+
import type { Doc, LoadedCapability, Rule, Skill } from "../types";
|
|
4
|
+
import { discoverCapabilities, loadCapability } from "./loader";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registry of loaded capabilities with helper functions.
|
|
8
|
+
*/
|
|
9
|
+
export interface CapabilityRegistry {
|
|
10
|
+
capabilities: Map<string, LoadedCapability>;
|
|
11
|
+
getCapability(id: string): LoadedCapability | undefined;
|
|
12
|
+
getAllCapabilities(): LoadedCapability[];
|
|
13
|
+
getAllSkills(): Skill[];
|
|
14
|
+
getAllRules(): Rule[];
|
|
15
|
+
getAllDocs(): Doc[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds a capability registry by discovering, loading, and filtering capabilities.
|
|
20
|
+
* Only enabled capabilities (based on active profile) are included.
|
|
21
|
+
*
|
|
22
|
+
* @returns Capability registry with helper functions
|
|
23
|
+
*/
|
|
24
|
+
export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
|
|
25
|
+
const env = await loadEnvironment();
|
|
26
|
+
const enabledIds = await getEnabledCapabilities();
|
|
27
|
+
|
|
28
|
+
const capabilityPaths = await discoverCapabilities();
|
|
29
|
+
const capabilities = new Map<string, LoadedCapability>();
|
|
30
|
+
|
|
31
|
+
for (const path of capabilityPaths) {
|
|
32
|
+
try {
|
|
33
|
+
const cap = await loadCapability(path, env);
|
|
34
|
+
|
|
35
|
+
// Only add if enabled
|
|
36
|
+
if (enabledIds.includes(cap.id)) {
|
|
37
|
+
capabilities.set(cap.id, cap);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Extract just the error message without stack trace for cleaner output
|
|
41
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
42
|
+
console.warn(`Warning: Skipping capability at ${path}`);
|
|
43
|
+
console.warn(` ${errorMessage}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
capabilities,
|
|
49
|
+
getCapability: (id: string) => capabilities.get(id),
|
|
50
|
+
getAllCapabilities: () => [...capabilities.values()],
|
|
51
|
+
getAllSkills: () => [...capabilities.values()].flatMap((c) => c.skills),
|
|
52
|
+
getAllRules: () => [...capabilities.values()].flatMap((c) => c.rules),
|
|
53
|
+
getAllDocs: () => [...capabilities.values()].flatMap((c) => c.docs),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadRules } from "./rules";
|
|
5
|
+
|
|
6
|
+
describe("loadRules", () => {
|
|
7
|
+
let testDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
testDir = join(process.cwd(), "test-capability-rules");
|
|
11
|
+
mkdirSync(testDir, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (testDir) {
|
|
16
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns empty array when rules directory does not exist", async () => {
|
|
21
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
22
|
+
expect(rules).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("returns empty array when rules directory is empty", async () => {
|
|
26
|
+
mkdirSync(join(testDir, "rules"));
|
|
27
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
28
|
+
expect(rules).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("loads single rule from rules directory", async () => {
|
|
32
|
+
const rulesDir = join(testDir, "rules");
|
|
33
|
+
mkdirSync(rulesDir);
|
|
34
|
+
writeFileSync(join(rulesDir, "test-rule.md"), "# Test Rule\n\nThis is a test rule.");
|
|
35
|
+
|
|
36
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
37
|
+
expect(rules).toHaveLength(1);
|
|
38
|
+
expect(rules[0]?.name).toBe("test-rule");
|
|
39
|
+
expect(rules[0]?.content).toBe("# Test Rule\n\nThis is a test rule.");
|
|
40
|
+
expect(rules[0]?.capabilityId).toBe("test-cap");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("loads multiple rules from rules directory", async () => {
|
|
44
|
+
const rulesDir = join(testDir, "rules");
|
|
45
|
+
mkdirSync(rulesDir);
|
|
46
|
+
writeFileSync(join(rulesDir, "rule-one.md"), "# Rule One");
|
|
47
|
+
writeFileSync(join(rulesDir, "rule-two.md"), "# Rule Two");
|
|
48
|
+
writeFileSync(join(rulesDir, "rule-three.md"), "# Rule Three");
|
|
49
|
+
|
|
50
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
51
|
+
expect(rules).toHaveLength(3);
|
|
52
|
+
|
|
53
|
+
const names = rules.map((r) => r.name).sort();
|
|
54
|
+
expect(names).toEqual(["rule-one", "rule-three", "rule-two"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("trims whitespace from rule content", async () => {
|
|
58
|
+
const rulesDir = join(testDir, "rules");
|
|
59
|
+
mkdirSync(rulesDir);
|
|
60
|
+
writeFileSync(join(rulesDir, "trimmed.md"), "\n\n # Trimmed Rule\n\nContent here.\n\n ");
|
|
61
|
+
|
|
62
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
63
|
+
expect(rules).toHaveLength(1);
|
|
64
|
+
expect(rules[0]?.content).toBe("# Trimmed Rule\n\nContent here.");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("ignores non-markdown files in rules directory", async () => {
|
|
68
|
+
const rulesDir = join(testDir, "rules");
|
|
69
|
+
mkdirSync(rulesDir);
|
|
70
|
+
writeFileSync(join(rulesDir, "rule.md"), "# Rule");
|
|
71
|
+
writeFileSync(join(rulesDir, "readme.txt"), "Not a markdown file");
|
|
72
|
+
writeFileSync(join(rulesDir, "config.json"), "{}");
|
|
73
|
+
|
|
74
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
75
|
+
expect(rules).toHaveLength(1);
|
|
76
|
+
expect(rules[0]?.name).toBe("rule");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("ignores directories in rules directory", async () => {
|
|
80
|
+
const rulesDir = join(testDir, "rules");
|
|
81
|
+
mkdirSync(rulesDir);
|
|
82
|
+
mkdirSync(join(rulesDir, "subdir"));
|
|
83
|
+
writeFileSync(join(rulesDir, "rule.md"), "# Rule");
|
|
84
|
+
|
|
85
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
86
|
+
expect(rules).toHaveLength(1);
|
|
87
|
+
expect(rules[0]?.name).toBe("rule");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("handles rules with complex markdown formatting", async () => {
|
|
91
|
+
const rulesDir = join(testDir, "rules");
|
|
92
|
+
mkdirSync(rulesDir);
|
|
93
|
+
const complexMarkdown = `# Complex Rule
|
|
94
|
+
|
|
95
|
+
## Section 1
|
|
96
|
+
|
|
97
|
+
- List item 1
|
|
98
|
+
- List item 2
|
|
99
|
+
|
|
100
|
+
\`\`\`typescript
|
|
101
|
+
const code = "example";
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
**Bold text** and *italic text*.`;
|
|
105
|
+
|
|
106
|
+
writeFileSync(join(rulesDir, "complex.md"), complexMarkdown);
|
|
107
|
+
|
|
108
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
109
|
+
expect(rules).toHaveLength(1);
|
|
110
|
+
expect(rules[0]?.content).toBe(complexMarkdown);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("handles empty rule files", async () => {
|
|
114
|
+
const rulesDir = join(testDir, "rules");
|
|
115
|
+
mkdirSync(rulesDir);
|
|
116
|
+
writeFileSync(join(rulesDir, "empty.md"), "");
|
|
117
|
+
|
|
118
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
119
|
+
expect(rules).toHaveLength(1);
|
|
120
|
+
expect(rules[0]?.content).toBe("");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("handles rules with only whitespace", async () => {
|
|
124
|
+
const rulesDir = join(testDir, "rules");
|
|
125
|
+
mkdirSync(rulesDir);
|
|
126
|
+
writeFileSync(join(rulesDir, "whitespace.md"), " \n\n ");
|
|
127
|
+
|
|
128
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
129
|
+
expect(rules).toHaveLength(1);
|
|
130
|
+
expect(rules[0]?.content).toBe("");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("handles rule names with hyphens and underscores", async () => {
|
|
134
|
+
const rulesDir = join(testDir, "rules");
|
|
135
|
+
mkdirSync(rulesDir);
|
|
136
|
+
writeFileSync(join(rulesDir, "my-rule-name.md"), "Content");
|
|
137
|
+
writeFileSync(join(rulesDir, "another_rule_name.md"), "Content");
|
|
138
|
+
|
|
139
|
+
const rules = await loadRules(testDir, "test-cap");
|
|
140
|
+
expect(rules).toHaveLength(2);
|
|
141
|
+
|
|
142
|
+
const names = rules.map((r) => r.name).sort();
|
|
143
|
+
expect(names).toEqual(["another_rule_name", "my-rule-name"]);
|
|
144
|
+
});
|
|
145
|
+
});
|