@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.
- package/package.json +1 -1
- package/src/capability/commands.test.ts +6 -10
- package/src/capability/docs.test.ts +39 -46
- package/src/capability/docs.ts +3 -1
- package/src/capability/loader.test.ts +10 -157
- package/src/capability/loader.ts +8 -69
- package/src/capability/registry.test.ts +9 -27
- package/src/capability/rules.test.ts +25 -35
- package/src/capability/rules.ts +3 -1
- package/src/capability/skills.test.ts +6 -10
- package/src/capability/sources.test.ts +142 -41
- package/src/capability/sources.ts +377 -345
- package/src/capability/subagents.test.ts +7 -11
- package/src/capability/subagents.ts +3 -1
- package/src/capability/wrapping-integration.test.ts +412 -0
- package/src/config/capabilities.ts +0 -28
- package/src/config/env.test.ts +4 -18
- package/src/config/loader.test.ts +4 -86
- package/src/config/loader.ts +88 -18
- package/src/config/parser.test.ts +0 -25
- package/src/config/profiles.test.ts +5 -39
- package/src/config/provider.test.ts +5 -19
- package/src/index.ts +1 -3
- package/src/mcp-json/manager.test.ts +77 -182
- package/src/mcp-json/manager.ts +22 -34
- package/src/state/active-profile.test.ts +4 -18
- package/src/state/index.ts +1 -0
- package/src/state/manifest.test.ts +25 -162
- package/src/state/manifest.ts +4 -31
- package/src/state/providers.test.ts +125 -0
- package/src/state/providers.ts +69 -0
- package/src/sync.ts +128 -53
- package/src/templates/claude.ts +9 -74
- package/src/test-utils/helpers.test.ts +18 -0
- package/src/test-utils/helpers.ts +87 -2
- package/src/test-utils/index.ts +3 -0
- package/src/types/capability-export.ts +0 -77
- package/src/types/index.ts +66 -22
- package/src/gitignore/manager.test.ts +0 -216
- package/src/gitignore/manager.ts +0 -167
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { setupTestDir } from "@omnidev-ai/core/test-utils";
|
|
5
3
|
import type { LoadedCapability } from "../types";
|
|
6
4
|
import {
|
|
7
5
|
buildManifestFromCapabilities,
|
|
@@ -12,20 +10,7 @@ import {
|
|
|
12
10
|
} from "./manifest";
|
|
13
11
|
|
|
14
12
|
describe("manifest", () => {
|
|
15
|
-
|
|
16
|
-
let tempDir: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
originalCwd = process.cwd();
|
|
20
|
-
tempDir = mkdtempSync(join(tmpdir(), "manifest-test-"));
|
|
21
|
-
mkdirSync(join(tempDir, ".omni"), { recursive: true });
|
|
22
|
-
process.chdir(tempDir);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
process.chdir(originalCwd);
|
|
27
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
-
});
|
|
13
|
+
setupTestDir("manifest-test-", { chdir: true, createOmniDir: true });
|
|
29
14
|
|
|
30
15
|
describe("loadManifest", () => {
|
|
31
16
|
test("returns empty manifest when file does not exist", async () => {
|
|
@@ -46,6 +31,7 @@ describe("manifest", () => {
|
|
|
46
31
|
rules: ["rule1"],
|
|
47
32
|
commands: ["cmd1"],
|
|
48
33
|
subagents: ["agent1"],
|
|
34
|
+
mcps: [],
|
|
49
35
|
},
|
|
50
36
|
},
|
|
51
37
|
};
|
|
@@ -69,6 +55,7 @@ describe("manifest", () => {
|
|
|
69
55
|
rules: ["r1"],
|
|
70
56
|
commands: [],
|
|
71
57
|
subagents: [],
|
|
58
|
+
mcps: [],
|
|
72
59
|
},
|
|
73
60
|
},
|
|
74
61
|
};
|
|
@@ -119,6 +106,7 @@ describe("manifest", () => {
|
|
|
119
106
|
rules: [],
|
|
120
107
|
docs: [],
|
|
121
108
|
subagents: [],
|
|
109
|
+
mcps: [],
|
|
122
110
|
commands: [],
|
|
123
111
|
exports: {},
|
|
124
112
|
},
|
|
@@ -132,12 +120,14 @@ describe("manifest", () => {
|
|
|
132
120
|
rules: ["rule-a"],
|
|
133
121
|
commands: ["cmd-a"],
|
|
134
122
|
subagents: ["agent-a"],
|
|
123
|
+
mcps: [],
|
|
135
124
|
});
|
|
136
125
|
expect(manifest.capabilities.cap2).toEqual({
|
|
137
126
|
skills: [],
|
|
138
127
|
rules: [],
|
|
139
128
|
commands: [],
|
|
140
129
|
subagents: [],
|
|
130
|
+
mcps: [],
|
|
141
131
|
});
|
|
142
132
|
});
|
|
143
133
|
|
|
@@ -148,146 +138,6 @@ describe("manifest", () => {
|
|
|
148
138
|
expect(manifest.capabilities).toEqual({});
|
|
149
139
|
expect(manifest.syncedAt).toBeDefined();
|
|
150
140
|
});
|
|
151
|
-
|
|
152
|
-
test("includes MCP entry when capability has MCP config", () => {
|
|
153
|
-
const capabilities: LoadedCapability[] = [
|
|
154
|
-
{
|
|
155
|
-
id: "context7",
|
|
156
|
-
path: "/path/to/context7",
|
|
157
|
-
config: {
|
|
158
|
-
capability: {
|
|
159
|
-
id: "context7",
|
|
160
|
-
name: "Context7",
|
|
161
|
-
version: "1.0.0",
|
|
162
|
-
description: "",
|
|
163
|
-
},
|
|
164
|
-
mcp: {
|
|
165
|
-
command: "npx",
|
|
166
|
-
args: ["-y", "@upstash/context7-mcp"],
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
skills: [],
|
|
170
|
-
rules: [],
|
|
171
|
-
docs: [],
|
|
172
|
-
subagents: [],
|
|
173
|
-
commands: [],
|
|
174
|
-
exports: {},
|
|
175
|
-
},
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
const manifest = buildManifestFromCapabilities(capabilities);
|
|
179
|
-
|
|
180
|
-
expect(manifest.capabilities.context7.mcp).toEqual({
|
|
181
|
-
serverName: "omni-context7",
|
|
182
|
-
command: "npx",
|
|
183
|
-
args: ["-y", "@upstash/context7-mcp"],
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("includes MCP env when present", () => {
|
|
188
|
-
const capabilities: LoadedCapability[] = [
|
|
189
|
-
{
|
|
190
|
-
id: "my-mcp",
|
|
191
|
-
path: "/path/to/my-mcp",
|
|
192
|
-
config: {
|
|
193
|
-
capability: {
|
|
194
|
-
id: "my-mcp",
|
|
195
|
-
name: "My MCP",
|
|
196
|
-
version: "1.0.0",
|
|
197
|
-
description: "",
|
|
198
|
-
},
|
|
199
|
-
mcp: {
|
|
200
|
-
command: "node",
|
|
201
|
-
args: ["server.js"],
|
|
202
|
-
env: { API_KEY: "secret" },
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
skills: [],
|
|
206
|
-
rules: [],
|
|
207
|
-
docs: [],
|
|
208
|
-
subagents: [],
|
|
209
|
-
commands: [],
|
|
210
|
-
exports: {},
|
|
211
|
-
},
|
|
212
|
-
];
|
|
213
|
-
|
|
214
|
-
const manifest = buildManifestFromCapabilities(capabilities);
|
|
215
|
-
|
|
216
|
-
expect(manifest.capabilities["my-mcp"].mcp).toEqual({
|
|
217
|
-
serverName: "omni-my-mcp",
|
|
218
|
-
command: "node",
|
|
219
|
-
args: ["server.js"],
|
|
220
|
-
env: { API_KEY: "secret" },
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("does not include MCP entry when capability has no MCP config", () => {
|
|
225
|
-
const capabilities: LoadedCapability[] = [
|
|
226
|
-
{
|
|
227
|
-
id: "tasks",
|
|
228
|
-
path: "/path/to/tasks",
|
|
229
|
-
config: {
|
|
230
|
-
capability: { id: "tasks", name: "Tasks", version: "1.0.0", description: "" },
|
|
231
|
-
},
|
|
232
|
-
skills: [],
|
|
233
|
-
rules: [],
|
|
234
|
-
docs: [],
|
|
235
|
-
subagents: [],
|
|
236
|
-
commands: [],
|
|
237
|
-
exports: {},
|
|
238
|
-
},
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
const manifest = buildManifestFromCapabilities(capabilities);
|
|
242
|
-
|
|
243
|
-
expect(manifest.capabilities.tasks.mcp).toBeUndefined();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("handles mixed capabilities with and without MCP", () => {
|
|
247
|
-
const capabilities: LoadedCapability[] = [
|
|
248
|
-
{
|
|
249
|
-
id: "tasks",
|
|
250
|
-
path: "/path/to/tasks",
|
|
251
|
-
config: {
|
|
252
|
-
capability: { id: "tasks", name: "Tasks", version: "1.0.0", description: "" },
|
|
253
|
-
},
|
|
254
|
-
skills: [],
|
|
255
|
-
rules: [],
|
|
256
|
-
docs: [],
|
|
257
|
-
subagents: [],
|
|
258
|
-
commands: [],
|
|
259
|
-
exports: {},
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
id: "context7",
|
|
263
|
-
path: "/path/to/context7",
|
|
264
|
-
config: {
|
|
265
|
-
capability: {
|
|
266
|
-
id: "context7",
|
|
267
|
-
name: "Context7",
|
|
268
|
-
version: "1.0.0",
|
|
269
|
-
description: "",
|
|
270
|
-
},
|
|
271
|
-
mcp: {
|
|
272
|
-
command: "npx",
|
|
273
|
-
args: ["context7-mcp"],
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
skills: [],
|
|
277
|
-
rules: [],
|
|
278
|
-
docs: [],
|
|
279
|
-
subagents: [],
|
|
280
|
-
commands: [],
|
|
281
|
-
exports: {},
|
|
282
|
-
},
|
|
283
|
-
];
|
|
284
|
-
|
|
285
|
-
const manifest = buildManifestFromCapabilities(capabilities);
|
|
286
|
-
|
|
287
|
-
expect(manifest.capabilities.tasks.mcp).toBeUndefined();
|
|
288
|
-
expect(manifest.capabilities.context7.mcp).toBeDefined();
|
|
289
|
-
expect(manifest.capabilities.context7.mcp?.serverName).toBe("omni-context7");
|
|
290
|
-
});
|
|
291
141
|
});
|
|
292
142
|
|
|
293
143
|
describe("saveManifest and loadManifest round-trip", () => {
|
|
@@ -296,8 +146,8 @@ describe("manifest", () => {
|
|
|
296
146
|
version: 1,
|
|
297
147
|
syncedAt: "2025-01-01T00:00:00.000Z",
|
|
298
148
|
capabilities: {
|
|
299
|
-
cap1: { skills: ["s1"], rules: ["r1"], commands: ["c1"], subagents: ["a1"] },
|
|
300
|
-
cap2: { skills: [], rules: [], commands: [], subagents: [] },
|
|
149
|
+
cap1: { skills: ["s1"], rules: ["r1"], commands: ["c1"], subagents: ["a1"], mcps: [] },
|
|
150
|
+
cap2: { skills: [], rules: [], commands: [], subagents: [], mcps: [] },
|
|
301
151
|
},
|
|
302
152
|
};
|
|
303
153
|
|
|
@@ -311,12 +161,16 @@ describe("manifest", () => {
|
|
|
311
161
|
const manifest1: ResourceManifest = {
|
|
312
162
|
version: 1,
|
|
313
163
|
syncedAt: "2025-01-01T00:00:00.000Z",
|
|
314
|
-
capabilities: {
|
|
164
|
+
capabilities: {
|
|
165
|
+
old: { skills: ["old"], rules: [], commands: [], subagents: [], mcps: [] },
|
|
166
|
+
},
|
|
315
167
|
};
|
|
316
168
|
const manifest2: ResourceManifest = {
|
|
317
169
|
version: 1,
|
|
318
170
|
syncedAt: "2025-01-02T00:00:00.000Z",
|
|
319
|
-
capabilities: {
|
|
171
|
+
capabilities: {
|
|
172
|
+
new: { skills: ["new"], rules: [], commands: [], subagents: [], mcps: [] },
|
|
173
|
+
},
|
|
320
174
|
};
|
|
321
175
|
|
|
322
176
|
await saveManifest(manifest1);
|
|
@@ -345,12 +199,14 @@ describe("manifest", () => {
|
|
|
345
199
|
rules: ["old-rule"],
|
|
346
200
|
commands: [],
|
|
347
201
|
subagents: [],
|
|
202
|
+
mcps: [],
|
|
348
203
|
},
|
|
349
204
|
"enabled-cap": {
|
|
350
205
|
skills: ["keep-skill"],
|
|
351
206
|
rules: ["keep-rule"],
|
|
352
207
|
commands: [],
|
|
353
208
|
subagents: [],
|
|
209
|
+
mcps: [],
|
|
354
210
|
},
|
|
355
211
|
},
|
|
356
212
|
};
|
|
@@ -382,6 +238,7 @@ describe("manifest", () => {
|
|
|
382
238
|
rules: [],
|
|
383
239
|
commands: [],
|
|
384
240
|
subagents: [],
|
|
241
|
+
mcps: [],
|
|
385
242
|
},
|
|
386
243
|
},
|
|
387
244
|
};
|
|
@@ -407,6 +264,7 @@ describe("manifest", () => {
|
|
|
407
264
|
rules: ["nonexistent-rule"],
|
|
408
265
|
commands: [],
|
|
409
266
|
subagents: [],
|
|
267
|
+
mcps: [],
|
|
410
268
|
},
|
|
411
269
|
},
|
|
412
270
|
};
|
|
@@ -456,6 +314,7 @@ describe("manifest", () => {
|
|
|
456
314
|
rules: ["rule-1", "rule-2"],
|
|
457
315
|
commands: [],
|
|
458
316
|
subagents: [],
|
|
317
|
+
mcps: [],
|
|
459
318
|
},
|
|
460
319
|
},
|
|
461
320
|
};
|
|
@@ -491,18 +350,21 @@ describe("manifest", () => {
|
|
|
491
350
|
rules: ["cap1-rule"],
|
|
492
351
|
commands: [],
|
|
493
352
|
subagents: [],
|
|
353
|
+
mcps: [],
|
|
494
354
|
},
|
|
495
355
|
cap2: {
|
|
496
356
|
skills: ["cap2-skill"],
|
|
497
357
|
rules: ["cap2-rule"],
|
|
498
358
|
commands: [],
|
|
499
359
|
subagents: [],
|
|
360
|
+
mcps: [],
|
|
500
361
|
},
|
|
501
362
|
cap3: {
|
|
502
363
|
skills: ["cap3-skill"],
|
|
503
364
|
rules: [],
|
|
504
365
|
commands: [],
|
|
505
366
|
subagents: [],
|
|
367
|
+
mcps: [],
|
|
506
368
|
},
|
|
507
369
|
},
|
|
508
370
|
};
|
|
@@ -532,6 +394,7 @@ describe("manifest", () => {
|
|
|
532
394
|
rules: ["only-rule"],
|
|
533
395
|
commands: [],
|
|
534
396
|
subagents: [],
|
|
397
|
+
mcps: [],
|
|
535
398
|
},
|
|
536
399
|
},
|
|
537
400
|
};
|
package/src/state/manifest.ts
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
2
|
import type { LoadedCapability } from "../types";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* MCP entry for a capability
|
|
6
|
-
*/
|
|
7
|
-
export interface McpEntry {
|
|
8
|
-
/** Server name in .mcp.json (e.g., "omni-{capabilityId}") */
|
|
9
|
-
serverName: string;
|
|
10
|
-
/** Command to run the MCP server */
|
|
11
|
-
command: string;
|
|
12
|
-
/** Arguments for the command */
|
|
13
|
-
args?: string[];
|
|
14
|
-
/** Environment variables */
|
|
15
|
-
env?: Record<string, string>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
4
|
/**
|
|
19
5
|
* Resources provided by a single capability
|
|
20
6
|
*/
|
|
@@ -23,8 +9,7 @@ export interface CapabilityResources {
|
|
|
23
9
|
rules: string[];
|
|
24
10
|
commands: string[];
|
|
25
11
|
subagents: string[];
|
|
26
|
-
|
|
27
|
-
mcp?: McpEntry;
|
|
12
|
+
mcps: string[];
|
|
28
13
|
}
|
|
29
14
|
|
|
30
15
|
/**
|
|
@@ -48,6 +33,7 @@ export interface CleanupResult {
|
|
|
48
33
|
deletedRules: string[];
|
|
49
34
|
deletedCommands: string[];
|
|
50
35
|
deletedSubagents: string[];
|
|
36
|
+
deletedMcps: string[];
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
const MANIFEST_PATH = ".omni/state/manifest.json";
|
|
@@ -94,23 +80,9 @@ export function buildManifestFromCapabilities(capabilities: LoadedCapability[]):
|
|
|
94
80
|
rules: cap.rules.map((r) => r.name),
|
|
95
81
|
commands: cap.commands.map((c) => c.name),
|
|
96
82
|
subagents: cap.subagents.map((s) => s.name),
|
|
83
|
+
mcps: cap.config.mcp ? [cap.id] : [],
|
|
97
84
|
};
|
|
98
85
|
|
|
99
|
-
// Track MCP if capability has one
|
|
100
|
-
if (cap.config.mcp) {
|
|
101
|
-
const mcpEntry: McpEntry = {
|
|
102
|
-
serverName: `omni-${cap.id}`,
|
|
103
|
-
command: cap.config.mcp.command,
|
|
104
|
-
};
|
|
105
|
-
if (cap.config.mcp.args) {
|
|
106
|
-
mcpEntry.args = cap.config.mcp.args;
|
|
107
|
-
}
|
|
108
|
-
if (cap.config.mcp.env) {
|
|
109
|
-
mcpEntry.env = cap.config.mcp.env;
|
|
110
|
-
}
|
|
111
|
-
resources.mcp = mcpEntry;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
86
|
manifest.capabilities[cap.id] = resources;
|
|
115
87
|
}
|
|
116
88
|
|
|
@@ -131,6 +103,7 @@ export async function cleanupStaleResources(
|
|
|
131
103
|
deletedRules: [],
|
|
132
104
|
deletedCommands: [],
|
|
133
105
|
deletedSubagents: [],
|
|
106
|
+
deletedMcps: [],
|
|
134
107
|
};
|
|
135
108
|
|
|
136
109
|
for (const [capId, resources] of Object.entries(previousManifest.capabilities)) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "../test-utils/index.js";
|
|
4
|
+
import {
|
|
5
|
+
disableProvider,
|
|
6
|
+
enableProvider,
|
|
7
|
+
isProviderEnabled,
|
|
8
|
+
readEnabledProviders,
|
|
9
|
+
writeEnabledProviders,
|
|
10
|
+
} from "./providers.js";
|
|
11
|
+
|
|
12
|
+
describe("providers state", () => {
|
|
13
|
+
let testDir: string;
|
|
14
|
+
let originalCwd: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalCwd = process.cwd();
|
|
18
|
+
testDir = tmpdir("providers-test-");
|
|
19
|
+
process.chdir(testDir);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.chdir(originalCwd);
|
|
24
|
+
if (existsSync(testDir)) {
|
|
25
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("readEnabledProviders", () => {
|
|
30
|
+
test("returns default providers when state file does not exist", async () => {
|
|
31
|
+
const providers = await readEnabledProviders();
|
|
32
|
+
expect(providers).toEqual(["claude-code"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("reads providers from state file", async () => {
|
|
36
|
+
await writeEnabledProviders(["cursor", "codex"]);
|
|
37
|
+
const providers = await readEnabledProviders();
|
|
38
|
+
expect(providers).toEqual(["cursor", "codex"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns default when state file is empty", async () => {
|
|
42
|
+
await writeEnabledProviders([]);
|
|
43
|
+
const providers = await readEnabledProviders();
|
|
44
|
+
expect(providers).toEqual(["claude-code"]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("writeEnabledProviders", () => {
|
|
49
|
+
test("creates state directory and writes providers", async () => {
|
|
50
|
+
await writeEnabledProviders(["claude-code", "cursor"]);
|
|
51
|
+
expect(existsSync(".omni/state/providers.json")).toBe(true);
|
|
52
|
+
|
|
53
|
+
const providers = await readEnabledProviders();
|
|
54
|
+
expect(providers).toEqual(["claude-code", "cursor"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("overwrites existing providers", async () => {
|
|
58
|
+
await writeEnabledProviders(["claude-code"]);
|
|
59
|
+
await writeEnabledProviders(["cursor", "codex"]);
|
|
60
|
+
|
|
61
|
+
const providers = await readEnabledProviders();
|
|
62
|
+
expect(providers).toEqual(["cursor", "codex"]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("enableProvider", () => {
|
|
67
|
+
test("adds provider to enabled list", async () => {
|
|
68
|
+
await writeEnabledProviders(["claude-code"]);
|
|
69
|
+
await enableProvider("cursor");
|
|
70
|
+
|
|
71
|
+
const providers = await readEnabledProviders();
|
|
72
|
+
expect(providers).toContain("claude-code");
|
|
73
|
+
expect(providers).toContain("cursor");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("does not duplicate provider if already enabled", async () => {
|
|
77
|
+
await writeEnabledProviders(["claude-code", "cursor"]);
|
|
78
|
+
await enableProvider("cursor");
|
|
79
|
+
|
|
80
|
+
const providers = await readEnabledProviders();
|
|
81
|
+
expect(providers.filter((p) => p === "cursor").length).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("disableProvider", () => {
|
|
86
|
+
test("removes provider from enabled list", async () => {
|
|
87
|
+
await writeEnabledProviders(["claude-code", "cursor"]);
|
|
88
|
+
await disableProvider("cursor");
|
|
89
|
+
|
|
90
|
+
const providers = await readEnabledProviders();
|
|
91
|
+
expect(providers).toEqual(["claude-code"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("handles disabling non-existent provider gracefully", async () => {
|
|
95
|
+
await writeEnabledProviders(["claude-code"]);
|
|
96
|
+
await disableProvider("cursor");
|
|
97
|
+
|
|
98
|
+
const providers = await readEnabledProviders();
|
|
99
|
+
expect(providers).toEqual(["claude-code"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("isProviderEnabled", () => {
|
|
104
|
+
test("returns true when provider is enabled", async () => {
|
|
105
|
+
await writeEnabledProviders(["claude-code", "cursor"]);
|
|
106
|
+
|
|
107
|
+
expect(await isProviderEnabled("cursor")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns false when provider is not enabled", async () => {
|
|
111
|
+
await writeEnabledProviders(["claude-code"]);
|
|
112
|
+
|
|
113
|
+
expect(await isProviderEnabled("cursor")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("round-trip", () => {
|
|
118
|
+
test("write then read returns same providers", async () => {
|
|
119
|
+
const original = ["claude-code", "cursor", "codex"];
|
|
120
|
+
await writeEnabledProviders(original);
|
|
121
|
+
const loaded = await readEnabledProviders();
|
|
122
|
+
expect(loaded).toEqual(original);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import type { ProviderId } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
const STATE_DIR = ".omni/state";
|
|
5
|
+
const PROVIDERS_PATH = `${STATE_DIR}/providers.json`;
|
|
6
|
+
|
|
7
|
+
export interface ProvidersState {
|
|
8
|
+
enabled: ProviderId[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PROVIDERS: ProviderId[] = ["claude-code"];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read the enabled providers from local state.
|
|
15
|
+
* Returns default providers if no state file exists.
|
|
16
|
+
*/
|
|
17
|
+
export async function readEnabledProviders(): Promise<ProviderId[]> {
|
|
18
|
+
if (!existsSync(PROVIDERS_PATH)) {
|
|
19
|
+
return DEFAULT_PROVIDERS;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const content = await Bun.file(PROVIDERS_PATH).text();
|
|
24
|
+
const state = JSON.parse(content) as ProvidersState;
|
|
25
|
+
return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
|
|
26
|
+
} catch {
|
|
27
|
+
return DEFAULT_PROVIDERS;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write enabled providers to local state.
|
|
33
|
+
* @param providers - List of provider IDs to enable
|
|
34
|
+
*/
|
|
35
|
+
export async function writeEnabledProviders(providers: ProviderId[]): Promise<void> {
|
|
36
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
37
|
+
const state: ProvidersState = { enabled: providers };
|
|
38
|
+
await Bun.write(PROVIDERS_PATH, JSON.stringify(state, null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enable a specific provider.
|
|
43
|
+
* @param providerId - The provider to enable
|
|
44
|
+
*/
|
|
45
|
+
export async function enableProvider(providerId: ProviderId): Promise<void> {
|
|
46
|
+
const current = await readEnabledProviders();
|
|
47
|
+
if (!current.includes(providerId)) {
|
|
48
|
+
await writeEnabledProviders([...current, providerId]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Disable a specific provider.
|
|
54
|
+
* @param providerId - The provider to disable
|
|
55
|
+
*/
|
|
56
|
+
export async function disableProvider(providerId: ProviderId): Promise<void> {
|
|
57
|
+
const current = await readEnabledProviders();
|
|
58
|
+
const filtered = current.filter((p) => p !== providerId);
|
|
59
|
+
await writeEnabledProviders(filtered);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if a provider is enabled.
|
|
64
|
+
* @param providerId - The provider to check
|
|
65
|
+
*/
|
|
66
|
+
export async function isProviderEnabled(providerId: ProviderId): Promise<boolean> {
|
|
67
|
+
const current = await readEnabledProviders();
|
|
68
|
+
return current.includes(providerId);
|
|
69
|
+
}
|