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