@omnidev-ai/cli 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.
@@ -0,0 +1,265 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { runInit } from "./init";
5
+
6
+ describe("init command", () => {
7
+ let testDir: string;
8
+ let originalCwd: string;
9
+
10
+ beforeEach(() => {
11
+ originalCwd = process.cwd();
12
+ testDir = join(import.meta.dir, `test-init-${Date.now()}`);
13
+ mkdirSync(testDir, { recursive: true });
14
+ process.chdir(testDir);
15
+ });
16
+
17
+ afterEach(() => {
18
+ process.chdir(originalCwd);
19
+ if (existsSync(testDir)) {
20
+ rmSync(testDir, { recursive: true, force: true });
21
+ }
22
+ });
23
+
24
+ test("creates .omni/ directory", async () => {
25
+ await runInit({}, "claude");
26
+
27
+ expect(existsSync(".omni")).toBe(true);
28
+ expect(existsSync(".omni/capabilities")).toBe(true);
29
+ });
30
+
31
+ test("creates omni.toml with default config", async () => {
32
+ await runInit({}, "claude");
33
+
34
+ expect(existsSync("omni.toml")).toBe(true);
35
+
36
+ const content = readFileSync("omni.toml", "utf-8");
37
+ expect(content).toContain('project = "my-project"');
38
+ // active_profile is stored in state file, not config.toml
39
+ expect(content).not.toContain("active_profile");
40
+ // profiles should be in config.toml
41
+ expect(content).toContain("[profiles.default]");
42
+ expect(content).toContain("[profiles.planning]");
43
+ expect(content).toContain("[profiles.coding]");
44
+ // providers should be in config.toml
45
+ expect(content).toContain("[providers]");
46
+ // should have documentation comments
47
+ expect(content).toContain("# OmniDev Configuration");
48
+ });
49
+
50
+ test("creates active profile in state file", async () => {
51
+ await runInit({}, "claude");
52
+
53
+ expect(existsSync(".omni/state/active-profile")).toBe(true);
54
+
55
+ const content = readFileSync(".omni/state/active-profile", "utf-8");
56
+ expect(content).toBe("default");
57
+ });
58
+
59
+ test("does not create separate capabilities.toml file", async () => {
60
+ await runInit({}, "claude");
61
+
62
+ // All config is unified in config.toml
63
+ expect(existsSync(".omni/capabilities.toml")).toBe(false);
64
+ });
65
+
66
+ test("does not create separate profiles.toml file", async () => {
67
+ await runInit({}, "claude");
68
+
69
+ // Profiles are in config.toml
70
+ expect(existsSync(".omni/profiles.toml")).toBe(false);
71
+ });
72
+
73
+ test("creates .omni/ directory with subdirectories", async () => {
74
+ await runInit({}, "claude");
75
+
76
+ expect(existsSync(".omni")).toBe(true);
77
+ expect(existsSync(".omni/state")).toBe(true);
78
+ expect(existsSync(".omni/sandbox")).toBe(true);
79
+ });
80
+
81
+ test("does not create separate provider.toml file", async () => {
82
+ await runInit({}, "claude");
83
+
84
+ // Providers are in config.toml
85
+ expect(existsSync(".omni/provider.toml")).toBe(false);
86
+
87
+ // Verify provider is in config.toml instead
88
+ const content = readFileSync("omni.toml", "utf-8");
89
+ expect(content).toContain('enabled = ["claude"]');
90
+ });
91
+
92
+ test("creates AGENTS.md for Codex provider", async () => {
93
+ await runInit({}, "codex");
94
+
95
+ expect(existsSync("AGENTS.md")).toBe(true);
96
+
97
+ const content = readFileSync("AGENTS.md", "utf-8");
98
+ expect(content).toContain("# Project Instructions");
99
+ expect(content).toContain("@import .omni/instructions.md");
100
+ });
101
+
102
+ test("creates .omni/instructions.md", async () => {
103
+ await runInit({}, "codex");
104
+
105
+ expect(existsSync(".omni/instructions.md")).toBe(true);
106
+
107
+ const content = readFileSync(".omni/instructions.md", "utf-8");
108
+ expect(content).toContain("# OmniDev Instructions");
109
+ expect(content).toContain("## Project Description");
110
+ expect(content).toContain("<!-- TODO: Add 2-3 sentences describing your project -->");
111
+ expect(content).toContain("## Capabilities");
112
+ expect(content).toContain("No capabilities enabled yet");
113
+ });
114
+
115
+ test("does not create AGENTS.md for Claude provider", async () => {
116
+ await runInit({}, "claude");
117
+
118
+ expect(existsSync("AGENTS.md")).toBe(false);
119
+ });
120
+
121
+ test("creates CLAUDE.md for Claude provider", async () => {
122
+ await runInit({}, "claude");
123
+
124
+ expect(existsSync("CLAUDE.md")).toBe(true);
125
+
126
+ const content = readFileSync("CLAUDE.md", "utf-8");
127
+ expect(content).toContain("# Project Instructions");
128
+ expect(content).toContain("@import .omni/instructions.md");
129
+ });
130
+
131
+ test("does not create CLAUDE.md for Codex provider", async () => {
132
+ await runInit({}, "codex");
133
+
134
+ expect(existsSync("CLAUDE.md")).toBe(false);
135
+ });
136
+
137
+ test("creates both AGENTS.md and CLAUDE.md for 'both' providers", async () => {
138
+ await runInit({}, "both");
139
+
140
+ expect(existsSync("AGENTS.md")).toBe(true);
141
+ expect(existsSync("CLAUDE.md")).toBe(true);
142
+
143
+ const agentsContent = readFileSync("AGENTS.md", "utf-8");
144
+ expect(agentsContent).toContain("# Project Instructions");
145
+ expect(agentsContent).toContain("@import .omni/instructions.md");
146
+
147
+ const claudeContent = readFileSync("CLAUDE.md", "utf-8");
148
+ expect(claudeContent).toContain("# Project Instructions");
149
+ expect(claudeContent).toContain("@import .omni/instructions.md");
150
+ });
151
+
152
+ test("does not modify existing CLAUDE.md", async () => {
153
+ const existingContent = "# My Existing Config\n\nExisting content here.\n";
154
+ await Bun.write("CLAUDE.md", existingContent);
155
+
156
+ await runInit({}, "claude");
157
+
158
+ const content = readFileSync("CLAUDE.md", "utf-8");
159
+ expect(content).toBe(existingContent);
160
+ });
161
+
162
+ test("does not modify existing AGENTS.md", async () => {
163
+ const existingContent = "# My Existing Agents\n\nExisting content here.\n";
164
+ await Bun.write("AGENTS.md", existingContent);
165
+
166
+ await runInit({}, "codex");
167
+
168
+ const content = readFileSync("AGENTS.md", "utf-8");
169
+ expect(content).toBe(existingContent);
170
+ });
171
+
172
+ test("creates .omni/.gitignore with internal patterns", async () => {
173
+ await runInit({}, "claude");
174
+
175
+ expect(existsSync(".omni/.gitignore")).toBe(true);
176
+
177
+ const content = readFileSync(".omni/.gitignore", "utf-8");
178
+ expect(content).toContain("# OmniDev working files - always ignored");
179
+ expect(content).toContain(".env");
180
+ expect(content).toContain("state/");
181
+ expect(content).toContain("sandbox/");
182
+ expect(content).toContain("*.log");
183
+ });
184
+
185
+ test("does not modify project's root .gitignore", async () => {
186
+ // Create a root .gitignore with custom content
187
+ await Bun.write(".gitignore", "node_modules/\n*.log\n");
188
+
189
+ await runInit({}, "claude");
190
+
191
+ // Verify .gitignore was not modified
192
+ const content = readFileSync(".gitignore", "utf-8");
193
+ expect(content).toBe("node_modules/\n*.log\n");
194
+ expect(content).not.toContain(".omni/");
195
+ });
196
+
197
+ test("does not create root .gitignore if it doesn't exist", async () => {
198
+ await runInit({}, "claude");
199
+
200
+ expect(existsSync(".gitignore")).toBe(false);
201
+ });
202
+
203
+ test("is idempotent - safe to run multiple times", async () => {
204
+ await runInit({}, "claude");
205
+ await runInit({}, "claude");
206
+ await runInit({}, "claude");
207
+
208
+ expect(existsSync("omni.toml")).toBe(true);
209
+ expect(existsSync(".omni")).toBe(true);
210
+ expect(existsSync("CLAUDE.md")).toBe(true);
211
+
212
+ // Should not create AGENTS.md for Claude
213
+ expect(existsSync("AGENTS.md")).toBe(false);
214
+ });
215
+
216
+ test("does not overwrite existing config.toml", async () => {
217
+ const customConfig = 'project = "custom"\n';
218
+ mkdirSync(".omni", { recursive: true });
219
+ await Bun.write("omni.toml", customConfig);
220
+
221
+ await runInit({}, "claude");
222
+
223
+ const content = readFileSync("omni.toml", "utf-8");
224
+ expect(content).toBe(customConfig);
225
+ });
226
+
227
+ test("does not overwrite existing AGENTS.md", async () => {
228
+ const customAgents = "# Custom agents\n";
229
+ await Bun.write("AGENTS.md", customAgents);
230
+
231
+ await runInit({}, "codex");
232
+
233
+ const content = readFileSync("AGENTS.md", "utf-8");
234
+ expect(content).toBe(customAgents);
235
+ });
236
+
237
+ test("creates all directories even if some already exist", async () => {
238
+ mkdirSync(".omni", { recursive: true });
239
+
240
+ await runInit({}, "claude");
241
+
242
+ expect(existsSync(".omni/capabilities")).toBe(true);
243
+ expect(existsSync(".omni")).toBe(true);
244
+ expect(existsSync(".omni/state")).toBe(true);
245
+ expect(existsSync(".omni/sandbox")).toBe(true);
246
+ });
247
+
248
+ test("accepts provider via positional parameter", async () => {
249
+ await runInit({}, "codex");
250
+
251
+ expect(existsSync(".omni/provider.toml")).toBe(false);
252
+
253
+ const content = readFileSync("omni.toml", "utf-8");
254
+ expect(content).toContain('enabled = ["codex"]');
255
+ });
256
+
257
+ test("accepts 'both' as provider parameter", async () => {
258
+ await runInit({}, "both");
259
+
260
+ expect(existsSync(".omni/provider.toml")).toBe(false);
261
+
262
+ const content = readFileSync("omni.toml", "utf-8");
263
+ expect(content).toContain('enabled = ["claude", "codex"]');
264
+ });
265
+ });
@@ -0,0 +1,192 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import type { Provider } from "@omnidev-ai/core";
3
+ import {
4
+ generateAgentsTemplate,
5
+ generateClaudeTemplate,
6
+ generateInstructionsTemplate,
7
+ parseProviderFlag,
8
+ setActiveProfile,
9
+ syncAgentConfiguration,
10
+ writeConfig,
11
+ } from "@omnidev-ai/core";
12
+ import { buildCommand } from "@stricli/core";
13
+ import { promptForProvider } from "../prompts/provider.js";
14
+
15
+ export async function runInit(_flags: Record<string, never>, provider?: string) {
16
+ console.log("Initializing OmniDev...");
17
+
18
+ // Create .omni/ directory structure
19
+ mkdirSync(".omni", { recursive: true });
20
+ mkdirSync(".omni/capabilities", { recursive: true });
21
+ mkdirSync(".omni/state", { recursive: true });
22
+ mkdirSync(".omni/sandbox", { recursive: true });
23
+
24
+ // Create .omni/.gitignore for internal working files
25
+ if (!existsSync(".omni/.gitignore")) {
26
+ await Bun.write(".omni/.gitignore", internalGitignore());
27
+ }
28
+
29
+ // Get provider selection
30
+ let providers: Provider[];
31
+ if (provider) {
32
+ providers = parseProviderFlag(provider);
33
+ } else {
34
+ providers = await promptForProvider();
35
+ }
36
+
37
+ // Create omni.toml at project root
38
+ if (!existsSync("omni.toml")) {
39
+ await writeConfig({
40
+ project: "my-project",
41
+ providers: {
42
+ enabled: providers,
43
+ },
44
+ profiles: {
45
+ default: {
46
+ capabilities: [],
47
+ },
48
+ planning: {
49
+ capabilities: [],
50
+ },
51
+ coding: {
52
+ capabilities: [],
53
+ },
54
+ },
55
+ });
56
+ // Set active profile in state file (not omni.toml)
57
+ await setActiveProfile("default");
58
+ }
59
+
60
+ // Create .omni/instructions.md
61
+ if (!existsSync(".omni/instructions.md")) {
62
+ await Bun.write(".omni/instructions.md", generateInstructionsTemplate());
63
+ }
64
+
65
+ // Create provider-specific files
66
+ const fileStatus = await createProviderFiles(providers);
67
+
68
+ // Run initial sync
69
+ await syncAgentConfiguration({ silent: false });
70
+
71
+ console.log("");
72
+ console.log(`✓ OmniDev initialized for ${providers.join(" and ")}!`);
73
+ console.log("");
74
+
75
+ // Show appropriate message based on file status
76
+ const hasNewFiles = fileStatus.created.length > 0;
77
+ const hasExistingFiles = fileStatus.existing.length > 0;
78
+
79
+ if (hasNewFiles) {
80
+ console.log("📝 Don't forget to add your project description to:");
81
+ console.log(" • .omni/instructions.md");
82
+ }
83
+
84
+ if (hasExistingFiles) {
85
+ console.log("📝 Add this line to your existing file(s):");
86
+ for (const file of fileStatus.existing) {
87
+ console.log(` • ${file}: @import .omni/instructions.md`);
88
+ }
89
+ }
90
+
91
+ console.log("");
92
+ console.log("🔌 Add OmniDev MCP Server to your AI provider:");
93
+ console.log("");
94
+ console.log(" Add to Claude Desktop config:");
95
+ console.log(" {");
96
+ console.log(' "mcpServers": {');
97
+ console.log(' "omnidev": {');
98
+ console.log(' "command": "npx",');
99
+ console.log(' "args": ["-y", "@omnidev-ai/cli", "serve"]');
100
+ console.log(" }");
101
+ console.log(" }");
102
+ console.log(" }");
103
+ console.log("");
104
+ console.log(" Or for local development:");
105
+ console.log(" {");
106
+ console.log(' "mcpServers": {');
107
+ console.log(' "omnidev": {');
108
+ console.log(' "command": "bun",');
109
+ console.log(' "args": ["run", "omnidev", "serve"],');
110
+ console.log(' "cwd": "/path/to/your/project"');
111
+ console.log(" }");
112
+ console.log(" }");
113
+ console.log(" }");
114
+ console.log("");
115
+ console.log("📁 File structure:");
116
+ console.log(" • omni.toml - Main config (commit to share with team)");
117
+ console.log(" • omni.lock.toml - Lock file (commit for reproducibility)");
118
+ console.log(" • omni.local.toml - Local overrides (add to .gitignore)");
119
+ console.log(" • .omni/ - Runtime directory (add to .gitignore)");
120
+ }
121
+
122
+ export const initCommand = buildCommand({
123
+ parameters: {
124
+ flags: {},
125
+ positional: {
126
+ kind: "tuple" as const,
127
+ parameters: [
128
+ {
129
+ brief: "AI provider: claude, codex, or both",
130
+ parse: String,
131
+ optional: true,
132
+ },
133
+ ],
134
+ },
135
+ },
136
+ docs: {
137
+ brief: "Initialize OmniDev in the current project",
138
+ },
139
+ func: runInit,
140
+ });
141
+
142
+ async function createProviderFiles(
143
+ providers: Provider[],
144
+ ): Promise<{ created: string[]; existing: string[] }> {
145
+ const created: string[] = [];
146
+ const existing: string[] = [];
147
+
148
+ // Create AGENTS.md for Codex
149
+ if (providers.includes("codex")) {
150
+ if (!existsSync("AGENTS.md")) {
151
+ await Bun.write("AGENTS.md", generateAgentsTemplate());
152
+ created.push("AGENTS.md");
153
+ } else {
154
+ existing.push("AGENTS.md");
155
+ }
156
+ }
157
+
158
+ // Create CLAUDE.md for Claude
159
+ if (providers.includes("claude")) {
160
+ if (!existsSync("CLAUDE.md")) {
161
+ await Bun.write("CLAUDE.md", generateClaudeTemplate());
162
+ created.push("CLAUDE.md");
163
+ } else {
164
+ existing.push("CLAUDE.md");
165
+ }
166
+ }
167
+
168
+ return { created, existing };
169
+ }
170
+
171
+ function internalGitignore(): string {
172
+ return `# OmniDev working files - always ignored
173
+ # These files change frequently and are machine-specific
174
+
175
+ # Secrets
176
+ .env
177
+
178
+ # Runtime state
179
+ state/
180
+
181
+ # Sandbox execution
182
+ sandbox/
183
+
184
+ # Logs
185
+ *.log
186
+
187
+ # MCP server process ID
188
+ server.pid
189
+
190
+ # Capability-specific patterns are appended below by each capability
191
+ `;
192
+ }
@@ -0,0 +1,113 @@
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
+ });