@omnidev-ai/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +31 -0
  2. package/src/capability/AGENTS.md +58 -0
  3. package/src/capability/commands.test.ts +414 -0
  4. package/src/capability/commands.ts +70 -0
  5. package/src/capability/docs.test.ts +199 -0
  6. package/src/capability/docs.ts +46 -0
  7. package/src/capability/index.ts +20 -0
  8. package/src/capability/loader.test.ts +815 -0
  9. package/src/capability/loader.ts +492 -0
  10. package/src/capability/registry.test.ts +473 -0
  11. package/src/capability/registry.ts +55 -0
  12. package/src/capability/rules.test.ts +145 -0
  13. package/src/capability/rules.ts +133 -0
  14. package/src/capability/skills.test.ts +316 -0
  15. package/src/capability/skills.ts +56 -0
  16. package/src/capability/sources.test.ts +338 -0
  17. package/src/capability/sources.ts +966 -0
  18. package/src/capability/subagents.test.ts +478 -0
  19. package/src/capability/subagents.ts +103 -0
  20. package/src/capability/yaml-parser.ts +81 -0
  21. package/src/config/AGENTS.md +46 -0
  22. package/src/config/capabilities.ts +82 -0
  23. package/src/config/env.test.ts +286 -0
  24. package/src/config/env.ts +96 -0
  25. package/src/config/index.ts +6 -0
  26. package/src/config/loader.test.ts +282 -0
  27. package/src/config/loader.ts +137 -0
  28. package/src/config/parser.test.ts +281 -0
  29. package/src/config/parser.ts +55 -0
  30. package/src/config/profiles.test.ts +259 -0
  31. package/src/config/profiles.ts +75 -0
  32. package/src/config/provider.test.ts +79 -0
  33. package/src/config/provider.ts +55 -0
  34. package/src/debug.ts +20 -0
  35. package/src/gitignore/manager.test.ts +219 -0
  36. package/src/gitignore/manager.ts +167 -0
  37. package/src/index.test.ts +26 -0
  38. package/src/index.ts +39 -0
  39. package/src/mcp-json/index.ts +1 -0
  40. package/src/mcp-json/manager.test.ts +415 -0
  41. package/src/mcp-json/manager.ts +118 -0
  42. package/src/state/active-profile.test.ts +131 -0
  43. package/src/state/active-profile.ts +41 -0
  44. package/src/state/index.ts +2 -0
  45. package/src/state/manifest.test.ts +548 -0
  46. package/src/state/manifest.ts +164 -0
  47. package/src/sync.ts +213 -0
  48. package/src/templates/agents.test.ts +23 -0
  49. package/src/templates/agents.ts +14 -0
  50. package/src/templates/claude.test.ts +48 -0
  51. package/src/templates/claude.ts +122 -0
  52. package/src/test-utils/helpers.test.ts +196 -0
  53. package/src/test-utils/helpers.ts +187 -0
  54. package/src/test-utils/index.ts +30 -0
  55. package/src/test-utils/mocks.test.ts +83 -0
  56. package/src/test-utils/mocks.ts +101 -0
  57. package/src/types/capability-export.ts +234 -0
  58. package/src/types/index.test.ts +28 -0
  59. package/src/types/index.ts +270 -0
@@ -0,0 +1,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
+ });