@omnidev-ai/cli 0.3.0 → 0.5.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.
@@ -1,289 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
- import { setupTestDir } from "@omnidev-ai/core/test-utils";
4
- import { runInit } from "./init";
5
-
6
- describe("init command", () => {
7
- setupTestDir("init-test-", { chdir: true });
8
-
9
- test("creates .omni/ directory", async () => {
10
- await runInit({}, "claude-code");
11
-
12
- expect(existsSync(".omni")).toBe(true);
13
- expect(existsSync(".omni/capabilities")).toBe(true);
14
- });
15
-
16
- test("creates omni.toml with default config", async () => {
17
- await runInit({}, "claude-code");
18
-
19
- expect(existsSync("omni.toml")).toBe(true);
20
-
21
- const content = readFileSync("omni.toml", "utf-8");
22
- expect(content).toContain('project = "my-project"');
23
- // active_profile is stored in state file, not config.toml
24
- expect(content).not.toContain("active_profile");
25
- // profiles should be in config.toml
26
- expect(content).toContain("[profiles.default]");
27
- expect(content).toContain("[profiles.planning]");
28
- expect(content).toContain("[profiles.coding]");
29
- // providers are stored in local state, not config.toml
30
- expect(content).not.toContain("[providers]");
31
- // should have documentation comments
32
- expect(content).toContain("# OmniDev Configuration");
33
- });
34
-
35
- test("creates active profile in state file", async () => {
36
- await runInit({}, "claude-code");
37
-
38
- expect(existsSync(".omni/state/active-profile")).toBe(true);
39
-
40
- const content = readFileSync(".omni/state/active-profile", "utf-8");
41
- expect(content).toBe("default");
42
- });
43
-
44
- test("stores enabled providers in local state file", async () => {
45
- await runInit({}, "claude-code");
46
-
47
- expect(existsSync(".omni/state/providers.json")).toBe(true);
48
-
49
- const content = readFileSync(".omni/state/providers.json", "utf-8");
50
- const state = JSON.parse(content);
51
- expect(state.enabled).toContain("claude-code");
52
- });
53
-
54
- test("does not create separate capabilities.toml file", async () => {
55
- await runInit({}, "claude-code");
56
-
57
- // All config is unified in config.toml
58
- expect(existsSync(".omni/capabilities.toml")).toBe(false);
59
- });
60
-
61
- test("does not create separate profiles.toml file", async () => {
62
- await runInit({}, "claude-code");
63
-
64
- // Profiles are in config.toml
65
- expect(existsSync(".omni/profiles.toml")).toBe(false);
66
- });
67
-
68
- test("creates .omni/ directory with subdirectories", async () => {
69
- await runInit({}, "claude-code");
70
-
71
- expect(existsSync(".omni")).toBe(true);
72
- expect(existsSync(".omni/state")).toBe(true);
73
- });
74
-
75
- test("does not create separate provider.toml file", async () => {
76
- await runInit({}, "claude-code");
77
-
78
- // Provider state is in .omni/state/providers.json
79
- expect(existsSync(".omni/provider.toml")).toBe(false);
80
-
81
- // Verify provider is in state file instead
82
- const content = readFileSync(".omni/state/providers.json", "utf-8");
83
- const state = JSON.parse(content);
84
- expect(state.enabled).toContain("claude-code");
85
- });
86
-
87
- test("creates AGENTS.md for Codex provider", async () => {
88
- await runInit({}, "codex");
89
-
90
- expect(existsSync("AGENTS.md")).toBe(true);
91
-
92
- const content = readFileSync("AGENTS.md", "utf-8");
93
- expect(content).toContain("# Project Instructions");
94
- expect(content).toContain("@import .omni/instructions.md");
95
- });
96
-
97
- test("creates .omni/instructions.md", async () => {
98
- await runInit({}, "codex");
99
-
100
- expect(existsSync(".omni/instructions.md")).toBe(true);
101
-
102
- const content = readFileSync(".omni/instructions.md", "utf-8");
103
- expect(content).toContain("# OmniDev Instructions");
104
- expect(content).toContain("## Project Description");
105
- expect(content).toContain("<!-- TODO: Add 2-3 sentences describing your project -->");
106
- expect(content).toContain("## Capabilities");
107
- expect(content).toContain("No capabilities enabled yet");
108
- });
109
-
110
- test("does not create AGENTS.md for Claude Code provider", async () => {
111
- await runInit({}, "claude-code");
112
-
113
- expect(existsSync("AGENTS.md")).toBe(false);
114
- });
115
-
116
- test("creates CLAUDE.md for Claude Code provider", async () => {
117
- await runInit({}, "claude-code");
118
-
119
- expect(existsSync("CLAUDE.md")).toBe(true);
120
-
121
- const content = readFileSync("CLAUDE.md", "utf-8");
122
- expect(content).toContain("# Project Instructions");
123
- expect(content).toContain("@import .omni/instructions.md");
124
- });
125
-
126
- test("does not create CLAUDE.md for Codex provider", async () => {
127
- await runInit({}, "codex");
128
-
129
- expect(existsSync("CLAUDE.md")).toBe(false);
130
- });
131
-
132
- test("creates both CLAUDE.md and .cursor/rules/ for 'both' providers", async () => {
133
- // "both" maps to claude-code and cursor
134
- await runInit({}, "both");
135
-
136
- expect(existsSync("CLAUDE.md")).toBe(true);
137
- expect(existsSync(".cursor/rules")).toBe(true);
138
-
139
- const claudeContent = readFileSync("CLAUDE.md", "utf-8");
140
- expect(claudeContent).toContain("# Project Instructions");
141
- expect(claudeContent).toContain("@import .omni/instructions.md");
142
- });
143
-
144
- test("does not modify existing CLAUDE.md", async () => {
145
- const existingContent = "# My Existing Config\n\nExisting content here.\n";
146
- await Bun.write("CLAUDE.md", existingContent);
147
-
148
- await runInit({}, "claude-code");
149
-
150
- const content = readFileSync("CLAUDE.md", "utf-8");
151
- expect(content).toBe(existingContent);
152
- });
153
-
154
- test("does not modify existing AGENTS.md", async () => {
155
- const existingContent = "# My Existing Agents\n\nExisting content here.\n";
156
- await Bun.write("AGENTS.md", existingContent);
157
-
158
- await runInit({}, "codex");
159
-
160
- const content = readFileSync("AGENTS.md", "utf-8");
161
- expect(content).toBe(existingContent);
162
- });
163
-
164
- test("does not create .omni/.gitignore", async () => {
165
- await runInit({}, "claude-code");
166
-
167
- // The whole .omni/ directory is gitignored, no need for internal .gitignore
168
- expect(existsSync(".omni/.gitignore")).toBe(false);
169
- });
170
-
171
- test("updates root .gitignore with omnidev entries", async () => {
172
- // Create a root .gitignore with custom content
173
- await Bun.write(".gitignore", "node_modules/\n*.log\n");
174
-
175
- await runInit({}, "claude-code");
176
-
177
- const content = readFileSync(".gitignore", "utf-8");
178
- expect(content).toContain("node_modules/");
179
- expect(content).toContain("*.log");
180
- expect(content).toContain("# OmniDev");
181
- expect(content).toContain(".omni/");
182
- expect(content).toContain("omni.local.toml");
183
- });
184
-
185
- test("creates root .gitignore if it doesn't exist", async () => {
186
- await runInit({}, "claude-code");
187
-
188
- expect(existsSync(".gitignore")).toBe(true);
189
-
190
- const content = readFileSync(".gitignore", "utf-8");
191
- expect(content).toContain("# OmniDev");
192
- expect(content).toContain(".omni/");
193
- expect(content).toContain("omni.local.toml");
194
- });
195
-
196
- test("does not duplicate gitignore entries on multiple runs", async () => {
197
- await runInit({}, "claude-code");
198
- await runInit({}, "claude-code");
199
-
200
- const content = readFileSync(".gitignore", "utf-8");
201
- // Should only have one occurrence of each entry
202
- expect(content.match(/\.omni\//g)?.length).toBe(1);
203
- expect(content.match(/omni\.local\.toml/g)?.length).toBe(1);
204
- });
205
-
206
- test("is idempotent - safe to run multiple times", async () => {
207
- await runInit({}, "claude-code");
208
- await runInit({}, "claude-code");
209
- await runInit({}, "claude-code");
210
-
211
- expect(existsSync("omni.toml")).toBe(true);
212
- expect(existsSync(".omni")).toBe(true);
213
- expect(existsSync("CLAUDE.md")).toBe(true);
214
-
215
- // Should not create AGENTS.md for Claude Code
216
- expect(existsSync("AGENTS.md")).toBe(false);
217
- });
218
-
219
- test("does not overwrite existing config.toml", async () => {
220
- const customConfig = 'project = "custom"\n';
221
- mkdirSync(".omni", { recursive: true });
222
- await Bun.write("omni.toml", customConfig);
223
-
224
- await runInit({}, "claude-code");
225
-
226
- const content = readFileSync("omni.toml", "utf-8");
227
- expect(content).toBe(customConfig);
228
- });
229
-
230
- test("does not overwrite existing AGENTS.md", async () => {
231
- const customAgents = "# Custom agents\n";
232
- await Bun.write("AGENTS.md", customAgents);
233
-
234
- await runInit({}, "codex");
235
-
236
- const content = readFileSync("AGENTS.md", "utf-8");
237
- expect(content).toBe(customAgents);
238
- });
239
-
240
- test("creates all directories even if some already exist", async () => {
241
- mkdirSync(".omni", { recursive: true });
242
-
243
- await runInit({}, "claude-code");
244
-
245
- expect(existsSync(".omni/capabilities")).toBe(true);
246
- expect(existsSync(".omni")).toBe(true);
247
- expect(existsSync(".omni/state")).toBe(true);
248
- });
249
-
250
- test("accepts provider via positional parameter", async () => {
251
- await runInit({}, "codex");
252
-
253
- expect(existsSync(".omni/provider.toml")).toBe(false);
254
-
255
- const content = readFileSync(".omni/state/providers.json", "utf-8");
256
- const state = JSON.parse(content);
257
- expect(state.enabled).toContain("codex");
258
- });
259
-
260
- test("accepts 'both' as provider parameter", async () => {
261
- await runInit({}, "both");
262
-
263
- expect(existsSync(".omni/provider.toml")).toBe(false);
264
-
265
- const content = readFileSync(".omni/state/providers.json", "utf-8");
266
- const state = JSON.parse(content);
267
- // "both" maps to claude-code and cursor
268
- expect(state.enabled).toContain("claude-code");
269
- expect(state.enabled).toContain("cursor");
270
- });
271
-
272
- test("supports legacy 'claude' name mapping to claude-code", async () => {
273
- await runInit({}, "claude");
274
-
275
- const content = readFileSync(".omni/state/providers.json", "utf-8");
276
- const state = JSON.parse(content);
277
- expect(state.enabled).toContain("claude-code");
278
- });
279
-
280
- test("supports comma-separated providers", async () => {
281
- await runInit({}, "claude-code,codex,cursor");
282
-
283
- const content = readFileSync(".omni/state/providers.json", "utf-8");
284
- const state = JSON.parse(content);
285
- expect(state.enabled).toContain("claude-code");
286
- expect(state.enabled).toContain("codex");
287
- expect(state.enabled).toContain("cursor");
288
- });
289
- });
@@ -1,188 +0,0 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import { getAllAdapters, getEnabledAdapters } from "@omnidev-ai/adapters";
3
- import type { ProviderId, ProviderContext } from "@omnidev-ai/core";
4
- import {
5
- generateInstructionsTemplate,
6
- loadConfig,
7
- setActiveProfile,
8
- syncAgentConfiguration,
9
- writeConfig,
10
- writeEnabledProviders,
11
- } from "@omnidev-ai/core";
12
- import { buildCommand } from "@stricli/core";
13
- import { promptForProviders } from "../prompts/provider.js";
14
-
15
- export async function runInit(_flags: Record<string, never>, providerArg?: 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
-
23
- // Update root .gitignore to ignore .omni/ and omni.local.toml
24
- await updateRootGitignore();
25
-
26
- // Get provider selection
27
- let providerIds: ProviderId[];
28
- if (providerArg) {
29
- providerIds = parseProviderArg(providerArg);
30
- } else {
31
- providerIds = await promptForProviders();
32
- }
33
-
34
- // Save enabled providers to local state (not omni.toml)
35
- await writeEnabledProviders(providerIds);
36
-
37
- // Create omni.toml at project root (without provider config - that's in state)
38
- if (!existsSync("omni.toml")) {
39
- await writeConfig({
40
- project: "my-project",
41
- profiles: {
42
- default: {
43
- capabilities: [],
44
- },
45
- planning: {
46
- capabilities: [],
47
- },
48
- coding: {
49
- capabilities: [],
50
- },
51
- },
52
- });
53
- // Set active profile in state file
54
- await setActiveProfile("default");
55
- }
56
-
57
- // Create .omni/instructions.md
58
- if (!existsSync(".omni/instructions.md")) {
59
- await Bun.write(".omni/instructions.md", generateInstructionsTemplate());
60
- }
61
-
62
- // Load config and create provider context
63
- const config = await loadConfig();
64
- const ctx: ProviderContext = {
65
- projectRoot: process.cwd(),
66
- config,
67
- };
68
-
69
- // Initialize enabled adapters (create their root files)
70
- const allAdapters = getAllAdapters();
71
- const selectedAdapters = allAdapters.filter((a) => providerIds.includes(a.id));
72
- const filesCreated: string[] = [];
73
- const filesExisting: string[] = [];
74
-
75
- for (const adapter of selectedAdapters) {
76
- if (adapter.init) {
77
- const result = await adapter.init(ctx);
78
- if (result.filesCreated) {
79
- filesCreated.push(...result.filesCreated);
80
- }
81
- }
82
- }
83
-
84
- // Run initial sync with enabled adapters (silent - no need to show details)
85
- const enabledAdapters = await getEnabledAdapters();
86
- await syncAgentConfiguration({ silent: true, adapters: enabledAdapters });
87
-
88
- // Output success message
89
- console.log("");
90
- console.log(
91
- `✓ OmniDev initialized for ${selectedAdapters.map((a) => a.displayName).join(" and ")}!`,
92
- );
93
- console.log("");
94
-
95
- // Show appropriate message based on file status
96
- if (filesCreated.length > 0) {
97
- console.log("📝 Don't forget to add your project description to:");
98
- console.log(" • .omni/instructions.md");
99
- }
100
-
101
- if (filesExisting.length > 0) {
102
- console.log("📝 Add this line to your existing file(s):");
103
- for (const file of filesExisting) {
104
- console.log(` • ${file}: @import .omni/instructions.md`);
105
- }
106
- }
107
-
108
- console.log("");
109
- console.log("💡 Recommendation:");
110
- console.log(" Add provider-specific files to .gitignore:");
111
- console.log(" CLAUDE.md, .claude/, AGENTS.md, .cursor/, .mcp.json");
112
- console.log("");
113
- console.log(" Run 'omnidev capability list' to see available capabilities.");
114
- }
115
-
116
- export const initCommand = buildCommand({
117
- parameters: {
118
- flags: {},
119
- positional: {
120
- kind: "tuple" as const,
121
- parameters: [
122
- {
123
- brief: "AI provider(s): claude-code, cursor, codex, opencode, or comma-separated",
124
- parse: String,
125
- optional: true,
126
- },
127
- ],
128
- },
129
- },
130
- docs: {
131
- brief: "Initialize OmniDev in the current project",
132
- },
133
- func: runInit,
134
- });
135
-
136
- function parseProviderArg(arg: string): ProviderId[] {
137
- const allAdapters = getAllAdapters();
138
- const validIds = new Set(allAdapters.map((a) => a.id));
139
-
140
- // Handle legacy "both" argument
141
- if (arg.toLowerCase() === "both") {
142
- return ["claude-code", "cursor"];
143
- }
144
-
145
- // Handle comma-separated list
146
- const parts = arg.split(",").map((p) => p.trim().toLowerCase());
147
- const result: ProviderId[] = [];
148
-
149
- for (const part of parts) {
150
- // Map legacy names
151
- let id = part;
152
- if (id === "claude") {
153
- id = "claude-code";
154
- }
155
-
156
- if (!validIds.has(id)) {
157
- throw new Error(`Invalid provider: ${part}. Valid providers: ${[...validIds].join(", ")}`);
158
- }
159
- result.push(id as ProviderId);
160
- }
161
-
162
- return result;
163
- }
164
-
165
- async function updateRootGitignore(): Promise<void> {
166
- const gitignorePath = ".gitignore";
167
- const entriesToAdd = [".omni/", "omni.local.toml"];
168
-
169
- let content = "";
170
- if (existsSync(gitignorePath)) {
171
- content = await Bun.file(gitignorePath).text();
172
- }
173
-
174
- const lines = content.split("\n");
175
- const missingEntries = entriesToAdd.filter(
176
- (entry) => !lines.some((line) => line.trim() === entry),
177
- );
178
-
179
- if (missingEntries.length === 0) {
180
- return;
181
- }
182
-
183
- // Add a newline before our section if the file doesn't end with one
184
- const needsNewline = content.length > 0 && !content.endsWith("\n");
185
- const section = `${needsNewline ? "\n" : ""}# OmniDev\n${missingEntries.join("\n")}\n`;
186
-
187
- await Bun.write(gitignorePath, content + section);
188
- }