@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,22 +1,18 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
5
  import { loadSubagents } from "./subagents";
5
6
 
6
7
  describe("loadSubagents", () => {
7
- const testDir = join(process.cwd(), "test-subagents-temp");
8
- const capabilityPath = join(testDir, "test-capability");
8
+ const testDir = setupTestDir("capability-subagents-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 subagents directory does not exist", async () => {
21
17
  const subagents = await loadSubagents(capabilityPath, "test-cap");
22
18
  expect(subagents).toEqual([]);
@@ -167,7 +163,7 @@ tools: Read, Glob, Grep, Bash
167
163
  disallowedTools: Write, Edit
168
164
  model: sonnet
169
165
  permissionMode: acceptEdits
170
- skills: prd, ralph, mcp-builder
166
+ skills: prd, ralph, capability-builder
171
167
  ---
172
168
 
173
169
  You are a fully configured subagent.`;
@@ -183,7 +179,7 @@ You are a fully configured subagent.`;
183
179
  expect(subagents[0]?.disallowedTools).toEqual(["Write", "Edit"]);
184
180
  expect(subagents[0]?.model).toBe("sonnet");
185
181
  expect(subagents[0]?.permissionMode).toBe("acceptEdits");
186
- expect(subagents[0]?.skills).toEqual(["prd", "ralph", "mcp-builder"]);
182
+ expect(subagents[0]?.skills).toEqual(["prd", "ralph", "capability-builder"]);
187
183
  expect(subagents[0]?.systemPrompt).toBe("You are a fully configured subagent.");
188
184
  });
189
185
 
@@ -29,7 +29,9 @@ export async function loadSubagents(
29
29
  }
30
30
 
31
31
  const subagents: Subagent[] = [];
32
- const entries = readdirSync(subagentsDir, { withFileTypes: true });
32
+ const entries = readdirSync(subagentsDir, { withFileTypes: true }).sort((a, b) =>
33
+ a.name.localeCompare(b.name),
34
+ );
33
35
 
34
36
  for (const entry of entries) {
35
37
  if (entry.isDirectory()) {
@@ -0,0 +1,412 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { parse as parseToml } from "smol-toml";
6
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
7
+ import type { CapabilityConfig, OmniConfig } from "../types/index.js";
8
+ import { generateMcpCapabilities } from "./sources.js";
9
+
10
+ describe("wrapping integration - expo-like structure", () => {
11
+ const testDir = setupTestDir("test-expo-", { chdir: true });
12
+
13
+ test("wraps expo-like plugin with .claude-plugin/plugin.json", () => {
14
+ // Simulate Expo skills structure: plugins/expo-app-design/
15
+ const pluginDir = join(testDir.path, "plugins", "expo-app-design");
16
+ mkdirSync(join(pluginDir, ".claude-plugin"), { recursive: true });
17
+ mkdirSync(join(pluginDir, "skills"));
18
+
19
+ // Create plugin.json (like Expo has)
20
+ writeFileSync(
21
+ join(pluginDir, ".claude-plugin", "plugin.json"),
22
+ JSON.stringify({
23
+ name: "expo-app-design",
24
+ version: "1.0.0",
25
+ description: "Build robust, productivity apps with Expo",
26
+ author: {
27
+ name: "Evan Bacon",
28
+ email: "bacon@expo.io",
29
+ },
30
+ }),
31
+ );
32
+
33
+ // Create README
34
+ writeFileSync(
35
+ join(pluginDir, "README.md"),
36
+ `# Expo App Design
37
+
38
+ Design amazing Expo applications with best practices.
39
+
40
+ This plugin provides tools for building production-ready Expo apps.`,
41
+ );
42
+
43
+ // Create a skill
44
+ const skillDir = join(pluginDir, "skills");
45
+ writeFileSync(
46
+ join(skillDir, "example-skill.md"),
47
+ `---
48
+ name: example-skill
49
+ description: Example skill for testing
50
+ ---
51
+
52
+ # Example Skill
53
+
54
+ This is an example skill.`,
55
+ );
56
+
57
+ // Verify structure exists
58
+ expect(existsSync(join(pluginDir, ".claude-plugin", "plugin.json"))).toBe(true);
59
+ expect(existsSync(join(pluginDir, "README.md"))).toBe(true);
60
+ expect(existsSync(join(pluginDir, "skills", "example-skill.md"))).toBe(true);
61
+ expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
62
+
63
+ // This should be detected as needing wrapping
64
+ // When fetchGitCapabilitySource runs, it will:
65
+ // 1. Check for capability.toml (not found)
66
+ // 2. Call shouldWrapDirectory
67
+ // 3. Find .claude-plugin/plugin.json
68
+ // 4. Generate capability.toml with metadata from plugin.json
69
+ });
70
+
71
+ test("wraps plugin with only directory structure (no plugin.json)", () => {
72
+ const pluginDir = join(testDir.path, "plugins", "simple-plugin");
73
+ mkdirSync(join(pluginDir, "skills"), { recursive: true });
74
+
75
+ // Create README
76
+ writeFileSync(
77
+ join(pluginDir, "README.md"),
78
+ `# Simple Plugin
79
+
80
+ A simple plugin without plugin.json metadata.
81
+
82
+ This demonstrates wrapping based on directory structure alone.`,
83
+ );
84
+
85
+ // Create a skill
86
+ writeFileSync(
87
+ join(pluginDir, "skills", "test-skill.md"),
88
+ `---
89
+ name: test-skill
90
+ description: Test skill
91
+ ---
92
+
93
+ # Test Skill
94
+
95
+ Content here.`,
96
+ );
97
+
98
+ // Verify structure exists
99
+ expect(existsSync(join(pluginDir, ".claude-plugin"))).toBe(false);
100
+ expect(existsSync(join(pluginDir, "skills"))).toBe(true);
101
+ expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
102
+
103
+ // Should still be detected for wrapping due to skills/ directory
104
+ });
105
+
106
+ test("does not wrap when capability.toml already exists", () => {
107
+ const pluginDir = join(testDir.path, "plugins", "proper-capability");
108
+ mkdirSync(join(pluginDir, "skills"), { recursive: true });
109
+
110
+ // Create proper capability.toml
111
+ writeFileSync(
112
+ join(pluginDir, "capability.toml"),
113
+ `[capability]
114
+ id = "proper-capability"
115
+ name = "Proper Capability"
116
+ version = "1.0.0"
117
+ description = "This is a proper capability with its own TOML"
118
+ `,
119
+ );
120
+
121
+ // Create a skill
122
+ writeFileSync(join(pluginDir, "skills", "skill.md"), "# Skill\n");
123
+
124
+ // Verify capability.toml exists
125
+ expect(existsSync(join(pluginDir, "capability.toml"))).toBe(true);
126
+ expect(existsSync(join(pluginDir, "skills"))).toBe(true);
127
+
128
+ // Should NOT be wrapped - already has capability.toml
129
+ });
130
+
131
+ test("multiple plugins in monorepo structure", () => {
132
+ // Simulate full Expo skills structure
133
+ const plugins = ["expo-app-design", "expo-deployment", "upgrading-expo"];
134
+
135
+ for (const pluginName of plugins) {
136
+ const pluginDir = join(testDir.path, "plugins", pluginName);
137
+ mkdirSync(join(pluginDir, ".claude-plugin"), { recursive: true });
138
+ mkdirSync(join(pluginDir, "skills"), { recursive: true });
139
+
140
+ writeFileSync(
141
+ join(pluginDir, ".claude-plugin", "plugin.json"),
142
+ JSON.stringify({
143
+ name: pluginName,
144
+ version: "1.0.0",
145
+ description: `${pluginName} plugin`,
146
+ }),
147
+ );
148
+
149
+ writeFileSync(join(pluginDir, "README.md"), `# ${pluginName}\n\nDescription here.`);
150
+ writeFileSync(join(pluginDir, "skills", "example.md"), "# Skill\n");
151
+ }
152
+
153
+ // Verify all three plugins have correct structure
154
+ for (const pluginName of plugins) {
155
+ const pluginDir = join(testDir.path, "plugins", pluginName);
156
+ expect(existsSync(join(pluginDir, ".claude-plugin", "plugin.json"))).toBe(true);
157
+ expect(existsSync(join(pluginDir, "skills"))).toBe(true);
158
+ expect(existsSync(join(pluginDir, "capability.toml"))).toBe(false);
159
+ }
160
+
161
+ // Each plugin would be loaded separately with:
162
+ // expo-app-design = { source = "github:expo/skills", path = "plugins/expo-app-design" }
163
+ // expo-deployment = { source = "github:expo/skills", path = "plugins/expo-deployment" }
164
+ // upgrading-expo = { source = "github:expo/skills", path = "plugins/upgrading-expo" }
165
+ });
166
+ });
167
+
168
+ describe("MCP capability generation", () => {
169
+ const _testDir = setupTestDir("mcp-wrapping-test-", { chdir: true });
170
+
171
+ test("generates capability from omni.toml [mcps] section", async () => {
172
+ const config: OmniConfig = {
173
+ mcps: {
174
+ context7: {
175
+ command: "npx",
176
+ args: ["-y", "@upstash/context7-mcp"],
177
+ transport: "stdio",
178
+ env: {
179
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
180
+ API_KEY: "${CONTEXT7_API_KEY}",
181
+ },
182
+ },
183
+ },
184
+ };
185
+
186
+ await generateMcpCapabilities(config);
187
+
188
+ const capabilityDir = join(".omni", "capabilities", "context7");
189
+ expect(existsSync(capabilityDir)).toBe(true);
190
+
191
+ const tomlPath = join(capabilityDir, "capability.toml");
192
+ expect(existsSync(tomlPath)).toBe(true);
193
+
194
+ const tomlContent = await readFile(tomlPath, "utf-8");
195
+ const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
196
+
197
+ expect(parsed.capability.id).toBe("context7");
198
+ expect(parsed.capability.name).toBe("context7 (MCP)");
199
+ expect(parsed.capability.version).toBe("1.0.0");
200
+ expect(parsed.capability.metadata?.wrapped).toBe(true);
201
+ expect(parsed.capability.metadata?.generated_from_omni_toml).toBe(true);
202
+
203
+ expect(parsed.mcp?.command).toBe("npx");
204
+ expect(parsed.mcp?.args).toEqual(["-y", "@upstash/context7-mcp"]);
205
+ expect(parsed.mcp?.transport).toBe("stdio");
206
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
207
+ expect(parsed.mcp?.env?.API_KEY).toBe("${CONTEXT7_API_KEY}");
208
+ });
209
+
210
+ test("generates multiple MCP capabilities", async () => {
211
+ const config: OmniConfig = {
212
+ mcps: {
213
+ context7: {
214
+ command: "npx",
215
+ args: ["-y", "@upstash/context7-mcp"],
216
+ },
217
+ filesystem: {
218
+ command: "node",
219
+ args: ["server.js"],
220
+ cwd: "./mcp-servers/filesystem",
221
+ },
222
+ },
223
+ };
224
+
225
+ await generateMcpCapabilities(config);
226
+
227
+ const context7Dir = join(".omni", "capabilities", "context7");
228
+ const filesystemDir = join(".omni", "capabilities", "filesystem");
229
+
230
+ expect(existsSync(context7Dir)).toBe(true);
231
+ expect(existsSync(filesystemDir)).toBe(true);
232
+
233
+ const context7TomlPath = join(context7Dir, "capability.toml");
234
+ const filesystemTomlPath = join(filesystemDir, "capability.toml");
235
+
236
+ expect(existsSync(context7TomlPath)).toBe(true);
237
+ expect(existsSync(filesystemTomlPath)).toBe(true);
238
+
239
+ const filesystemToml = await readFile(filesystemTomlPath, "utf-8");
240
+ const parsed = parseToml(filesystemToml) as unknown as CapabilityConfig;
241
+
242
+ expect(parsed.capability.id).toBe("filesystem");
243
+ expect(parsed.mcp?.cwd).toBe("./mcp-servers/filesystem");
244
+ });
245
+
246
+ test("cleans up stale MCP capabilities", async () => {
247
+ // First generation with two MCPs
248
+ const config1: OmniConfig = {
249
+ mcps: {
250
+ context7: {
251
+ command: "npx",
252
+ args: ["-y", "@upstash/context7-mcp"],
253
+ },
254
+ filesystem: {
255
+ command: "node",
256
+ args: ["server.js"],
257
+ },
258
+ },
259
+ };
260
+
261
+ await generateMcpCapabilities(config1);
262
+
263
+ const context7Dir = join(".omni", "capabilities", "context7");
264
+ const filesystemDir = join(".omni", "capabilities", "filesystem");
265
+
266
+ expect(existsSync(context7Dir)).toBe(true);
267
+ expect(existsSync(filesystemDir)).toBe(true);
268
+
269
+ // Second generation with only one MCP
270
+ const config2: OmniConfig = {
271
+ mcps: {
272
+ context7: {
273
+ command: "npx",
274
+ args: ["-y", "@upstash/context7-mcp"],
275
+ },
276
+ },
277
+ };
278
+
279
+ await generateMcpCapabilities(config2);
280
+
281
+ // context7 should still exist
282
+ expect(existsSync(context7Dir)).toBe(true);
283
+
284
+ // filesystem should be removed
285
+ expect(existsSync(filesystemDir)).toBe(false);
286
+ });
287
+
288
+ test("cleans up all MCP capabilities when mcps section is empty", async () => {
289
+ // First generation with MCPs
290
+ const config1: OmniConfig = {
291
+ mcps: {
292
+ context7: {
293
+ command: "npx",
294
+ args: ["-y", "@upstash/context7-mcp"],
295
+ },
296
+ },
297
+ };
298
+
299
+ await generateMcpCapabilities(config1);
300
+
301
+ const context7Dir = join(".omni", "capabilities", "context7");
302
+ expect(existsSync(context7Dir)).toBe(true);
303
+
304
+ // Second generation with no MCPs
305
+ const config2: OmniConfig = {
306
+ mcps: {},
307
+ };
308
+
309
+ await generateMcpCapabilities(config2);
310
+
311
+ // All MCP capabilities should be removed
312
+ expect(existsSync(context7Dir)).toBe(false);
313
+ });
314
+
315
+ test("cleans up all MCP capabilities when mcps is undefined", async () => {
316
+ // First generation with MCPs
317
+ const config1: OmniConfig = {
318
+ mcps: {
319
+ context7: {
320
+ command: "npx",
321
+ args: ["-y", "@upstash/context7-mcp"],
322
+ },
323
+ },
324
+ };
325
+
326
+ await generateMcpCapabilities(config1);
327
+
328
+ const context7Dir = join(".omni", "capabilities", "context7");
329
+ expect(existsSync(context7Dir)).toBe(true);
330
+
331
+ // Second generation with undefined mcps
332
+ const config2: OmniConfig = {};
333
+
334
+ await generateMcpCapabilities(config2);
335
+
336
+ // All MCP capabilities should be removed
337
+ expect(existsSync(context7Dir)).toBe(false);
338
+ });
339
+
340
+ test("generates capability without optional fields", async () => {
341
+ const config: OmniConfig = {
342
+ mcps: {
343
+ simple: {
344
+ command: "simple-mcp",
345
+ },
346
+ },
347
+ };
348
+
349
+ await generateMcpCapabilities(config);
350
+
351
+ const capabilityDir = join(".omni", "capabilities", "simple");
352
+ expect(existsSync(capabilityDir)).toBe(true);
353
+
354
+ const tomlPath = join(capabilityDir, "capability.toml");
355
+ const tomlContent = await readFile(tomlPath, "utf-8");
356
+ const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
357
+
358
+ expect(parsed.capability.id).toBe("simple");
359
+ expect(parsed.mcp?.command).toBe("simple-mcp");
360
+ expect(parsed.mcp?.args).toBeUndefined();
361
+ expect(parsed.mcp?.env).toBeUndefined();
362
+ expect(parsed.mcp?.cwd).toBeUndefined();
363
+ expect(parsed.mcp?.transport).toBeUndefined();
364
+ });
365
+
366
+ test("merges MCP env variables correctly", async () => {
367
+ const config: OmniConfig = {
368
+ mcps: {
369
+ github: {
370
+ command: "npx",
371
+ args: ["-y", "@modelcontextprotocol/server-github"],
372
+ env: {
373
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
374
+ GITHUB_TOKEN: "${GITHUB_TOKEN}",
375
+ GITHUB_API_URL: "https://api.github.com",
376
+ },
377
+ },
378
+ },
379
+ };
380
+
381
+ await generateMcpCapabilities(config);
382
+
383
+ const tomlPath = join(".omni", "capabilities", "github", "capability.toml");
384
+ const tomlContent = await readFile(tomlPath, "utf-8");
385
+ const parsed = parseToml(tomlContent) as unknown as CapabilityConfig;
386
+
387
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing literal env var syntax
388
+ expect(parsed.mcp?.env?.GITHUB_TOKEN).toBe("${GITHUB_TOKEN}");
389
+ expect(parsed.mcp?.env?.GITHUB_API_URL).toBe("https://api.github.com");
390
+ });
391
+
392
+ test("does not affect non-MCP capabilities", async () => {
393
+ // Create a non-MCP capability directory
394
+ const nonMcpDir = join(".omni", "capabilities", "my-capability");
395
+ mkdirSync(nonMcpDir, { recursive: true });
396
+ writeFileSync(join(nonMcpDir, "capability.toml"), "# test");
397
+
398
+ const config: OmniConfig = {
399
+ mcps: {
400
+ context7: {
401
+ command: "npx",
402
+ args: ["-y", "@upstash/context7-mcp"],
403
+ },
404
+ },
405
+ };
406
+
407
+ await generateMcpCapabilities(config);
408
+
409
+ // Non-MCP capability should still exist
410
+ expect(existsSync(nonMcpDir)).toBe(true);
411
+ });
412
+ });
@@ -1,5 +1,3 @@
1
- import { discoverCapabilities, loadCapability } from "../capability/loader.js";
2
- import { addCapabilityPatterns, removeCapabilityPatterns } from "../gitignore/manager.js";
3
1
  import { loadConfig, writeConfig } from "./loader.js";
4
2
  import { getActiveProfile, resolveEnabledCapabilities } from "./profiles.js";
5
3
 
@@ -16,7 +14,6 @@ export async function getEnabledCapabilities(): Promise<string[]> {
16
14
 
17
15
  /**
18
16
  * Enable a capability by adding it to the active profile's capabilities list
19
- * Also adds the capability's gitignore patterns to .omni/.gitignore if present
20
17
  * @param capabilityId - The ID of the capability to enable
21
18
  */
22
19
  export async function enableCapability(capabilityId: string): Promise<void> {
@@ -35,27 +32,10 @@ export async function enableCapability(capabilityId: string): Promise<void> {
35
32
  config.profiles[activeProfile].capabilities = Array.from(capabilities);
36
33
 
37
34
  await writeConfig(config);
38
-
39
- // Add gitignore patterns if the capability exports them
40
- try {
41
- const capabilityPaths = await discoverCapabilities();
42
- for (const path of capabilityPaths) {
43
- const capability = await loadCapability(path, process.env as Record<string, string>);
44
- if (capability.id === capabilityId && capability.gitignore) {
45
- await addCapabilityPatterns(capabilityId, capability.gitignore);
46
- break;
47
- }
48
- }
49
- } catch (error) {
50
- // If we can't load the capability or add patterns, log but don't fail
51
- // This allows enabling capabilities even if gitignore management fails
52
- console.warn(`Warning: Could not add gitignore patterns for ${capabilityId}:`, error);
53
- }
54
35
  }
55
36
 
56
37
  /**
57
38
  * Disable a capability by removing it from the active profile's capabilities list
58
- * Also removes the capability's gitignore patterns from .omni/.gitignore
59
39
  * @param capabilityId - The ID of the capability to disable
60
40
  */
61
41
  export async function disableCapability(capabilityId: string): Promise<void> {
@@ -71,12 +51,4 @@ export async function disableCapability(capabilityId: string): Promise<void> {
71
51
  config.profiles[activeProfile].capabilities = Array.from(capabilities);
72
52
 
73
53
  await writeConfig(config);
74
-
75
- // Remove gitignore patterns
76
- try {
77
- await removeCapabilityPatterns(capabilityId);
78
- } catch (error) {
79
- // If we can't remove patterns, log but don't fail
80
- console.warn(`Warning: Could not remove gitignore patterns for ${capabilityId}:`, error);
81
- }
82
54
  }
@@ -1,25 +1,11 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "../test-utils/index.js";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
4
  import type { EnvDeclaration } from "../types";
5
5
  import { isSecretEnvVar, loadEnvironment, validateEnv } from "./env";
6
6
 
7
7
  describe("loadEnvironment", () => {
8
- const originalCwd = process.cwd();
9
- let testDir: string;
10
-
11
- beforeEach(() => {
12
- // Create test directory in /tmp
13
- testDir = tmpdir("env-test-");
14
- process.chdir(testDir);
15
- });
16
-
17
- afterEach(() => {
18
- process.chdir(originalCwd);
19
- if (existsSync(testDir)) {
20
- rmSync(testDir, { recursive: true });
21
- }
22
- });
8
+ setupTestDir("env-test-", { chdir: true });
23
9
 
24
10
  test("returns empty object when no .omni/.env file exists", async () => {
25
11
  const env = await loadEnvironment();
@@ -1,35 +1,13 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "../test-utils/index.js";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
4
  import { loadConfig } from "./loader";
5
5
 
6
6
  const CONFIG_PATH = "omni.toml";
7
7
  const LOCAL_CONFIG = "omni.local.toml";
8
8
 
9
- // Save and restore the current working directory
10
- let originalCwd: string;
11
- let TEST_DIR: string;
12
-
13
- beforeEach(() => {
14
- // Save original cwd
15
- originalCwd = process.cwd();
16
-
17
- // Create test directory in /tmp
18
- TEST_DIR = tmpdir("loader-test-");
19
- process.chdir(TEST_DIR);
20
- });
21
-
22
- afterEach(() => {
23
- // Restore original cwd
24
- process.chdir(originalCwd);
25
-
26
- // Clean up test directory
27
- if (existsSync(TEST_DIR)) {
28
- rmSync(TEST_DIR, { recursive: true });
29
- }
30
- });
31
-
32
9
  describe("loadConfig", () => {
10
+ setupTestDir("loader-test-", { chdir: true });
33
11
  test("returns empty config when no files exist", async () => {
34
12
  const config = await loadConfig();
35
13
  expect(config).toEqual({
@@ -217,64 +195,4 @@ active_profile = "production"
217
195
  // but new writes go to state file via setActiveProfile()
218
196
  expect(config.active_profile).toBe("production");
219
197
  });
220
-
221
- test("loads sandbox_enabled = true from config", async () => {
222
- mkdirSync(".omni", { recursive: true });
223
- writeFileSync(
224
- CONFIG_PATH,
225
- `
226
- sandbox_enabled = true
227
- `,
228
- );
229
-
230
- const config = await loadConfig();
231
- expect(config.sandbox_enabled).toBe(true);
232
- });
233
-
234
- test("loads sandbox_enabled = false from config", async () => {
235
- mkdirSync(".omni", { recursive: true });
236
- writeFileSync(
237
- CONFIG_PATH,
238
- `
239
- sandbox_enabled = false
240
- `,
241
- );
242
-
243
- const config = await loadConfig();
244
- expect(config.sandbox_enabled).toBe(false);
245
- });
246
-
247
- test("sandbox_enabled is undefined when not specified", async () => {
248
- mkdirSync(".omni", { recursive: true });
249
- writeFileSync(
250
- CONFIG_PATH,
251
- `
252
- project = "test"
253
- `,
254
- );
255
-
256
- const config = await loadConfig();
257
- expect(config.sandbox_enabled).toBeUndefined();
258
- });
259
-
260
- test("local config can override sandbox_enabled", async () => {
261
- mkdirSync(".omni", { recursive: true });
262
-
263
- writeFileSync(
264
- CONFIG_PATH,
265
- `
266
- sandbox_enabled = true
267
- `,
268
- );
269
-
270
- writeFileSync(
271
- LOCAL_CONFIG,
272
- `
273
- sandbox_enabled = false
274
- `,
275
- );
276
-
277
- const config = await loadConfig();
278
- expect(config.sandbox_enabled).toBe(false);
279
- });
280
198
  });