@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 ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@omnidev-ai/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Nikola-Milovic/omnidev.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "exports": {
12
+ ".": "./src/index.ts"
13
+ },
14
+ "bin": {
15
+ "omnidev": "./src/index.ts"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org"
24
+ },
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "build": "echo 'Build not needed for Bun runtime'"
28
+ },
29
+ "dependencies": {
30
+ "@inquirer/prompts": "^8.1.0",
31
+ "@omnidev-ai/core": "workspace:*",
32
+ "@stricli/core": "^1.2.5"
33
+ },
34
+ "devDependencies": {}
35
+ }
@@ -0,0 +1,43 @@
1
+ # CLI COMMANDS
2
+
3
+ **Generated:** 2026-01-12
4
+ **Location:** packages/cli/src/commands/
5
+
6
+ ## OVERVIEW
7
+ Static CLI commands built with Stricli framework for OmniDev project management.
8
+
9
+ ## WHERE TO LOOK
10
+ | Command | File | Purpose |
11
+ |---------|------|---------|
12
+ | `omnidev init` | init.ts | Creates .omni/ directory, config.toml, provider.toml, instructions.md, internal gitignore |
13
+ | `omnidev serve` | serve.ts | Starts MCP server via @omnidev-ai/mcp, optional --profile flag sets active profile |
14
+ | `omnidev doctor` | doctor.ts | Validates Bun version (≥1.0), .omni/ directory, config.toml, internal gitignore |
15
+ | `omnidev capability` | capability.ts | Subcommands: list, enable <name>, disable <name> (auto-syncs on enable/disable) |
16
+ | `omnidev profile` | profile.ts | Subcommands: list, set <name> (auto-syncs on set, shows active with ● indicator) |
17
+ | `omnidev ralph` | ralph.ts | Orchestrator: init, start [--agent/--iterations/--prd], stop, status; prd/story/spec/log/patterns subcommands |
18
+ | `omnidev sync` | sync.ts | Manual agent configuration sync (capabilities, skills, rules, .omni/.gitignore) |
19
+ | `omnidev mcp status` | mcp.ts | Shows MCP controller status from .omni/state/mcp-status.json |
20
+
21
+ ## CONVENTIONS
22
+
23
+ **Command Structure:**
24
+ - All commands use Stricli's `buildCommand()` or `buildRouteMap()` from `@stricli/core`
25
+ - Commands import from `@omnidev-ai/core` for config loading, capability management
26
+ - Route maps exported as `xxxRoutes` for subcommands (e.g., `capabilityRoutes`, `ralphRoutes`)
27
+
28
+ **State Management:**
29
+ - State-changing commands (capability enable/disable, profile set) auto-call `syncAgentConfiguration()`
30
+ - Commands validate `.omni/` exists before attempting operations
31
+ - Error handling: console.error() + process.exit(1)
32
+
33
+ **Output Style:**
34
+ - ✓ for success, ✗ for failure, ● for active items
35
+ - Brief fix suggestions for doctor check failures
36
+
37
+ ## ANTI-PATTERNS
38
+
39
+ - **NEVER** add static commands to app.ts - dynamic capability commands loaded via dynamic-app.ts
40
+ - **NEVER** skip syncAgentConfiguration() after state changes - keeps agent configs in sync
41
+ - **NEVER** assume .omni/ exists - check with existsSync() before loading config
42
+ - **NEVER** use any for flags - Stricli parameters define types explicitly
43
+ - **NEVER** hardcode agent commands in ralph.ts - loaded from capabilities/ralph/index.js
@@ -0,0 +1,483 @@
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 { runCapabilityDisable, runCapabilityEnable, runCapabilityList } from "./capability";
5
+
6
+ describe("capability list command", () => {
7
+ let testDir: string;
8
+ let originalCwd: string;
9
+ let originalExit: typeof process.exit;
10
+ let exitCode: number | undefined;
11
+
12
+ beforeEach(() => {
13
+ originalCwd = process.cwd();
14
+ testDir = join(import.meta.dir, `test-capability-${Date.now()}`);
15
+ mkdirSync(testDir, { recursive: true });
16
+ process.chdir(testDir);
17
+
18
+ // Mock process.exit
19
+ exitCode = undefined;
20
+ originalExit = process.exit;
21
+ process.exit = ((code?: number) => {
22
+ exitCode = code;
23
+ throw new Error(`process.exit(${code})`);
24
+ }) as typeof process.exit;
25
+ });
26
+
27
+ afterEach(() => {
28
+ process.exit = originalExit;
29
+ process.chdir(originalCwd);
30
+ if (existsSync(testDir)) {
31
+ rmSync(testDir, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("shows message when no capabilities found", async () => {
36
+ // Create minimal setup
37
+ mkdirSync(".omni", { recursive: true });
38
+ mkdirSync(".omni/capabilities", { recursive: true });
39
+ await Bun.write(
40
+ "omni.toml",
41
+ `project = "test"
42
+
43
+ [profiles.default]
44
+ capabilities = []
45
+ `,
46
+ );
47
+
48
+ const consoleLogs: string[] = [];
49
+ const originalLog = console.log;
50
+ console.log = (...args: unknown[]) => {
51
+ consoleLogs.push(args.join(" "));
52
+ };
53
+
54
+ await runCapabilityList();
55
+
56
+ console.log = originalLog;
57
+
58
+ expect(consoleLogs.some((log) => log.includes("No capabilities found"))).toBe(true);
59
+ expect(
60
+ consoleLogs.some((log) => log.includes("create directories in omni/capabilities/")),
61
+ ).toBe(true);
62
+ });
63
+
64
+ test("lists all discovered capabilities with enabled status", async () => {
65
+ // Create test structure
66
+ mkdirSync(".omni/capabilities/tasks", { recursive: true });
67
+ await Bun.write(
68
+ ".omni/capabilities/tasks/capability.toml",
69
+ `[capability]
70
+ id = "tasks"
71
+ name = "Task Management"
72
+ version = "1.0.0"
73
+ description = "Task tracking"
74
+ `,
75
+ );
76
+
77
+ mkdirSync(".omni/capabilities/notes", { recursive: true });
78
+ await Bun.write(
79
+ ".omni/capabilities/notes/capability.toml",
80
+ `[capability]
81
+ id = "notes"
82
+ name = "Note Taking"
83
+ version = "0.5.0"
84
+ description = "Note management"
85
+ `,
86
+ );
87
+
88
+ await Bun.write(
89
+ "omni.toml",
90
+ `project = "test"
91
+
92
+ [profiles.default]
93
+ capabilities = ["tasks"]
94
+ `,
95
+ );
96
+
97
+ const consoleLogs: string[] = [];
98
+ const originalLog = console.log;
99
+ console.log = (...args: unknown[]) => {
100
+ consoleLogs.push(args.join(" "));
101
+ };
102
+
103
+ await runCapabilityList();
104
+
105
+ console.log = originalLog;
106
+
107
+ const output = consoleLogs.join("\n");
108
+
109
+ // Check that both capabilities are shown
110
+ expect(output).toContain("Task Management");
111
+ expect(output).toContain("tasks");
112
+ expect(output).toContain("1.0.0");
113
+ expect(output).toContain("Note Taking");
114
+ expect(output).toContain("notes");
115
+ expect(output).toContain("0.5.0");
116
+
117
+ // Check enabled/disabled status
118
+ expect(output).toContain("✓ enabled");
119
+ expect(output).toContain("✗ disabled");
120
+ });
121
+
122
+ test("shows capability id and version", async () => {
123
+ mkdirSync(".omni/capabilities/test-cap", { recursive: true });
124
+ await Bun.write(
125
+ ".omni/capabilities/test-cap/capability.toml",
126
+ `[capability]
127
+ id = "test-cap"
128
+ name = "Test Capability"
129
+ version = "2.3.4"
130
+ description = "Test"
131
+ `,
132
+ );
133
+
134
+ await Bun.write(
135
+ "omni.toml",
136
+ `project = "test"
137
+
138
+ [profiles.default]
139
+ capabilities = ["test-cap"]
140
+ `,
141
+ );
142
+
143
+ const consoleLogs: string[] = [];
144
+ const originalLog = console.log;
145
+ console.log = (...args: unknown[]) => {
146
+ consoleLogs.push(args.join(" "));
147
+ };
148
+
149
+ await runCapabilityList();
150
+
151
+ console.log = originalLog;
152
+
153
+ const output = consoleLogs.join("\n");
154
+
155
+ expect(output).toContain("ID: test-cap");
156
+ expect(output).toContain("Version: 2.3.4");
157
+ });
158
+
159
+ test("handles invalid capability gracefully", async () => {
160
+ mkdirSync(".omni/capabilities/valid", { recursive: true });
161
+ await Bun.write(
162
+ ".omni/capabilities/valid/capability.toml",
163
+ `[capability]
164
+ id = "valid"
165
+ name = "Valid"
166
+ version = "1.0.0"
167
+ description = "Valid capability"
168
+ `,
169
+ );
170
+
171
+ mkdirSync(".omni/capabilities/invalid", { recursive: true });
172
+ await Bun.write(".omni/capabilities/invalid/capability.toml", "invalid toml [[[");
173
+
174
+ await Bun.write(
175
+ "omni.toml",
176
+ `project = "test"
177
+
178
+ [profiles.default]
179
+ capabilities = ["valid", "invalid"]
180
+ `,
181
+ );
182
+
183
+ const consoleLogs: string[] = [];
184
+ const consoleErrors: string[] = [];
185
+ const originalLog = console.log;
186
+ const originalError = console.error;
187
+ console.log = (...args: unknown[]) => {
188
+ consoleLogs.push(args.join(" "));
189
+ };
190
+ console.error = (...args: unknown[]) => {
191
+ consoleErrors.push(args.join(" "));
192
+ };
193
+
194
+ await runCapabilityList();
195
+
196
+ console.log = originalLog;
197
+ console.error = originalError;
198
+
199
+ // Valid capability should be shown
200
+ expect(consoleLogs.join("\n")).toContain("Valid");
201
+
202
+ // Invalid capability should show error
203
+ expect(consoleErrors.some((log) => log.includes("Failed to load capability"))).toBe(true);
204
+ });
205
+
206
+ test("respects profile when determining enabled status", async () => {
207
+ mkdirSync(".omni/capabilities/tasks", { recursive: true });
208
+ await Bun.write(
209
+ ".omni/capabilities/tasks/capability.toml",
210
+ `[capability]
211
+ id = "tasks"
212
+ name = "Tasks"
213
+ version = "1.0.0"
214
+ description = "Task tracking"
215
+ `,
216
+ );
217
+
218
+ await Bun.write(
219
+ "omni.toml",
220
+ `project = "test"
221
+ active_profile = "coding"
222
+
223
+ [profiles.coding]
224
+ capabilities = ["tasks"]
225
+ `,
226
+ );
227
+
228
+ const consoleLogs: string[] = [];
229
+ const originalLog = console.log;
230
+ console.log = (...args: unknown[]) => {
231
+ consoleLogs.push(args.join(" "));
232
+ };
233
+
234
+ await runCapabilityList();
235
+
236
+ console.log = originalLog;
237
+
238
+ const output = consoleLogs.join("\n");
239
+
240
+ expect(output).toContain("✓ enabled");
241
+ expect(output).toContain("Tasks");
242
+ });
243
+
244
+ test("exits with code 1 on error", async () => {
245
+ // Create an omni directory but with invalid config to trigger error
246
+ mkdirSync(".omni", { recursive: true });
247
+ mkdirSync(".omni/capabilities", { recursive: true });
248
+ await Bun.write("omni.toml", "invalid toml [[[");
249
+ mkdirSync(".omni", { recursive: true });
250
+
251
+ const originalError = console.error;
252
+ console.error = () => {}; // Suppress error output
253
+
254
+ try {
255
+ await runCapabilityList();
256
+ } catch (_error) {
257
+ // Expected to throw from process.exit mock
258
+ }
259
+
260
+ console.error = originalError;
261
+
262
+ expect(exitCode).toBe(1);
263
+ });
264
+
265
+ test("shows multiple capabilities in order", async () => {
266
+ const capabilities = ["alpha", "beta", "gamma"];
267
+
268
+ for (const cap of capabilities) {
269
+ mkdirSync(`.omni/capabilities/${cap}`, { recursive: true });
270
+ await Bun.write(
271
+ `.omni/capabilities/${cap}/capability.toml`,
272
+ `[capability]
273
+ id = "${cap}"
274
+ name = "${cap.toUpperCase()}"
275
+ version = "1.0.0"
276
+ description = "${cap} capability"
277
+ `,
278
+ );
279
+ }
280
+
281
+ await Bun.write(
282
+ "omni.toml",
283
+ `project = "test"
284
+
285
+ [profiles.default]
286
+ capabilities = ["alpha", "beta", "gamma"]
287
+ `,
288
+ );
289
+
290
+ const consoleLogs: string[] = [];
291
+ const originalLog = console.log;
292
+ console.log = (...args: unknown[]) => {
293
+ consoleLogs.push(args.join(" "));
294
+ };
295
+
296
+ await runCapabilityList();
297
+
298
+ console.log = originalLog;
299
+
300
+ const output = consoleLogs.join("\n");
301
+
302
+ for (const cap of capabilities) {
303
+ expect(output).toContain(cap.toUpperCase());
304
+ expect(output).toContain(`ID: ${cap}`);
305
+ }
306
+ });
307
+ });
308
+
309
+ describe("capability enable command", () => {
310
+ let testDir: string;
311
+ let originalCwd: string;
312
+ let originalExit: typeof process.exit;
313
+ let exitCode: number | undefined;
314
+
315
+ beforeEach(() => {
316
+ originalCwd = process.cwd();
317
+ testDir = join(import.meta.dir, `test-capability-enable-${Date.now()}`);
318
+ mkdirSync(testDir, { recursive: true });
319
+ process.chdir(testDir);
320
+
321
+ // Mock process.exit
322
+ exitCode = undefined;
323
+ originalExit = process.exit;
324
+ process.exit = ((code?: number) => {
325
+ exitCode = code;
326
+ throw new Error(`process.exit(${code})`);
327
+ }) as typeof process.exit;
328
+ });
329
+
330
+ afterEach(() => {
331
+ process.exit = originalExit;
332
+ process.chdir(originalCwd);
333
+ if (existsSync(testDir)) {
334
+ rmSync(testDir, { recursive: true, force: true });
335
+ }
336
+ });
337
+
338
+ test("enables a capability", async () => {
339
+ mkdirSync(".omni/capabilities/tasks", { recursive: true });
340
+ await Bun.write(
341
+ ".omni/capabilities/tasks/capability.toml",
342
+ `[capability]
343
+ id = "tasks"
344
+ name = "Tasks"
345
+ version = "1.0.0"
346
+ description = "Task tracking"
347
+ `,
348
+ );
349
+
350
+ await Bun.write(
351
+ "omni.toml",
352
+ `project = "test"
353
+
354
+ [profiles.default]
355
+ capabilities = []
356
+ `,
357
+ );
358
+
359
+ await runCapabilityEnable({}, "tasks");
360
+
361
+ const content = await Bun.file("omni.toml").text();
362
+ expect(content).toContain('capabilities = ["tasks"]');
363
+ });
364
+
365
+ test("adds capability to profile when enabling", async () => {
366
+ mkdirSync(".omni/capabilities/tasks", { recursive: true });
367
+ await Bun.write(
368
+ ".omni/capabilities/tasks/capability.toml",
369
+ `[capability]
370
+ id = "tasks"
371
+ name = "Tasks"
372
+ version = "1.0.0"
373
+ description = "Task tracking"
374
+ `,
375
+ );
376
+
377
+ await Bun.write(
378
+ "omni.toml",
379
+ `project = "test"
380
+
381
+ [profiles.default]
382
+ capabilities = []
383
+ `,
384
+ );
385
+
386
+ await runCapabilityEnable({}, "tasks");
387
+
388
+ const content = await Bun.file("omni.toml").text();
389
+ expect(content).toContain('capabilities = ["tasks"]');
390
+ });
391
+
392
+ test("exits with error if capability doesn't exist", async () => {
393
+ mkdirSync(".omni", { recursive: true });
394
+ await Bun.write(
395
+ "omni.toml",
396
+ `project = "test"
397
+
398
+ [profiles.default]
399
+ capabilities = []
400
+ `,
401
+ );
402
+
403
+ const originalError = console.error;
404
+ const originalLog = console.log;
405
+ console.error = () => {};
406
+ console.log = () => {};
407
+
408
+ try {
409
+ await runCapabilityEnable({}, "nonexistent");
410
+ } catch (_error) {
411
+ // Expected to throw from process.exit mock
412
+ }
413
+
414
+ console.error = originalError;
415
+ console.log = originalLog;
416
+
417
+ expect(exitCode).toBe(1);
418
+ });
419
+ });
420
+
421
+ describe("capability disable command", () => {
422
+ let testDir: string;
423
+ let originalCwd: string;
424
+ let originalExit: typeof process.exit;
425
+ let _exitCode: number | undefined;
426
+
427
+ beforeEach(() => {
428
+ originalCwd = process.cwd();
429
+ testDir = join(import.meta.dir, `test-capability-disable-${Date.now()}`);
430
+ mkdirSync(testDir, { recursive: true });
431
+ process.chdir(testDir);
432
+
433
+ // Mock process.exit
434
+ _exitCode = undefined;
435
+ originalExit = process.exit;
436
+ process.exit = ((code?: number) => {
437
+ _exitCode = code;
438
+ throw new Error(`process.exit(${code})`);
439
+ }) as typeof process.exit;
440
+ });
441
+
442
+ afterEach(() => {
443
+ process.exit = originalExit;
444
+ process.chdir(originalCwd);
445
+ if (existsSync(testDir)) {
446
+ rmSync(testDir, { recursive: true, force: true });
447
+ }
448
+ });
449
+
450
+ test("disables a capability", async () => {
451
+ mkdirSync(".omni", { recursive: true });
452
+ await Bun.write(
453
+ "omni.toml",
454
+ `project = "test"
455
+
456
+ [profiles.default]
457
+ capabilities = ["tasks"]
458
+ `,
459
+ );
460
+
461
+ await runCapabilityDisable({}, "tasks");
462
+
463
+ const content = await Bun.file("omni.toml").text();
464
+ expect(content).toContain("capabilities = []");
465
+ });
466
+
467
+ test("removes capability from profile", async () => {
468
+ mkdirSync(".omni", { recursive: true });
469
+ await Bun.write(
470
+ "omni.toml",
471
+ `project = "test"
472
+
473
+ [profiles.default]
474
+ capabilities = ["tasks", "notes"]
475
+ `,
476
+ );
477
+
478
+ await runCapabilityDisable({}, "tasks");
479
+
480
+ const content = await Bun.file("omni.toml").text();
481
+ expect(content).toContain('capabilities = ["notes"]');
482
+ });
483
+ });