@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,163 @@
1
+ import {
2
+ disableCapability,
3
+ discoverCapabilities,
4
+ enableCapability,
5
+ getEnabledCapabilities,
6
+ loadCapabilityConfig,
7
+ syncAgentConfiguration,
8
+ } from "@omnidev-ai/core";
9
+ import { buildCommand, buildRouteMap } from "@stricli/core";
10
+
11
+ /**
12
+ * Run the capability list command.
13
+ */
14
+ export async function runCapabilityList(): Promise<void> {
15
+ try {
16
+ const enabledIds = await getEnabledCapabilities();
17
+ const capabilityPaths = await discoverCapabilities();
18
+
19
+ if (capabilityPaths.length === 0) {
20
+ console.log("No capabilities found.");
21
+ console.log("");
22
+ console.log("To add capabilities, create directories in omni/capabilities/");
23
+ console.log("Each capability must have a capability.toml file.");
24
+ return;
25
+ }
26
+
27
+ console.log("Capabilities:");
28
+ console.log("");
29
+
30
+ for (const path of capabilityPaths) {
31
+ try {
32
+ const capConfig = await loadCapabilityConfig(path);
33
+ const isEnabled = enabledIds.includes(capConfig.capability.id);
34
+ const status = isEnabled ? "✓ enabled" : "✗ disabled";
35
+ const { id, name, version } = capConfig.capability;
36
+
37
+ console.log(` ${status} ${name}`);
38
+ console.log(` ID: ${id}`);
39
+ console.log(` Version: ${version}`);
40
+ console.log("");
41
+ } catch (error) {
42
+ console.error(` ✗ Failed to load capability at ${path}:`, error);
43
+ console.log("");
44
+ }
45
+ }
46
+ } catch (error) {
47
+ console.error("Error listing capabilities:", error);
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Run the capability enable command.
54
+ */
55
+ export async function runCapabilityEnable(
56
+ _flags: Record<string, never>,
57
+ name: string,
58
+ ): Promise<void> {
59
+ try {
60
+ // Check if capability exists
61
+ const capabilityPaths = await discoverCapabilities();
62
+ const capabilityExists = capabilityPaths.some(async (path) => {
63
+ const config = await loadCapabilityConfig(path);
64
+ return config.capability.id === name;
65
+ });
66
+
67
+ if (!capabilityExists) {
68
+ console.error(`Error: Capability '${name}' not found`);
69
+ console.log("");
70
+ console.log("Run 'dev capability list' to see available capabilities");
71
+ process.exit(1);
72
+ }
73
+
74
+ await enableCapability(name);
75
+ console.log(`✓ Enabled capability: ${name}`);
76
+ console.log("");
77
+
78
+ // Auto-sync agent configuration
79
+ await syncAgentConfiguration();
80
+ } catch (error) {
81
+ console.error("Error enabling capability:", error);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run the capability disable command.
88
+ */
89
+ export async function runCapabilityDisable(
90
+ _flags: Record<string, never>,
91
+ name: string,
92
+ ): Promise<void> {
93
+ try {
94
+ await disableCapability(name);
95
+ console.log(`✓ Disabled capability: ${name}`);
96
+ console.log("");
97
+
98
+ // Auto-sync agent configuration
99
+ await syncAgentConfiguration();
100
+ } catch (error) {
101
+ console.error("Error disabling capability:", error);
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ const listCommand = buildCommand({
107
+ docs: {
108
+ brief: "List all discovered capabilities",
109
+ },
110
+ parameters: {},
111
+ async func() {
112
+ await runCapabilityList();
113
+ },
114
+ });
115
+
116
+ const enableCommand = buildCommand({
117
+ docs: {
118
+ brief: "Enable a capability",
119
+ },
120
+ parameters: {
121
+ flags: {},
122
+ positional: {
123
+ kind: "tuple" as const,
124
+ parameters: [
125
+ {
126
+ brief: "Capability name to enable",
127
+ parse: String,
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ func: runCapabilityEnable,
133
+ });
134
+
135
+ const disableCommand = buildCommand({
136
+ docs: {
137
+ brief: "Disable a capability",
138
+ },
139
+ parameters: {
140
+ flags: {},
141
+ positional: {
142
+ kind: "tuple" as const,
143
+ parameters: [
144
+ {
145
+ brief: "Capability name to disable",
146
+ parse: String,
147
+ },
148
+ ],
149
+ },
150
+ },
151
+ func: runCapabilityDisable,
152
+ });
153
+
154
+ export const capabilityRoutes = buildRouteMap({
155
+ routes: {
156
+ list: listCommand,
157
+ enable: enableCommand,
158
+ disable: disableCommand,
159
+ },
160
+ docs: {
161
+ brief: "Manage capabilities",
162
+ },
163
+ });
@@ -0,0 +1,197 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { runDoctor } from "./doctor";
5
+
6
+ describe("doctor command", () => {
7
+ let testDir: string;
8
+ let originalCwd: string;
9
+ let originalExit: typeof process.exit;
10
+ let exitCalled: boolean;
11
+ let exitCode: number;
12
+
13
+ // Helper to create complete .omni structure
14
+ function createCompleteStructure() {
15
+ mkdirSync(".omni", { recursive: true });
16
+ writeFileSync(
17
+ "omni.toml",
18
+ `project = "test"
19
+ active_profile = "default"
20
+
21
+ [providers]
22
+ enabled = ["claude"]
23
+
24
+ [profiles.default]
25
+ capabilities = []
26
+
27
+ [profiles.coding]
28
+ capabilities = []
29
+ `,
30
+ );
31
+ writeFileSync(
32
+ ".omni/.gitignore",
33
+ `# OmniDev Core
34
+ .env
35
+ generated/
36
+ state/
37
+ sandbox/
38
+ *.log
39
+ `,
40
+ );
41
+ }
42
+
43
+ beforeEach(() => {
44
+ // Create a unique test directory
45
+ testDir = join(
46
+ process.cwd(),
47
+ ".test-tmp",
48
+ `doctor-test-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
49
+ );
50
+ mkdirSync(testDir, { recursive: true });
51
+
52
+ // Change to test directory
53
+ originalCwd = process.cwd();
54
+ process.chdir(testDir);
55
+
56
+ // Mock process.exit
57
+ exitCalled = false;
58
+ exitCode = 0;
59
+ originalExit = process.exit;
60
+ process.exit = ((code?: number) => {
61
+ exitCalled = true;
62
+ exitCode = code ?? 0;
63
+ }) as typeof process.exit;
64
+ });
65
+
66
+ afterEach(() => {
67
+ // Restore process.exit
68
+ process.exit = originalExit;
69
+
70
+ // Restore working directory
71
+ process.chdir(originalCwd);
72
+
73
+ // Clean up test directory
74
+ if (existsSync(testDir)) {
75
+ rmSync(testDir, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ test("should pass all checks when setup is complete", async () => {
80
+ // Setup complete OmniDev structure
81
+ createCompleteStructure();
82
+
83
+ await runDoctor();
84
+
85
+ expect(exitCalled).toBe(false);
86
+ });
87
+
88
+ test("should fail when .omni/ directory is missing", async () => {
89
+ // Don't create .omni - the test expects it to be missing
90
+
91
+ await runDoctor();
92
+
93
+ expect(exitCalled).toBe(true);
94
+ expect(exitCode).toBe(1);
95
+ });
96
+
97
+ test("should fail when config.toml is missing", async () => {
98
+ mkdirSync(".omni", { recursive: true });
99
+
100
+ await runDoctor();
101
+
102
+ expect(exitCalled).toBe(true);
103
+ expect(exitCode).toBe(1);
104
+ });
105
+
106
+ test("should fail when config.toml is invalid", async () => {
107
+ mkdirSync(".omni", { recursive: true });
108
+ writeFileSync("omni.toml", "invalid toml [[[");
109
+
110
+ await runDoctor();
111
+
112
+ expect(exitCalled).toBe(true);
113
+ expect(exitCode).toBe(1);
114
+ });
115
+
116
+ test("should check Bun version is 1.0+", async () => {
117
+ // We can't change Bun.version in tests, but we can verify the check runs
118
+ // The version check should pass in our dev environment
119
+ createCompleteStructure();
120
+
121
+ await runDoctor();
122
+
123
+ // Should not exit with error (Bun version should be >= 1.0)
124
+ expect(exitCalled).toBe(false);
125
+ });
126
+
127
+ test("should suggest fixes for missing directories", async () => {
128
+ // No setup - all checks should fail
129
+
130
+ await runDoctor();
131
+
132
+ expect(exitCalled).toBe(true);
133
+ expect(exitCode).toBe(1);
134
+ });
135
+
136
+ test("should handle partial setup", async () => {
137
+ // Only create omni/ directory
138
+ mkdirSync(".omni", { recursive: true });
139
+
140
+ await runDoctor();
141
+
142
+ expect(exitCalled).toBe(true);
143
+ expect(exitCode).toBe(1);
144
+ });
145
+
146
+ test("should fail when config has invalid syntax", async () => {
147
+ mkdirSync(".omni", { recursive: true });
148
+
149
+ // Create a config with invalid TOML syntax
150
+ writeFileSync("omni.toml", "invalid = [[[]]");
151
+
152
+ await runDoctor();
153
+
154
+ expect(exitCalled).toBe(true);
155
+ expect(exitCode).toBe(1);
156
+ });
157
+
158
+ test("should pass with minimal valid config", async () => {
159
+ createCompleteStructure();
160
+
161
+ await runDoctor();
162
+
163
+ expect(exitCalled).toBe(false);
164
+ });
165
+
166
+ test("should validate internal .gitignore exists", async () => {
167
+ mkdirSync(".omni", { recursive: true });
168
+ writeFileSync(
169
+ "omni.toml",
170
+ `project = "test"
171
+ active_profile = "default"
172
+
173
+ [providers]
174
+ enabled = ["claude"]
175
+
176
+ [profiles.default]
177
+ capabilities = []
178
+ `,
179
+ );
180
+ // Missing .omni/.gitignore
181
+
182
+ await runDoctor();
183
+
184
+ expect(exitCalled).toBe(true);
185
+ expect(exitCode).toBe(1);
186
+ });
187
+
188
+ test("should pass when capabilities directory is missing (no custom capabilities)", async () => {
189
+ createCompleteStructure();
190
+ // Don't create .omni/capabilities directory
191
+
192
+ await runDoctor();
193
+
194
+ // Should still pass - capabilities directory is optional
195
+ expect(exitCalled).toBe(false);
196
+ });
197
+ });
@@ -0,0 +1,164 @@
1
+ import { existsSync } from "node:fs";
2
+ import { buildCommand } from "@stricli/core";
3
+
4
+ export const doctorCommand = buildCommand({
5
+ docs: {
6
+ brief: "Check OmniDev setup and dependencies",
7
+ },
8
+ parameters: {},
9
+ async func() {
10
+ return await runDoctor();
11
+ },
12
+ });
13
+
14
+ export async function runDoctor(): Promise<void> {
15
+ console.log("OmniDev Doctor");
16
+ console.log("==============");
17
+ console.log("");
18
+
19
+ const checks = [
20
+ checkBunVersion(),
21
+ checkOmniLocalDir(),
22
+ checkConfig(),
23
+ checkInternalGitignore(),
24
+ checkCapabilitiesDir(),
25
+ ];
26
+
27
+ let allPassed = true;
28
+ for (const check of checks) {
29
+ const { name, passed, message, fix } = await check;
30
+ const icon = passed ? "✓" : "✗";
31
+ console.log(`${icon} ${name}: ${message}`);
32
+ if (!passed && fix) {
33
+ console.log(` Fix: ${fix}`);
34
+ }
35
+ if (!passed) allPassed = false;
36
+ }
37
+
38
+ console.log("");
39
+ if (allPassed) {
40
+ console.log("All checks passed!");
41
+ } else {
42
+ console.log("Some checks failed. Please fix the issues above.");
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ interface Check {
48
+ name: string;
49
+ passed: boolean;
50
+ message: string;
51
+ fix?: string;
52
+ }
53
+
54
+ async function checkBunVersion(): Promise<Check> {
55
+ const version = Bun.version;
56
+ const parts = version.split(".");
57
+ const firstPart = parts[0];
58
+ if (!firstPart) {
59
+ return {
60
+ name: "Bun Version",
61
+ passed: false,
62
+ message: `Invalid version format: ${version}`,
63
+ fix: "Reinstall Bun: curl -fsSL https://bun.sh/install | bash",
64
+ };
65
+ }
66
+ const major = Number.parseInt(firstPart, 10);
67
+
68
+ if (major < 1) {
69
+ return {
70
+ name: "Bun Version",
71
+ passed: false,
72
+ message: `v${version}`,
73
+ fix: "Upgrade Bun: curl -fsSL https://bun.sh/install | bash",
74
+ };
75
+ }
76
+
77
+ return {
78
+ name: "Bun Version",
79
+ passed: true,
80
+ message: `v${version}`,
81
+ };
82
+ }
83
+
84
+ async function checkOmniLocalDir(): Promise<Check> {
85
+ const exists = existsSync(".omni");
86
+ if (!exists) {
87
+ return {
88
+ name: ".omni/ directory",
89
+ passed: false,
90
+ message: "Not found",
91
+ fix: "Run: omnidev init",
92
+ };
93
+ }
94
+
95
+ return {
96
+ name: ".omni/ directory",
97
+ passed: true,
98
+ message: "Found",
99
+ };
100
+ }
101
+
102
+ async function checkConfig(): Promise<Check> {
103
+ const configPath = "omni.toml";
104
+ if (!existsSync(configPath)) {
105
+ return {
106
+ name: "Configuration",
107
+ passed: false,
108
+ message: "omni.toml not found",
109
+ fix: "Run: omnidev init",
110
+ };
111
+ }
112
+
113
+ try {
114
+ const { loadConfig } = await import("@omnidev-ai/core");
115
+ await loadConfig();
116
+ return {
117
+ name: "Configuration",
118
+ passed: true,
119
+ message: "Valid",
120
+ };
121
+ } catch (error) {
122
+ return {
123
+ name: "Configuration",
124
+ passed: false,
125
+ message: `Invalid: ${error instanceof Error ? error.message : String(error)}`,
126
+ fix: "Check omni.toml syntax",
127
+ };
128
+ }
129
+ }
130
+
131
+ async function checkInternalGitignore(): Promise<Check> {
132
+ const gitignorePath = ".omni/.gitignore";
133
+ if (!existsSync(gitignorePath)) {
134
+ return {
135
+ name: "Internal .gitignore",
136
+ passed: false,
137
+ message: ".omni/.gitignore not found",
138
+ fix: "Run: omnidev init",
139
+ };
140
+ }
141
+
142
+ return {
143
+ name: "Internal .gitignore",
144
+ passed: true,
145
+ message: "Found",
146
+ };
147
+ }
148
+
149
+ async function checkCapabilitiesDir(): Promise<Check> {
150
+ const capabilitiesDirPath = ".omni/capabilities";
151
+ if (!existsSync(capabilitiesDirPath)) {
152
+ return {
153
+ name: "Capabilities Directory",
154
+ passed: true,
155
+ message: "Not found (no custom capabilities)",
156
+ };
157
+ }
158
+
159
+ return {
160
+ name: "Capabilities Directory",
161
+ passed: true,
162
+ message: "Found",
163
+ };
164
+ }