@omnidev-ai/cli 0.1.0 → 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 +6 -3
- package/src/commands/AGENTS.md +0 -2
- package/src/commands/capability.test.ts +5 -35
- package/src/commands/capability.ts +7 -4
- package/src/commands/doctor.test.ts +32 -33
- package/src/commands/doctor.ts +24 -7
- package/src/commands/init.test.ts +97 -73
- package/src/commands/init.ts +93 -97
- package/src/commands/profile.test.ts +9 -22
- package/src/commands/profile.ts +4 -2
- package/src/commands/provider.test.ts +128 -0
- package/src/commands/provider.ts +139 -0
- package/src/commands/sync.ts +10 -4
- package/src/index.ts +2 -2
- package/src/lib/dynamic-app.ts +2 -4
- package/src/prompts/provider.ts +6 -4
- package/src/commands/mcp.ts +0 -113
- package/src/commands/serve.test.ts +0 -184
- package/src/commands/serve.ts +0 -63
package/src/lib/dynamic-app.ts
CHANGED
|
@@ -6,9 +6,8 @@ import { capabilityRoutes } from "../commands/capability";
|
|
|
6
6
|
// Core commands
|
|
7
7
|
import { doctorCommand } from "../commands/doctor";
|
|
8
8
|
import { initCommand } from "../commands/init";
|
|
9
|
-
import { mcpRoutes } from "../commands/mcp";
|
|
10
9
|
import { profileRoutes } from "../commands/profile";
|
|
11
|
-
import {
|
|
10
|
+
import { providerRoutes } from "../commands/provider";
|
|
12
11
|
import { syncCommand } from "../commands/sync";
|
|
13
12
|
import { debug } from "./debug";
|
|
14
13
|
|
|
@@ -20,11 +19,10 @@ export async function buildDynamicApp() {
|
|
|
20
19
|
const routes: Record<string, unknown> = {
|
|
21
20
|
init: initCommand,
|
|
22
21
|
doctor: doctorCommand,
|
|
23
|
-
serve: serveCommand,
|
|
24
22
|
sync: syncCommand,
|
|
25
23
|
capability: capabilityRoutes,
|
|
26
24
|
profile: profileRoutes,
|
|
27
|
-
|
|
25
|
+
provider: providerRoutes,
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
debug("Core routes registered", Object.keys(routes));
|
package/src/prompts/provider.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { checkbox } from "@inquirer/prompts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProviderId } from "@omnidev-ai/core";
|
|
3
3
|
|
|
4
|
-
export async function
|
|
4
|
+
export async function promptForProviders(): Promise<ProviderId[]> {
|
|
5
5
|
const answers = await checkbox({
|
|
6
6
|
message: "Select your AI provider(s):",
|
|
7
7
|
choices: [
|
|
8
|
-
{ name: "Claude (
|
|
8
|
+
{ name: "Claude Code (Claude CLI)", value: "claude-code", checked: true },
|
|
9
|
+
{ name: "Cursor", value: "cursor", checked: false },
|
|
9
10
|
{ name: "Codex", value: "codex", checked: false },
|
|
11
|
+
{ name: "OpenCode", value: "opencode", checked: false },
|
|
10
12
|
],
|
|
11
13
|
required: true,
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
return answers as
|
|
16
|
+
return answers as ProviderId[];
|
|
15
17
|
}
|
package/src/commands/mcp.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
-
|
|
4
|
-
const STATUS_FILE = ".omni/state/mcp-status.json";
|
|
5
|
-
|
|
6
|
-
// Local type definition to avoid circular dependency with @omnidev-ai/mcp
|
|
7
|
-
interface McpChildProcess {
|
|
8
|
-
capabilityId: string;
|
|
9
|
-
pid: number | null;
|
|
10
|
-
status: "starting" | "connected" | "disconnected" | "error";
|
|
11
|
-
transport: "stdio" | "sse" | "http";
|
|
12
|
-
lastHealthCheck?: string;
|
|
13
|
-
error?: string;
|
|
14
|
-
toolCount?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface McpStatusFile {
|
|
18
|
-
lastUpdated: string;
|
|
19
|
-
relayPort: number;
|
|
20
|
-
children: McpChildProcess[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Run the mcp status command.
|
|
25
|
-
*/
|
|
26
|
-
export async function runMcpStatus(): Promise<void> {
|
|
27
|
-
try {
|
|
28
|
-
if (!existsSync(STATUS_FILE)) {
|
|
29
|
-
console.log("No MCP status found.");
|
|
30
|
-
console.log("");
|
|
31
|
-
console.log("Is the OmniDev server running? Start it with:");
|
|
32
|
-
console.log(" omnidev serve");
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const statusText = await Bun.file(STATUS_FILE).text();
|
|
37
|
-
const status = JSON.parse(statusText) as McpStatusFile;
|
|
38
|
-
|
|
39
|
-
console.log("");
|
|
40
|
-
console.log("=== MCP Controller Status ===");
|
|
41
|
-
console.log("");
|
|
42
|
-
console.log(`Last updated: ${status.lastUpdated}`);
|
|
43
|
-
console.log(`Relay port: ${status.relayPort}`);
|
|
44
|
-
console.log("");
|
|
45
|
-
|
|
46
|
-
if (status.children.length === 0) {
|
|
47
|
-
console.log("No MCP children running.");
|
|
48
|
-
console.log("");
|
|
49
|
-
console.log("Enable a capability with [mcp] configuration to spawn child MCPs.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.log(`Child Processes (${status.children.length}):`);
|
|
54
|
-
console.log("");
|
|
55
|
-
|
|
56
|
-
for (const child of status.children) {
|
|
57
|
-
const statusIcon = getStatusIcon(child.status);
|
|
58
|
-
console.log(` ${statusIcon} ${child.capabilityId}`);
|
|
59
|
-
console.log(` Status: ${child.status}`);
|
|
60
|
-
console.log(` Transport: ${child.transport}`);
|
|
61
|
-
if (child.pid) {
|
|
62
|
-
console.log(` PID: ${child.pid}`);
|
|
63
|
-
}
|
|
64
|
-
if (child.toolCount !== undefined) {
|
|
65
|
-
console.log(` Tools: ${child.toolCount}`);
|
|
66
|
-
}
|
|
67
|
-
if (child.lastHealthCheck) {
|
|
68
|
-
console.log(` Last check: ${child.lastHealthCheck}`);
|
|
69
|
-
}
|
|
70
|
-
if (child.error) {
|
|
71
|
-
console.log(` Error: ${child.error}`);
|
|
72
|
-
}
|
|
73
|
-
console.log("");
|
|
74
|
-
}
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error("Error reading MCP status:", error);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getStatusIcon(status: string): string {
|
|
82
|
-
switch (status) {
|
|
83
|
-
case "connected":
|
|
84
|
-
return "\u2713"; // checkmark
|
|
85
|
-
case "starting":
|
|
86
|
-
return "\u2022"; // bullet
|
|
87
|
-
case "disconnected":
|
|
88
|
-
return "\u2717"; // x mark
|
|
89
|
-
case "error":
|
|
90
|
-
return "\u2717"; // x mark
|
|
91
|
-
default:
|
|
92
|
-
return "?";
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const statusCommand = buildCommand({
|
|
97
|
-
docs: {
|
|
98
|
-
brief: "Show MCP controller status",
|
|
99
|
-
},
|
|
100
|
-
parameters: {},
|
|
101
|
-
async func() {
|
|
102
|
-
await runMcpStatus();
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
export const mcpRoutes = buildRouteMap({
|
|
107
|
-
routes: {
|
|
108
|
-
status: statusCommand,
|
|
109
|
-
},
|
|
110
|
-
docs: {
|
|
111
|
-
brief: "MCP controller commands",
|
|
112
|
-
},
|
|
113
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { runServe } from "./serve";
|
|
5
|
-
|
|
6
|
-
// Create test fixtures directory
|
|
7
|
-
const testDir = join(process.cwd(), "test-fixtures-serve");
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
// Clean up and create fresh test directory
|
|
11
|
-
if (existsSync(testDir)) {
|
|
12
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
13
|
-
}
|
|
14
|
-
mkdirSync(testDir, { recursive: true });
|
|
15
|
-
process.chdir(testDir);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
// Return to original directory and clean up
|
|
20
|
-
process.chdir(join(testDir, ".."));
|
|
21
|
-
if (existsSync(testDir)) {
|
|
22
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("serve command", () => {
|
|
27
|
-
test("should fail when OmniDev is not initialized", async () => {
|
|
28
|
-
const mockExit = mock((code?: number) => {
|
|
29
|
-
throw new Error(`process.exit: ${code}`);
|
|
30
|
-
}) as typeof process.exit;
|
|
31
|
-
const originalExit = process.exit;
|
|
32
|
-
process.exit = mockExit;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
await expect(runServe({})).rejects.toThrow("process.exit: 1");
|
|
36
|
-
expect(mockExit).toHaveBeenCalledWith(1);
|
|
37
|
-
} finally {
|
|
38
|
-
process.exit = originalExit;
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("should fail when .omni/ directory is missing", async () => {
|
|
43
|
-
// Don't create .omni/ - test expects it to be missing
|
|
44
|
-
|
|
45
|
-
const mockExit = mock((code?: number) => {
|
|
46
|
-
throw new Error(`process.exit: ${code}`);
|
|
47
|
-
}) as typeof process.exit;
|
|
48
|
-
const originalExit = process.exit;
|
|
49
|
-
process.exit = mockExit;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
await expect(runServe({})).rejects.toThrow("process.exit: 1");
|
|
53
|
-
expect(mockExit).toHaveBeenCalledWith(1);
|
|
54
|
-
} finally {
|
|
55
|
-
process.exit = originalExit;
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("should fail when profile does not exist", async () => {
|
|
60
|
-
// Set up directories
|
|
61
|
-
mkdirSync(".omni", { recursive: true });
|
|
62
|
-
mkdirSync(".omni", { recursive: true });
|
|
63
|
-
|
|
64
|
-
// Create a config without the requested profile
|
|
65
|
-
writeFileSync(
|
|
66
|
-
"omni.toml",
|
|
67
|
-
`
|
|
68
|
-
[capability]
|
|
69
|
-
project = "test"
|
|
70
|
-
|
|
71
|
-
[profiles.default]
|
|
72
|
-
`,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const mockExit = mock((code?: number) => {
|
|
76
|
-
throw new Error(`process.exit: ${code}`);
|
|
77
|
-
}) as typeof process.exit;
|
|
78
|
-
const originalExit = process.exit;
|
|
79
|
-
process.exit = mockExit;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
await expect(runServe({ profile: "nonexistent" })).rejects.toThrow("process.exit: 1");
|
|
83
|
-
expect(mockExit).toHaveBeenCalledWith(1);
|
|
84
|
-
} finally {
|
|
85
|
-
process.exit = originalExit;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("should set profile when provided and valid", async () => {
|
|
90
|
-
// Set up directories
|
|
91
|
-
mkdirSync(".omni", { recursive: true });
|
|
92
|
-
mkdirSync(".omni", { recursive: true });
|
|
93
|
-
|
|
94
|
-
// Create config with profiles
|
|
95
|
-
writeFileSync(
|
|
96
|
-
"omni.toml",
|
|
97
|
-
`project = "test"
|
|
98
|
-
active_profile = "default"
|
|
99
|
-
|
|
100
|
-
[profiles.default]
|
|
101
|
-
capabilities = []
|
|
102
|
-
|
|
103
|
-
[profiles.testing]
|
|
104
|
-
capabilities = []
|
|
105
|
-
`,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Mock startServer to prevent actual server start
|
|
109
|
-
const mockStartServer = mock(async () => {
|
|
110
|
-
// Server started successfully, do nothing
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Mock the import of @omnidev-ai/mcp
|
|
114
|
-
const originalImport = globalThis[Symbol.for("Bun.lazy")];
|
|
115
|
-
// biome-ignore lint/suspicious/noExplicitAny: Testing requires dynamic mocking
|
|
116
|
-
(globalThis as any).import = mock(async (module: string) => {
|
|
117
|
-
if (module === "@omnidev-ai/mcp") {
|
|
118
|
-
return { startServer: mockStartServer };
|
|
119
|
-
}
|
|
120
|
-
throw new Error(`Unexpected import: ${module}`);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const mockExit = mock((code?: number) => {
|
|
124
|
-
throw new Error(`process.exit: ${code}`);
|
|
125
|
-
}) as typeof process.exit;
|
|
126
|
-
const originalExit = process.exit;
|
|
127
|
-
process.exit = mockExit;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// This should fail because startServer will actually run, but that's OK for this test
|
|
131
|
-
// We just want to verify that setActiveProfile was called
|
|
132
|
-
await runServe({ profile: "testing" }).catch(() => {
|
|
133
|
-
// Ignore the error from startServer
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Check that active profile was written to state file (not config.toml)
|
|
137
|
-
const stateContent = await Bun.file(".omni/state/active-profile").text();
|
|
138
|
-
expect(stateContent).toBe("testing");
|
|
139
|
-
} finally {
|
|
140
|
-
process.exit = originalExit;
|
|
141
|
-
// biome-ignore lint/suspicious/noExplicitAny: Restore original import
|
|
142
|
-
if (originalImport) (globalThis as any).import = originalImport;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("should start server without profile flag", async () => {
|
|
147
|
-
// Set up directories
|
|
148
|
-
mkdirSync(".omni", { recursive: true });
|
|
149
|
-
mkdirSync(".omni", { recursive: true });
|
|
150
|
-
|
|
151
|
-
// Create config
|
|
152
|
-
writeFileSync(
|
|
153
|
-
"omni.toml",
|
|
154
|
-
`
|
|
155
|
-
[capability]
|
|
156
|
-
project = "test"
|
|
157
|
-
default_profile = "default"
|
|
158
|
-
|
|
159
|
-
[profiles.default]
|
|
160
|
-
`,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// We can't actually test server startup without complex mocking,
|
|
164
|
-
// so we'll just verify the command passes initial checks
|
|
165
|
-
const mockExit = mock((code?: number) => {
|
|
166
|
-
throw new Error(`process.exit: ${code}`);
|
|
167
|
-
}) as typeof process.exit;
|
|
168
|
-
const originalExit = process.exit;
|
|
169
|
-
process.exit = mockExit;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
// This will fail at the import stage, but that's expected
|
|
173
|
-
await runServe({}).catch((error) => {
|
|
174
|
-
// Should fail on import or server start, not on validation
|
|
175
|
-
expect(error).toBeDefined();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// No profile should be written when flag not provided
|
|
179
|
-
expect(existsSync(".omni/active-profile")).toBe(false);
|
|
180
|
-
} finally {
|
|
181
|
-
process.exit = originalExit;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
});
|
package/src/commands/serve.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { setActiveProfile } from "@omnidev-ai/core";
|
|
3
|
-
import { buildCommand } from "@stricli/core";
|
|
4
|
-
|
|
5
|
-
interface ServeFlags {
|
|
6
|
-
profile?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function runServe(flags: ServeFlags): Promise<void> {
|
|
10
|
-
console.log("Starting OmniDev MCP server...");
|
|
11
|
-
|
|
12
|
-
// Check if OmniDev is initialized
|
|
13
|
-
if (!existsSync(".omni")) {
|
|
14
|
-
console.error("✗ OmniDev not initialized. Run: omnidev init");
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Set profile if provided
|
|
19
|
-
if (flags.profile) {
|
|
20
|
-
console.log(`Setting profile to: ${flags.profile}`);
|
|
21
|
-
try {
|
|
22
|
-
// Validate profile exists in config
|
|
23
|
-
const { loadConfig } = await import("@omnidev-ai/core");
|
|
24
|
-
const config = await loadConfig();
|
|
25
|
-
if (!config.profiles?.[flags.profile]) {
|
|
26
|
-
console.error(`✗ Profile "${flags.profile}" not found in config`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
await setActiveProfile(flags.profile);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error(`✗ Failed to set profile: ${error}`);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Import and start the MCP server
|
|
37
|
-
try {
|
|
38
|
-
const { startServer } = await import("@omnidev-ai/mcp");
|
|
39
|
-
console.log("✓ Server starting...");
|
|
40
|
-
await startServer();
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error(`✗ Failed to start server: ${error}`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const serveCommand = buildCommand({
|
|
48
|
-
docs: {
|
|
49
|
-
brief: "Start the OmniDev MCP server",
|
|
50
|
-
fullDescription: "Starts the MCP server that exposes omni_query and omni_execute tools to LLMs",
|
|
51
|
-
},
|
|
52
|
-
parameters: {
|
|
53
|
-
flags: {
|
|
54
|
-
profile: {
|
|
55
|
-
kind: "parsed" as const,
|
|
56
|
-
parse: String,
|
|
57
|
-
brief: "Set active profile before starting server",
|
|
58
|
-
optional: true,
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
func: runServe,
|
|
63
|
-
});
|