@omnidev-ai/core 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/capability/commands.test.ts +6 -10
  3. package/src/capability/docs.test.ts +39 -46
  4. package/src/capability/docs.ts +3 -1
  5. package/src/capability/loader.test.ts +10 -157
  6. package/src/capability/loader.ts +8 -69
  7. package/src/capability/registry.test.ts +9 -27
  8. package/src/capability/rules.test.ts +25 -35
  9. package/src/capability/rules.ts +3 -1
  10. package/src/capability/skills.test.ts +6 -10
  11. package/src/capability/sources.test.ts +142 -41
  12. package/src/capability/sources.ts +377 -345
  13. package/src/capability/subagents.test.ts +7 -11
  14. package/src/capability/subagents.ts +3 -1
  15. package/src/capability/wrapping-integration.test.ts +412 -0
  16. package/src/config/capabilities.ts +0 -28
  17. package/src/config/env.test.ts +4 -18
  18. package/src/config/loader.test.ts +4 -86
  19. package/src/config/loader.ts +88 -18
  20. package/src/config/parser.test.ts +0 -25
  21. package/src/config/profiles.test.ts +5 -39
  22. package/src/config/provider.test.ts +5 -19
  23. package/src/index.ts +1 -3
  24. package/src/mcp-json/manager.test.ts +77 -182
  25. package/src/mcp-json/manager.ts +22 -34
  26. package/src/state/active-profile.test.ts +4 -18
  27. package/src/state/index.ts +1 -0
  28. package/src/state/manifest.test.ts +25 -162
  29. package/src/state/manifest.ts +4 -31
  30. package/src/state/providers.test.ts +125 -0
  31. package/src/state/providers.ts +69 -0
  32. package/src/sync.ts +128 -53
  33. package/src/templates/claude.ts +9 -74
  34. package/src/test-utils/helpers.test.ts +18 -0
  35. package/src/test-utils/helpers.ts +87 -2
  36. package/src/test-utils/index.ts +3 -0
  37. package/src/types/capability-export.ts +0 -77
  38. package/src/types/index.ts +66 -22
  39. package/src/gitignore/manager.test.ts +0 -216
  40. package/src/gitignore/manager.ts +0 -167
@@ -1,39 +1,29 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
5
  import { loadRules } from "./rules";
5
6
 
6
7
  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
- });
8
+ const testDir = setupTestDir("capability-rules-test-");
19
9
 
20
10
  test("returns empty array when rules directory does not exist", async () => {
21
- const rules = await loadRules(testDir, "test-cap");
11
+ const rules = await loadRules(testDir.path, "test-cap");
22
12
  expect(rules).toEqual([]);
23
13
  });
24
14
 
25
15
  test("returns empty array when rules directory is empty", async () => {
26
- mkdirSync(join(testDir, "rules"));
27
- const rules = await loadRules(testDir, "test-cap");
16
+ mkdirSync(join(testDir.path, "rules"));
17
+ const rules = await loadRules(testDir.path, "test-cap");
28
18
  expect(rules).toEqual([]);
29
19
  });
30
20
 
