@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.
- package/package.json +35 -0
- package/src/commands/AGENTS.md +43 -0
- package/src/commands/capability.test.ts +483 -0
- package/src/commands/capability.ts +163 -0
- package/src/commands/doctor.test.ts +197 -0
- package/src/commands/doctor.ts +164 -0
- package/src/commands/init.test.ts +265 -0
- package/src/commands/init.ts +192 -0
- package/src/commands/mcp.ts +113 -0
- package/src/commands/profile.test.ts +352 -0
- package/src/commands/profile.ts +151 -0
- package/src/commands/serve.test.ts +184 -0
- package/src/commands/serve.ts +63 -0
- package/src/commands/sync.ts +43 -0
- package/src/index.ts +55 -0
- package/src/lib/debug.ts +4 -0
- package/src/lib/dynamic-app.ts +182 -0
- package/src/prompts/provider.ts +15 -0
|
@@ -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
|
+
});
|