@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,352 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { runProfileList, runProfileSet } from "./profile";
5
+
6
+ describe("profile commands", () => {
7
+ let testDir: string;
8
+ let originalCwd: string;
9
+ let originalExit: typeof process.exit;
10
+ let exitCode: number | undefined;
11
+ let consoleOutput: string[];
12
+ let consoleErrors: string[];
13
+
14
+ beforeEach(() => {
15
+ // Create a unique test directory
16
+ testDir = join(import.meta.dir, "..", "..", "..", "test-temp", `profile-test-${Date.now()}`);
17
+ mkdirSync(testDir, { recursive: true });
18
+ originalCwd = process.cwd();
19
+ process.chdir(testDir);
20
+
21
+ // Mock process.exit
22
+ exitCode = undefined;
23
+ originalExit = process.exit;
24
+ process.exit = ((code?: number) => {
25
+ exitCode = code ?? 0;
26
+ throw new Error(`process.exit(${code})`);
27
+ }) as typeof process.exit;
28
+
29
+ // Mock console
30
+ consoleOutput = [];
31
+ consoleErrors = [];
32
+ const originalLog = console.log;
33
+ const originalError = console.error;
34
+ console.log = (...args: unknown[]) => {
35
+ consoleOutput.push(args.join(" "));
36
+ };
37
+ console.error = (...args: unknown[]) => {
38
+ consoleErrors.push(args.join(" "));
39
+ };
40
+
41
+ // Restore after test (in afterEach)
42
+ return () => {
43
+ console.log = originalLog;
44
+ console.error = originalError;
45
+ };
46
+ });
47
+
48
+ afterEach(() => {
49
+ process.exit = originalExit;
50
+ process.chdir(originalCwd);
51
+ if (existsSync(testDir)) {
52
+ rmSync(testDir, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ describe("runProfileList", () => {
57
+ test("should show error when config file does not exist", async () => {
58
+ try {
59
+ await runProfileList();
60
+ } catch {
61
+ // Expected to throw due to process.exit mock
62
+ }
63
+
64
+ expect(exitCode).toBe(1);
65
+ expect(consoleOutput.join("\n")).toContain("No config file found");
66
+ expect(consoleOutput.join("\n")).toContain("Run: omnidev init");
67
+ });
68
+
69
+ test("should show message when no profiles defined", async () => {
70
+ // Create minimal config without profiles
71
+ mkdirSync(".omni", { recursive: true });
72
+ await Bun.write(
73
+ "omni.toml",
74
+ `project = "test-project"
75
+ active_profile = "default"
76
+ `,
77
+ );
78
+
79
+ await runProfileList();
80
+
81
+ expect(exitCode).toBeUndefined();
82
+ expect(consoleOutput.join("\n")).toContain("No profiles defined");
83
+ expect(consoleOutput.join("\n")).toContain("Using default capabilities");
84
+ });
85
+
86
+ test("should list all profiles from config", async () => {
87
+ // Create config with profiles
88
+ mkdirSync(".omni", { recursive: true });
89
+ await Bun.write(
90
+ "omni.toml",
91
+ `project = "test-project"
92
+ active_profile = "default"
93
+
94
+ [profiles.default]
95
+ capabilities = []
96
+
97
+ [profiles.planning]
98
+ capabilities = ["tasks", "planner"]
99
+
100
+ [profiles.coding]
101
+ capabilities = []
102
+ `,
103
+ );
104
+
105
+ await runProfileList();
106
+
107
+ expect(exitCode).toBeUndefined();
108
+ const output = consoleOutput.join("\n");
109
+ expect(output).toContain("Available Profiles:");
110
+ expect(output).toContain("default");
111
+ expect(output).toContain("planning");
112
+ expect(output).toContain("coding");
113
+ });
114
+
115
+ test("should show active profile with marker", async () => {
116
+ // Create config with profiles
117
+ mkdirSync(".omni", { recursive: true });
118
+ await Bun.write(
119
+ "omni.toml",
120
+ `project = "test-project"
121
+ active_profile = "planning"
122
+
123
+ [profiles.default]
124
+ capabilities = []
125
+
126
+ [profiles.planning]
127
+ capabilities = ["planner"]
128
+ `,
129
+ );
130
+
131
+ await runProfileList();
132
+
133
+ expect(exitCode).toBeUndefined();
134
+ const output = consoleOutput.join("\n");
135
+ expect(output).toContain("● planning (active)");
136
+ expect(output).toContain("○ default");
137
+ });
138
+
139
+ test("should show profile capabilities", async () => {
140
+ // Create config with profiles
141
+ mkdirSync(".omni", { recursive: true });
142
+ await Bun.write(
143
+ "omni.toml",
144
+ `project = "test-project"
145
+ active_profile = "default"
146
+
147
+ [profiles.default]
148
+ capabilities = []
149
+
150
+ [profiles.planning]
151
+ capabilities = ["planner", "tasks"]
152
+ `,
153
+ );
154
+
155
+ await runProfileList();
156
+
157
+ expect(exitCode).toBeUndefined();
158
+ const output = consoleOutput.join("\n");
159
+ expect(output).toContain("Capabilities: planner, tasks");
160
+ });
161
+
162
+ test("should use default_profile when no active profile", async () => {
163
+ // Create config with active_profile
164
+ mkdirSync(".omni", { recursive: true });
165
+ await Bun.write(
166
+ "omni.toml",
167
+ `project = "test-project"
168
+ active_profile = "planning"
169
+
170
+ [profiles.planning]
171
+ capabilities = ["planner"]
172
+ `,
173
+ );
174
+
175
+ await runProfileList();
176
+
177
+ expect(exitCode).toBeUndefined();
178
+ const output = consoleOutput.join("\n");
179
+ expect(output).toContain("● planning (active)");
180
+ });
181
+
182
+ test("should handle invalid config gracefully", async () => {
183
+ // Create invalid config
184
+ mkdirSync(".omni", { recursive: true });
185
+ await Bun.write("omni.toml", "invalid toml [[[");
186
+
187
+ try {
188
+ await runProfileList();
189
+ } catch {
190
+ // Expected to throw due to process.exit mock
191
+ }
192
+
193
+ expect(exitCode).toBe(1);
194
+ expect(consoleErrors.join("\n")).toContain("Error loading profiles");
195
+ });
196
+ });
197
+
198
+ describe("runProfileSet", () => {
199
+ test("should show error when config file does not exist", async () => {
200
+ try {
201
+ await runProfileSet("planning");
202
+ } catch {
203
+ // Expected to throw due to process.exit mock
204
+ }
205
+
206
+ expect(exitCode).toBe(1);
207
+ expect(consoleOutput.join("\n")).toContain("No config file found");
208
+ expect(consoleOutput.join("\n")).toContain("Run: omnidev init");
209
+ });
210
+
211
+ test("should show error when profile does not exist", async () => {
212
+ // Create config without the requested profile
213
+ mkdirSync(".omni", { recursive: true });
214
+ await Bun.write(
215
+ "omni.toml",
216
+ `project = "test-project"
217
+
218
+ [profiles.default]
219
+ capabilities = []
220
+ `,
221
+ );
222
+
223
+ try {
224
+ await runProfileSet("nonexistent");
225
+ } catch {
226
+ // Expected to throw due to process.exit mock
227
+ }
228
+
229
+ expect(exitCode).toBe(1);
230
+ const output = consoleOutput.join("\n");
231
+ expect(output).toContain('Profile "nonexistent" not found');
232
+ expect(output).toContain("Available profiles:");
233
+ expect(output).toContain("- default");
234
+ });
235
+
236
+ test("should set active profile", async () => {
237
+ // Create config with profiles
238
+ mkdirSync(".omni", { recursive: true });
239
+ await Bun.write(
240
+ "omni.toml",
241
+ `project = "test-project"
242
+
243
+ [profiles.default]
244
+ capabilities = []
245
+
246
+ [profiles.planning]
247
+ capabilities = ["planner"]
248
+ `,
249
+ );
250
+
251
+ await runProfileSet("planning");
252
+
253
+ expect(exitCode).toBeUndefined();
254
+ expect(consoleOutput.join("\n")).toContain("Active profile set to: planning");
255
+
256
+ // Verify active_profile was written to state file (not config.toml)
257
+ const stateContent = await Bun.file(".omni/state/active-profile").text();
258
+ expect(stateContent).toBe("planning");
259
+ });
260
+
261
+ test("should trigger agents sync after setting profile", async () => {
262
+ // Create config with profiles
263
+ mkdirSync(".omni", { recursive: true });
264
+ await Bun.write(
265
+ "omni.toml",
266
+ `project = "test-project"
267
+ active_profile = "default"
268
+
269
+ [profiles.default]
270
+ capabilities = []
271
+
272
+ [profiles.planning]
273
+ capabilities = []
274
+ `,
275
+ );
276
+
277
+ await runProfileSet("planning");
278
+
279
+ expect(exitCode).toBeUndefined();
280
+ const output = consoleOutput.join("\n");
281
+ expect(output).toContain("Syncing agent configuration");
282
+ });
283
+
284
+ test("should show list of available profiles when profile not found", async () => {
285
+ // Create config with multiple profiles
286
+ mkdirSync(".omni", { recursive: true });
287
+ await Bun.write(
288
+ "omni.toml",
289
+ `project = "test-project"
290
+
291
+ [profiles.default]
292
+ capabilities = []
293
+
294
+ [profiles.planning]
295
+ capabilities = []
296
+
297
+ [profiles.coding]
298
+ capabilities = []
299
+ `,
300
+ );
301
+
302
+ try {
303
+ await runProfileSet("nonexistent");
304
+ } catch {
305
+ // Expected to throw due to process.exit mock
306
+ }
307
+
308
+ expect(exitCode).toBe(1);
309
+ const output = consoleOutput.join("\n");
310
+ expect(output).toContain("Available profiles:");
311
+ expect(output).toContain("- default");
312
+ expect(output).toContain("- planning");
313
+ expect(output).toContain("- coding");
314
+ });
315
+
316
+ test("should handle empty profiles config", async () => {
317
+ // Create config without any profiles
318
+ mkdirSync(".omni", { recursive: true });
319
+ await Bun.write(
320
+ "omni.toml",
321
+ `project = "test-project"
322
+ `,
323
+ );
324
+
325
+ try {
326
+ await runProfileSet("default");
327
+ } catch {
328
+ // Expected to throw due to process.exit mock
329
+ }
330
+
331
+ expect(exitCode).toBe(1);
332
+ const output = consoleOutput.join("\n");
333
+ expect(output).toContain('Profile "default" not found');
334
+ expect(output).toContain("(none defined)");
335
+ });
336
+
337
+ test("should handle invalid config gracefully", async () => {
338
+ // Create invalid config
339
+ mkdirSync(".omni", { recursive: true });
340
+ await Bun.write("omni.toml", "invalid toml [[[");
341
+
342
+ try {
343
+ await runProfileSet("planning");
344
+ } catch {
345
+ // Expected to throw due to process.exit mock
346
+ }
347
+
348
+ expect(exitCode).toBe(1);
349
+ expect(consoleErrors.join("\n")).toContain("Error setting profile");
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,151 @@
1
+ import { existsSync } from "node:fs";
2
+ import {
3
+ getActiveProfile,
4
+ loadConfig,
5
+ resolveEnabledCapabilities,
6
+ setActiveProfile,
7
+ syncAgentConfiguration,
8
+ } from "@omnidev-ai/core";
9
+ import { buildCommand, buildRouteMap } from "@stricli/core";
10
+
11
+ const listCommand = buildCommand({
12
+ docs: {
13
+ brief: "List available profiles",
14
+ },
15
+ parameters: {},
16
+ async func() {
17
+ await runProfileList();
18
+ },
19
+ });
20
+
21
+ async function runSetCommand(_flags: Record<string, never>, profileName: string): Promise<void> {
22
+ await runProfileSet(profileName);
23
+ }
24
+
25
+ const setCommand = buildCommand({
26
+ docs: {
27
+ brief: "Set the active profile",
28
+ },
29
+ parameters: {
30
+ flags: {},
31
+ positional: {
32
+ kind: "tuple" as const,
33
+ parameters: [
34
+ {
35
+ brief: "Profile name",
36
+ parse: String,
37
+ },
38
+ ],
39
+ },
40
+ },
41
+ func: runSetCommand,
42
+ });
43
+
44
+ export const profileRoutes = buildRouteMap({
45
+ routes: {
46
+ list: listCommand,
47
+ set: setCommand,
48
+ },
49
+ docs: {
50
+ brief: "Manage capability profiles",
51
+ },
52
+ });
53
+
54
+ export async function runProfileList(): Promise<void> {
55
+ try {
56
+ // Check if omni.toml exists
57
+ if (!existsSync("omni.toml")) {
58
+ console.log("✗ No config file found");
59
+ console.log(" Run: omnidev init");
60
+ process.exit(1);
61
+ }
62
+
63
+ // Load config
64
+ const config = await loadConfig();
65
+
66
+ // Get active profile
67
+ const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
68
+
69
+ // Check if profiles exist
70
+ const profiles = config.profiles ?? {};
71
+ const profileNames = Object.keys(profiles);
72
+
73
+ if (profileNames.length === 0) {
74
+ console.log("No profiles defined in omni.toml");
75
+ console.log("");
76
+ console.log("Using default capabilities from omni.toml");
77
+ return;
78
+ }
79
+
80
+ // Display profiles
81
+ console.log("Available Profiles:");
82
+ console.log("");
83
+
84
+ for (const name of profileNames) {
85
+ const isActive = name === activeProfile;
86
+ const icon = isActive ? "●" : "○";
87
+ const profile = profiles[name];
88
+
89
+ if (profile === undefined) {
90
+ continue;
91
+ }
92
+
93
+ console.log(`${icon} ${name}${isActive ? " (active)" : ""}`);
94
+
95
+ // Show capabilities (including always-enabled)
96
+ const capabilities = resolveEnabledCapabilities(config, name);
97
+ if (capabilities.length > 0) {
98
+ console.log(` Capabilities: ${capabilities.join(", ")}`);
99
+ } else {
100
+ console.log(" Capabilities: none");
101
+ }
102
+ console.log("");
103
+ }
104
+ } catch (error) {
105
+ console.error("✗ Error loading profiles:", error);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ export async function runProfileSet(profileName: string): Promise<void> {
111
+ try {
112
+ // Check if omni.toml exists
113
+ if (!existsSync("omni.toml")) {
114
+ console.log("✗ No config file found");
115
+ console.log(" Run: omnidev init");
116
+ process.exit(1);
117
+ }
118
+
119
+ // Load config
120
+ const config = await loadConfig();
121
+
122
+ // Validate profile exists
123
+ const profiles = config.profiles ?? {};
124
+ if (!(profileName in profiles)) {
125
+ console.log(`✗ Profile "${profileName}" not found in omni.toml`);
126
+ console.log("");
127
+ console.log("Available profiles:");
128
+ const profileNames = Object.keys(profiles);
129
+ if (profileNames.length === 0) {
130
+ console.log(" (none defined)");
131
+ } else {
132
+ for (const name of profileNames) {
133
+ console.log(` - ${name}`);
134
+ }
135
+ }
136
+ process.exit(1);
137
+ }
138
+
139
+ // Set active profile
140
+ await setActiveProfile(profileName);
141
+
142
+ console.log(`✓ Active profile set to: ${profileName}`);
143
+ console.log("");
144
+
145
+ // Auto-sync agent configuration
146
+ await syncAgentConfiguration();
147
+ } catch (error) {
148
+ console.error("✗ Error setting profile:", error);
149
+ process.exit(1);
150
+ }
151
+ }
@@ -0,0 +1,184 @@
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
+ });