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