@omnidev-ai/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -0
- package/src/capability/AGENTS.md +58 -0
- package/src/capability/commands.test.ts +414 -0
- package/src/capability/commands.ts +70 -0
- package/src/capability/docs.test.ts +199 -0
- package/src/capability/docs.ts +46 -0
- package/src/capability/index.ts +20 -0
- package/src/capability/loader.test.ts +815 -0
- package/src/capability/loader.ts +492 -0
- package/src/capability/registry.test.ts +473 -0
- package/src/capability/registry.ts +55 -0
- package/src/capability/rules.test.ts +145 -0
- package/src/capability/rules.ts +133 -0
- package/src/capability/skills.test.ts +316 -0
- package/src/capability/skills.ts +56 -0
- package/src/capability/sources.test.ts +338 -0
- package/src/capability/sources.ts +966 -0
- package/src/capability/subagents.test.ts +478 -0
- package/src/capability/subagents.ts +103 -0
- package/src/capability/yaml-parser.ts +81 -0
- package/src/config/AGENTS.md +46 -0
- package/src/config/capabilities.ts +82 -0
- package/src/config/env.test.ts +286 -0
- package/src/config/env.ts +96 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +282 -0
- package/src/config/loader.ts +137 -0
- package/src/config/parser.test.ts +281 -0
- package/src/config/parser.ts +55 -0
- package/src/config/profiles.test.ts +259 -0
- package/src/config/profiles.ts +75 -0
- package/src/config/provider.test.ts +79 -0
- package/src/config/provider.ts +55 -0
- package/src/debug.ts +20 -0
- package/src/gitignore/manager.test.ts +219 -0
- package/src/gitignore/manager.ts +167 -0
- package/src/index.test.ts +26 -0
- package/src/index.ts +39 -0
- package/src/mcp-json/index.ts +1 -0
- package/src/mcp-json/manager.test.ts +415 -0
- package/src/mcp-json/manager.ts +118 -0
- package/src/state/active-profile.test.ts +131 -0
- package/src/state/active-profile.ts +41 -0
- package/src/state/index.ts +2 -0
- package/src/state/manifest.test.ts +548 -0
- package/src/state/manifest.ts +164 -0
- package/src/sync.ts +213 -0
- package/src/templates/agents.test.ts +23 -0
- package/src/templates/agents.ts +14 -0
- package/src/templates/claude.test.ts +48 -0
- package/src/templates/claude.ts +122 -0
- package/src/test-utils/helpers.test.ts +196 -0
- package/src/test-utils/helpers.ts +187 -0
- package/src/test-utils/index.ts +30 -0
- package/src/test-utils/mocks.test.ts +83 -0
- package/src/test-utils/mocks.ts +101 -0
- package/src/types/capability-export.ts +234 -0
- package/src/types/index.test.ts +28 -0
- package/src/types/index.ts +270 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parse } from "smol-toml";
|
|
2
|
+
import type { CapabilityConfig, OmniConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a TOML string into an OmniConfig object
|
|
6
|
+
* @param tomlContent - The TOML content to parse
|
|
7
|
+
* @returns Parsed OmniConfig object
|
|
8
|
+
* @throws Error if TOML is invalid
|
|
9
|
+
*/
|
|
10
|
+
export function parseOmniConfig(tomlContent: string): OmniConfig {
|
|
11
|
+
try {
|
|
12
|
+
return parse(tomlContent) as OmniConfig;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
throw new Error(`Invalid TOML in config: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate that parsed TOML has required capability fields
|
|
21
|
+
*/
|
|
22
|
+
function validateCapabilityConfig(parsed: Record<string, unknown>): void {
|
|
23
|
+
const cap = parsed["capability"];
|
|
24
|
+
if (typeof cap !== "object" || cap === null) {
|
|
25
|
+
throw new Error("capability.id is required in capability.toml");
|
|
26
|
+
}
|
|
27
|
+
const capability = cap as Record<string, unknown>;
|
|
28
|
+
if (typeof capability["id"] !== "string") {
|
|
29
|
+
throw new Error("capability.id is required in capability.toml");
|
|
30
|
+
}
|
|
31
|
+
if (typeof capability["name"] !== "string") {
|
|
32
|
+
throw new Error("capability.name is required in capability.toml");
|
|
33
|
+
}
|
|
34
|
+
if (typeof capability["version"] !== "string") {
|
|
35
|
+
throw new Error("capability.version is required in capability.toml");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a TOML string into a CapabilityConfig object
|
|
41
|
+
* @param tomlContent - The TOML content to parse
|
|
42
|
+
* @returns Parsed CapabilityConfig object
|
|
43
|
+
* @throws Error if TOML is invalid or required fields are missing
|
|
44
|
+
*/
|
|
45
|
+
export function parseCapabilityConfig(tomlContent: string): CapabilityConfig {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = parse(tomlContent) as Record<string, unknown>;
|
|
48
|
+
validateCapabilityConfig(parsed);
|
|
49
|
+
// After validation, we know the structure matches CapabilityConfig
|
|
50
|
+
return parsed as unknown as CapabilityConfig;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
throw new Error(`Invalid capability.toml: ${message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { readActiveProfileState } from "../state/active-profile.js";
|
|
4
|
+
import type { OmniConfig } from "../types/index.js";
|
|
5
|
+
import { getActiveProfile, resolveEnabledCapabilities, setActiveProfile } from "./profiles.js";
|
|
6
|
+
|
|
7
|
+
describe("getActiveProfile", () => {
|
|
8
|
+
const TEST_DIR = ".omni-test-profiles";
|
|
9
|
+
let originalCwd: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
if (!existsSync(TEST_DIR)) {
|
|
13
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
originalCwd = process.cwd();
|
|
16
|
+
process.chdir(TEST_DIR);
|
|
17
|
+
if (!existsSync(".omni")) {
|
|
18
|
+
mkdirSync(".omni", { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.chdir(originalCwd);
|
|
24
|
+
if (existsSync(TEST_DIR)) {
|
|
25
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("returns null when no state file or config exists", async () => {
|
|
30
|
+
const profile = await getActiveProfile();
|
|
31
|
+
expect(profile).toBe(null);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns profile from state file when set", async () => {
|
|
35
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
36
|
+
await Bun.write(".omni/state/active-profile", "dev");
|
|
37
|
+
const profile = await getActiveProfile();
|
|
38
|
+
expect(profile).toBe("dev");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("falls back to config.toml for backwards compatibility", async () => {
|
|
42
|
+
writeFileSync("omni.toml", 'active_profile = "legacy"', "utf-8");
|
|
43
|
+
const profile = await getActiveProfile();
|
|
44
|
+
expect(profile).toBe("legacy");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("state file takes precedence over config.toml", async () => {
|
|
48
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
49
|
+
await Bun.write(".omni/state/active-profile", "from-state");
|
|
50
|
+
writeFileSync("omni.toml", 'active_profile = "from-config"', "utf-8");
|
|
51
|
+
const profile = await getActiveProfile();
|
|
52
|
+
expect(profile).toBe("from-state");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns null when config has no active_profile", async () => {
|
|
56
|
+
writeFileSync("omni.toml", 'project = "test"', "utf-8");
|
|
57
|
+
const profile = await getActiveProfile();
|
|
58
|
+
expect(profile).toBe(null);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("setActiveProfile", () => {
|
|
63
|
+
const TEST_DIR = ".omni-test-profiles-set";
|
|
64
|
+
let originalCwd: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
if (!existsSync(TEST_DIR)) {
|
|
68
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
originalCwd = process.cwd();
|
|
71
|
+
process.chdir(TEST_DIR);
|
|
72
|
+
if (!existsSync(".omni")) {
|
|
73
|
+
mkdirSync(".omni", { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.chdir(originalCwd);
|
|
79
|
+
if (existsSync(TEST_DIR)) {
|
|
80
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("sets active_profile in state file", async () => {
|
|
85
|
+
await setActiveProfile("staging");
|
|
86
|
+
const stateProfile = await readActiveProfileState();
|
|
87
|
+
expect(stateProfile).toBe("staging");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("overwrites existing active_profile in state file", async () => {
|
|
91
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
92
|
+
await Bun.write(".omni/state/active-profile", "dev");
|
|
93
|
+
await setActiveProfile("prod");
|
|
94
|
+
const stateProfile = await readActiveProfileState();
|
|
95
|
+
expect(stateProfile).toBe("prod");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("does not modify config.toml", async () => {
|
|
99
|
+
writeFileSync("omni.toml", 'project = "test"', "utf-8");
|
|
100
|
+
await setActiveProfile("staging");
|
|
101
|
+
const content = await Bun.file("omni.toml").text();
|
|
102
|
+
expect(content).not.toContain("active_profile");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("resolveEnabledCapabilities", () => {
|
|
107
|
+
test("returns empty array when no profiles configured", () => {
|
|
108
|
+
const config: OmniConfig = {};
|
|
109
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
110
|
+
expect(result).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("returns empty array when profile has no capabilities", () => {
|
|
114
|
+
const config: OmniConfig = {
|
|
115
|
+
profiles: {
|
|
116
|
+
default: {
|
|
117
|
+
capabilities: [],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
122
|
+
expect(result).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("returns capabilities from specified profile", () => {
|
|
126
|
+
const config: OmniConfig = {
|
|
127
|
+
profiles: {
|
|
128
|
+
dev: {
|
|
129
|
+
capabilities: ["tasks", "filesystem", "debug"],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
134
|
+
expect(result).toEqual(["tasks", "filesystem", "debug"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("uses active_profile when profileName is null", () => {
|
|
138
|
+
const config: OmniConfig = {
|
|
139
|
+
active_profile: "dev",
|
|
140
|
+
profiles: {
|
|
141
|
+
dev: {
|
|
142
|
+
capabilities: ["tasks", "debug"],
|
|
143
|
+
},
|
|
144
|
+
default: {
|
|
145
|
+
capabilities: ["tasks"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
150
|
+
expect(result).toEqual(["tasks", "debug"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('falls back to "default" profile when active_profile not set', () => {
|
|
154
|
+
const config: OmniConfig = {
|
|
155
|
+
profiles: {
|
|
156
|
+
default: {
|
|
157
|
+
capabilities: ["tasks", "filesystem"],
|
|
158
|
+
},
|
|
159
|
+
dev: {
|
|
160
|
+
capabilities: ["tasks", "debug"],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
165
|
+
expect(result).toEqual(["tasks", "filesystem"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("handles non-existent profile gracefully", () => {
|
|
169
|
+
const config: OmniConfig = {
|
|
170
|
+
profiles: {
|
|
171
|
+
dev: {
|
|
172
|
+
capabilities: ["debug"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const result = resolveEnabledCapabilities(config, "nonexistent");
|
|
177
|
+
expect(result).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("handles config with no profiles defined", () => {
|
|
181
|
+
const config: OmniConfig = {};
|
|
182
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
183
|
+
expect(result).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("handles profile with undefined capabilities field", () => {
|
|
187
|
+
const config: OmniConfig = {
|
|
188
|
+
profiles: {
|
|
189
|
+
dev: {},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
193
|
+
expect(result).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("includes always-enabled capabilities with profile capabilities", () => {
|
|
197
|
+
const config: OmniConfig = {
|
|
198
|
+
always_enabled_capabilities: ["logging", "telemetry"],
|
|
199
|
+
profiles: {
|
|
200
|
+
dev: {
|
|
201
|
+
capabilities: ["tasks", "debug"],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
206
|
+
expect(result).toEqual(["logging", "telemetry", "tasks", "debug"]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("removes duplicates when capability is in both always-enabled and profile", () => {
|
|
210
|
+
const config: OmniConfig = {
|
|
211
|
+
always_enabled_capabilities: ["logging", "tasks"],
|
|
212
|
+
profiles: {
|
|
213
|
+
dev: {
|
|
214
|
+
capabilities: ["tasks", "debug"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
219
|
+
expect(result).toEqual(["logging", "tasks", "debug"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("returns only always-enabled capabilities when profile has none", () => {
|
|
223
|
+
const config: OmniConfig = {
|
|
224
|
+
always_enabled_capabilities: ["logging", "telemetry"],
|
|
225
|
+
profiles: {
|
|
226
|
+
dev: {
|
|
227
|
+
capabilities: [],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
const result = resolveEnabledCapabilities(config, "dev");
|
|
232
|
+
expect(result).toEqual(["logging", "telemetry"]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("returns always-enabled capabilities even when no profiles exist", () => {
|
|
236
|
+
const config: OmniConfig = {
|
|
237
|
+
always_enabled_capabilities: ["logging", "telemetry"],
|
|
238
|
+
};
|
|
239
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
240
|
+
expect(result).toEqual(["logging", "telemetry"]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("always-enabled capabilities work with active_profile", () => {
|
|
244
|
+
const config: OmniConfig = {
|
|
245
|
+
active_profile: "dev",
|
|
246
|
+
always_enabled_capabilities: ["logging"],
|
|
247
|
+
profiles: {
|
|
248
|
+
dev: {
|
|
249
|
+
capabilities: ["tasks", "debug"],
|
|
250
|
+
},
|
|
251
|
+
default: {
|
|
252
|
+
capabilities: ["tasks"],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const result = resolveEnabledCapabilities(config, null);
|
|
257
|
+
expect(result).toEqual(["logging", "tasks", "debug"]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readActiveProfileState, writeActiveProfileState } from "../state/active-profile.js";
|
|
2
|
+
import type { OmniConfig, ProfileConfig } from "../types/index.js";
|
|
3
|
+
import { loadConfig, writeConfig } from "./loader.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the name of the currently active profile.
|
|
7
|
+
* Reads from state file first, falls back to config.toml for backwards compatibility.
|
|
8
|
+
* Returns null if no profile is set.
|
|
9
|
+
*/
|
|
10
|
+
export async function getActiveProfile(): Promise<string | null> {
|
|
11
|
+
// First check state file (new location)
|
|
12
|
+
const stateProfile = await readActiveProfileState();
|
|
13
|
+
if (stateProfile) {
|
|
14
|
+
return stateProfile;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Fall back to config.toml for backwards compatibility
|
|
18
|
+
const config = await loadConfig();
|
|
19
|
+
return config.active_profile ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sets the active profile by writing to state file.
|
|
24
|
+
* @param name - The name of the profile to activate
|
|
25
|
+
*/
|
|
26
|
+
export async function setActiveProfile(name: string): Promise<void> {
|
|
27
|
+
await writeActiveProfileState(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the enabled capabilities for a given profile
|
|
32
|
+
*
|
|
33
|
+
* @param config - The merged OmniConfig
|
|
34
|
+
* @param profileName - The name of the profile to apply, or null to use active
|
|
35
|
+
* @returns Array of capability IDs that should be enabled
|
|
36
|
+
*/
|
|
37
|
+
export function resolveEnabledCapabilities(
|
|
38
|
+
config: OmniConfig,
|
|
39
|
+
profileName: string | null,
|
|
40
|
+
): string[] {
|
|
41
|
+
// Determine which profile to use
|
|
42
|
+
const profile = profileName
|
|
43
|
+
? config.profiles?.[profileName]
|
|
44
|
+
: config.profiles?.[config.active_profile ?? "default"];
|
|
45
|
+
|
|
46
|
+
const profileCapabilities = profile?.capabilities ?? [];
|
|
47
|
+
const alwaysEnabled = config.always_enabled_capabilities ?? [];
|
|
48
|
+
|
|
49
|
+
// Merge always-enabled capabilities with profile capabilities (no duplicates)
|
|
50
|
+
return [...new Set([...alwaysEnabled, ...profileCapabilities])];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load a specific profile configuration from config.toml
|
|
55
|
+
* @param profileName - Name of the profile to load
|
|
56
|
+
* @returns ProfileConfig if found, undefined otherwise
|
|
57
|
+
*/
|
|
58
|
+
export async function loadProfileConfig(profileName: string): Promise<ProfileConfig | undefined> {
|
|
59
|
+
const config = await loadConfig();
|
|
60
|
+
return config.profiles?.[profileName];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set a profile configuration in config.toml
|
|
65
|
+
* @param profileName - Name of the profile to set
|
|
66
|
+
* @param profileConfig - Profile configuration
|
|
67
|
+
*/
|
|
68
|
+
export async function setProfile(profileName: string, profileConfig: ProfileConfig): Promise<void> {
|
|
69
|
+
const config = await loadConfig();
|
|
70
|
+
if (!config.profiles) {
|
|
71
|
+
config.profiles = {};
|
|
72
|
+
}
|
|
73
|
+
config.profiles[profileName] = profileConfig;
|
|
74
|
+
await writeConfig(config);
|
|
75
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { parseProviderFlag } from "./provider.js";
|
|
4
|
+
|
|
5
|
+
const TEST_DIR = ".test-omni";
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
if (existsSync(TEST_DIR)) {
|
|
9
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (existsSync(TEST_DIR)) {
|
|
16
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("parseProviderFlag", () => {
|
|
21
|
+
test("parses 'claude' flag", () => {
|
|
22
|
+
expect(parseProviderFlag("claude")).toEqual(["claude"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("parses 'codex' flag", () => {
|
|
26
|
+
expect(parseProviderFlag("codex")).toEqual(["codex"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("parses 'both' flag", () => {
|
|
30
|
+
expect(parseProviderFlag("both")).toEqual(["claude", "codex"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("handles case-insensitive input", () => {
|
|
34
|
+
expect(parseProviderFlag("CLAUDE")).toEqual(["claude"]);
|
|
35
|
+
expect(parseProviderFlag("Codex")).toEqual(["codex"]);
|
|
36
|
+
expect(parseProviderFlag("BOTH")).toEqual(["claude", "codex"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("throws on invalid provider", () => {
|
|
40
|
+
expect(() => parseProviderFlag("invalid")).toThrow("Invalid provider: invalid");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("writeProviderConfig", () => {
|
|
45
|
+
test("writes single provider config", async () => {
|
|
46
|
+
const testPath = `${TEST_DIR}/provider.toml`;
|
|
47
|
+
|
|
48
|
+
// Manually write for testing
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
lines.push("# OmniDev Provider Configuration");
|
|
51
|
+
lines.push("# Selected AI provider(s) for this project");
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push("# Single provider");
|
|
54
|
+
lines.push('provider = "claude"');
|
|
55
|
+
|
|
56
|
+
await Bun.write(testPath, `${lines.join("\n")}\n`);
|
|
57
|
+
|
|
58
|
+
const content = await Bun.file(testPath).text();
|
|
59
|
+
expect(content).toContain('provider = "claude"');
|
|
60
|
+
expect(content).toContain("# Single provider");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("writes multiple providers config", async () => {
|
|
64
|
+
const testPath = `${TEST_DIR}/provider.toml`;
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
lines.push("# OmniDev Provider Configuration");
|
|
68
|
+
lines.push("# Selected AI provider(s) for this project");
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("# Multiple providers enabled");
|
|
71
|
+
lines.push('providers = ["claude", "codex"]');
|
|
72
|
+
|
|
73
|
+
await Bun.write(testPath, `${lines.join("\n")}\n`);
|
|
74
|
+
|
|
75
|
+
const content = await Bun.file(testPath).text();
|
|
76
|
+
expect(content).toContain('providers = ["claude", "codex"]');
|
|
77
|
+
expect(content).toContain("# Multiple providers");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { parse } from "smol-toml";
|
|
3
|
+
import type { Provider, ProviderConfig } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_CONFIG_PATH = ".omni/provider.toml";
|
|
6
|
+
|
|
7
|
+
export async function loadProviderConfig(): Promise<ProviderConfig> {
|
|
8
|
+
if (!existsSync(PROVIDER_CONFIG_PATH)) {
|
|
9
|
+
return { provider: "claude" };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const content = await Bun.file(PROVIDER_CONFIG_PATH).text();
|
|
13
|
+
const parsed = parse(content) as unknown as ProviderConfig;
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function writeProviderConfig(config: ProviderConfig): Promise<void> {
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
|
|
20
|
+
lines.push("# OmniDev Provider Configuration");
|
|
21
|
+
lines.push("# Selected AI provider(s) for this project");
|
|
22
|
+
lines.push("#");
|
|
23
|
+
lines.push("# This file controls which AI provider(s) you're using:");
|
|
24
|
+
lines.push("# - claude: Generates .claude/claude.md instruction file");
|
|
25
|
+
lines.push("# - codex: Generates AGENTS.md instruction file");
|
|
26
|
+
lines.push("# - both: Generates both instruction files");
|
|
27
|
+
lines.push("");
|
|
28
|
+
|
|
29
|
+
if (config.providers && config.providers.length > 1) {
|
|
30
|
+
lines.push("# Multiple providers enabled");
|
|
31
|
+
lines.push(`providers = [${config.providers.map((p) => `"${p}"`).join(", ")}]`);
|
|
32
|
+
} else if (config.providers && config.providers.length === 1) {
|
|
33
|
+
lines.push("# Single provider");
|
|
34
|
+
lines.push(`provider = "${config.providers[0]}"`);
|
|
35
|
+
} else if (config.provider) {
|
|
36
|
+
lines.push("# Single provider");
|
|
37
|
+
lines.push(`provider = "${config.provider}"`);
|
|
38
|
+
} else {
|
|
39
|
+
lines.push("# Default: Claude");
|
|
40
|
+
lines.push('provider = "claude"');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await Bun.write(PROVIDER_CONFIG_PATH, `${lines.join("\n")}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseProviderFlag(flag: string): Provider[] {
|
|
47
|
+
const lower = flag.toLowerCase();
|
|
48
|
+
if (lower === "both") {
|
|
49
|
+
return ["claude", "codex"];
|
|
50
|
+
}
|
|
51
|
+
if (lower === "claude" || lower === "codex") {
|
|
52
|
+
return [lower as Provider];
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
|
|
55
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug logger that writes to stdout when OMNIDEV_DEBUG=1
|
|
3
|
+
*/
|
|
4
|
+
export function debug(message: string, data?: unknown): void {
|
|
5
|
+
if (process.env["OMNIDEV_DEBUG"] !== "1") {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const timestamp = new Date().toISOString();
|
|
10
|
+
let logLine: string;
|
|
11
|
+
|
|
12
|
+
if (data !== undefined) {
|
|
13
|
+
logLine = `[${timestamp}] [omnidev] ${message} ${JSON.stringify(data, null, 2)}`;
|
|
14
|
+
} else {
|
|
15
|
+
logLine = `[${timestamp}] [omnidev] ${message}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Write to stdout
|
|
19
|
+
console.log(logLine);
|
|
20
|
+
}
|