@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,415 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
import type { LoadedCapability } from "../types";
|
|
6
|
+
import { isOmniDevMcp, readMcpJson, syncMcpJson, writeMcpJson } from "./manager";
|
|
7
|
+
|
|
8
|
+
describe("mcp-json manager", () => {
|
|
9
|
+
let originalCwd: string;
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
originalCwd = process.cwd();
|
|
14
|
+
tempDir = mkdtempSync(join(tmpdir(), "mcp-json-test-"));
|
|
15
|
+
mkdirSync(join(tempDir, ".omni"), { recursive: true });
|
|
16
|
+
process.chdir(tempDir);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.chdir(originalCwd);
|
|
21
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("isOmniDevMcp", () => {
|
|
25
|
+
test("returns true for 'omnidev' server name", () => {
|
|
26
|
+
expect(isOmniDevMcp("omnidev")).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("returns true for 'omni-' prefixed server names", () => {
|
|
30
|
+
expect(isOmniDevMcp("omni-tasks")).toBe(true);
|
|
31
|
+
expect(isOmniDevMcp("omni-context7")).toBe(true);
|
|
32
|
+
expect(isOmniDevMcp("omni-my-capability")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns false for non-OmniDev server names", () => {
|
|
36
|
+
expect(isOmniDevMcp("myserver")).toBe(false);
|
|
37
|
+
expect(isOmniDevMcp("playwright")).toBe(false);
|
|
38
|
+
expect(isOmniDevMcp("custom-mcp")).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns false for similar but different names", () => {
|
|
42
|
+
expect(isOmniDevMcp("omnidev-extra")).toBe(false);
|
|
43
|
+
expect(isOmniDevMcp("my-omnidev")).toBe(false);
|
|
44
|
+
expect(isOmniDevMcp("omnisomething")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("readMcpJson", () => {
|
|
49
|
+
test("returns empty config when file does not exist", async () => {
|
|
50
|
+
const config = await readMcpJson();
|
|
51
|
+
expect(config).toEqual({ mcpServers: {} });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("reads existing .mcp.json file", async () => {
|
|
55
|
+
const existingConfig = {
|
|
56
|
+
mcpServers: {
|
|
57
|
+
myserver: {
|
|
58
|
+
command: "node",
|
|
59
|
+
args: ["server.js"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
await Bun.write(".mcp.json", JSON.stringify(existingConfig));
|
|
64
|
+
|
|
65
|
+
const config = await readMcpJson();
|
|
66
|
+
expect(config).toEqual(existingConfig);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("handles invalid JSON gracefully", async () => {
|
|
70
|
+
await Bun.write(".mcp.json", "invalid json {{{");
|
|
71
|
+
|
|
72
|
+
const config = await readMcpJson();
|
|
73
|
+
expect(config).toEqual({ mcpServers: {} });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("handles missing mcpServers field", async () => {
|
|
77
|
+
await Bun.write(".mcp.json", JSON.stringify({ other: "field" }));
|
|
78
|
+
|
|
79
|
+
const config = await readMcpJson();
|
|
80
|
+
expect(config).toEqual({ mcpServers: {} });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("writeMcpJson", () => {
|
|
85
|
+
test("writes config to .mcp.json", async () => {
|
|
86
|
+
const config = {
|
|
87
|
+
mcpServers: {
|
|
88
|
+
test: {
|
|
89
|
+
command: "test-cmd",
|
|
90
|
+
args: ["arg1", "arg2"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
await writeMcpJson(config);
|
|
96
|
+
|
|
97
|
+
const content = await Bun.file(".mcp.json").text();
|
|
98
|
+
expect(JSON.parse(content)).toEqual(config);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("overwrites existing .mcp.json", async () => {
|
|
102
|
+
await Bun.write(".mcp.json", JSON.stringify({ mcpServers: { old: { command: "old" } } }));
|
|
103
|
+
|
|
104
|
+
const newConfig = {
|
|
105
|
+
mcpServers: {
|
|
106
|
+
new: { command: "new" },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await writeMcpJson(newConfig);
|
|
111
|
+
|
|
112
|
+
const content = await Bun.file(".mcp.json").text();
|
|
113
|
+
expect(JSON.parse(content)).toEqual(newConfig);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("formats JSON with indentation", async () => {
|
|
117
|
+
const config = {
|
|
118
|
+
mcpServers: {
|
|
119
|
+
test: { command: "cmd" },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await writeMcpJson(config);
|
|
124
|
+
|
|
125
|
+
const content = await Bun.file(".mcp.json").text();
|
|
126
|
+
expect(content).toContain("\n");
|
|
127
|
+
expect(content).toContain(" ");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("syncMcpJson", () => {
|
|
132
|
+
const createMockCapability = (
|
|
133
|
+
id: string,
|
|
134
|
+
mcp?: { command: string; args?: string[]; env?: Record<string, string> },
|
|
135
|
+
): LoadedCapability => ({
|
|
136
|
+
id,
|
|
137
|
+
path: `/path/to/${id}`,
|
|
138
|
+
config: {
|
|
139
|
+
capability: { id, name: id, version: "1.0.0", description: "" },
|
|
140
|
+
mcp,
|
|
141
|
+
},
|
|
142
|
+
skills: [],
|
|
143
|
+
rules: [],
|
|
144
|
+
docs: [],
|
|
145
|
+
subagents: [],
|
|
146
|
+
commands: [],
|
|
147
|
+
exports: {},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("sandbox enabled mode (default)", () => {
|
|
151
|
+
test("adds only omnidev server when sandbox enabled", async () => {
|
|
152
|
+
const capabilities = [createMockCapability("tasks")];
|
|
153
|
+
|
|
154
|
+
await syncMcpJson(capabilities, true, { silent: true });
|
|
155
|
+
|
|
156
|
+
const config = await readMcpJson();
|
|
157
|
+
expect(config.mcpServers).toHaveProperty("omnidev");
|
|
158
|
+
expect(config.mcpServers.omnidev).toEqual({
|
|
159
|
+
command: "bunx",
|
|
160
|
+
args: ["omnidev", "serve"],
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("removes omni- prefixed servers when switching to sandbox enabled", async () => {
|
|
165
|
+
// Pre-populate with omni- entries
|
|
166
|
+
await Bun.write(
|
|
167
|
+
".mcp.json",
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
mcpServers: {
|
|
170
|
+
"omni-tasks": { command: "npx", args: ["tasks-mcp"] },
|
|
171
|
+
"omni-context7": { command: "npx", args: ["context7-mcp"] },
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const capabilities = [
|
|
177
|
+
createMockCapability("tasks", { command: "npx", args: ["tasks-mcp"] }),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
await syncMcpJson(capabilities, true, { silent: true });
|
|
181
|
+
|
|
182
|
+
const config = await readMcpJson();
|
|
183
|
+
expect(config.mcpServers).not.toHaveProperty("omni-tasks");
|
|
184
|
+
expect(config.mcpServers).not.toHaveProperty("omni-context7");
|
|
185
|
+
expect(config.mcpServers).toHaveProperty("omnidev");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("preserves user MCPs when sandbox enabled", async () => {
|
|
189
|
+
await Bun.write(
|
|
190
|
+
".mcp.json",
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
mcpServers: {
|
|
193
|
+
myserver: { command: "node", args: ["my-server.js"] },
|
|
194
|
+
playwright: { command: "npx", args: ["@playwright/mcp"] },
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const capabilities = [createMockCapability("tasks")];
|
|
200
|
+
|
|
201
|
+
await syncMcpJson(capabilities, true, { silent: true });
|
|
202
|
+
|
|
203
|
+
const config = await readMcpJson();
|
|
204
|
+
expect(config.mcpServers).toHaveProperty("myserver");
|
|
205
|
+
expect(config.mcpServers).toHaveProperty("playwright");
|
|
206
|
+
expect(config.mcpServers).toHaveProperty("omnidev");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("sandbox disabled mode", () => {
|
|
211
|
+
test("adds omni- prefixed servers for MCP capabilities", async () => {
|
|
212
|
+
const capabilities = [
|
|
213
|
+
createMockCapability("context7", {
|
|
214
|
+
command: "npx",
|
|
215
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
220
|
+
|
|
221
|
+
const config = await readMcpJson();
|
|
222
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
223
|
+
expect(config.mcpServers["omni-context7"]).toEqual({
|
|
224
|
+
command: "npx",
|
|
225
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
226
|
+
});
|
|
227
|
+
expect(config.mcpServers).not.toHaveProperty("omnidev");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("does not add entries for capabilities without MCP", async () => {
|
|
231
|
+
const capabilities = [
|
|
232
|
+
createMockCapability("tasks"), // No MCP
|
|
233
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
237
|
+
|
|
238
|
+
const config = await readMcpJson();
|
|
239
|
+
expect(config.mcpServers).not.toHaveProperty("omni-tasks");
|
|
240
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("includes env when present in MCP config", async () => {
|
|
244
|
+
const capabilities = [
|
|
245
|
+
createMockCapability("my-cap", {
|
|
246
|
+
command: "node",
|
|
247
|
+
args: ["server.js"],
|
|
248
|
+
env: { API_KEY: "secret", DEBUG: "true" },
|
|
249
|
+
}),
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
253
|
+
|
|
254
|
+
const config = await readMcpJson();
|
|
255
|
+
expect(config.mcpServers["omni-my-cap"].env).toEqual({
|
|
256
|
+
API_KEY: "secret",
|
|
257
|
+
DEBUG: "true",
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("removes omnidev server when switching to sandbox disabled", async () => {
|
|
262
|
+
await Bun.write(
|
|
263
|
+
".mcp.json",
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
mcpServers: {
|
|
266
|
+
omnidev: { command: "bunx", args: ["omnidev", "serve"] },
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const capabilities = [
|
|
272
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
276
|
+
|
|
277
|
+
const config = await readMcpJson();
|
|
278
|
+
expect(config.mcpServers).not.toHaveProperty("omnidev");
|
|
279
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("preserves user MCPs when sandbox disabled", async () => {
|
|
283
|
+
await Bun.write(
|
|
284
|
+
".mcp.json",
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
mcpServers: {
|
|
287
|
+
myserver: { command: "node", args: ["my-server.js"] },
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const capabilities = [
|
|
293
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
297
|
+
|
|
298
|
+
const config = await readMcpJson();
|
|
299
|
+
expect(config.mcpServers).toHaveProperty("myserver");
|
|
300
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("results in empty omni entries when no MCP capabilities", async () => {
|
|
304
|
+
const capabilities = [
|
|
305
|
+
createMockCapability("tasks"), // No MCP
|
|
306
|
+
createMockCapability("ralph"), // No MCP
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
310
|
+
|
|
311
|
+
const config = await readMcpJson();
|
|
312
|
+
const omniEntries = Object.keys(config.mcpServers).filter(isOmniDevMcp);
|
|
313
|
+
expect(omniEntries).toHaveLength(0);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("mode switching", () => {
|
|
318
|
+
test("switching from sandbox disabled to enabled cleans up correctly", async () => {
|
|
319
|
+
// Start in sandbox disabled mode
|
|
320
|
+
const capabilities = [
|
|
321
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
322
|
+
];
|
|
323
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
324
|
+
|
|
325
|
+
let config = await readMcpJson();
|
|
326
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
327
|
+
expect(config.mcpServers).not.toHaveProperty("omnidev");
|
|
328
|
+
|
|
329
|
+
// Switch to sandbox enabled
|
|
330
|
+
await syncMcpJson(capabilities, true, { silent: true });
|
|
331
|
+
|
|
332
|
+
config = await readMcpJson();
|
|
333
|
+
expect(config.mcpServers).not.toHaveProperty("omni-context7");
|
|
334
|
+
expect(config.mcpServers).toHaveProperty("omnidev");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("switching from sandbox enabled to disabled cleans up correctly", async () => {
|
|
338
|
+
// Start in sandbox enabled mode
|
|
339
|
+
const capabilities = [
|
|
340
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
341
|
+
];
|
|
342
|
+
await syncMcpJson(capabilities, true, { silent: true });
|
|
343
|
+
|
|
344
|
+
let config = await readMcpJson();
|
|
345
|
+
expect(config.mcpServers).toHaveProperty("omnidev");
|
|
346
|
+
expect(config.mcpServers).not.toHaveProperty("omni-context7");
|
|
347
|
+
|
|
348
|
+
// Switch to sandbox disabled
|
|
349
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
350
|
+
|
|
351
|
+
config = await readMcpJson();
|
|
352
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
353
|
+
expect(config.mcpServers).not.toHaveProperty("omnidev");
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("capability toggle (sandbox disabled)", () => {
|
|
358
|
+
test("enabling MCP capability adds its entry", async () => {
|
|
359
|
+
// Start with no MCP capabilities
|
|
360
|
+
await syncMcpJson([createMockCapability("tasks")], false, { silent: true });
|
|
361
|
+
|
|
362
|
+
let config = await readMcpJson();
|
|
363
|
+
expect(Object.keys(config.mcpServers).filter(isOmniDevMcp)).toHaveLength(0);
|
|
364
|
+
|
|
365
|
+
// Enable MCP capability
|
|
366
|
+
await syncMcpJson(
|
|
367
|
+
[
|
|
368
|
+
createMockCapability("tasks"),
|
|
369
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
370
|
+
],
|
|
371
|
+
false,
|
|
372
|
+
{ silent: true },
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
config = await readMcpJson();
|
|
376
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("disabling MCP capability removes its entry", async () => {
|
|
380
|
+
// Start with MCP capability
|
|
381
|
+
await syncMcpJson(
|
|
382
|
+
[createMockCapability("context7", { command: "npx", args: ["context7-mcp"] })],
|
|
383
|
+
false,
|
|
384
|
+
{ silent: true },
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
let config = await readMcpJson();
|
|
388
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
389
|
+
|
|
390
|
+
// Disable the capability (only non-MCP capabilities remain)
|
|
391
|
+
await syncMcpJson([createMockCapability("tasks")], false, { silent: true });
|
|
392
|
+
|
|
393
|
+
config = await readMcpJson();
|
|
394
|
+
expect(config.mcpServers).not.toHaveProperty("omni-context7");
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("multiple MCP capabilities", () => {
|
|
399
|
+
test("adds all MCP capabilities when sandbox disabled", async () => {
|
|
400
|
+
const capabilities = [
|
|
401
|
+
createMockCapability("context7", { command: "npx", args: ["context7-mcp"] }),
|
|
402
|
+
createMockCapability("playwright", { command: "npx", args: ["playwright-mcp"] }),
|
|
403
|
+
createMockCapability("tasks"), // No MCP
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
await syncMcpJson(capabilities, false, { silent: true });
|
|
407
|
+
|
|
408
|
+
const config = await readMcpJson();
|
|
409
|
+
expect(config.mcpServers).toHaveProperty("omni-context7");
|
|
410
|
+
expect(config.mcpServers).toHaveProperty("omni-playwright");
|
|
411
|
+
expect(config.mcpServers).not.toHaveProperty("omni-tasks");
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import type { LoadedCapability, McpConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP server configuration in .mcp.json
|
|
6
|
+
*/
|
|
7
|
+
export interface McpServerConfig {
|
|
8
|
+
command: string;
|
|
9
|
+
args?: string[];
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Structure of .mcp.json file
|
|
15
|
+
*/
|
|
16
|
+
export interface McpJsonConfig {
|
|
17
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MCP_JSON_PATH = ".mcp.json";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a server name is managed by OmniDev
|
|
24
|
+
*/
|
|
25
|
+
export function isOmniDevMcp(serverName: string): boolean {
|
|
26
|
+
return serverName === "omnidev" || serverName.startsWith("omni-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read .mcp.json or return empty config if doesn't exist
|
|
31
|
+
*/
|
|
32
|
+
export async function readMcpJson(): Promise<McpJsonConfig> {
|
|
33
|
+
if (!existsSync(MCP_JSON_PATH)) {
|
|
34
|
+
return { mcpServers: {} };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const content = await Bun.file(MCP_JSON_PATH).text();
|
|
39
|
+
const parsed = JSON.parse(content);
|
|
40
|
+
return {
|
|
41
|
+
mcpServers: parsed.mcpServers || {},
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
// If file is invalid JSON, return empty config
|
|
45
|
+
return { mcpServers: {} };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write .mcp.json, preserving non-OmniDev entries
|
|
51
|
+
*/
|
|
52
|
+
export async function writeMcpJson(config: McpJsonConfig): Promise<void> {
|
|
53
|
+
await Bun.write(MCP_JSON_PATH, JSON.stringify(config, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build MCP server config from capability's mcp section
|
|
58
|
+
*/
|
|
59
|
+
function buildMcpServerConfig(mcp: McpConfig): McpServerConfig {
|
|
60
|
+
const config: McpServerConfig = {
|
|
61
|
+
command: mcp.command,
|
|
62
|
+
};
|
|
63
|
+
if (mcp.args) {
|
|
64
|
+
config.args = mcp.args;
|
|
65
|
+
}
|
|
66
|
+
if (mcp.env) {
|
|
67
|
+
config.env = mcp.env;
|
|
68
|
+
}
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sync .mcp.json based on sandbox mode
|
|
74
|
+
*
|
|
75
|
+
* When sandboxEnabled = true (default):
|
|
76
|
+
* - Only "omnidev" MCP server is registered
|
|
77
|
+
* - Capability MCPs run as children of OmniDev server
|
|
78
|
+
*
|
|
79
|
+
* When sandboxEnabled = false:
|
|
80
|
+
* - Each capability's MCP is registered as "omni-{capabilityId}"
|
|
81
|
+
* - OmniDev server is NOT registered
|
|
82
|
+
*/
|
|
83
|
+
export async function syncMcpJson(
|
|
84
|
+
capabilities: LoadedCapability[],
|
|
85
|
+
sandboxEnabled: boolean,
|
|
86
|
+
options: { silent?: boolean } = {},
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const mcpJson = await readMcpJson();
|
|
89
|
+
|
|
90
|
+
// Remove all OmniDev-managed MCPs first
|
|
91
|
+
for (const serverName of Object.keys(mcpJson.mcpServers)) {
|
|
92
|
+
if (isOmniDevMcp(serverName)) {
|
|
93
|
+
delete mcpJson.mcpServers[serverName];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (sandboxEnabled) {
|
|
98
|
+
// Add only OmniDev MCP server
|
|
99
|
+
mcpJson.mcpServers["omnidev"] = {
|
|
100
|
+
command: "bunx",
|
|
101
|
+
args: ["omnidev", "serve"],
|
|
102
|
+
};
|
|
103
|
+
} else {
|
|
104
|
+
// Add MCPs from all enabled capabilities
|
|
105
|
+
for (const cap of capabilities) {
|
|
106
|
+
if (cap.config.mcp) {
|
|
107
|
+
mcpJson.mcpServers[`omni-${cap.id}`] = buildMcpServerConfig(cap.config.mcp);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await writeMcpJson(mcpJson);
|
|
113
|
+
|
|
114
|
+
if (!options.silent) {
|
|
115
|
+
const count = Object.keys(mcpJson.mcpServers).filter(isOmniDevMcp).length;
|
|
116
|
+
console.log(` - .mcp.json (${count} MCP server(s))`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
clearActiveProfileState,
|
|
7
|
+
readActiveProfileState,
|
|
8
|
+
writeActiveProfileState,
|
|
9
|
+
} from "./active-profile";
|
|
10
|
+
|
|
11
|
+
describe("active-profile state", () => {
|
|
12
|
+
let originalCwd: string;
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
originalCwd = process.cwd();
|
|
17
|
+
tempDir = mkdtempSync(join(tmpdir(), "active-profile-test-"));
|
|
18
|
+
mkdirSync(join(tempDir, ".omni"), { recursive: true });
|
|
19
|
+
process.chdir(tempDir);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
process.chdir(originalCwd);
|
|
24
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("readActiveProfileState", () => {
|
|
28
|
+
test("returns null when state file does not exist", async () => {
|
|
29
|
+
const profile = await readActiveProfileState();
|
|
30
|
+
expect(profile).toBe(null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("reads profile from state file", async () => {
|
|
34
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
35
|
+
await Bun.write(".omni/state/active-profile", "my-profile");
|
|
36
|
+
|
|
37
|
+
const profile = await readActiveProfileState();
|
|
38
|
+
expect(profile).toBe("my-profile");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("trims whitespace from profile name", async () => {
|
|
42
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
43
|
+
await Bun.write(".omni/state/active-profile", " my-profile \n");
|
|
44
|
+
|
|
45
|
+
const profile = await readActiveProfileState();
|
|
46
|
+
expect(profile).toBe("my-profile");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns null for empty file", async () => {
|
|
50
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
51
|
+
await Bun.write(".omni/state/active-profile", "");
|
|
52
|
+
|
|
53
|
+
const profile = await readActiveProfileState();
|
|
54
|
+
expect(profile).toBe(null);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("returns null for whitespace-only file", async () => {
|
|
58
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
59
|
+
await Bun.write(".omni/state/active-profile", " \n ");
|
|
60
|
+
|
|
61
|
+
const profile = await readActiveProfileState();
|
|
62
|
+
expect(profile).toBe(null);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("writeActiveProfileState", () => {
|
|
67
|
+
test("writes profile to state file", async () => {
|
|
68
|
+
await writeActiveProfileState("production");
|
|
69
|
+
|
|
70
|
+
const content = await Bun.file(".omni/state/active-profile").text();
|
|
71
|
+
expect(content).toBe("production");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("creates state directory if it does not exist", async () => {
|
|
75
|
+
expect(existsSync(".omni/state")).toBe(false);
|
|
76
|
+
|
|
77
|
+
await writeActiveProfileState("dev");
|
|
78
|
+
|
|
79
|
+
expect(existsSync(".omni/state")).toBe(true);
|
|
80
|
+
const content = await Bun.file(".omni/state/active-profile").text();
|
|
81
|
+
expect(content).toBe("dev");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("overwrites existing state file", async () => {
|
|
85
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
86
|
+
await Bun.write(".omni/state/active-profile", "old-profile");
|
|
87
|
+
|
|
88
|
+
await writeActiveProfileState("new-profile");
|
|
89
|
+
|
|
90
|
+
const content = await Bun.file(".omni/state/active-profile").text();
|
|
91
|
+
expect(content).toBe("new-profile");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("clearActiveProfileState", () => {
|
|
96
|
+
test("deletes state file when it exists", async () => {
|
|
97
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
98
|
+
await Bun.write(".omni/state/active-profile", "some-profile");
|
|
99
|
+
|
|
100
|
+
expect(existsSync(".omni/state/active-profile")).toBe(true);
|
|
101
|
+
|
|
102
|
+
await clearActiveProfileState();
|
|
103
|
+
|
|
104
|
+
expect(existsSync(".omni/state/active-profile")).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("does nothing when state file does not exist", async () => {
|
|
108
|
+
expect(existsSync(".omni/state/active-profile")).toBe(false);
|
|
109
|
+
|
|
110
|
+
// Should not throw
|
|
111
|
+
await clearActiveProfileState();
|
|
112
|
+
|
|
113
|
+
expect(existsSync(".omni/state/active-profile")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("round-trip", () => {
|
|
118
|
+
test("write then read returns same profile", async () => {
|
|
119
|
+
await writeActiveProfileState("test-profile");
|
|
120
|
+
const profile = await readActiveProfileState();
|
|
121
|
+
expect(profile).toBe("test-profile");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("clear then read returns null", async () => {
|
|
125
|
+
await writeActiveProfileState("test-profile");
|
|
126
|
+
await clearActiveProfileState();
|
|
127
|
+
const profile = await readActiveProfileState();
|
|
128
|
+
expect(profile).toBe(null);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const STATE_DIR = ".omni/state";
|
|
4
|
+
const ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read the active profile from state file.
|
|
8
|
+
* Returns null if no active profile is set in state.
|
|
9
|
+
*/
|
|
10
|
+
export async function readActiveProfileState(): Promise<string | null> {
|
|
11
|
+
if (!existsSync(ACTIVE_PROFILE_PATH)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const content = await Bun.file(ACTIVE_PROFILE_PATH).text();
|
|
17
|
+
const trimmed = content.trim();
|
|
18
|
+
return trimmed || null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write the active profile to state file.
|
|
26
|
+
* @param profileName - The name of the profile to set as active
|
|
27
|
+
*/
|
|
28
|
+
export async function writeActiveProfileState(profileName: string): Promise<void> {
|
|
29
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
30
|
+
await Bun.write(ACTIVE_PROFILE_PATH, profileName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear the active profile state (delete the state file).
|
|
35
|
+
*/
|
|
36
|
+
export async function clearActiveProfileState(): Promise<void> {
|
|
37
|
+
if (existsSync(ACTIVE_PROFILE_PATH)) {
|
|
38
|
+
const file = Bun.file(ACTIVE_PROFILE_PATH);
|
|
39
|
+
await file.delete();
|
|
40
|
+
}
|
|
41
|
+
}
|