31
21
  test("loads single rule from rules directory", async () => {
32
- const rulesDir = join(testDir, "rules");
22
+ const rulesDir = join(testDir.path, "rules");
33
23
  mkdirSync(rulesDir);
34
24
  writeFileSync(join(rulesDir, "test-rule.md"), "# Test Rule\n\nThis is a test rule.");
35
25
 
36
- const rules = await loadRules(testDir, "test-cap");
26
+ const rules = await loadRules(testDir.path, "test-cap");
37
27
  expect(rules).toHaveLength(1);
38
28
  expect(rules[0]?.name).toBe("test-rule");
39
29
  expect(rules[0]?.content).toBe("# Test Rule\n\nThis is a test rule.");
@@ -41,13 +31,13 @@ describe("loadRules", () => {
41
31
  });
42
32
 
43
33
  test("loads multiple rules from rules directory", async () => {
44
- const rulesDir = join(testDir, "rules");
34
+ const rulesDir = join(testDir.path, "rules");
45
35
  mkdirSync(rulesDir);
46
36
  writeFileSync(join(rulesDir, "rule-one.md"), "# Rule One");
47
37
  writeFileSync(join(rulesDir, "rule-two.md"), "# Rule Two");
48
38
  writeFileSync(join(rulesDir, "rule-three.md"), "# Rule Three");
49
39
 
50
- const rules = await loadRules(testDir, "test-cap");
40
+ const rules = await loadRules(testDir.path, "test-cap");
51
41
  expect(rules).toHaveLength(3);
52
42
 
53
43
  const names = rules.map((r) => r.name).sort();
@@ -55,40 +45,40 @@ describe("loadRules", () => {
55
45
  });
56
46
 
57
47
  test("trims whitespace from rule content", async () => {
58
- const rulesDir = join(testDir, "rules");
48
+ const rulesDir = join(testDir.path, "rules");
59
49
  mkdirSync(rulesDir);
60
50
  writeFileSync(join(rulesDir, "trimmed.md"), "\n\n # Trimmed Rule\n\nContent here.\n\n ");
61
51
 
62
- const rules = await loadRules(testDir, "test-cap");
52
+ const rules = await loadRules(testDir.path, "test-cap");
63
53
  expect(rules).toHaveLength(1);
64
54
  expect(rules[0]?.content).toBe("# Trimmed Rule\n\nContent here.");
65
55
  });
66
56
 
67
57
  test("ignores non-markdown files in rules directory", async () => {
68
- const rulesDir = join(testDir, "rules");
58
+ const rulesDir = join(testDir.path, "rules");
69
59
  mkdirSync(rulesDir);
70
60
  writeFileSync(join(rulesDir, "rule.md"), "# Rule");
71
61
  writeFileSync(join(rulesDir, "readme.txt"), "Not a markdown file");
72
62
  writeFileSync(join(rulesDir, "config.json"), "{}");
73
63
 
74
- const rules = await loadRules(testDir, "test-cap");
64
+ const rules = await loadRules(testDir.path, "test-cap");
75
65
  expect(rules).toHaveLength(1);
76
66
  expect(rules[0]?.name).toBe("rule");
77
67
  });
78
68
 
79
69
  test("ignores directories in rules directory", async () => {
80
- const rulesDir = join(testDir, "rules");
70
+ const rulesDir = join(testDir.path, "rules");
81
71
  mkdirSync(rulesDir);
82
72
  mkdirSync(join(rulesDir, "subdir"));
83
73
  writeFileSync(join(rulesDir, "rule.md"), "# Rule");
84
74
 
85
- const rules = await loadRules(testDir, "test-cap");
75
+ const rules = await loadRules(testDir.path, "test-cap");
86
76
  expect(rules).toHaveLength(1);
87
77
  expect(rules[0]?.name).toBe("rule");
88
78
  });
89
79
 
90
80
  test("handles rules with complex markdown formatting", async () => {
91
- const rulesDir = join(testDir, "rules");
81
+ const rulesDir = join(testDir.path, "rules");
92
82
  mkdirSync(rulesDir);
93
83
  const complexMarkdown = `# Complex Rule
94
84
 
@@ -105,38 +95,38 @@ const code = "example";
105
95
 
106
96
  writeFileSync(join(rulesDir, "complex.md"), complexMarkdown);
107
97
 
108
- const rules = await loadRules(testDir, "test-cap");
98
+ const rules = await loadRules(testDir.path, "test-cap");
109
99
  expect(rules).toHaveLength(1);
110
100
  expect(rules[0]?.content).toBe(complexMarkdown);
111
101
  });
112
102
 
113
103
  test("handles empty rule files", async () => {
114
- const rulesDir = join(testDir, "rules");
104
+ const rulesDir = join(testDir.path, "rules");
115
105
  mkdirSync(rulesDir);
116
106
  writeFileSync(join(rulesDir, "empty.md"), "");
117
107
 
118
- const rules = await loadRules(testDir, "test-cap");
108
+ const rules = await loadRules(testDir.path, "test-cap");
119
109
  expect(rules).toHaveLength(1);
120
110
  expect(rules[0]?.content).toBe("");
121
111
  });
122
112
 
123
113
  test("handles rules with only whitespace", async () => {
124
- const rulesDir = join(testDir, "rules");
114
+ const rulesDir = join(testDir.path, "rules");
125
115
  mkdirSync(rulesDir);
126
116
  writeFileSync(join(rulesDir, "whitespace.md"), " \n\n ");
127
117
 
128
- const rules = await loadRules(testDir, "test-cap");
118
+ const rules = await loadRules(testDir.path, "test-cap");
129
119
  expect(rules).toHaveLength(1);
130
120
  expect(rules[0]?.content).toBe("");
131
121
  });
132
122
 
133
123
  test("handles rule names with hyphens and underscores", async () => {
134
- const rulesDir = join(testDir, "rules");
124
+ const rulesDir = join(testDir.path, "rules");
135
125
  mkdirSync(rulesDir);
136
126
  writeFileSync(join(rulesDir, "my-rule-name.md"), "Content");
137
127
  writeFileSync(join(rulesDir, "another_rule_name.md"), "Content");
138
128
 
139
- const rules = await loadRules(testDir, "test-cap");
129
+ const rules = await loadRules(testDir.path, "test-cap");
140
130
  expect(rules).toHaveLength(2);
141
131
 
142
132
  const names = rules.map((r) => r.name).sort();
@@ -16,7 +16,9 @@ export async function loadRules(capabilityPath: string, capabilityId: string): P
16
16
  }
17
17
 
18
18
  const rules: Rule[] = [];
19
- const entries = readdirSync(rulesDir, { withFileTypes: true });
19
+ const entries = readdirSync(rulesDir, { withFileTypes: true }).sort((a, b) =>
20
+ a.name.localeCompare(b.name),
21
+ );
20
22
 
21
23
  for (const entry of entries) {
22
24
  if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -1,22 +1,18 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
5
  import { loadSkills } from "./skills";
5
6
 
6
7
  describe("loadSkills", () => {
7
- const testDir = join(process.cwd(), "test-skills-temp");
8
- const capabilityPath = join(testDir, "test-capability");
8
+ const testDir = setupTestDir("capability-skills-test-");
9
+ let capabilityPath: string;
9
10
 
10
11
  beforeEach(() => {
12
+ capabilityPath = join(testDir.path, "test-capability");
11
13
  mkdirSync(capabilityPath, { recursive: true });
12
14
  });
13
15
 
14
- afterEach(() => {
15
- if (testDir) {
16
- rmSync(testDir, { recursive: true, force: true });
17
- }
18
- });
19
-
20
16
  test("returns empty array when skills directory does not exist", async () => {
21
17
  const skills = await loadSkills(capabilityPath, "test-cap");
22
18
  expect(skills).toEqual([]);
@@ -1,7 +1,7 @@
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";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
3
  import { join } from "node:path";
4
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
5
5
  import {
6
6
  parseSourceConfig,
7
7
  sourceToGitUrl,
@@ -61,12 +61,10 @@ describe("parseSourceConfig", () => {
61
61
  const config = parseSourceConfig({
62
62
  source: "github:user/repo",
63
63
  ref: "v2.0.0",
64
- type: "wrap",
65
64
  }) as GitCapabilitySourceConfig;
66
65
 
67
66
  expect(config.source).toBe("github:user/repo");
68
67
  expect(config.ref).toBe("v2.0.0");
69
- expect(config.type).toBe("wrap");
70
68
  });
71
69
 
72
70
  test("passes through config with path", () => {
@@ -135,23 +133,7 @@ describe("getLockFilePath", () => {
135
133
  });
136
134
 
137
135
  describe("loadLockFile", () => {
138
- let testDir: string;
139
- let originalCwd: string;
140
-
141
- beforeEach(() => {
142
- originalCwd = process.cwd();
143
- testDir = mkdtempSync(join(tmpdir(), "test-lock-file-"));
144
- mkdirSync(join(testDir, ".omni"), { recursive: true });
145
- process.chdir(testDir);
146
- });
147
-
148
- afterEach(() => {
149
- process.chdir(originalCwd);
150
- if (existsSync(testDir)) {
151
- rmSync(testDir, { recursive: true, force: true });
152
- }
153
- });
154
-
136
+ setupTestDir("test-lock-file-", { chdir: true, createOmniDir: true });
155
137
  test("returns empty capabilities when lock file does not exist", async () => {
156
138
  const lockFile = await loadLockFile();
157
139
 
@@ -213,23 +195,7 @@ updated_at = "2026-01-02T00:00:00Z"
213
195
  });
214
196
 
215
197
  describe("saveLockFile", () => {
216
- let testDir: string;
217
- let originalCwd: string;
218
-
219
- beforeEach(() => {
220
- originalCwd = process.cwd();
221
- testDir = mkdtempSync(join(tmpdir(), "test-save-lock-"));
222
- mkdirSync(join(testDir, ".omni"), { recursive: true });
223
- process.chdir(testDir);
224
- });
225
-
226
- afterEach(() => {
227
- process.chdir(originalCwd);
228
- if (existsSync(testDir)) {
229
- rmSync(testDir, { recursive: true, force: true });
230
- }
231
- });
232
-
198
+ setupTestDir("test-save-lock-", { chdir: true, createOmniDir: true });
233
199
  test("creates lock file with single capability", async () => {
234
200
  const lockFile: CapabilitiesLockFile = {
235
201
  capabilities: {
@@ -332,7 +298,142 @@ describe("saveLockFile", () => {
332
298
  });
333
299
  });
334
300
 
335
- // Note: Tests for fetchCapabilitySource and fetchAllCapabilitySources require
336
- // network access and git operations. These should be tested via integration tests
301
+ describe("wrapping detection", () => {
302
+ const testDir = setupTestDir("test-wrapping-", { chdir: true, createOmniDir: true });
303
+
304
+ beforeEach(() => {
305
+ mkdirSync(join(testDir.path, ".omni", "capabilities"), { recursive: true });
306
+ });
307
+ test("detects wrapping needed when .claude-plugin/plugin.json exists", () => {
308
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
309
+ mkdirSync(join(capDir, ".claude-plugin"), { recursive: true });
310
+
311
+ writeFileSync(
312
+ join(capDir, ".claude-plugin", "plugin.json"),
313
+ JSON.stringify({
314
+ name: "test-capability",
315
+ version: "1.0.0",
316
+ description: "Test capability",
317
+ author: {
318
+ name: "Test Author",
319
+ email: "test@example.com",
320
+ },
321
+ }),
322
+ );
323
+
324
+ // Should be detected as needing wrapping since no capability.toml exists
325
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
326
+ expect(existsSync(join(capDir, ".claude-plugin", "plugin.json"))).toBe(true);
327
+ });
328
+
329
+ test("detects wrapping needed when skills directory exists", () => {
330
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
331
+ mkdirSync(join(capDir, "skills"), { recursive: true });
332
+
333
+ writeFileSync(join(capDir, "skills", "example-skill.md"), "# Example Skill\n");
334
+
335
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
336
+ expect(existsSync(join(capDir, "skills"))).toBe(true);
337
+ });
338
+
339
+ test("detects wrapping needed when agents directory exists", () => {
340
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
341
+ mkdirSync(join(capDir, "agents"), { recursive: true });
342
+
343
+ writeFileSync(join(capDir, "agents", "example-agent.md"), "# Example Agent\n");
344
+
345
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
346
+ expect(existsSync(join(capDir, "agents"))).toBe(true);
347
+ });
348
+
349
+ test("detects wrapping needed when commands directory exists", () => {
350
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
351
+ mkdirSync(join(capDir, "commands"), { recursive: true });
352
+
353
+ writeFileSync(join(capDir, "commands", "example-command.md"), "# Example Command\n");
354
+
355
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
356
+ expect(existsSync(join(capDir, "commands"))).toBe(true);
357
+ });
358
+
359
+ test("detects wrapping needed when rules directory exists", () => {
360
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
361
+ mkdirSync(join(capDir, "rules"), { recursive: true });
362
+
363
+ writeFileSync(join(capDir, "rules", "example-rule.md"), "# Example Rule\n");
364
+
365
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
366
+ expect(existsSync(join(capDir, "rules"))).toBe(true);
367
+ });
368
+
369
+ test("detects wrapping needed when docs directory exists", () => {
370
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
371
+ mkdirSync(join(capDir, "docs"), { recursive: true });
372
+
373
+ writeFileSync(join(capDir, "docs", "getting-started.md"), "# Getting Started\n");
374
+
375
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
376
+ expect(existsSync(join(capDir, "docs"))).toBe(true);
377
+ });
378
+
379
+ test("does not wrap when capability.toml exists", () => {
380
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
381
+ mkdirSync(join(capDir, "skills"), { recursive: true });
382
+
383
+ writeFileSync(
384
+ join(capDir, "capability.toml"),
385
+ `[capability]
386
+ id = "test-cap"
387
+ name = "Test Capability"
388
+ version = "1.0.0"
389
+ description = "Test"
390
+ `,
391
+ );
392
+
393
+ writeFileSync(join(capDir, "skills", "example-skill.md"), "# Example Skill\n");
394
+
395
+ // Has capability.toml, so should NOT be wrapped even with skills dir
396
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(true);
397
+ expect(existsSync(join(capDir, "skills"))).toBe(true);
398
+ });
399
+
400
+ test("does not wrap directory with no recognized structure", () => {
401
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
402
+ mkdirSync(capDir, { recursive: true });
403
+
404
+ // Create some random files/dirs that shouldn't trigger wrapping
405
+ writeFileSync(join(capDir, "README.md"), "# Test\n");
406
+ writeFileSync(join(capDir, "package.json"), "{}");
407
+ mkdirSync(join(capDir, "src"));
408
+
409
+ expect(existsSync(join(capDir, "capability.toml"))).toBe(false);
410
+ expect(existsSync(join(capDir, ".claude-plugin"))).toBe(false);
411
+ expect(existsSync(join(capDir, "skills"))).toBe(false);
412
+ // Should not trigger wrapping
413
+ });
414
+
415
+ test("recognizes singular directory names", () => {
416
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
417
+ mkdirSync(join(capDir, "skill"), { recursive: true });
418
+
419
+ writeFileSync(join(capDir, "skill", "example.md"), "# Example\n");
420
+
421
+ // Singular 'skill' should also be detected
422
+ expect(existsSync(join(capDir, "skill"))).toBe(true);
423
+ });
424
+
425
+ test("recognizes alternative directory names", () => {
426
+ const capDir = join(testDir.path, ".omni", "capabilities", "test-cap");
427
+ mkdirSync(join(capDir, "subagents"), { recursive: true });
428
+
429
+ writeFileSync(join(capDir, "subagents", "example.md"), "# Example\n");
430
+
431
+ // 'subagents' should also be detected as agent dir
432
+ expect(existsSync(join(capDir, "subagents"))).toBe(true);
433
+ });
434
+ });
435
+
436
+ // Note: Tests for fetchCapabilitySource and fetchAllCapabilitySources with actual
437
+ // git operations require network access. These should be tested via integration tests
337
438
  // or with mocked git commands. The manual test in /tmp/omni-test verified the
338
439
  // full flow works correctly with the real obsidian-skills repository.